blob: 2ff2b326500011698e4d38268940d32615106f31 [file] [log] [blame]
// 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:animations/animations.dart';
import 'package:gallery/data/gallery_options.dart';
import 'package:flutter_gen/gen_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,
enabled: true,
label: semanticsLabel,
),
excludeSemantics: true,
child: OpenContainer(
transitionDuration: const Duration(milliseconds: 350),
transitionType: ContainerTransitionType.fade,
openBuilder: (context, openContainer) =>
FinancialEntityCategoryDetailsPage(),
openColor: RallyColors.primaryBackground,
closedColor: RallyColors.primaryBackground,
closedElevation: 0,
closedBuilder: (context, openContainer) {
return FlatButton(
onPressed: openContainer,
child: Column(
children: [
Container(
padding: const 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.bodyText2
.copyWith(fontSize: 16),
),
Text(
subtitle,
style: textTheme.bodyText2
.copyWith(color: RallyColors.gray60),
),
],
),
Text(
amount,
style: textTheme.bodyText1.copyWith(
fontSize: 20,
color: RallyColors.gray,
),
),
],
),
),
Container(
constraints: const BoxConstraints(minWidth: 32),
padding: const 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
.bodyText2
.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.bodyText2.copyWith(fontSize: 18),
),
),
body: Column(
children: [
SizedBox(
height: 200,
width: double.infinity,
child: RallyLineChart(events: items),
),
Expanded(
child: Padding(
padding: isDesktop ? const 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: const EdgeInsets.symmetric(horizontal: 16),
child: Column(
children: [
Container(
padding: const 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.bodyText1.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.bodyText2.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.bodyText2.copyWith(fontSize: 16),
);
}
}