| // 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<RallyPieChartSegment> segments; |
| final String heroLabel; |
| final double heroAmount; |
| final double wholeAmount; |
| final List<FinancialEntityCategoryView> 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<FinancialEntityCategoryDetailsPage>( |
| 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<FinancialEntityCategoryView> buildAccountDataListViews( |
| List<AccountData> items, |
| BuildContext context, |
| ) { |
| return List<FinancialEntityCategoryView>.generate( |
| items.length, |
| (i) => buildFinancialEntityFromAccountData(items[i], i, context), |
| ); |
| } |
| |
| List<FinancialEntityCategoryView> buildBillDataListViews( |
| List<BillData> items, |
| BuildContext context, |
| ) { |
| return List<FinancialEntityCategoryView>.generate( |
| items.length, |
| (i) => buildFinancialEntityFromBillData(items[i], i, context), |
| ); |
| } |
| |
| List<FinancialEntityCategoryView> buildBudgetDataListViews( |
| List<BudgetData> items, |
| BuildContext context, |
| ) { |
| return <FinancialEntityCategoryView>[ |
| for (int i = 0; i < items.length; i++) |
| buildFinancialEntityFromBudgetData(items[i], i, context) |
| ]; |
| } |
| |
| class FinancialEntityCategoryDetailsPage extends StatelessWidget { |
| final List<DetailedEventData> 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), |
| ); |
| } |
| } |