From a214b4628e556b2e356523dc09a79ddfaa5ab285 Mon Sep 17 00:00:00 2001 From: Per Classon Date: Tue, 5 Nov 2019 17:27:40 +0100 Subject: [PATCH] [Gallery] Add Material Study app Rally as an example app (#42236) * Add Material Study app Rally to examples --- examples/flutter_gallery/lib/demo/all.dart | 1 + .../flutter_gallery/lib/demo/rally/app.dart | 88 +++++ .../lib/demo/rally/charts/line_chart.dart | 187 ++++++++++ .../lib/demo/rally/charts/pie_chart.dart | 258 ++++++++++++++ .../rally/charts/vertical_fraction_bar.dart | 44 +++ .../lib/demo/rally/colors.dart | 69 ++++ .../flutter_gallery/lib/demo/rally/data.dart | 228 ++++++++++++ .../lib/demo/rally/finance.dart | 330 ++++++++++++++++++ .../lib/demo/rally/formatters.dart | 19 + .../flutter_gallery/lib/demo/rally/home.dart | 206 +++++++++++ .../flutter_gallery/lib/demo/rally/login.dart | 91 +++++ .../flutter_gallery/lib/demo/rally/main.dart | 18 + .../lib/demo/rally/tabs/accounts.dart | 36 ++ .../lib/demo/rally/tabs/bills.dart | 42 +++ .../lib/demo/rally/tabs/budgets.dart | 42 +++ .../lib/demo/rally/tabs/overview.dart | 151 ++++++++ .../lib/demo/rally/tabs/settings.dart | 59 ++++ .../flutter_gallery/lib/demo/rally_demo.dart | 15 + .../flutter_gallery/lib/gallery/demos.dart | 8 + examples/flutter_gallery/pubspec.yaml | 21 +- 20 files changed, 1912 insertions(+), 1 deletion(-) create mode 100644 examples/flutter_gallery/lib/demo/rally/app.dart create mode 100644 examples/flutter_gallery/lib/demo/rally/charts/line_chart.dart create mode 100644 examples/flutter_gallery/lib/demo/rally/charts/pie_chart.dart create mode 100644 examples/flutter_gallery/lib/demo/rally/charts/vertical_fraction_bar.dart create mode 100644 examples/flutter_gallery/lib/demo/rally/colors.dart create mode 100644 examples/flutter_gallery/lib/demo/rally/data.dart create mode 100644 examples/flutter_gallery/lib/demo/rally/finance.dart create mode 100644 examples/flutter_gallery/lib/demo/rally/formatters.dart create mode 100644 examples/flutter_gallery/lib/demo/rally/home.dart create mode 100644 examples/flutter_gallery/lib/demo/rally/login.dart create mode 100644 examples/flutter_gallery/lib/demo/rally/main.dart create mode 100644 examples/flutter_gallery/lib/demo/rally/tabs/accounts.dart create mode 100644 examples/flutter_gallery/lib/demo/rally/tabs/bills.dart create mode 100644 examples/flutter_gallery/lib/demo/rally/tabs/budgets.dart create mode 100644 examples/flutter_gallery/lib/demo/rally/tabs/overview.dart create mode 100644 examples/flutter_gallery/lib/demo/rally/tabs/settings.dart create mode 100644 examples/flutter_gallery/lib/demo/rally_demo.dart diff --git a/examples/flutter_gallery/lib/demo/all.dart b/examples/flutter_gallery/lib/demo/all.dart index 0dfe26be87..a121dfc774 100644 --- a/examples/flutter_gallery/lib/demo/all.dart +++ b/examples/flutter_gallery/lib/demo/all.dart @@ -11,6 +11,7 @@ export 'fortnightly/fortnightly.dart'; export 'images_demo.dart'; export 'material/material.dart'; export 'pesto_demo.dart'; +export 'rally_demo.dart'; export 'shrine_demo.dart'; export 'transformations/transformations_demo.dart'; export 'typography_demo.dart'; diff --git a/examples/flutter_gallery/lib/demo/rally/app.dart b/examples/flutter_gallery/lib/demo/rally/app.dart new file mode 100644 index 0000000000..8e16d992b3 --- /dev/null +++ b/examples/flutter_gallery/lib/demo/rally/app.dart @@ -0,0 +1,88 @@ +// Copyright 2019-present the Flutter authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'package:flutter/material.dart'; + +import 'package:flutter_gallery/demo/rally/colors.dart'; +import 'package:flutter_gallery/demo/rally/home.dart'; +import 'package:flutter_gallery/demo/rally/login.dart'; + +/// The RallyApp is a MaterialApp with a theme and 2 routes. +/// +/// The home route is the main page with tabs for sub pages. +/// The login route is the initial route. +class RallyApp extends StatelessWidget { + @override + Widget build(BuildContext context) { + return MaterialApp( + title: 'Rally', + theme: _buildRallyTheme(), + home: HomePage(), + initialRoute: '/login', + routes: { + '/login': (BuildContext context) => LoginPage(), + }, + ); + } + + ThemeData _buildRallyTheme() { + final ThemeData base = ThemeData.dark(); + return ThemeData( + scaffoldBackgroundColor: RallyColors.primaryBackground, + primaryColor: RallyColors.primaryBackground, + textTheme: _buildRallyTextTheme(base.textTheme), + inputDecorationTheme: InputDecorationTheme( + labelStyle: const TextStyle( + color: RallyColors.gray, + fontWeight: FontWeight.w500, + ), + filled: true, + fillColor: RallyColors.inputBackground, + focusedBorder: InputBorder.none, + ), + ); + } + + TextTheme _buildRallyTextTheme(TextTheme base) { + return base + .copyWith( + body1: base.body1.copyWith( + fontFamily: 'Roboto Condensed', + fontSize: 14, + fontWeight: FontWeight.w400, + ), + body2: base.body2.copyWith( + fontFamily: 'Eczar', + fontSize: 40, + fontWeight: FontWeight.w400, + letterSpacing: 1.4, + ), + button: base.button.copyWith( + fontFamily: 'Roboto Condensed', + fontWeight: FontWeight.w700, + letterSpacing: 2.8, + ), + headline: base.body2.copyWith( + fontFamily: 'Eczar', + fontSize: 40, + fontWeight: FontWeight.w600, + letterSpacing: 1.4, + ), + ) + .apply( + displayColor: Colors.white, + bodyColor: Colors.white, + ); + } +} diff --git a/examples/flutter_gallery/lib/demo/rally/charts/line_chart.dart b/examples/flutter_gallery/lib/demo/rally/charts/line_chart.dart new file mode 100644 index 0000000000..65a985e154 --- /dev/null +++ b/examples/flutter_gallery/lib/demo/rally/charts/line_chart.dart @@ -0,0 +1,187 @@ +// Copyright 2019-present the Flutter authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + + +import 'package:flutter/material.dart'; + +import 'package:flutter_gallery/demo/rally/colors.dart'; +import 'package:flutter_gallery/demo/rally/data.dart'; + +class RallyLineChart extends StatelessWidget { + const RallyLineChart({ this.events = const [] }) : assert(events != null); + + final List events; + + @override + Widget build(BuildContext context) { + return CustomPaint(painter: RallyLineChartPainter(Theme.of(context).textTheme.body1, events)); + } +} + +class RallyLineChartPainter extends CustomPainter { + RallyLineChartPainter(this.labelStyle, this.events); + + final TextStyle labelStyle; + + // Events to plot on the line as points. + final List events; + + // Number of days to plot. + // This is hardcoded to reflect the dummy data, but would be dynamic in a real + // app. + final int numDays = 52; + + // Beginning of window. The end is this plus numDays. + // This is hardcoded to reflect the dummy data, but would be dynamic in a real + // app. + final DateTime startDate = DateTime.utc(2018, 12, 1); + + // Ranges uses to lerp the pixel points. + // This is hardcoded to reflect the dummy data, but would be dynamic in a real + // app. + final double maxAmount = 3000; // minAmount is assumed to be 0 + + // The number of milliseconds in a day. This is the inherit period fot the + // points in this line. + static const int millisInDay = 24 * 60 * 60 * 1000; + + // Amount to shift the tick drawing by so that the Sunday ticks do not start + // on the edge. + final int tickShift = 3; + + // Arbitrary unit of space for absolute positioned painting. + final double space = 16; + + @override + void paint(Canvas canvas, Size size) { + final double ticksTop = size.height - space * 5; + final double labelsTop = size.height - space * 2; + _drawLine( + canvas, + Rect.fromLTWH(0, 0, size.width, ticksTop), + ); + _drawXAxisTicks( + canvas, + Rect.fromLTWH(0, ticksTop, size.width, labelsTop - ticksTop), + ); + _drawXAxisLabels( + canvas, + Rect.fromLTWH(0, labelsTop, size.width, size.height - labelsTop), + ); + } + + // Since we're only using fixed dummy data, we can set this to false. In a + // real app we would have the data as part of the state and repaint when it's + // changed. + @override + bool shouldRepaint(CustomPainter oldDelegate) => false; + + void _drawLine(Canvas canvas, Rect rect) { + final Paint linePaint = Paint() + ..color = RallyColors.accountColor(2) + ..style = PaintingStyle.stroke + ..strokeWidth = 2; + + // Arbitrary value for the first point. In a real app, a wider range of + // points would be used that go beyond the boundaries of the screen. + double lastAmount = 800; + + // Try changing this value between 1, 7, 15, etc. + const int smoothing = 7; + + // Align the points with equal deltas (1 day) as a cumulative sum. + int startMillis = startDate.millisecondsSinceEpoch; + final List points = [ + Offset(0, (maxAmount - lastAmount) / maxAmount * rect.height) + ]; + for (int i = 0; i < numDays + smoothing; i++) { + final int endMillis = startMillis + millisInDay * 1; + final List filteredEvents = events.where( + (DetailedEventData e) { + return startMillis <= e.date.millisecondsSinceEpoch && e.date.millisecondsSinceEpoch <= endMillis; + }, + ).toList(); + lastAmount += sumOf(filteredEvents, (DetailedEventData e) => e.amount); + final double x = i / numDays * rect.width; + final double y = (maxAmount - lastAmount) / maxAmount * rect.height; + points.add(Offset(x, y)); + startMillis = endMillis; + } + + final Path path = Path(); + path.moveTo(points[0].dx, points[0].dy); + for (int i = 1; i < points.length - smoothing; i += smoothing) { + final double x1 = points[i].dx; + final double y1 = points[i].dy; + final double x2 = (x1 + points[i + smoothing].dx) / 2; + final double y2 = (y1 + points[i + smoothing].dy) / 2; + path.quadraticBezierTo(x1, y1, x2, y2); + } + canvas.drawPath(path, linePaint); + } + + /// Draw the X-axis increment markers at constant width intervals. + void _drawXAxisTicks(Canvas canvas, Rect rect) { + for (int i = 0; i < numDays; i++) { + final double x = rect.width / numDays * i; + canvas.drawRect( + Rect.fromPoints( + Offset(x, i % 7 == tickShift ? rect.top : rect.center.dy), + Offset(x, rect.bottom), + ), + Paint() + ..style = PaintingStyle.stroke + ..strokeWidth = 1 + ..color = RallyColors.gray25, + ); + } + } + + /// Set X-axis labels under the X-axis increment markers. + void _drawXAxisLabels(Canvas canvas, Rect rect) { + final TextStyle selectedLabelStyle = labelStyle.copyWith( + fontWeight: FontWeight.w700, + ); + final TextStyle unselectedLabelStyle = labelStyle.copyWith( + fontWeight: FontWeight.w700, + color: RallyColors.gray25, + ); + + final TextPainter leftLabel = TextPainter( + text: TextSpan(text: 'AUGUST 2019', style: unselectedLabelStyle), + textDirection: TextDirection.ltr, + ); + leftLabel.layout(); + leftLabel.paint(canvas, Offset(rect.left + space / 2, rect.center.dy)); + + final TextPainter centerLabel = TextPainter( + text: TextSpan(text: 'SEPTEMBER 2019', style: selectedLabelStyle), + textDirection: TextDirection.ltr, + ); + centerLabel.layout(); + final double x = (rect.width - centerLabel.width) / 2; + final double y = rect.center.dy; + centerLabel.paint(canvas, Offset(x, y)); + + final TextPainter rightLabel = TextPainter( + text: TextSpan(text: 'OCTOBER 2019', style: unselectedLabelStyle), + textDirection: TextDirection.ltr, + ); + rightLabel.layout(); + rightLabel.paint( + canvas, + Offset(rect.right - centerLabel.width - space / 2, rect.center.dy), + ); + } +} diff --git a/examples/flutter_gallery/lib/demo/rally/charts/pie_chart.dart b/examples/flutter_gallery/lib/demo/rally/charts/pie_chart.dart new file mode 100644 index 0000000000..3834d94ebd --- /dev/null +++ b/examples/flutter_gallery/lib/demo/rally/charts/pie_chart.dart @@ -0,0 +1,258 @@ +// Copyright 2019-present the Flutter authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'dart:math'; + +import 'package:flutter/material.dart'; + +import 'package:flutter_gallery/demo/rally/colors.dart'; +import 'package:flutter_gallery/demo/rally/data.dart'; +import 'package:flutter_gallery/demo/rally/formatters.dart'; + +/// A colored piece of the [RallyPieChart]. +class RallyPieChartSegment { + const RallyPieChartSegment({ this.color, this.value }); + + final Color color; + final double value; +} + +List buildSegmentsFromAccountItems( + List items) { + return List.generate( + items.length, + (int i) { + return RallyPieChartSegment( + color: RallyColors.accountColor(i), + value: items[i].primaryAmount, + ); + }, + ); +} + +List buildSegmentsFromBillItems(List items) { + return List.generate( + items.length, + (int i) { + return RallyPieChartSegment( + color: RallyColors.billColor(i), + value: items[i].primaryAmount, + ); + }, + ); +} + +List buildSegmentsFromBudgetItems( + List items) { + return List.generate( + items.length, + (int i) { + return RallyPieChartSegment( + color: RallyColors.budgetColor(i), + value: items[i].primaryAmount - items[i].amountUsed, + ); + }, + ); +} + +/// An animated circular pie chart to represent pieces of a whole, which can +/// have empty space. +class RallyPieChart extends StatefulWidget { + const RallyPieChart({ this.heroLabel, this.heroAmount, this.wholeAmount, this.segments }); + + final String heroLabel; + final double heroAmount; + final double wholeAmount; + final List segments; + + @override + _RallyPieChartState createState() => _RallyPieChartState(); +} + +class _RallyPieChartState extends State + with SingleTickerProviderStateMixin { + AnimationController controller; + Animation animation; + + @override + void initState() { + super.initState(); + controller = AnimationController( + duration: const Duration(milliseconds: 600), + vsync: this, + ); + animation = CurvedAnimation( + parent: TweenSequence(>[ + TweenSequenceItem( + tween: Tween(begin: 0, end: 0), + weight: 1, + ), + TweenSequenceItem( + tween: Tween(begin: 0, end: 1), + weight: 1.5, + ), + ]).animate(controller), + curve: Curves.decelerate); + controller.forward(); + } + + @override + void dispose() { + controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return _AnimatedRallyPieChart( + animation: animation, + centerLabel: widget.heroLabel, + centerAmount: widget.heroAmount, + total: widget.wholeAmount, + segments: widget.segments, + ); + } +} + +class _AnimatedRallyPieChart extends AnimatedWidget { + const _AnimatedRallyPieChart({ + Key key, + this.animation, + this.centerLabel, + this.centerAmount, + this.total, + this.segments, + }) : super(key: key, listenable: animation); + + final Animation animation; + final String centerLabel; + final double centerAmount; + final double total; + final List segments; + + @override + Widget build(BuildContext context) { + final TextStyle labelTextStyle = Theme.of(context).textTheme.body1.copyWith( + fontSize: 14, + letterSpacing: 0.5, + ); + + return DecoratedBox( + decoration: _RallyPieChartOutlineDecoration( + maxFraction: animation.value, + total: total, + segments: segments, + ), + child: SizedBox( + height: 300, + child: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + centerLabel, + style: labelTextStyle, + ), + Text( + usdWithSignFormat.format(centerAmount), + style: Theme.of(context).textTheme.headline, + ), + ], + ), + ), + ), + ); + } +} + +class _RallyPieChartOutlineDecoration extends Decoration { + const _RallyPieChartOutlineDecoration({this.maxFraction, this.total, this.segments}); + + final double maxFraction; + final double total; + final List segments; + + @override + BoxPainter createBoxPainter([VoidCallback onChanged]) { + return _RallyPieChartOutlineBoxPainter( + maxFraction: maxFraction, + wholeAmount: total, + segments: segments, + ); + } +} + +class _RallyPieChartOutlineBoxPainter extends BoxPainter { + _RallyPieChartOutlineBoxPainter({this.maxFraction, this.wholeAmount, this.segments}); + + final double maxFraction; + final double wholeAmount; + final List segments; + static const double wholeRadians = 2 * pi; + static const double spaceRadians = wholeRadians / 180; + + @override + void paint(Canvas canvas, Offset offset, ImageConfiguration configuration) { + // Create two padded reacts to draw arcs in: one for colored arcs and one for + // inner bg arc. + const double strokeWidth = 4; + final double outerRadius = min( + configuration.size.width, + configuration.size.height, + ) / 2; + final Rect outerRect = Rect.fromCircle( + center: configuration.size.center(Offset.zero), + radius: outerRadius - strokeWidth * 3, + ); + final Rect innerRect = Rect.fromCircle( + center: configuration.size.center(Offset.zero), + radius: outerRadius - strokeWidth * 4, + ); + + // Paint each arc with spacing. + double cumulativeSpace = 0; + double cumulativeTotal = 0; + for (RallyPieChartSegment segment in segments) { + final Paint paint = Paint()..color = segment.color; + final double startAngle = _calculateStartAngle(cumulativeTotal, cumulativeSpace); + final double sweepAngle = _calculateSweepAngle(segment.value, 0); + canvas.drawArc(outerRect, startAngle, sweepAngle, true, paint); + cumulativeTotal += segment.value; + cumulativeSpace += spaceRadians; + } + + // Paint any remaining space black (e.g. budget amount remaining). + final double remaining = wholeAmount - cumulativeTotal; + if (remaining > 0) { + final Paint paint = Paint()..color = Colors.black; + final double startAngle = _calculateStartAngle(cumulativeTotal, spaceRadians * segments.length); + final double sweepAngle = _calculateSweepAngle(remaining, -spaceRadians); + canvas.drawArc(outerRect, startAngle, sweepAngle, true, paint); + } + + // Paint a smaller inner circle to cover the painted arcs, so they are + // display as segments. + final Paint bgPaint = Paint()..color = RallyColors.primaryBackground; + canvas.drawArc(innerRect, 0, 2 * pi, true, bgPaint); + } + + double _calculateAngle(double amount, double offset) { + final double wholeMinusSpacesRadians = wholeRadians - (segments.length * spaceRadians); + return maxFraction * (amount / wholeAmount * wholeMinusSpacesRadians + offset); + } + + double _calculateStartAngle(double total, double offset) => _calculateAngle(total, offset) - pi / 2; + + double _calculateSweepAngle(double total, double offset) => _calculateAngle(total, offset); +} diff --git a/examples/flutter_gallery/lib/demo/rally/charts/vertical_fraction_bar.dart b/examples/flutter_gallery/lib/demo/rally/charts/vertical_fraction_bar.dart new file mode 100644 index 0000000000..803487bd63 --- /dev/null +++ b/examples/flutter_gallery/lib/demo/rally/charts/vertical_fraction_bar.dart @@ -0,0 +1,44 @@ +// Copyright 2019-present the Flutter authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'package:flutter/material.dart'; + +class VerticalFractionBar extends StatelessWidget { + const VerticalFractionBar({ this.color, this.fraction }); + + final Color color; + final double fraction; + + @override + Widget build(BuildContext context) { + return SizedBox( + height: 32, + width: 4, + child: Column( + children: [ + SizedBox( + height: (1 - fraction) * 32, + child: Container( + color: Colors.black, + ), + ), + SizedBox( + height: fraction * 32, + child: Container(color: color), + ), + ], + ), + ); + } +} diff --git a/examples/flutter_gallery/lib/demo/rally/colors.dart b/examples/flutter_gallery/lib/demo/rally/colors.dart new file mode 100644 index 0000000000..cc4cf32446 --- /dev/null +++ b/examples/flutter_gallery/lib/demo/rally/colors.dart @@ -0,0 +1,69 @@ +// Copyright 2019-present the Flutter authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'dart:ui'; + +/// Most color assignments in Rally are not like the the typical color +/// assignments that are common in other apps. Instead of primarily mapping to +/// component type and part, they are assigned round robin based on layout. +class RallyColors { + static const List accountColors = [ + Color(0xFF005D57), + Color(0xFF04B97F), + Color(0xFF37EFBA), + Color(0xFF007D51), + ]; + + static const List billColors = [ + Color(0xFFFFDC78), + Color(0xFFFF6951), + Color(0xFFFFD7D0), + Color(0xFFFFAC12), + ]; + + static const List budgetColors = [ + Color(0xFFB2F2FF), + Color(0xFFB15DFF), + Color(0xFF72DEFF), + Color(0xFF0082FB), + ]; + + static const Color gray = Color(0xFFD8D8D8); + static const Color gray60 = Color(0x99D8D8D8); + static const Color gray25 = Color(0x40D8D8D8); + static const Color white60 = Color(0x99FFFFFF); + static const Color primaryBackground = Color(0xFF33333D); + static const Color inputBackground = Color(0xFF26282F); + static const Color cardBackground = Color(0x03FEFEFE); + + /// Convenience method to get a single account color with position i. + static Color accountColor(int i) { + return cycledColor(accountColors, i); + } + + /// Convenience method to get a single bill color with position i. + static Color billColor(int i) { + return cycledColor(billColors, i); + } + + /// Convenience method to get a single budget color with position i. + static Color budgetColor(int i) { + return cycledColor(budgetColors, i); + } + + /// Gets a color from a list that is considered to be infinitely repeating. + static Color cycledColor(List colors, int i) { + return colors[i % colors.length]; + } +} diff --git a/examples/flutter_gallery/lib/demo/rally/data.dart b/examples/flutter_gallery/lib/demo/rally/data.dart new file mode 100644 index 0000000000..ff681a83b9 --- /dev/null +++ b/examples/flutter_gallery/lib/demo/rally/data.dart @@ -0,0 +1,228 @@ +// Copyright 2019-present the Flutter authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/// Calculates the sum of the primary amounts of a list of [AccountData]. +double sumAccountDataPrimaryAmount(List items) => sumOf(items, (AccountData item) => item.primaryAmount); + +/// Calculates the sum of the primary amounts of a list of [BillData]. +double sumBillDataPrimaryAmount(List items) => sumOf(items, (BillData item) => item.primaryAmount); + +/// Calculates the sum of the primary amounts of a list of [BudgetData]. +double sumBudgetDataPrimaryAmount(List items) => sumOf(items, (BudgetData item) => item.primaryAmount); + +/// Calculates the sum of the amounts used of a list of [BudgetData]. +double sumBudgetDataAmountUsed(List items) => sumOf(items, (BudgetData item) => item.amountUsed); + +/// Utility function to sum up values in a list. +double sumOf(List list, double getValue(T elt)) { + double sum = 0; + for (T elt in list) + sum += getValue(elt); + return sum; +} + + +/// A data model for an account. +/// +/// The [primaryAmount] is the balance of the account in USD. +class AccountData { + const AccountData({this.name, this.primaryAmount, this.accountNumber}); + + /// The display name of this entity. + final String name; + + // The primary amount or value of this entity. + final double primaryAmount; + + /// The full displayable account number. + final String accountNumber; +} + +/// A data model for a bill. +/// +/// The [primaryAmount] is the amount due in USD. +class BillData { + const BillData({this.name, this.primaryAmount, this.dueDate}); + + /// The display name of this entity. + final String name; + + // The primary amount or value of this entity. + final double primaryAmount; + + /// The due date of this bill. + final String dueDate; +} + +/// A data model for a budget. +/// +/// The [primaryAmount] is the budget cap in USD. +class BudgetData { + const BudgetData({this.name, this.primaryAmount, this.amountUsed}); + + /// The display name of this entity. + final String name; + + // The primary amount or value of this entity. + final double primaryAmount; + + /// Amount of the budget that is consumed or used. + final double amountUsed; +} + +class DetailedEventData { + const DetailedEventData({ + this.title, + this.date, + this.amount, + }); + + final String title; + final DateTime date; + final double amount; +} + +/// Class to return dummy data lists. +/// +/// In a real app, this might be replaced with some asynchronous service. +class DummyDataService { + static List getAccountDataList() { + return [ + const AccountData( + name: 'Checking', + primaryAmount: 2215.13, + accountNumber: '1234561234', + ), + const AccountData( + name: 'Home Savings', + primaryAmount: 8678.88, + accountNumber: '8888885678', + ), + const AccountData( + name: 'Car Savings', + primaryAmount: 987.48, + accountNumber: '8888889012', + ), + const AccountData( + name: 'Vacation', + primaryAmount: 253, + accountNumber: '1231233456', + ), + ]; + } + + static List getDetailedEventItems() { + return [ + DetailedEventData( + title: 'Genoe', + date: DateTime.utc(2019, 1, 24), + amount: -16.54, + ), + DetailedEventData( + title: 'Fortnightly Subscribe', + date: DateTime.utc(2019, 1, 5), + amount: -12.54, + ), + DetailedEventData( + title: 'Circle Cash', + date: DateTime.utc(2019, 1, 5), + amount: 365.65, + ), + DetailedEventData( + title: 'Crane Hospitality', + date: DateTime.utc(2019, 1, 4), + amount: -705.13, + ), + DetailedEventData( + title: 'ABC Payroll', + date: DateTime.utc(2018, 12, 15), + amount: 1141.43, + ), + DetailedEventData( + title: 'Shrine', + date: DateTime.utc(2018, 12, 15), + amount: -88.88, + ), + DetailedEventData( + title: 'Foodmates', + date: DateTime.utc(2018, 12, 4), + amount: -11.69, + ), + ]; + } + + static List getBillDataList() { + return [ + const BillData( + name: 'RedPay Credit', + primaryAmount: 45.36, + dueDate: 'Jan 29', + ), + const BillData( + name: 'Rent', + primaryAmount: 1200, + dueDate: 'Feb 9', + ), + const BillData( + name: 'TabFine Credit', + primaryAmount: 87.33, + dueDate: 'Feb 22', + ), + const BillData( + name: 'ABC Loans', + primaryAmount: 400, + dueDate: 'Feb 29', + ), + ]; + } + + static List getBudgetDataList() { + return [ + const BudgetData( + name: 'Coffee Shops', + primaryAmount: 70, + amountUsed: 45.49, + ), + const BudgetData( + name: 'Groceries', + primaryAmount: 170, + amountUsed: 16.45, + ), + const BudgetData( + name: 'Restaurants', + primaryAmount: 170, + amountUsed: 123.25, + ), + const BudgetData( + name: 'Clothing', + primaryAmount: 70, + amountUsed: 19.45, + ), + ]; + } + + static List getSettingsTitles() { + return [ + 'Manage Accounts', + 'Tax Documents', + 'Passcode and Touch ID', + 'Notifications', + 'Personal Information', + 'Paperless Settings', + 'Find ATMs', + 'Help', + 'Sign out', + ]; + } +} diff --git a/examples/flutter_gallery/lib/demo/rally/finance.dart b/examples/flutter_gallery/lib/demo/rally/finance.dart new file mode 100644 index 0000000000..03bcec4b0c --- /dev/null +++ b/examples/flutter_gallery/lib/demo/rally/finance.dart @@ -0,0 +1,330 @@ +// Copyright 2019-present the Flutter authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'package:flutter/material.dart'; + +import 'package:flutter_gallery/demo/rally/charts/pie_chart.dart'; +import 'package:flutter_gallery/demo/rally/charts/line_chart.dart'; +import 'package:flutter_gallery/demo/rally/charts/vertical_fraction_bar.dart'; +import 'package:flutter_gallery/demo/rally/colors.dart'; +import 'package:flutter_gallery/demo/rally/data.dart'; +import 'package:flutter_gallery/demo/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) { + return Column( + children: [ + RallyPieChart( + heroLabel: heroLabel, + heroAmount: heroAmount, + wholeAmount: wholeAmount, + segments: segments, + ), + SizedBox( + height: 1, + child: Container( + color: const Color(0xA026282F), + ), + ), + ListView(shrinkWrap: true, 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.amount, + @required this.suffix, + }); + + final Color indicatorColor; + final double indicatorFraction; + final String title; + final String subtitle; + final double amount; + final Widget suffix; + + @override + Widget build(BuildContext context) { + return FlatButton( + onPressed: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (BuildContext context) => FinancialEntityCategoryDetailsPage(), + ), + ); + }, + child: SizedBox( + height: 68, + child: Column( + children: [ + Expanded( + child: Row( + children: [ + Padding( + padding: const EdgeInsets.only(left: 12, right: 12), + child: VerticalFractionBar( + color: indicatorColor, + fraction: indicatorFraction, + ), + ), + Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: Theme.of(context).textTheme.body1.copyWith(fontSize: 16), + ), + Text( + subtitle, + style: Theme.of(context).textTheme.body1.copyWith(color: RallyColors.gray60), + ), + ], + ), + const Spacer(), + Text( + '\$ ' + usdFormat.format(amount), + style: Theme.of(context).textTheme.body2.copyWith(fontSize: 20, color: RallyColors.gray), + ), + SizedBox(width: 32, child: suffix), + ], + ), + ), + const Divider( + height: 1, + indent: 16, + endIndent: 16, + color: Color(0xAA282828), + ), + ], + ), + ), + ); + } +} + +/// 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, +) { + return FinancialEntityCategoryView( + suffix: const Icon(Icons.chevron_right, color: Colors.grey), + title: model.name, + subtitle: '• • • • • • ${model.accountNumber.substring(6)}', + indicatorColor: RallyColors.accountColor(accountDataIndex), + indicatorFraction: 1, + amount: model.primaryAmount, + ); +} + +FinancialEntityCategoryView buildFinancialEntityFromBillData( + BillData model, + int billDataInex, +) { + return FinancialEntityCategoryView( + suffix: const Icon(Icons.chevron_right, color: Colors.grey), + title: model.name, + subtitle: model.dueDate, + indicatorColor: RallyColors.billColor(billDataInex), + indicatorFraction: 1, + amount: model.primaryAmount, + ); +} + +FinancialEntityCategoryView buildFinancialEntityFromBudgetData( + BudgetData item, + int budgetDataIndex, + BuildContext context, +) { + final String amountUsed = usdWithSignFormat.format(item.amountUsed); + final String primaryAmount = usdWithSignFormat.format(item.primaryAmount); + + return FinancialEntityCategoryView( + suffix: Text( + ' LEFT', + style: Theme.of(context).textTheme.body1.copyWith(color: RallyColors.gray60, fontSize: 10), + ), + title: item.name, + subtitle: amountUsed + ' / ' + primaryAmount, + indicatorColor: RallyColors.budgetColor(budgetDataIndex), + indicatorFraction: item.amountUsed / item.primaryAmount, + amount: item.primaryAmount - item.amountUsed, + ); +} + +List buildAccountDataListViews( + List items) { + return List.generate( + items.length, + (int i) => buildFinancialEntityFromAccountData(items[i], i), + ); +} + +List buildBillDataListViews(List items) { + return List.generate( + items.length, + (int i) => buildFinancialEntityFromBillData(items[i], i), + ); +} + +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 List<_DetailedEventCard> cards = items.map((DetailedEventData detailedEventData) { + return _DetailedEventCard( + title: detailedEventData.title, + subtitle: dateFormat.format(detailedEventData.date), + amount: detailedEventData.amount, + ); + }).toList(); + + return Scaffold( + appBar: AppBar( + elevation: 0, + centerTitle: true, + title: Text( + 'Checking', + style: Theme.of(context).textTheme.body1.copyWith(fontSize: 18), + ), + ), + body: Column( + children: [ + SizedBox( + height: 200, + width: double.infinity, + child: RallyLineChart(events: items), + ), + Flexible( + child: ListView(shrinkWrap: true, children: cards), + ), + ], + ), + ); + } +} + +class _DetailedEventCard extends StatelessWidget { + const _DetailedEventCard({ + @required this.title, + @required this.subtitle, + @required this.amount, + }); + + final String title; + final String subtitle; + final double amount; + + @override + Widget build(BuildContext context) { + final TextTheme textTheme = Theme.of(context).textTheme; + return FlatButton( + onPressed: () {}, + child: SizedBox( + height: 68, + child: Column( + children: [ + SizedBox( + height: 67, + child: Row( + 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), + ) + ], + ), + const Spacer(), + Text( + '\$${usdFormat.format(amount)}', + style: textTheme.body2.copyWith(fontSize: 20, color: RallyColors.gray), + ), + ], + ), + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: SizedBox( + height: 1, + child: Container( + color: const Color(0xAA282828), + ), + ), + ), + ], + ), + ), + ); + } +} diff --git a/examples/flutter_gallery/lib/demo/rally/formatters.dart b/examples/flutter_gallery/lib/demo/rally/formatters.dart new file mode 100644 index 0000000000..4fa8ca3415 --- /dev/null +++ b/examples/flutter_gallery/lib/demo/rally/formatters.dart @@ -0,0 +1,19 @@ +// Copyright 2019-present the Flutter authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'package:intl/intl.dart'; + +NumberFormat usdFormat = NumberFormat.currency(name: ''); +NumberFormat usdWithSignFormat = NumberFormat.currency(name: '\$'); +DateFormat dateFormat = DateFormat('MM-dd-yy'); diff --git a/examples/flutter_gallery/lib/demo/rally/home.dart b/examples/flutter_gallery/lib/demo/rally/home.dart new file mode 100644 index 0000000000..8b64419ea8 --- /dev/null +++ b/examples/flutter_gallery/lib/demo/rally/home.dart @@ -0,0 +1,206 @@ +// Copyright 2019-present the Flutter authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'package:flutter/material.dart'; + +import 'package:flutter_gallery/demo/rally/tabs/accounts.dart'; +import 'package:flutter_gallery/demo/rally/tabs/bills.dart'; +import 'package:flutter_gallery/demo/rally/tabs/budgets.dart'; +import 'package:flutter_gallery/demo/rally/tabs/overview.dart'; +import 'package:flutter_gallery/demo/rally/tabs/settings.dart'; + +const int tabCount = 5; + +class HomePage extends StatefulWidget { + @override + _HomePageState createState() => _HomePageState(); +} + +class _HomePageState extends State + with SingleTickerProviderStateMixin { + + TabController _tabController; + + @override + void initState() { + super.initState(); + _tabController = TabController(length: tabCount, vsync: this) + ..addListener(() { + // Set state to make sure that the [_RallyTab] widgets get updated when changing tabs. + setState(() { }); + }); + } + + @override + void dispose() { + _tabController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final ThemeData theme = Theme.of(context); + + return Scaffold( + body: SafeArea( + child: Column( + children: [ + Theme( + // This theme effectively removes the default visual touch + // feedback for tapping a tab, which is replaced with a custom + // animation. + data: theme.copyWith( + splashColor: Colors.transparent, + highlightColor: Colors.transparent, + ), + child: TabBar( + // Setting isScrollable to true prevents the tabs from being + // wrapped in [Expanded] widgets, which allows for more + // flexible sizes and size animations among tabs. + isScrollable: true, + labelPadding: EdgeInsets.zero, + tabs: _buildTabs(theme), + controller: _tabController, + // This hides the tab indicator. + indicatorColor: Colors.transparent, + ), + ), + Expanded( + child: TabBarView( + controller: _tabController, + children: _buildTabViews(), + ), + ), + ], + ), + ), + ); + } + + List _buildTabs(ThemeData theme) { + return [ + _RallyTab(theme, Icons.pie_chart, 'OVERVIEW', 0, _tabController), + _RallyTab(theme, Icons.attach_money, 'ACCOUNTS', 1, _tabController), + _RallyTab(theme, Icons.money_off, 'BILLS', 2, _tabController), + _RallyTab(theme, Icons.table_chart, 'BUDGETS', 3, _tabController), + _RallyTab(theme, Icons.settings, 'SETTINGS', 4, _tabController), + ]; + } + + List _buildTabViews() { + return [ + OverviewView(), + AccountsView(), + BillsView(), + BudgetsView(), + SettingsView(), + ]; + } +} + +class _RallyTab extends StatefulWidget { + _RallyTab( + ThemeData theme, + IconData iconData, + String title, + int tabIndex, + TabController tabController, + ) : titleText = Text(title, style: theme.textTheme.button), + isExpanded = tabController.index == tabIndex, + icon = Icon(iconData); + + final Text titleText; + final Icon icon; + final bool isExpanded; + + @override + _RallyTabState createState() => _RallyTabState(); +} + +class _RallyTabState extends State<_RallyTab> + with SingleTickerProviderStateMixin { + Animation _titleSizeAnimation; + Animation _titleFadeAnimation; + Animation _iconFadeAnimation; + AnimationController _controller; + + @override + void initState() { + super.initState(); + _controller = AnimationController( + duration: const Duration(milliseconds: 200), + vsync: this, + ); + _titleSizeAnimation = _controller.view; + _titleFadeAnimation = _controller.drive(CurveTween(curve: Curves.easeOut)); + _iconFadeAnimation = _controller.drive(Tween(begin: 0.6, end: 1)); + if (widget.isExpanded) { + _controller.value = 1; + } + } + + @override + void didUpdateWidget(_RallyTab oldWidget) { + super.didUpdateWidget(oldWidget); + if (widget.isExpanded) { + _controller.forward(); + } else { + _controller.reverse(); + } + } + + @override + Widget build(BuildContext context) { + // Calculate the width of each unexpanded tab by counting the number of + // units and dividing it into the screen width. Each unexpanded tab is 1 + // unit, and there is always 1 expanded tab which is 1 unit + any extra + // space determined by the multiplier. + final double width = MediaQuery.of(context).size.width; + const double expandedTitleWidthMultiplier = 2; + final double unitWidth = width / (tabCount + expandedTitleWidthMultiplier); + + return SizedBox( + height: 56, + child: Row( + children: [ + FadeTransition( + child: SizedBox( + width: unitWidth, + child: widget.icon, + ), + opacity: _iconFadeAnimation, + ), + FadeTransition( + child: SizeTransition( + child: SizedBox( + width: unitWidth * expandedTitleWidthMultiplier, + child: Center(child: widget.titleText), + ), + axis: Axis.horizontal, + axisAlignment: -1, + sizeFactor: _titleSizeAnimation, + ), + opacity: _titleFadeAnimation, + ), + ], + ), + ); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } +} diff --git a/examples/flutter_gallery/lib/demo/rally/login.dart b/examples/flutter_gallery/lib/demo/rally/login.dart new file mode 100644 index 0000000000..c4401e9c64 --- /dev/null +++ b/examples/flutter_gallery/lib/demo/rally/login.dart @@ -0,0 +1,91 @@ +// Copyright 2019-present the Flutter authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'package:flutter/material.dart'; + +class LoginPage extends StatefulWidget { + @override + _LoginPageState createState() => _LoginPageState(); +} + +class _LoginPageState extends State { + final TextEditingController _usernameController = TextEditingController(); + final TextEditingController _passwordController = TextEditingController(); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + elevation: 0, + leading: IconButton( + icon: const BackButtonIcon(), + tooltip: MaterialLocalizations.of(context).backButtonTooltip, + onPressed: () { + Navigator.of(context, rootNavigator: true).pop(); + }, + ), + ), + body: SafeArea( + child: GestureDetector( + onTap: () { + Navigator.pop(context); + }, + child: ListView( + padding: const EdgeInsets.symmetric(horizontal: 24), + children: [ + Padding( + padding: const EdgeInsets.symmetric(vertical: 64), + child: SizedBox( + height: 160, + child: Image.asset( + 'logo.png', + package: 'rally_assets', + ), + ), + ), + TextField( + controller: _usernameController, + decoration: const InputDecoration( + labelText: 'Username', + ), + ), + const SizedBox(height: 12), + TextField( + controller: _passwordController, + decoration: const InputDecoration( + labelText: 'Password', + ), + obscureText: true, + ), + SizedBox( + height: 120, + child: Image.asset( + 'thumb.png', + package: 'rally_assets', + ), + ), + ], + ), + ), + ), + ); + } + + @override + void dispose() { + _usernameController.dispose(); + _passwordController.dispose(); + super.dispose(); + } +} diff --git a/examples/flutter_gallery/lib/demo/rally/main.dart b/examples/flutter_gallery/lib/demo/rally/main.dart new file mode 100644 index 0000000000..14fd0de0fb --- /dev/null +++ b/examples/flutter_gallery/lib/demo/rally/main.dart @@ -0,0 +1,18 @@ +// Copyright 2019-present the Flutter authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'package:flutter/widgets.dart'; +import 'package:flutter_gallery/demo/rally/app.dart'; + +void main() => runApp(RallyApp()); diff --git a/examples/flutter_gallery/lib/demo/rally/tabs/accounts.dart b/examples/flutter_gallery/lib/demo/rally/tabs/accounts.dart new file mode 100644 index 0000000000..10ac7c44d0 --- /dev/null +++ b/examples/flutter_gallery/lib/demo/rally/tabs/accounts.dart @@ -0,0 +1,36 @@ +// Copyright 2019-present the Flutter authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'package:flutter/widgets.dart'; + +import 'package:flutter_gallery/demo/rally/data.dart'; +import 'package:flutter_gallery/demo/rally/finance.dart'; +import 'package:flutter_gallery/demo/rally/charts/pie_chart.dart'; + +/// A page that shows a summary of accounts. +class AccountsView extends StatelessWidget { + final List items = DummyDataService.getAccountDataList(); + + @override + Widget build(BuildContext context) { + final double balanceTotal = sumAccountDataPrimaryAmount(items); + return FinancialEntityView( + heroLabel: 'Total', + heroAmount: balanceTotal, + segments: buildSegmentsFromAccountItems(items), + wholeAmount: balanceTotal, + financialEntityCards: buildAccountDataListViews(items), + ); + } +} diff --git a/examples/flutter_gallery/lib/demo/rally/tabs/bills.dart b/examples/flutter_gallery/lib/demo/rally/tabs/bills.dart new file mode 100644 index 0000000000..2d1053a9ac --- /dev/null +++ b/examples/flutter_gallery/lib/demo/rally/tabs/bills.dart @@ -0,0 +1,42 @@ +// Copyright 2019-present the Flutter authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + + +import 'package:flutter/widgets.dart'; +import 'package:flutter_gallery/demo/rally/data.dart'; +import 'package:flutter_gallery/demo/rally/finance.dart'; +import 'package:flutter_gallery/demo/rally/charts/pie_chart.dart'; + +/// A page that shows a summary of bills. +class BillsView extends StatefulWidget { + @override + _BillsViewState createState() => _BillsViewState(); +} + +class _BillsViewState extends State + with SingleTickerProviderStateMixin { + final List items = DummyDataService.getBillDataList(); + + @override + Widget build(BuildContext context) { + final double dueTotal = sumBillDataPrimaryAmount(items); + return FinancialEntityView( + heroLabel: 'Due', + heroAmount: dueTotal, + segments: buildSegmentsFromBillItems(items), + wholeAmount: dueTotal, + financialEntityCards: buildBillDataListViews(items), + ); + } +} diff --git a/examples/flutter_gallery/lib/demo/rally/tabs/budgets.dart b/examples/flutter_gallery/lib/demo/rally/tabs/budgets.dart new file mode 100644 index 0000000000..f22315c667 --- /dev/null +++ b/examples/flutter_gallery/lib/demo/rally/tabs/budgets.dart @@ -0,0 +1,42 @@ +// Copyright 2019-present the Flutter authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'package:flutter/widgets.dart'; + +import 'package:flutter_gallery/demo/rally/charts/pie_chart.dart'; +import 'package:flutter_gallery/demo/rally/data.dart'; +import 'package:flutter_gallery/demo/rally/finance.dart'; + +class BudgetsView extends StatefulWidget { + @override + _BudgetsViewState createState() => _BudgetsViewState(); +} + +class _BudgetsViewState extends State + with SingleTickerProviderStateMixin { + final List items = DummyDataService.getBudgetDataList(); + + @override + Widget build(BuildContext context) { + final double capTotal = sumBudgetDataPrimaryAmount(items); + final double usedTotal = sumBudgetDataAmountUsed(items); + return FinancialEntityView( + heroLabel: 'Left', + heroAmount: capTotal - usedTotal, + segments: buildSegmentsFromBudgetItems(items), + wholeAmount: capTotal, + financialEntityCards: buildBudgetDataListViews(items, context), + ); + } +} diff --git a/examples/flutter_gallery/lib/demo/rally/tabs/overview.dart b/examples/flutter_gallery/lib/demo/rally/tabs/overview.dart new file mode 100644 index 0000000000..7d8f1f3a85 --- /dev/null +++ b/examples/flutter_gallery/lib/demo/rally/tabs/overview.dart @@ -0,0 +1,151 @@ +// Copyright 2019-present the Flutter authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'dart:math'; + +import 'package:flutter/material.dart'; + +import 'package:flutter_gallery/demo/rally/colors.dart'; +import 'package:flutter_gallery/demo/rally/data.dart'; +import 'package:flutter_gallery/demo/rally/finance.dart'; +import 'package:flutter_gallery/demo/rally/formatters.dart'; + +/// A page that shows a status overview. +class OverviewView extends StatefulWidget { + @override + _OverviewViewState createState() => _OverviewViewState(); +} + +class _OverviewViewState extends State { + @override + Widget build(BuildContext context) { + final List accountDataList = DummyDataService.getAccountDataList(); + final List billDataList = DummyDataService.getBillDataList(); + final List budgetDataList = DummyDataService.getBudgetDataList(); + + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + child: ListView( + children: [ + _AlertsView(), + const SizedBox(height: 16), + _FinancialView( + title: 'Accounts', + total: sumAccountDataPrimaryAmount(accountDataList), + financialItemViews: buildAccountDataListViews(accountDataList), + ), + const SizedBox(height: 16), + _FinancialView( + title: 'Bills', + total: sumBillDataPrimaryAmount(billDataList), + financialItemViews: buildBillDataListViews(billDataList), + ), + const SizedBox(height: 16), + _FinancialView( + title: 'Budgets', + total: sumBudgetDataPrimaryAmount(budgetDataList), + financialItemViews: + buildBudgetDataListViews(budgetDataList, context), + ), + const SizedBox(height: 16), + ], + ), + ); + } +} + +class _AlertsView extends StatelessWidget { + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.only(left: 16, top: 4, bottom: 4), + color: RallyColors.cardBackground, + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text('Alerts'), + FlatButton( + onPressed: () {}, + child: const Text('SEE ALL'), + textColor: Colors.white, + ), + ], + ), + Container(color: RallyColors.primaryBackground, height: 1), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Expanded( + child: Text('Heads up, you’ve used up 90% of your Shopping budget for this month.'), + ), + SizedBox( + width: 100, + child: Align( + alignment: Alignment.topRight, + child: IconButton( + onPressed: () {}, + icon: Icon(Icons.sort, color: RallyColors.white60), + ), + ), + ), + ], + ), + ], + ), + ); + } +} + +class _FinancialView extends StatelessWidget { + const _FinancialView({this.title, this.total, this.financialItemViews}); + + final String title; + final double total; + final List financialItemViews; + + @override + Widget build(BuildContext context) { + final ThemeData theme = Theme.of(context); + return Container( + color: RallyColors.cardBackground, + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Padding( + padding: const EdgeInsets.all(16), + child: Text(title), + ), + Padding( + padding: const EdgeInsets.only(left: 16, right: 16), + child: Text( + usdWithSignFormat.format(total), + style: theme.textTheme.body2.copyWith( + fontSize: 44, + fontWeight: FontWeight.w600, + ), + ), + ), + ...financialItemViews.sublist(0, min(financialItemViews.length, 3)), + FlatButton( + child: const Text('SEE ALL'), + textColor: Colors.white, + onPressed: () {}, + ), + ], + ), + ); + } +} diff --git a/examples/flutter_gallery/lib/demo/rally/tabs/settings.dart b/examples/flutter_gallery/lib/demo/rally/tabs/settings.dart new file mode 100644 index 0000000000..4883f87da4 --- /dev/null +++ b/examples/flutter_gallery/lib/demo/rally/tabs/settings.dart @@ -0,0 +1,59 @@ +// Copyright 2019-present the Flutter authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'package:flutter/material.dart'; + +import 'package:flutter_gallery/demo/rally/data.dart'; +import 'package:flutter_gallery/demo/rally/login.dart'; + +class SettingsView extends StatefulWidget { + @override + _SettingsViewState createState() => _SettingsViewState(); +} + +class _SettingsViewState extends State { + List items = DummyDataService.getSettingsTitles() + .map((String title) => _SettingsItem(title)) + .toList(); + + @override + Widget build(BuildContext context) { + return ListView(children: items); + } +} + +class _SettingsItem extends StatelessWidget { + const _SettingsItem(this.title); + + final String title; + + @override + Widget build(BuildContext context) { + return FlatButton( + textColor: Colors.white, + child: SizedBox( + height: 60, + child: Row(children: [ + Text(title), + ]), + ), + onPressed: () { + Navigator.push( + context, + MaterialPageRoute(builder: (BuildContext context) => LoginPage()), + ); + }, + ); + } +} diff --git a/examples/flutter_gallery/lib/demo/rally_demo.dart b/examples/flutter_gallery/lib/demo/rally_demo.dart new file mode 100644 index 0000000000..6ff0df23ed --- /dev/null +++ b/examples/flutter_gallery/lib/demo/rally_demo.dart @@ -0,0 +1,15 @@ +// Copyright 2019 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/material.dart'; +import 'package:flutter_gallery/demo/rally/app.dart'; + +class RallyDemo extends StatelessWidget { + const RallyDemo({ Key key }) : super(key: key); + + static const String routeName = '/rally'; // Used by the Gallery app. + + @override + Widget build(BuildContext context) => RallyApp(); +} diff --git a/examples/flutter_gallery/lib/gallery/demos.dart b/examples/flutter_gallery/lib/gallery/demos.dart index a212305b26..05c0ee1928 100644 --- a/examples/flutter_gallery/lib/gallery/demos.dart +++ b/examples/flutter_gallery/lib/gallery/demos.dart @@ -131,6 +131,14 @@ List _buildGalleryDemos() { routeName: TransformationsDemo.routeName, buildRoute: (BuildContext context) => const TransformationsDemo(), ), + GalleryDemo( + title: 'Rally', + subtitle: 'A personal finance app', + icon: GalleryIcons.data_table, + category: _kDemos, + routeName: RallyDemo.routeName, + buildRoute: (BuildContext context) => const RallyDemo(), + ), // Style GalleryDemo( diff --git a/examples/flutter_gallery/pubspec.yaml b/examples/flutter_gallery/pubspec.yaml index 4d150c326b..bee1c8bfb1 100644 --- a/examples/flutter_gallery/pubspec.yaml +++ b/examples/flutter_gallery/pubspec.yaml @@ -17,6 +17,7 @@ dependencies: video_player: 0.10.2+5 scoped_model: 1.0.1 shrine_images: 1.1.2 + rally_assets: 1.0.0 # Also update dev/benchmarks/complex_layout/pubspec.yaml flutter_gallery_assets: 0.1.9+2 @@ -199,6 +200,8 @@ flutter: - packages/shrine_images/35-0.jpg - packages/shrine_images/36-0.jpg - packages/shrine_images/37-0.jpg + - packages/rally_assets/logo.png + - packages/rally_assets/thumb.png fonts: - family: Raleway @@ -260,5 +263,21 @@ flutter: - asset: packages/flutter_gallery_assets/fonts/merriweather/Merriweather-Italic.ttf - asset: packages/flutter_gallery_assets/fonts/merriweather/Merriweather-Regular.ttf - asset: packages/flutter_gallery_assets/fonts/merriweather/Merriweather-Light.ttf + - family: Roboto Condensed + fonts: + - asset: packages/rally_assets/RobotoCondensed-Light.ttf + weight: 400 + - asset: packages/rally_assets/RobotoCondensed-Regular.ttf + weight: 500 + - asset: packages/rally_assets/RobotoCondensed-Bold.ttf + weight: 700 + - family: Eczar + fonts: + - asset: packages/rally_assets/Eczar-Regular.ttf + weight: 400 + - asset: packages/rally_assets/Eczar-SemiBold.ttf + weight: 600 + - asset: packages/rally_assets/Eczar-Bold.ttf + weight: 700 -# PUBSPEC CHECKSUM: 8b43 +# PUBSPEC CHECKSUM: 55a6