// Copyright 2019 The Flutter team. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. import 'dart:math' as math; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:gallery/data/gallery_options.dart'; import 'package:gallery/l10n/gallery_localizations.dart'; import 'package:gallery/layout/adaptive.dart'; import 'package:gallery/layout/text_scale.dart'; import 'package:gallery/studies/rally/charts/line_chart.dart'; import 'package:gallery/studies/rally/charts/pie_chart.dart'; import 'package:gallery/studies/rally/charts/vertical_fraction_bar.dart'; import 'package:gallery/studies/rally/colors.dart'; import 'package:gallery/studies/rally/data.dart'; import 'package:gallery/studies/rally/formatters.dart'; class FinancialEntityView extends StatelessWidget { const FinancialEntityView({ this.heroLabel, this.heroAmount, this.wholeAmount, this.segments, this.financialEntityCards, }) : assert(segments.length == financialEntityCards.length); /// The amounts to assign each item. final List segments; final String heroLabel; final double heroAmount; final double wholeAmount; final List financialEntityCards; @override Widget build(BuildContext context) { final maxWidth = pieChartMaxSize + (cappedTextScale(context) - 1.0) * 100.0; return LayoutBuilder(builder: (context, constraints) { return Column( children: [ ConstrainedBox( constraints: BoxConstraints( // We decrease the max height to ensure the [RallyPieChart] does // not take up the full height when it is smaller than // [kPieChartMaxSize]. maxHeight: math.min( constraints.biggest.shortestSide * 0.9, maxWidth, ), ), child: RallyPieChart( heroLabel: heroLabel, heroAmount: heroAmount, wholeAmount: wholeAmount, segments: segments, ), ), const SizedBox(height: 24), Container( height: 1, constraints: BoxConstraints(maxWidth: maxWidth), color: RallyColors.inputBackground, ), Container( constraints: BoxConstraints(maxWidth: maxWidth), color: RallyColors.cardBackground, child: Column( children: financialEntityCards, ), ), ], ); }); } } /// A reusable widget to show balance information of a single entity as a card. class FinancialEntityCategoryView extends StatelessWidget { const FinancialEntityCategoryView({ @required this.indicatorColor, @required this.indicatorFraction, @required this.title, @required this.subtitle, @required this.semanticsLabel, @required this.amount, @required this.suffix, }); final Color indicatorColor; final double indicatorFraction; final String title; final String subtitle; final String semanticsLabel; final String amount; final Widget suffix; @override Widget build(BuildContext context) { final textTheme = Theme.of(context).textTheme; return Semantics.fromProperties( properties: SemanticsProperties( button: true, label: semanticsLabel, ), excludeSemantics: true, child: FlatButton( onPressed: () { Navigator.push( context, MaterialPageRoute( builder: (context) => FinancialEntityCategoryDetailsPage(), ), ); }, child: Column( children: [ Container( padding: EdgeInsets.symmetric(vertical: 16), child: Row( children: [ Container( alignment: Alignment.center, height: 32 + 60 * (cappedTextScale(context) - 1), padding: const EdgeInsets.symmetric(horizontal: 12), child: VerticalFractionBar( color: indicatorColor, fraction: indicatorFraction, ), ), Expanded( child: Wrap( alignment: WrapAlignment.spaceBetween, crossAxisAlignment: WrapCrossAlignment.center, children: [ Column( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( title, style: textTheme.body1.copyWith(fontSize: 16), ), Text( subtitle, style: textTheme.body1 .copyWith(color: RallyColors.gray60), ), ], ), Text( amount, style: textTheme.body2.copyWith( fontSize: 20, color: RallyColors.gray, ), ), ], ), ), Container( constraints: BoxConstraints(minWidth: 32), padding: EdgeInsetsDirectional.only(start: 12), child: suffix, ), ], ), ), const Divider( height: 1, indent: 16, endIndent: 16, color: RallyColors.dividerColor, ), ], ), ), ); } } /// Data model for [FinancialEntityCategoryView]. class FinancialEntityCategoryModel { const FinancialEntityCategoryModel( this.indicatorColor, this.indicatorFraction, this.title, this.subtitle, this.usdAmount, this.suffix, ); final Color indicatorColor; final double indicatorFraction; final String title; final String subtitle; final double usdAmount; final Widget suffix; } FinancialEntityCategoryView buildFinancialEntityFromAccountData( AccountData model, int accountDataIndex, BuildContext context, ) { final amount = usdWithSignFormat(context).format(model.primaryAmount); final shortAccountNumber = model.accountNumber.substring(6); return FinancialEntityCategoryView( suffix: const Icon(Icons.chevron_right, color: Colors.grey), title: model.name, subtitle: '• • • • • • $shortAccountNumber', semanticsLabel: GalleryLocalizations.of(context).rallyAccountAmount( model.name, shortAccountNumber, amount, ), indicatorColor: RallyColors.accountColor(accountDataIndex), indicatorFraction: 1, amount: amount, ); } FinancialEntityCategoryView buildFinancialEntityFromBillData( BillData model, int billDataIndex, BuildContext context, ) { final amount = usdWithSignFormat(context).format(model.primaryAmount); return FinancialEntityCategoryView( suffix: const Icon(Icons.chevron_right, color: Colors.grey), title: model.name, subtitle: model.dueDate, semanticsLabel: GalleryLocalizations.of(context).rallyBillAmount( model.name, model.dueDate, amount, ), indicatorColor: RallyColors.billColor(billDataIndex), indicatorFraction: 1, amount: amount, ); } FinancialEntityCategoryView buildFinancialEntityFromBudgetData( BudgetData model, int budgetDataIndex, BuildContext context, ) { final amountUsed = usdWithSignFormat(context).format(model.amountUsed); final primaryAmount = usdWithSignFormat(context).format(model.primaryAmount); final amount = usdWithSignFormat(context).format(model.primaryAmount - model.amountUsed); return FinancialEntityCategoryView( suffix: Text( GalleryLocalizations.of(context).rallyFinanceLeft, style: Theme.of(context) .textTheme .body1 .copyWith(color: RallyColors.gray60, fontSize: 10), ), title: model.name, subtitle: amountUsed + ' / ' + primaryAmount, semanticsLabel: GalleryLocalizations.of(context).rallyBudgetAmount( model.name, model.amountUsed, model.primaryAmount, amount, ), indicatorColor: RallyColors.budgetColor(budgetDataIndex), indicatorFraction: model.amountUsed / model.primaryAmount, amount: amount, ); } List buildAccountDataListViews( List items, BuildContext context, ) { return List.generate( items.length, (i) => buildFinancialEntityFromAccountData(items[i], i, context), ); } List buildBillDataListViews( List items, BuildContext context, ) { return List.generate( items.length, (i) => buildFinancialEntityFromBillData(items[i], i, context), ); } List buildBudgetDataListViews( List items, BuildContext context, ) { return [ for (int i = 0; i < items.length; i++) buildFinancialEntityFromBudgetData(items[i], i, context) ]; } class FinancialEntityCategoryDetailsPage extends StatelessWidget { final List items = DummyDataService.getDetailedEventItems(); @override Widget build(BuildContext context) { final isDesktop = isDisplayDesktop(context); return ApplyTextOptions( child: Scaffold( appBar: AppBar( elevation: 0, centerTitle: true, title: Text( GalleryLocalizations.of(context).rallyAccountDataChecking, style: Theme.of(context).textTheme.body1.copyWith(fontSize: 18), ), ), body: Column( children: [ SizedBox( height: 200, width: double.infinity, child: RallyLineChart(events: items), ), Expanded( child: Padding( padding: isDesktop ? EdgeInsets.all(40) : EdgeInsets.zero, child: ListView( shrinkWrap: true, children: [ for (DetailedEventData detailedEventData in items) _DetailedEventCard( title: detailedEventData.title, date: detailedEventData.date, amount: detailedEventData.amount, ), ], ), ), ), ], ), ), ); } } class _DetailedEventCard extends StatelessWidget { const _DetailedEventCard({ @required this.title, @required this.date, @required this.amount, }); final String title; final DateTime date; final double amount; @override Widget build(BuildContext context) { final isDesktop = isDisplayDesktop(context); return FlatButton( onPressed: () {}, padding: EdgeInsets.symmetric(horizontal: 16), child: Column( children: [ Container( padding: EdgeInsets.symmetric(vertical: 16), width: double.infinity, child: isDesktop ? Row( children: [ Expanded( flex: 1, child: _EventTitle(title: title), ), _EventDate(date: date), Expanded( flex: 1, child: Align( alignment: AlignmentDirectional.centerEnd, child: _EventAmount(amount: amount), ), ), ], ) : Wrap( alignment: WrapAlignment.spaceBetween, children: [ Column( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.start, children: [ _EventTitle(title: title), _EventDate(date: date), ], ), _EventAmount(amount: amount), ], ), ), SizedBox( height: 1, child: Container( color: RallyColors.dividerColor, ), ), ], ), ); } } class _EventAmount extends StatelessWidget { const _EventAmount({Key key, @required this.amount}) : super(key: key); final double amount; @override Widget build(BuildContext context) { final textTheme = Theme.of(context).textTheme; return Text( usdWithSignFormat(context).format(amount), style: textTheme.body2.copyWith(fontSize: 20, color: RallyColors.gray), ); } } class _EventDate extends StatelessWidget { const _EventDate({Key key, @required this.date}) : super(key: key); final DateTime date; @override Widget build(BuildContext context) { final textTheme = Theme.of(context).textTheme; return Text( shortDateFormat(context).format(date), semanticsLabel: longDateFormat(context).format(date), style: textTheme.body1.copyWith(color: RallyColors.gray60), ); } } class _EventTitle extends StatelessWidget { const _EventTitle({Key key, @required this.title}) : super(key: key); final String title; @override Widget build(BuildContext context) { final textTheme = Theme.of(context).textTheme; return Text( title, style: textTheme.body1.copyWith(fontSize: 16), ); } }