mirror of
https://github.com/flutter/samples.git
synced 2025-11-12 07:48:55 +00:00
[Gallery] Fix directory structure (#312)
This commit is contained in:
95
gallery/lib/studies/rally/app.dart
Normal file
95
gallery/lib/studies/rally/app.dart
Normal file
@@ -0,0 +1,95 @@
|
||||
// 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 'package:flutter/material.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
import 'package:gallery/data/gallery_options.dart';
|
||||
import 'package:gallery/l10n/gallery_localizations.dart';
|
||||
|
||||
import 'package:gallery/studies/rally/colors.dart';
|
||||
import 'package:gallery/studies/rally/home.dart';
|
||||
import 'package:gallery/studies/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 {
|
||||
const RallyApp({Key key, this.navigatorKey}) : super(key: key);
|
||||
|
||||
final GlobalKey<NavigatorState> navigatorKey;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MaterialApp(
|
||||
navigatorKey: navigatorKey,
|
||||
title: 'Rally',
|
||||
debugShowCheckedModeBanner: false,
|
||||
theme: _buildRallyTheme().copyWith(
|
||||
platform: GalleryOptions.of(context).platform,
|
||||
),
|
||||
localizationsDelegates: GalleryLocalizations.localizationsDelegates,
|
||||
supportedLocales: GalleryLocalizations.supportedLocales,
|
||||
locale: GalleryOptions.of(context).locale,
|
||||
home: HomePage(),
|
||||
initialRoute: '/login',
|
||||
routes: <String, WidgetBuilder>{
|
||||
'/login': (context) => LoginPage(),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
ThemeData _buildRallyTheme() {
|
||||
final base = ThemeData.dark();
|
||||
return ThemeData(
|
||||
scaffoldBackgroundColor: RallyColors.primaryBackground,
|
||||
primaryColor: RallyColors.primaryBackground,
|
||||
focusColor: RallyColors.focusColor,
|
||||
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(
|
||||
// TODO: Use GoogleFonts.robotoCondensed when available
|
||||
body1: base.body1.copyWith(
|
||||
fontFamily: 'Roboto Condensed',
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w400,
|
||||
),
|
||||
body2: GoogleFonts.eczar(
|
||||
fontSize: 40,
|
||||
fontWeight: FontWeight.w400,
|
||||
letterSpacing: 1.4,
|
||||
textStyle: base.body2,
|
||||
),
|
||||
// TODO: Use GoogleFonts.robotoCondensed when available
|
||||
button: base.button.copyWith(
|
||||
fontFamily: 'Roboto Condensed',
|
||||
fontWeight: FontWeight.w700,
|
||||
letterSpacing: 2.8,
|
||||
),
|
||||
headline: GoogleFonts.eczar(
|
||||
fontSize: 40,
|
||||
fontWeight: FontWeight.w600,
|
||||
letterSpacing: 1.4,
|
||||
textStyle: base.body2,
|
||||
),
|
||||
)
|
||||
.apply(
|
||||
displayColor: Colors.white,
|
||||
bodyColor: Colors.white,
|
||||
);
|
||||
}
|
||||
}
|
||||
287
gallery/lib/studies/rally/charts/line_chart.dart
Normal file
287
gallery/lib/studies/rally/charts/line_chart.dart
Normal file
@@ -0,0 +1,287 @@
|
||||
// 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 'package:intl/intl.dart' as intl;
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/semantics.dart';
|
||||
|
||||
import 'package:gallery/data/gallery_options.dart';
|
||||
import 'package:gallery/layout/text_scale.dart';
|
||||
import 'package:gallery/studies/rally/colors.dart';
|
||||
import 'package:gallery/studies/rally/data.dart';
|
||||
import 'package:gallery/studies/rally/formatters.dart';
|
||||
|
||||
class RallyLineChart extends StatelessWidget {
|
||||
const RallyLineChart({this.events = const <DetailedEventData>[]})
|
||||
: assert(events != null);
|
||||
|
||||
final List<DetailedEventData> events;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return CustomPaint(
|
||||
painter: RallyLineChartPainter(
|
||||
dateFormat: dateFormatMonthYear(context),
|
||||
numberFormat: usdWithSignFormat(context),
|
||||
events: events,
|
||||
labelStyle: Theme.of(context).textTheme.body1,
|
||||
textDirection: GalleryOptions.of(context).textDirection(),
|
||||
textScaleFactor: reducedTextScale(context),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class RallyLineChartPainter extends CustomPainter {
|
||||
RallyLineChartPainter({
|
||||
@required this.dateFormat,
|
||||
@required this.numberFormat,
|
||||
@required this.events,
|
||||
@required this.labelStyle,
|
||||
@required this.textDirection,
|
||||
@required this.textScaleFactor,
|
||||
});
|
||||
|
||||
// The style for the labels.
|
||||
final TextStyle labelStyle;
|
||||
|
||||
// The text direction for the text.
|
||||
final TextDirection textDirection;
|
||||
|
||||
// The text scale factor for the text.
|
||||
final double textScaleFactor;
|
||||
|
||||
// The format for the dates.
|
||||
final intl.DateFormat dateFormat;
|
||||
|
||||
// The currency format.
|
||||
final intl.NumberFormat numberFormat;
|
||||
|
||||
// Events to plot on the line as points.
|
||||
final List<DetailedEventData> 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 = 2000; // 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 labelHeight = space + space * (textScaleFactor - 1);
|
||||
final ticksHeight = 3 * space;
|
||||
final ticksTop = size.height - labelHeight - ticksHeight - space;
|
||||
final labelsTop = size.height - labelHeight;
|
||||
_drawLine(
|
||||
canvas,
|
||||
Rect.fromLTWH(0, 0, size.width, size.height - labelHeight - ticksHeight),
|
||||
);
|
||||
_drawXAxisTicks(
|
||||
canvas,
|
||||
Rect.fromLTWH(0, ticksTop, size.width, ticksHeight),
|
||||
);
|
||||
_drawXAxisLabels(
|
||||
canvas,
|
||||
Rect.fromLTWH(0, labelsTop, size.width, labelHeight),
|
||||
);
|
||||
}
|
||||
|
||||
// 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;
|
||||
|
||||
@override
|
||||
SemanticsBuilderCallback get semanticsBuilder {
|
||||
return (size) {
|
||||
final amounts = _amountsPerDay(numDays);
|
||||
|
||||
// We divide the graph and the amounts into [numGroups] groups, with
|
||||
// [numItemsPerGroup] amounts per group.
|
||||
final numGroups = 10;
|
||||
final numItemsPerGroup = amounts.length ~/ numGroups;
|
||||
|
||||
// For each group we calculate the median value.
|
||||
final medians = List.generate(
|
||||
numGroups,
|
||||
(i) {
|
||||
final middleIndex = i * numItemsPerGroup + numItemsPerGroup ~/ 2;
|
||||
if (numItemsPerGroup.isEven) {
|
||||
return (amounts[middleIndex] + amounts[middleIndex + 1]) / 2;
|
||||
} else {
|
||||
return amounts[middleIndex];
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
// Return a list of [CustomPainterSemantics] with the length of
|
||||
// [numGroups], all have the same width with the median amount as label.
|
||||
return List.generate(numGroups, (i) {
|
||||
return CustomPainterSemantics(
|
||||
rect: Offset((i / numGroups) * size.width, 0) &
|
||||
Size(size.width / numGroups, size.height),
|
||||
properties: SemanticsProperties(
|
||||
label: numberFormat.format(medians[i]),
|
||||
textDirection: textDirection,
|
||||
),
|
||||
);
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
/// Returns the amount of money in the account for the [numDays] given
|
||||
/// from the [startDate].
|
||||
List<double> _amountsPerDay(int numDays) {
|
||||
// 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 = 600;
|
||||
|
||||
// Align the points with equal deltas (1 day) as a cumulative sum.
|
||||
int startMillis = startDate.millisecondsSinceEpoch;
|
||||
|
||||
final amounts = <double>[];
|
||||
for (var i = 0; i < numDays; i++) {
|
||||
final endMillis = startMillis + millisInDay * 1;
|
||||
final filteredEvents = events.where(
|
||||
(e) {
|
||||
return startMillis <= e.date.millisecondsSinceEpoch &&
|
||||
e.date.millisecondsSinceEpoch < endMillis;
|
||||
},
|
||||
).toList();
|
||||
lastAmount += sumOf<DetailedEventData>(filteredEvents, (e) => e.amount);
|
||||
amounts.add(lastAmount);
|
||||
startMillis = endMillis;
|
||||
}
|
||||
return amounts;
|
||||
}
|
||||
|
||||
void _drawLine(Canvas canvas, Rect rect) {
|
||||
final Paint linePaint = Paint()
|
||||
..color = RallyColors.accountColor(2)
|
||||
..style = PaintingStyle.stroke
|
||||
..strokeWidth = 2;
|
||||
|
||||
// Try changing this value between 1, 7, 15, etc.
|
||||
const smoothing = 1;
|
||||
|
||||
final amounts = _amountsPerDay(numDays + smoothing);
|
||||
final points = <Offset>[];
|
||||
for (int i = 0; i < amounts.length; i++) {
|
||||
final x = i / numDays * rect.width;
|
||||
final y = (maxAmount - amounts[i]) / maxAmount * rect.height;
|
||||
points.add(Offset(x, y));
|
||||
}
|
||||
|
||||
// Add last point of the graph to make sure we take up the full width.
|
||||
points.add(
|
||||
Offset(
|
||||
rect.width,
|
||||
(maxAmount - amounts[numDays - 1]) / maxAmount * rect.height,
|
||||
),
|
||||
);
|
||||
|
||||
final path = Path();
|
||||
path.moveTo(points[0].dx, points[0].dy);
|
||||
for (int i = 1; i < numDays - smoothing + 2; i += smoothing) {
|
||||
final x1 = points[i].dx;
|
||||
final y1 = points[i].dy;
|
||||
final x2 = (x1 + points[i + smoothing].dx) / 2;
|
||||
final 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 selectedLabelStyle = labelStyle.copyWith(
|
||||
fontWeight: FontWeight.w700,
|
||||
fontSize: labelStyle.fontSize * textScaleFactor,
|
||||
);
|
||||
final unselectedLabelStyle = labelStyle.copyWith(
|
||||
fontWeight: FontWeight.w700,
|
||||
color: RallyColors.gray25,
|
||||
fontSize: labelStyle.fontSize * textScaleFactor,
|
||||
);
|
||||
|
||||
// We use toUpperCase to format the dates. This function uses the language
|
||||
// independent Unicode mapping and thus only works in some languages.
|
||||
final leftLabel = TextPainter(
|
||||
text: TextSpan(
|
||||
text: dateFormat.format(startDate).toUpperCase(),
|
||||
style: unselectedLabelStyle,
|
||||
),
|
||||
textDirection: textDirection,
|
||||
);
|
||||
leftLabel.layout();
|
||||
leftLabel.paint(canvas, Offset(rect.left + space / 2, rect.topCenter.dy));
|
||||
|
||||
final centerLabel = TextPainter(
|
||||
text: TextSpan(
|
||||
text: dateFormat
|
||||
.format(DateTime(startDate.year, startDate.month + 1))
|
||||
.toUpperCase(),
|
||||
style: selectedLabelStyle,
|
||||
),
|
||||
textDirection: textDirection,
|
||||
);
|
||||
centerLabel.layout();
|
||||
final x = (rect.width - centerLabel.width) / 2;
|
||||
final y = rect.topCenter.dy;
|
||||
centerLabel.paint(canvas, Offset(x, y));
|
||||
|
||||
final rightLabel = TextPainter(
|
||||
text: TextSpan(
|
||||
text: dateFormat
|
||||
.format(DateTime(startDate.year, startDate.month + 2))
|
||||
.toUpperCase(),
|
||||
style: unselectedLabelStyle,
|
||||
),
|
||||
textDirection: textDirection,
|
||||
);
|
||||
rightLabel.layout();
|
||||
rightLabel.paint(
|
||||
canvas,
|
||||
Offset(rect.right - centerLabel.width - space / 2, rect.topCenter.dy),
|
||||
);
|
||||
}
|
||||
}
|
||||
278
gallery/lib/studies/rally/charts/pie_chart.dart
Normal file
278
gallery/lib/studies/rally/charts/pie_chart.dart
Normal file
@@ -0,0 +1,278 @@
|
||||
// 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:gallery/data/gallery_options.dart';
|
||||
import 'package:gallery/layout/text_scale.dart';
|
||||
import 'package:gallery/studies/rally/colors.dart';
|
||||
import 'package:gallery/studies/rally/data.dart';
|
||||
import 'package:gallery/studies/rally/formatters.dart';
|
||||
|
||||
/// A colored piece of the [RallyPieChart].
|
||||
class RallyPieChartSegment {
|
||||
const RallyPieChartSegment({this.color, this.value});
|
||||
|
||||
final Color color;
|
||||
final double value;
|
||||
}
|
||||
|
||||
/// The max height and width of the [RallyPieChart].
|
||||
const pieChartMaxSize = 500.0;
|
||||
|
||||
List<RallyPieChartSegment> buildSegmentsFromAccountItems(
|
||||
List<AccountData> items) {
|
||||
return List<RallyPieChartSegment>.generate(
|
||||
items.length,
|
||||
(i) {
|
||||
return RallyPieChartSegment(
|
||||
color: RallyColors.accountColor(i),
|
||||
value: items[i].primaryAmount,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
List<RallyPieChartSegment> buildSegmentsFromBillItems(List<BillData> items) {
|
||||
return List<RallyPieChartSegment>.generate(
|
||||
items.length,
|
||||
(i) {
|
||||
return RallyPieChartSegment(
|
||||
color: RallyColors.billColor(i),
|
||||
value: items[i].primaryAmount,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
List<RallyPieChartSegment> buildSegmentsFromBudgetItems(
|
||||
List<BudgetData> items) {
|
||||
return List<RallyPieChartSegment>.generate(
|
||||
items.length,
|
||||
(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<RallyPieChartSegment> segments;
|
||||
|
||||
@override
|
||||
_RallyPieChartState createState() => _RallyPieChartState();
|
||||
}
|
||||
|
||||
class _RallyPieChartState extends State<RallyPieChart>
|
||||
with SingleTickerProviderStateMixin {
|
||||
AnimationController controller;
|
||||
Animation<double> animation;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
controller = AnimationController(
|
||||
duration: const Duration(milliseconds: 600),
|
||||
vsync: this,
|
||||
);
|
||||
animation = CurvedAnimation(
|
||||
parent: TweenSequence<double>(<TweenSequenceItem<double>>[
|
||||
TweenSequenceItem<double>(
|
||||
tween: Tween<double>(begin: 0, end: 0),
|
||||
weight: 1,
|
||||
),
|
||||
TweenSequenceItem<double>(
|
||||
tween: Tween<double>(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 MergeSemantics(
|
||||
child: _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<double> animation;
|
||||
final String centerLabel;
|
||||
final double centerAmount;
|
||||
final double total;
|
||||
final List<RallyPieChartSegment> segments;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final textTheme = Theme.of(context).textTheme;
|
||||
final labelTextStyle = textTheme.body1.copyWith(
|
||||
fontSize: 14,
|
||||
letterSpacing: 0.5,
|
||||
);
|
||||
|
||||
return LayoutBuilder(builder: (context, constraints) {
|
||||
// When the widget is larger, we increase the font size.
|
||||
TextStyle headlineStyle = constraints.maxHeight >= pieChartMaxSize
|
||||
? textTheme.headline.copyWith(fontSize: 70)
|
||||
: textTheme.headline;
|
||||
|
||||
// With a large text scale factor, we set a max font size.
|
||||
if (GalleryOptions.of(context).textScaleFactor(context) > 1.0) {
|
||||
headlineStyle = headlineStyle.copyWith(
|
||||
fontSize: (headlineStyle.fontSize / reducedTextScale(context)),
|
||||
);
|
||||
}
|
||||
|
||||
return DecoratedBox(
|
||||
decoration: _RallyPieChartOutlineDecoration(
|
||||
maxFraction: animation.value,
|
||||
total: total,
|
||||
segments: segments,
|
||||
),
|
||||
child: Container(
|
||||
height: constraints.maxHeight,
|
||||
alignment: Alignment.center,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
centerLabel,
|
||||
style: labelTextStyle,
|
||||
),
|
||||
Text(
|
||||
usdWithSignFormat(context).format(centerAmount),
|
||||
style: headlineStyle,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class _RallyPieChartOutlineDecoration extends Decoration {
|
||||
const _RallyPieChartOutlineDecoration(
|
||||
{this.maxFraction, this.total, this.segments});
|
||||
|
||||
final double maxFraction;
|
||||
final double total;
|
||||
final List<RallyPieChartSegment> 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<RallyPieChartSegment> segments;
|
||||
static const double wholeRadians = 2 * math.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 strokeWidth = 4.0;
|
||||
final outerRadius = math.min(
|
||||
configuration.size.width,
|
||||
configuration.size.height,
|
||||
) /
|
||||
2;
|
||||
final outerRect = Rect.fromCircle(
|
||||
center: configuration.size.center(offset),
|
||||
radius: outerRadius - strokeWidth * 3,
|
||||
);
|
||||
final innerRect = Rect.fromCircle(
|
||||
center: configuration.size.center(offset),
|
||||
radius: outerRadius - strokeWidth * 4,
|
||||
);
|
||||
|
||||
// Paint each arc with spacing.
|
||||
double cumulativeSpace = 0;
|
||||
double cumulativeTotal = 0;
|
||||
for (RallyPieChartSegment segment in segments) {
|
||||
final paint = Paint()..color = segment.color;
|
||||
final startAngle = _calculateStartAngle(cumulativeTotal, cumulativeSpace);
|
||||
final 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 remaining = wholeAmount - cumulativeTotal;
|
||||
if (remaining > 0) {
|
||||
final paint = Paint()..color = Colors.black;
|
||||
final startAngle =
|
||||
_calculateStartAngle(cumulativeTotal, spaceRadians * segments.length);
|
||||
final 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 bgPaint = Paint()..color = RallyColors.primaryBackground;
|
||||
canvas.drawArc(innerRect, 0, 2 * math.pi, true, bgPaint);
|
||||
}
|
||||
|
||||
double _calculateAngle(double amount, double offset) {
|
||||
final wholeMinusSpacesRadians =
|
||||
wholeRadians - (segments.length * spaceRadians);
|
||||
return maxFraction *
|
||||
(amount / wholeAmount * wholeMinusSpacesRadians + offset);
|
||||
}
|
||||
|
||||
double _calculateStartAngle(double total, double offset) =>
|
||||
_calculateAngle(total, offset) - math.pi / 2;
|
||||
|
||||
double _calculateSweepAngle(double total, double offset) =>
|
||||
_calculateAngle(total, offset);
|
||||
}
|
||||
36
gallery/lib/studies/rally/charts/vertical_fraction_bar.dart
Normal file
36
gallery/lib/studies/rally/charts/vertical_fraction_bar.dart
Normal file
@@ -0,0 +1,36 @@
|
||||
// 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 '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 LayoutBuilder(builder: (context, constraints) {
|
||||
return SizedBox(
|
||||
height: constraints.maxHeight,
|
||||
width: 4,
|
||||
child: Column(
|
||||
children: [
|
||||
SizedBox(
|
||||
height: (1 - fraction) * constraints.maxHeight,
|
||||
child: Container(
|
||||
color: Colors.black,
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
height: fraction * constraints.maxHeight,
|
||||
child: Container(color: color),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
61
gallery/lib/studies/rally/colors.dart
Normal file
61
gallery/lib/studies/rally/colors.dart
Normal file
@@ -0,0 +1,61 @@
|
||||
// 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: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<Color> accountColors = <Color>[
|
||||
Color(0xFF005D57),
|
||||
Color(0xFF04B97F),
|
||||
Color(0xFF37EFBA),
|
||||
Color(0xFF007D51),
|
||||
];
|
||||
|
||||
static const List<Color> billColors = <Color>[
|
||||
Color(0xFFFFDC78),
|
||||
Color(0xFFFF6951),
|
||||
Color(0xFFFFD7D0),
|
||||
Color(0xFFFFAC12),
|
||||
];
|
||||
|
||||
static const List<Color> budgetColors = <Color>[
|
||||
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);
|
||||
static const Color buttonColor = Color(0xFF09AF79);
|
||||
static const Color focusColor = Color(0xCCFFFFFF);
|
||||
|
||||
/// 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<Color> colors, int i) {
|
||||
return colors[i % colors.length];
|
||||
}
|
||||
}
|
||||
323
gallery/lib/studies/rally/data.dart
Normal file
323
gallery/lib/studies/rally/data.dart
Normal file
@@ -0,0 +1,323 @@
|
||||
// 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 'package:flutter/material.dart';
|
||||
|
||||
import 'package:gallery/l10n/gallery_localizations.dart';
|
||||
import 'package:gallery/studies/rally/formatters.dart';
|
||||
|
||||
/// Calculates the sum of the primary amounts of a list of [AccountData].
|
||||
double sumAccountDataPrimaryAmount(List<AccountData> items) =>
|
||||
sumOf<AccountData>(items, (item) => item.primaryAmount);
|
||||
|
||||
/// Calculates the sum of the primary amounts of a list of [BillData].
|
||||
double sumBillDataPrimaryAmount(List<BillData> items) =>
|
||||
sumOf<BillData>(items, (item) => item.primaryAmount);
|
||||
|
||||
/// Calculates the sum of the primary amounts of a list of [BudgetData].
|
||||
double sumBudgetDataPrimaryAmount(List<BudgetData> items) =>
|
||||
sumOf<BudgetData>(items, (item) => item.primaryAmount);
|
||||
|
||||
/// Calculates the sum of the amounts used of a list of [BudgetData].
|
||||
double sumBudgetDataAmountUsed(List<BudgetData> items) =>
|
||||
sumOf<BudgetData>(items, (item) => item.amountUsed);
|
||||
|
||||
/// Utility function to sum up values in a list.
|
||||
double sumOf<T>(List<T> 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;
|
||||
}
|
||||
|
||||
/// A data model for an alert.
|
||||
class AlertData {
|
||||
AlertData({this.message, this.iconData});
|
||||
|
||||
/// The alert message to display.
|
||||
final String message;
|
||||
|
||||
/// The icon to display with the alert.
|
||||
final IconData iconData;
|
||||
}
|
||||
|
||||
class DetailedEventData {
|
||||
const DetailedEventData({
|
||||
this.title,
|
||||
this.date,
|
||||
this.amount,
|
||||
});
|
||||
|
||||
final String title;
|
||||
final DateTime date;
|
||||
final double amount;
|
||||
}
|
||||
|
||||
/// A data model for account data.
|
||||
class AccountDetailData {
|
||||
AccountDetailData({this.title, this.value});
|
||||
|
||||
/// The display name of this entity.
|
||||
final String title;
|
||||
|
||||
/// The value of this entity.
|
||||
final String value;
|
||||
}
|
||||
|
||||
/// Class to return dummy data lists.
|
||||
///
|
||||
/// In a real app, this might be replaced with some asynchronous service.
|
||||
class DummyDataService {
|
||||
static List<AccountData> getAccountDataList(BuildContext context) {
|
||||
return <AccountData>[
|
||||
AccountData(
|
||||
name: GalleryLocalizations.of(context).rallyAccountDataChecking,
|
||||
primaryAmount: 2215.13,
|
||||
accountNumber: '1234561234',
|
||||
),
|
||||
AccountData(
|
||||
name: GalleryLocalizations.of(context).rallyAccountDataHomeSavings,
|
||||
primaryAmount: 8678.88,
|
||||
accountNumber: '8888885678',
|
||||
),
|
||||
AccountData(
|
||||
name: GalleryLocalizations.of(context).rallyAccountDataCarSavings,
|
||||
primaryAmount: 987.48,
|
||||
accountNumber: '8888889012',
|
||||
),
|
||||
AccountData(
|
||||
name: GalleryLocalizations.of(context).rallyAccountDataVacation,
|
||||
primaryAmount: 253,
|
||||
accountNumber: '1231233456',
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
static List<AccountDetailData> getAccountDetailList(BuildContext context) {
|
||||
return <AccountDetailData>[
|
||||
AccountDetailData(
|
||||
title: GalleryLocalizations.of(context)
|
||||
.rallyAccountDetailDataAnnualPercentageYield,
|
||||
value: percentFormat(context).format(0.001),
|
||||
),
|
||||
AccountDetailData(
|
||||
title:
|
||||
GalleryLocalizations.of(context).rallyAccountDetailDataInterestRate,
|
||||
value: usdWithSignFormat(context).format(1676.14),
|
||||
),
|
||||
AccountDetailData(
|
||||
title:
|
||||
GalleryLocalizations.of(context).rallyAccountDetailDataInterestYtd,
|
||||
value: usdWithSignFormat(context).format(81.45),
|
||||
),
|
||||
AccountDetailData(
|
||||
title: GalleryLocalizations.of(context)
|
||||
.rallyAccountDetailDataInterestPaidLastYear,
|
||||
value: usdWithSignFormat(context).format(987.12),
|
||||
),
|
||||
AccountDetailData(
|
||||
title: GalleryLocalizations.of(context)
|
||||
.rallyAccountDetailDataNextStatement,
|
||||
value: shortDateFormat(context).format(DateTime.utc(2019, 12, 25)),
|
||||
),
|
||||
AccountDetailData(
|
||||
title:
|
||||
GalleryLocalizations.of(context).rallyAccountDetailDataAccountOwner,
|
||||
value: 'Philip Cao',
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
static List<DetailedEventData> getDetailedEventItems() {
|
||||
// The following titles are not localized as they're product/brand names.
|
||||
return <DetailedEventData>[
|
||||
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<BillData> getBillDataList(BuildContext context) {
|
||||
// The following names are not localized as they're product/brand names.
|
||||
return <BillData>[
|
||||
BillData(
|
||||
name: 'RedPay Credit',
|
||||
primaryAmount: 45.36,
|
||||
dueDate: dateFormatAbbreviatedMonthDay(context)
|
||||
.format(DateTime.utc(2019, 1, 29)),
|
||||
),
|
||||
BillData(
|
||||
name: 'Rent',
|
||||
primaryAmount: 1200,
|
||||
dueDate: dateFormatAbbreviatedMonthDay(context)
|
||||
.format(DateTime.utc(2019, 2, 9)),
|
||||
),
|
||||
BillData(
|
||||
name: 'TabFine Credit',
|
||||
primaryAmount: 87.33,
|
||||
dueDate: dateFormatAbbreviatedMonthDay(context)
|
||||
.format(DateTime.utc(2019, 2, 22)),
|
||||
),
|
||||
BillData(
|
||||
name: 'ABC Loans',
|
||||
primaryAmount: 400,
|
||||
dueDate: dateFormatAbbreviatedMonthDay(context)
|
||||
.format(DateTime.utc(2019, 2, 29)),
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
static List<BudgetData> getBudgetDataList(BuildContext context) {
|
||||
return <BudgetData>[
|
||||
BudgetData(
|
||||
name: GalleryLocalizations.of(context).rallyBudgetCategoryCoffeeShops,
|
||||
primaryAmount: 70,
|
||||
amountUsed: 45.49,
|
||||
),
|
||||
BudgetData(
|
||||
name: GalleryLocalizations.of(context).rallyBudgetCategoryGroceries,
|
||||
primaryAmount: 170,
|
||||
amountUsed: 16.45,
|
||||
),
|
||||
BudgetData(
|
||||
name: GalleryLocalizations.of(context).rallyBudgetCategoryRestaurants,
|
||||
primaryAmount: 170,
|
||||
amountUsed: 123.25,
|
||||
),
|
||||
BudgetData(
|
||||
name: GalleryLocalizations.of(context).rallyBudgetCategoryClothing,
|
||||
primaryAmount: 70,
|
||||
amountUsed: 19.45,
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
static List<String> getSettingsTitles(BuildContext context) {
|
||||
return <String>[
|
||||
GalleryLocalizations.of(context).rallySettingsManageAccounts,
|
||||
GalleryLocalizations.of(context).rallySettingsTaxDocuments,
|
||||
GalleryLocalizations.of(context).rallySettingsPasscodeAndTouchId,
|
||||
GalleryLocalizations.of(context).rallySettingsNotifications,
|
||||
GalleryLocalizations.of(context).rallySettingsPersonalInformation,
|
||||
GalleryLocalizations.of(context).rallySettingsPaperlessSettings,
|
||||
GalleryLocalizations.of(context).rallySettingsFindAtms,
|
||||
GalleryLocalizations.of(context).rallySettingsHelp,
|
||||
GalleryLocalizations.of(context).rallySettingsSignOut,
|
||||
];
|
||||
}
|
||||
|
||||
static List<AlertData> getAlerts(BuildContext context) {
|
||||
return <AlertData>[
|
||||
AlertData(
|
||||
message: GalleryLocalizations.of(context)
|
||||
.rallyAlertsMessageHeadsUpShopping(
|
||||
percentFormat(context, decimalDigits: 0).format(0.9)),
|
||||
iconData: Icons.sort,
|
||||
),
|
||||
AlertData(
|
||||
message: GalleryLocalizations.of(context)
|
||||
.rallyAlertsMessageSpentOnRestaurants(
|
||||
usdWithSignFormat(context, decimalDigits: 0).format(120)),
|
||||
iconData: Icons.sort,
|
||||
),
|
||||
AlertData(
|
||||
message: GalleryLocalizations.of(context).rallyAlertsMessageATMFees(
|
||||
usdWithSignFormat(context, decimalDigits: 0).format(24)),
|
||||
iconData: Icons.credit_card,
|
||||
),
|
||||
AlertData(
|
||||
message: GalleryLocalizations.of(context)
|
||||
.rallyAlertsMessageCheckingAccount(
|
||||
percentFormat(context, decimalDigits: 0).format(0.04)),
|
||||
iconData: Icons.attach_money,
|
||||
),
|
||||
AlertData(
|
||||
message: GalleryLocalizations.of(context)
|
||||
.rallyAlertsMessageUnassignedTransactions(16),
|
||||
iconData: Icons.not_interested,
|
||||
),
|
||||
];
|
||||
}
|
||||
}
|
||||
406
gallery/lib/studies/rally/finance.dart
Normal file
406
gallery/lib/studies/rally/finance.dart
Normal file
@@ -0,0 +1,406 @@
|
||||
// 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/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: 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,
|
||||
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 List<_DetailedEventCard> cards = items.map((detailedEventData) {
|
||||
return _DetailedEventCard(
|
||||
title: detailedEventData.title,
|
||||
date: detailedEventData.date,
|
||||
amount: detailedEventData.amount,
|
||||
);
|
||||
}).toList();
|
||||
|
||||
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: ListView(shrinkWrap: true, children: cards),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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 textTheme = Theme.of(context).textTheme;
|
||||
return FlatButton(
|
||||
onPressed: () {},
|
||||
padding: EdgeInsets.symmetric(horizontal: 16),
|
||||
child: Column(
|
||||
children: [
|
||||
Container(
|
||||
padding: EdgeInsets.symmetric(vertical: 16),
|
||||
width: double.infinity,
|
||||
child: Wrap(
|
||||
alignment: WrapAlignment.spaceBetween,
|
||||
children: [
|
||||
Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
title,
|
||||
style: textTheme.body1.copyWith(fontSize: 16),
|
||||
),
|
||||
Text(
|
||||
shortDateFormat(context).format(date),
|
||||
semanticsLabel: longDateFormat(context).format(date),
|
||||
style:
|
||||
textTheme.body1.copyWith(color: RallyColors.gray60),
|
||||
),
|
||||
],
|
||||
),
|
||||
Text(
|
||||
usdWithSignFormat(context).format(amount),
|
||||
style: textTheme.body2
|
||||
.copyWith(fontSize: 20, color: RallyColors.gray),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
height: 1,
|
||||
child: Container(
|
||||
color: const Color(0xAA282828),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
44
gallery/lib/studies/rally/formatters.dart
Normal file
44
gallery/lib/studies/rally/formatters.dart
Normal file
@@ -0,0 +1,44 @@
|
||||
// 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 'package:flutter/material.dart';
|
||||
import 'package:gallery/data/gallery_options.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
|
||||
/// Get the locale string for the context.
|
||||
String locale(BuildContext context) =>
|
||||
GalleryOptions.of(context).locale.toString();
|
||||
|
||||
/// Currency formatter for USD.
|
||||
NumberFormat usdWithSignFormat(BuildContext context, {int decimalDigits = 2}) {
|
||||
return NumberFormat.currency(
|
||||
locale: locale(context),
|
||||
name: '\$',
|
||||
decimalDigits: decimalDigits,
|
||||
);
|
||||
}
|
||||
|
||||
/// Percent formatter with two decimal points.
|
||||
NumberFormat percentFormat(BuildContext context, {int decimalDigits = 2}) {
|
||||
return NumberFormat.decimalPercentPattern(
|
||||
locale: locale(context),
|
||||
decimalDigits: decimalDigits,
|
||||
);
|
||||
}
|
||||
|
||||
/// Date formatter with year / number month / day.
|
||||
DateFormat shortDateFormat(BuildContext context) =>
|
||||
DateFormat.yMd(locale(context));
|
||||
|
||||
/// Date formatter with year / month / day.
|
||||
DateFormat longDateFormat(BuildContext context) =>
|
||||
DateFormat.yMMMMd(locale(context));
|
||||
|
||||
/// Date formatter with abbreviated month and day.
|
||||
DateFormat dateFormatAbbreviatedMonthDay(BuildContext context) =>
|
||||
DateFormat.MMMd(locale(context));
|
||||
|
||||
/// Date formatter with year and abbreviated month.
|
||||
DateFormat dateFormatMonthYear(BuildContext context) =>
|
||||
DateFormat.yMMM(locale(context));
|
||||
368
gallery/lib/studies/rally/home.dart
Normal file
368
gallery/lib/studies/rally/home.dart
Normal file
@@ -0,0 +1,368 @@
|
||||
// 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 'package:flutter/material.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/pages/home.dart';
|
||||
import 'package:gallery/layout/focus_traversal_policy.dart';
|
||||
import 'package:gallery/studies/rally/tabs/accounts.dart';
|
||||
import 'package:gallery/studies/rally/tabs/bills.dart';
|
||||
import 'package:gallery/studies/rally/tabs/budgets.dart';
|
||||
import 'package:gallery/studies/rally/tabs/overview.dart';
|
||||
import 'package:gallery/studies/rally/tabs/settings.dart';
|
||||
|
||||
const int tabCount = 5;
|
||||
const int turnsToRotateRight = 1;
|
||||
const int turnsToRotateLeft = 3;
|
||||
|
||||
class HomePage extends StatefulWidget {
|
||||
@override
|
||||
_HomePageState createState() => _HomePageState();
|
||||
}
|
||||
|
||||
class _HomePageState extends State<HomePage>
|
||||
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);
|
||||
final isDesktop = isDisplayDesktop(context);
|
||||
Widget tabBarView;
|
||||
if (isDesktop) {
|
||||
final isTextDirectionRtl =
|
||||
GalleryOptions.of(context).textDirection() == TextDirection.rtl;
|
||||
final verticalRotation =
|
||||
isTextDirectionRtl ? turnsToRotateLeft : turnsToRotateRight;
|
||||
final revertVerticalRotation =
|
||||
isTextDirectionRtl ? turnsToRotateRight : turnsToRotateLeft;
|
||||
tabBarView = Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 150 + 50 * (cappedTextScale(context) - 1),
|
||||
alignment: Alignment.topCenter,
|
||||
padding: const EdgeInsets.symmetric(vertical: 32),
|
||||
child: Column(
|
||||
children: [
|
||||
const SizedBox(height: 24),
|
||||
ExcludeSemantics(
|
||||
child: SizedBox(
|
||||
height: 80,
|
||||
child: Image.asset(
|
||||
'logo.png',
|
||||
package: 'rally_assets',
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
// Rotate the tab bar, so the animation is vertical for desktops.
|
||||
RotatedBox(
|
||||
quarterTurns: verticalRotation,
|
||||
child: _RallyTabBar(
|
||||
tabs: _buildTabs(
|
||||
context: context, theme: theme, isVertical: true)
|
||||
.map(
|
||||
(widget) {
|
||||
// Revert the rotation on the tabs.
|
||||
return RotatedBox(
|
||||
quarterTurns: revertVerticalRotation,
|
||||
child: widget,
|
||||
);
|
||||
},
|
||||
).toList(),
|
||||
tabController: _tabController,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
// Rotate the tab views so we can swipe up and down.
|
||||
child: RotatedBox(
|
||||
quarterTurns: verticalRotation,
|
||||
child: TabBarView(
|
||||
controller: _tabController,
|
||||
children: _buildTabViews().map(
|
||||
(widget) {
|
||||
// Revert the rotation on the tab views.
|
||||
return RotatedBox(
|
||||
quarterTurns: revertVerticalRotation,
|
||||
child: widget,
|
||||
);
|
||||
},
|
||||
).toList(),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
} else {
|
||||
tabBarView = Column(
|
||||
children: [
|
||||
_RallyTabBar(
|
||||
tabs: _buildTabs(context: context, theme: theme),
|
||||
tabController: _tabController,
|
||||
),
|
||||
Expanded(
|
||||
child: TabBarView(
|
||||
controller: _tabController,
|
||||
children: _buildTabViews(),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
final backButtonFocusNode =
|
||||
InheritedFocusNodes.of(context).backButtonFocusNode;
|
||||
|
||||
return DefaultFocusTraversal(
|
||||
policy: EdgeChildrenFocusTraversalPolicy(
|
||||
firstFocusNodeOutsideScope: backButtonFocusNode,
|
||||
lastFocusNodeOutsideScope: backButtonFocusNode,
|
||||
focusScope: FocusScope.of(context),
|
||||
),
|
||||
child: ApplyTextOptions(
|
||||
child: Scaffold(
|
||||
body: SafeArea(
|
||||
// For desktop layout we do not want to have SafeArea at the top and
|
||||
// bottom to display 100% height content on the accounts view.
|
||||
top: !isDesktop,
|
||||
bottom: !isDesktop,
|
||||
child: 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: tabBarView,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
List<Widget> _buildTabs(
|
||||
{BuildContext context, ThemeData theme, bool isVertical = false}) {
|
||||
return [
|
||||
_RallyTab(
|
||||
theme: theme,
|
||||
iconData: Icons.pie_chart,
|
||||
title: GalleryLocalizations.of(context).rallyTitleOverview,
|
||||
tabIndex: 0,
|
||||
tabController: _tabController,
|
||||
isVertical: isVertical,
|
||||
),
|
||||
_RallyTab(
|
||||
theme: theme,
|
||||
iconData: Icons.attach_money,
|
||||
title: GalleryLocalizations.of(context).rallyTitleAccounts,
|
||||
tabIndex: 1,
|
||||
tabController: _tabController,
|
||||
isVertical: isVertical,
|
||||
),
|
||||
_RallyTab(
|
||||
theme: theme,
|
||||
iconData: Icons.money_off,
|
||||
title: GalleryLocalizations.of(context).rallyTitleBills,
|
||||
tabIndex: 2,
|
||||
tabController: _tabController,
|
||||
isVertical: isVertical,
|
||||
),
|
||||
_RallyTab(
|
||||
theme: theme,
|
||||
iconData: Icons.table_chart,
|
||||
title: GalleryLocalizations.of(context).rallyTitleBudgets,
|
||||
tabIndex: 3,
|
||||
tabController: _tabController,
|
||||
isVertical: isVertical,
|
||||
),
|
||||
_RallyTab(
|
||||
theme: theme,
|
||||
iconData: Icons.settings,
|
||||
title: GalleryLocalizations.of(context).rallyTitleSettings,
|
||||
tabIndex: 4,
|
||||
tabController: _tabController,
|
||||
isVertical: isVertical,
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
List<Widget> _buildTabViews() {
|
||||
return [
|
||||
OverviewView(),
|
||||
AccountsView(),
|
||||
BillsView(),
|
||||
BudgetsView(),
|
||||
SettingsView(),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
class _RallyTabBar extends StatelessWidget {
|
||||
const _RallyTabBar({Key key, this.tabs, this.tabController})
|
||||
: super(key: key);
|
||||
|
||||
final List<Widget> tabs;
|
||||
final TabController tabController;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return 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: tabs,
|
||||
controller: tabController,
|
||||
// This hides the tab indicator.
|
||||
indicatorColor: Colors.transparent,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _RallyTab extends StatefulWidget {
|
||||
_RallyTab({
|
||||
ThemeData theme,
|
||||
IconData iconData,
|
||||
String title,
|
||||
int tabIndex,
|
||||
TabController tabController,
|
||||
this.isVertical,
|
||||
}) : titleText = Text(title, style: theme.textTheme.button),
|
||||
isExpanded = tabController.index == tabIndex,
|
||||
icon = Icon(iconData, semanticLabel: title);
|
||||
|
||||
final Text titleText;
|
||||
final Icon icon;
|
||||
final bool isExpanded;
|
||||
final bool isVertical;
|
||||
|
||||
@override
|
||||
_RallyTabState createState() => _RallyTabState();
|
||||
}
|
||||
|
||||
class _RallyTabState extends State<_RallyTab>
|
||||
with SingleTickerProviderStateMixin {
|
||||
Animation<double> _titleSizeAnimation;
|
||||
Animation<double> _titleFadeAnimation;
|
||||
Animation<double> _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<double>(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) {
|
||||
if (widget.isVertical) {
|
||||
return Column(
|
||||
children: [
|
||||
const SizedBox(height: 18),
|
||||
FadeTransition(
|
||||
child: widget.icon,
|
||||
opacity: _iconFadeAnimation,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
FadeTransition(
|
||||
child: SizeTransition(
|
||||
child: Center(child: ExcludeSemantics(child: widget.titleText)),
|
||||
axis: Axis.vertical,
|
||||
axisAlignment: -1,
|
||||
sizeFactor: _titleSizeAnimation,
|
||||
),
|
||||
opacity: _titleFadeAnimation,
|
||||
),
|
||||
const SizedBox(height: 18),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
// 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 width = MediaQuery.of(context).size.width;
|
||||
const expandedTitleWidthMultiplier = 2;
|
||||
final unitWidth = width / (tabCount + expandedTitleWidthMultiplier);
|
||||
|
||||
return ConstrainedBox(
|
||||
constraints: BoxConstraints(minHeight: 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: ExcludeSemantics(child: widget.titleText),
|
||||
),
|
||||
),
|
||||
axis: Axis.horizontal,
|
||||
axisAlignment: -1,
|
||||
sizeFactor: _titleSizeAnimation,
|
||||
),
|
||||
opacity: _titleFadeAnimation,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
412
gallery/lib/studies/rally/login.dart
Normal file
412
gallery/lib/studies/rally/login.dart
Normal file
@@ -0,0 +1,412 @@
|
||||
// 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 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.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/pages/home.dart';
|
||||
import 'package:gallery/studies/rally/colors.dart';
|
||||
import 'package:gallery/layout/focus_traversal_policy.dart';
|
||||
|
||||
class LoginPage extends StatefulWidget {
|
||||
@override
|
||||
_LoginPageState createState() => _LoginPageState();
|
||||
}
|
||||
|
||||
class _LoginPageState extends State<LoginPage> {
|
||||
final TextEditingController _usernameController = TextEditingController();
|
||||
final TextEditingController _passwordController = TextEditingController();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final backButtonFocusNode =
|
||||
InheritedFocusNodes.of(context).backButtonFocusNode;
|
||||
|
||||
return DefaultFocusTraversal(
|
||||
policy: EdgeChildrenFocusTraversalPolicy(
|
||||
firstFocusNodeOutsideScope: backButtonFocusNode,
|
||||
lastFocusNodeOutsideScope: backButtonFocusNode,
|
||||
focusScope: FocusScope.of(context),
|
||||
),
|
||||
child: ApplyTextOptions(
|
||||
child: Scaffold(
|
||||
body: SafeArea(
|
||||
child: _MainView(
|
||||
usernameController: _usernameController,
|
||||
passwordController: _passwordController,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_usernameController.dispose();
|
||||
_passwordController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
class _MainView extends StatelessWidget {
|
||||
const _MainView({
|
||||
Key key,
|
||||
this.usernameController,
|
||||
this.passwordController,
|
||||
}) : super(key: key);
|
||||
|
||||
final TextEditingController usernameController;
|
||||
final TextEditingController passwordController;
|
||||
|
||||
void _login(BuildContext context) {
|
||||
Navigator.pop(context);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isDesktop = isDisplayDesktop(context);
|
||||
List<Widget> listViewChildren;
|
||||
|
||||
if (isDesktop) {
|
||||
final desktopMaxWidth = 400.0 + 100.0 * (cappedTextScale(context) - 1);
|
||||
listViewChildren = [
|
||||
_UsernameInput(
|
||||
maxWidth: desktopMaxWidth,
|
||||
usernameController: usernameController,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
_PasswordInput(
|
||||
maxWidth: desktopMaxWidth,
|
||||
passwordController: passwordController,
|
||||
),
|
||||
_LoginButton(
|
||||
maxWidth: desktopMaxWidth,
|
||||
onTap: () {
|
||||
_login(context);
|
||||
},
|
||||
),
|
||||
];
|
||||
} else {
|
||||
listViewChildren = [
|
||||
_SmallLogo(),
|
||||
_UsernameInput(
|
||||
usernameController: usernameController,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
_PasswordInput(
|
||||
passwordController: passwordController,
|
||||
),
|
||||
_ThumbButton(
|
||||
onTap: () {
|
||||
_login(context);
|
||||
},
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
if (isDesktop) _TopBar(),
|
||||
Expanded(
|
||||
child: Align(
|
||||
alignment: isDesktop ? Alignment.center : Alignment.topCenter,
|
||||
child: ListView(
|
||||
shrinkWrap: true,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24),
|
||||
children: listViewChildren,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _TopBar extends StatelessWidget {
|
||||
const _TopBar({
|
||||
Key key,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final spacing = const SizedBox(width: 30);
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
margin: const EdgeInsets.only(top: 8),
|
||||
padding: EdgeInsets.symmetric(horizontal: 30),
|
||||
child: Wrap(
|
||||
alignment: WrapAlignment.spaceBetween,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
ExcludeSemantics(
|
||||
child: SizedBox(
|
||||
height: 80,
|
||||
child: Image.asset(
|
||||
'logo.png',
|
||||
package: 'rally_assets',
|
||||
),
|
||||
),
|
||||
),
|
||||
spacing,
|
||||
Text(
|
||||
GalleryLocalizations.of(context).rallyLoginLoginToRally,
|
||||
style: Theme.of(context).textTheme.body2.copyWith(
|
||||
fontSize: 35 / reducedTextScale(context),
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
GalleryLocalizations.of(context).rallyLoginNoAccount,
|
||||
style: Theme.of(context).textTheme.subhead,
|
||||
),
|
||||
spacing,
|
||||
_BorderButton(
|
||||
text: GalleryLocalizations.of(context).rallyLoginSignUp,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _SmallLogo extends StatelessWidget {
|
||||
const _SmallLogo({
|
||||
Key key,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 64),
|
||||
child: SizedBox(
|
||||
height: 160,
|
||||
child: ExcludeSemantics(
|
||||
child: Image.asset(
|
||||
'logo.png',
|
||||
package: 'rally_assets',
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _UsernameInput extends StatelessWidget {
|
||||
const _UsernameInput({
|
||||
Key key,
|
||||
this.maxWidth,
|
||||
this.usernameController,
|
||||
}) : super(key: key);
|
||||
|
||||
final double maxWidth;
|
||||
final TextEditingController usernameController;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Align(
|
||||
alignment: Alignment.center,
|
||||
child: Container(
|
||||
constraints: BoxConstraints(maxWidth: maxWidth ?? double.infinity),
|
||||
child: TextField(
|
||||
controller: usernameController,
|
||||
decoration: InputDecoration(
|
||||
labelText: GalleryLocalizations.of(context).rallyLoginUsername,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _PasswordInput extends StatelessWidget {
|
||||
const _PasswordInput({
|
||||
Key key,
|
||||
this.maxWidth,
|
||||
this.passwordController,
|
||||
}) : super(key: key);
|
||||
|
||||
final double maxWidth;
|
||||
final TextEditingController passwordController;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Align(
|
||||
alignment: Alignment.center,
|
||||
child: Container(
|
||||
constraints: BoxConstraints(maxWidth: maxWidth ?? double.infinity),
|
||||
child: TextField(
|
||||
controller: passwordController,
|
||||
decoration: InputDecoration(
|
||||
labelText: GalleryLocalizations.of(context).rallyLoginPassword,
|
||||
),
|
||||
obscureText: true,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ThumbButton extends StatefulWidget {
|
||||
_ThumbButton({
|
||||
@required this.onTap,
|
||||
});
|
||||
|
||||
final VoidCallback onTap;
|
||||
|
||||
@override
|
||||
_ThumbButtonState createState() => _ThumbButtonState();
|
||||
}
|
||||
|
||||
class _ThumbButtonState extends State<_ThumbButton> {
|
||||
BoxDecoration borderDecoration;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Semantics(
|
||||
button: true,
|
||||
enabled: true,
|
||||
label: GalleryLocalizations.of(context).rallyLoginLabelLogin,
|
||||
child: GestureDetector(
|
||||
onTap: widget.onTap,
|
||||
child: Focus(
|
||||
onKey: (node, event) {
|
||||
if (event is RawKeyDownEvent) {
|
||||
if (event.logicalKey == LogicalKeyboardKey.enter ||
|
||||
event.logicalKey == LogicalKeyboardKey.space) {
|
||||
widget.onTap();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
},
|
||||
onFocusChange: (hasFocus) {
|
||||
if (hasFocus) {
|
||||
setState(() {
|
||||
borderDecoration = BoxDecoration(
|
||||
border: Border.all(
|
||||
color: Colors.white.withOpacity(0.5),
|
||||
width: 2,
|
||||
),
|
||||
);
|
||||
});
|
||||
} else {
|
||||
setState(() {
|
||||
borderDecoration = null;
|
||||
});
|
||||
}
|
||||
},
|
||||
child: Container(
|
||||
decoration: borderDecoration,
|
||||
height: 120,
|
||||
child: ExcludeSemantics(
|
||||
child: Image.asset(
|
||||
'thumb.png',
|
||||
package: 'rally_assets',
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _LoginButton extends StatelessWidget {
|
||||
const _LoginButton({
|
||||
Key key,
|
||||
@required this.onTap,
|
||||
this.maxWidth,
|
||||
}) : super(key: key);
|
||||
|
||||
final double maxWidth;
|
||||
final VoidCallback onTap;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Align(
|
||||
alignment: Alignment.center,
|
||||
child: Container(
|
||||
constraints: BoxConstraints(maxWidth: maxWidth ?? double.infinity),
|
||||
padding: const EdgeInsets.symmetric(vertical: 30),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.check_circle_outline, color: RallyColors.buttonColor),
|
||||
const SizedBox(width: 12),
|
||||
Text(GalleryLocalizations.of(context).rallyLoginRememberMe),
|
||||
const Expanded(child: SizedBox.shrink()),
|
||||
_FilledButton(
|
||||
text: GalleryLocalizations.of(context).rallyLoginButtonLogin,
|
||||
onTap: onTap,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _BorderButton extends StatelessWidget {
|
||||
const _BorderButton({Key key, @required this.text}) : super(key: key);
|
||||
|
||||
final String text;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return OutlineButton(
|
||||
borderSide: const BorderSide(color: RallyColors.buttonColor),
|
||||
color: RallyColors.buttonColor,
|
||||
highlightedBorderColor: RallyColors.buttonColor,
|
||||
focusColor: RallyColors.buttonColor.withOpacity(0.8),
|
||||
padding: const EdgeInsets.symmetric(vertical: 14, horizontal: 24),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
textColor: Colors.white,
|
||||
onPressed: () {
|
||||
Navigator.pop(context);
|
||||
},
|
||||
child: Text(text),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _FilledButton extends StatelessWidget {
|
||||
const _FilledButton({Key key, @required this.text, @required this.onTap})
|
||||
: super(key: key);
|
||||
|
||||
final String text;
|
||||
final VoidCallback onTap;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return FlatButton(
|
||||
color: RallyColors.buttonColor,
|
||||
padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 24),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
onPressed: onTap,
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.lock),
|
||||
const SizedBox(width: 6),
|
||||
Text(text),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
92
gallery/lib/studies/rally/tabs/accounts.dart
Normal file
92
gallery/lib/studies/rally/tabs/accounts.dart
Normal file
@@ -0,0 +1,92 @@
|
||||
// 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 'package:flutter/material.dart';
|
||||
|
||||
import 'package:gallery/l10n/gallery_localizations.dart';
|
||||
import 'package:gallery/layout/adaptive.dart';
|
||||
import 'package:gallery/studies/rally/charts/pie_chart.dart';
|
||||
import 'package:gallery/studies/rally/colors.dart';
|
||||
import 'package:gallery/studies/rally/data.dart';
|
||||
import 'package:gallery/studies/rally/finance.dart';
|
||||
|
||||
/// A page that shows a summary of accounts.
|
||||
class AccountsView extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final items = DummyDataService.getAccountDataList(context);
|
||||
final detailItems = DummyDataService.getAccountDetailList(context);
|
||||
final balanceTotal = sumAccountDataPrimaryAmount(items);
|
||||
final view = FinancialEntityView(
|
||||
heroLabel: GalleryLocalizations.of(context).rallyAccountTotal,
|
||||
heroAmount: balanceTotal,
|
||||
segments: buildSegmentsFromAccountItems(items),
|
||||
wholeAmount: balanceTotal,
|
||||
financialEntityCards: buildAccountDataListViews(items, context),
|
||||
);
|
||||
if (isDisplayDesktop(context)) {
|
||||
return Row(
|
||||
children: [
|
||||
Flexible(
|
||||
flex: 2,
|
||||
child: SingleChildScrollView(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 24),
|
||||
child: view,
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
flex: 1,
|
||||
child: Container(
|
||||
color: RallyColors.inputBackground,
|
||||
padding: EdgeInsetsDirectional.only(start: 24),
|
||||
height: double.infinity,
|
||||
alignment: AlignmentDirectional.centerStart,
|
||||
child: ListView(
|
||||
shrinkWrap: true,
|
||||
children: [
|
||||
for (AccountDetailData item in detailItems)
|
||||
_AccountDetail(title: item.title, value: item.value),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
} else {
|
||||
return SingleChildScrollView(child: view);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class _AccountDetail extends StatelessWidget {
|
||||
const _AccountDetail({Key key, this.value, this.title}) : super(key: key);
|
||||
|
||||
final String value;
|
||||
final String title;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final textTheme = Theme.of(context).textTheme;
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
SizedBox(height: 8),
|
||||
Text(
|
||||
title,
|
||||
style:
|
||||
textTheme.body1.copyWith(fontSize: 16, color: RallyColors.gray60),
|
||||
),
|
||||
SizedBox(height: 8),
|
||||
Text(
|
||||
value,
|
||||
style: textTheme.body2.copyWith(fontSize: 20),
|
||||
),
|
||||
SizedBox(height: 8),
|
||||
Container(color: RallyColors.primaryBackground, height: 1),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
38
gallery/lib/studies/rally/tabs/bills.dart
Normal file
38
gallery/lib/studies/rally/tabs/bills.dart
Normal file
@@ -0,0 +1,38 @@
|
||||
// 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 'package:flutter/widgets.dart';
|
||||
|
||||
import 'package:gallery/l10n/gallery_localizations.dart';
|
||||
import 'package:gallery/layout/adaptive.dart';
|
||||
import 'package:gallery/studies/rally/charts/pie_chart.dart';
|
||||
import 'package:gallery/studies/rally/data.dart';
|
||||
import 'package:gallery/studies/rally/finance.dart';
|
||||
|
||||
/// A page that shows a summary of bills.
|
||||
class BillsView extends StatefulWidget {
|
||||
@override
|
||||
_BillsViewState createState() => _BillsViewState();
|
||||
}
|
||||
|
||||
class _BillsViewState extends State<BillsView>
|
||||
with SingleTickerProviderStateMixin {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
List<BillData> items = DummyDataService.getBillDataList(context);
|
||||
final dueTotal = sumBillDataPrimaryAmount(items);
|
||||
return SingleChildScrollView(
|
||||
child: Container(
|
||||
padding: isDisplayDesktop(context) ? EdgeInsets.only(top: 24) : null,
|
||||
child: FinancialEntityView(
|
||||
heroLabel: GalleryLocalizations.of(context).rallyBillsDue,
|
||||
heroAmount: dueTotal,
|
||||
segments: buildSegmentsFromBillItems(items),
|
||||
wholeAmount: dueTotal,
|
||||
financialEntityCards: buildBillDataListViews(items, context),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
38
gallery/lib/studies/rally/tabs/budgets.dart
Normal file
38
gallery/lib/studies/rally/tabs/budgets.dart
Normal file
@@ -0,0 +1,38 @@
|
||||
// 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 'package:flutter/widgets.dart';
|
||||
|
||||
import 'package:gallery/l10n/gallery_localizations.dart';
|
||||
import 'package:gallery/layout/adaptive.dart';
|
||||
import 'package:gallery/studies/rally/charts/pie_chart.dart';
|
||||
import 'package:gallery/studies/rally/data.dart';
|
||||
import 'package:gallery/studies/rally/finance.dart';
|
||||
|
||||
class BudgetsView extends StatefulWidget {
|
||||
@override
|
||||
_BudgetsViewState createState() => _BudgetsViewState();
|
||||
}
|
||||
|
||||
class _BudgetsViewState extends State<BudgetsView>
|
||||
with SingleTickerProviderStateMixin {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final items = DummyDataService.getBudgetDataList(context);
|
||||
final capTotal = sumBudgetDataPrimaryAmount(items);
|
||||
final usedTotal = sumBudgetDataAmountUsed(items);
|
||||
return SingleChildScrollView(
|
||||
child: Container(
|
||||
padding: isDisplayDesktop(context) ? EdgeInsets.only(top: 24) : null,
|
||||
child: FinancialEntityView(
|
||||
heroLabel: GalleryLocalizations.of(context).rallyBudgetLeft,
|
||||
heroAmount: capTotal - usedTotal,
|
||||
segments: buildSegmentsFromBudgetItems(items),
|
||||
wholeAmount: capTotal,
|
||||
financialEntityCards: buildBudgetDataListViews(items, context),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
279
gallery/lib/studies/rally/tabs/overview.dart
Normal file
279
gallery/lib/studies/rally/tabs/overview.dart
Normal file
@@ -0,0 +1,279 @@
|
||||
// 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/cupertino.dart';
|
||||
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/colors.dart';
|
||||
import 'package:gallery/studies/rally/data.dart';
|
||||
import 'package:gallery/studies/rally/finance.dart';
|
||||
import 'package:gallery/studies/rally/formatters.dart';
|
||||
|
||||
/// A page that shows a status overview.
|
||||
class OverviewView extends StatefulWidget {
|
||||
@override
|
||||
_OverviewViewState createState() => _OverviewViewState();
|
||||
}
|
||||
|
||||
class _OverviewViewState extends State<OverviewView> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final alerts = DummyDataService.getAlerts(context);
|
||||
|
||||
if (isDisplayDesktop(context)) {
|
||||
const sortKeyName = 'Overview';
|
||||
return SingleChildScrollView(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 24),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Flexible(
|
||||
flex: 7,
|
||||
child: Semantics(
|
||||
sortKey: const OrdinalSortKey(1, name: sortKeyName),
|
||||
child: _OverviewGrid(spacing: 24),
|
||||
),
|
||||
),
|
||||
SizedBox(width: 24),
|
||||
Flexible(
|
||||
flex: 3,
|
||||
child: Container(
|
||||
width: 400,
|
||||
child: Semantics(
|
||||
sortKey: const OrdinalSortKey(2, name: sortKeyName),
|
||||
child: _AlertsView(alerts: alerts),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
return SingleChildScrollView(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
child: Column(
|
||||
children: [
|
||||
_AlertsView(alerts: alerts.sublist(0, 1)),
|
||||
SizedBox(height: 12),
|
||||
_OverviewGrid(spacing: 12),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class _OverviewGrid extends StatelessWidget {
|
||||
const _OverviewGrid({Key key, @required this.spacing}) : super(key: key);
|
||||
|
||||
final double spacing;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final accountDataList = DummyDataService.getAccountDataList(context);
|
||||
final billDataList = DummyDataService.getBillDataList(context);
|
||||
final budgetDataList = DummyDataService.getBudgetDataList(context);
|
||||
|
||||
return LayoutBuilder(builder: (context, constraints) {
|
||||
final textScaleFactor =
|
||||
GalleryOptions.of(context).textScaleFactor(context);
|
||||
|
||||
// Only display multiple columns when the constraints allow it and we
|
||||
// have a regular text scale factor.
|
||||
final minWidthForTwoColumns = 600;
|
||||
final hasMultipleColumns = isDisplayDesktop(context) &&
|
||||
constraints.maxWidth > minWidthForTwoColumns &&
|
||||
textScaleFactor <= 2;
|
||||
final boxWidth = hasMultipleColumns
|
||||
? constraints.maxWidth / 2 - spacing / 2
|
||||
: double.infinity;
|
||||
|
||||
return Wrap(
|
||||
runSpacing: spacing,
|
||||
children: [
|
||||
Container(
|
||||
width: boxWidth,
|
||||
child: _FinancialView(
|
||||
title: GalleryLocalizations.of(context).rallyAccounts,
|
||||
total: sumAccountDataPrimaryAmount(accountDataList),
|
||||
financialItemViews:
|
||||
buildAccountDataListViews(accountDataList, context),
|
||||
buttonSemanticsLabel:
|
||||
GalleryLocalizations.of(context).rallySeeAllAccounts,
|
||||
),
|
||||
),
|
||||
if (hasMultipleColumns) SizedBox(width: spacing),
|
||||
Container(
|
||||
width: boxWidth,
|
||||
child: _FinancialView(
|
||||
title: GalleryLocalizations.of(context).rallyBills,
|
||||
total: sumBillDataPrimaryAmount(billDataList),
|
||||
financialItemViews: buildBillDataListViews(billDataList, context),
|
||||
buttonSemanticsLabel:
|
||||
GalleryLocalizations.of(context).rallySeeAllBills,
|
||||
),
|
||||
),
|
||||
_FinancialView(
|
||||
title: GalleryLocalizations.of(context).rallyBudgets,
|
||||
total: sumBudgetDataPrimaryAmount(budgetDataList),
|
||||
financialItemViews:
|
||||
buildBudgetDataListViews(budgetDataList, context),
|
||||
buttonSemanticsLabel:
|
||||
GalleryLocalizations.of(context).rallySeeAllBudgets,
|
||||
),
|
||||
],
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class _AlertsView extends StatelessWidget {
|
||||
const _AlertsView({Key key, this.alerts}) : super(key: key);
|
||||
|
||||
final List<AlertData> alerts;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isDesktop = isDisplayDesktop(context);
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsetsDirectional.only(start: 16, top: 4, bottom: 4),
|
||||
color: RallyColors.cardBackground,
|
||||
child: Column(
|
||||
children: [
|
||||
Container(
|
||||
width: double.infinity,
|
||||
padding: isDesktop ? EdgeInsets.symmetric(vertical: 16) : null,
|
||||
child: MergeSemantics(
|
||||
child: Wrap(
|
||||
alignment: WrapAlignment.spaceBetween,
|
||||
crossAxisAlignment: WrapCrossAlignment.center,
|
||||
children: [
|
||||
Text(GalleryLocalizations.of(context).rallyAlerts),
|
||||
if (!isDesktop)
|
||||
FlatButton(
|
||||
onPressed: () {},
|
||||
child: Text(GalleryLocalizations.of(context).rallySeeAll),
|
||||
textColor: Colors.white,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
for (AlertData alert in alerts) ...[
|
||||
Container(color: RallyColors.primaryBackground, height: 1),
|
||||
_Alert(alert: alert),
|
||||
]
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _Alert extends StatelessWidget {
|
||||
const _Alert({
|
||||
Key key,
|
||||
@required this.alert,
|
||||
}) : super(key: key);
|
||||
|
||||
final AlertData alert;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MergeSemantics(
|
||||
child: Container(
|
||||
padding: isDisplayDesktop(context)
|
||||
? EdgeInsets.symmetric(vertical: 8)
|
||||
: null,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(alert.message),
|
||||
),
|
||||
SizedBox(
|
||||
width: 100,
|
||||
child: Align(
|
||||
alignment: Alignment.topRight,
|
||||
child: IconButton(
|
||||
onPressed: () {},
|
||||
icon: Icon(alert.iconData, color: RallyColors.white60),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _FinancialView extends StatelessWidget {
|
||||
const _FinancialView({
|
||||
this.title,
|
||||
this.total,
|
||||
this.financialItemViews,
|
||||
this.buttonSemanticsLabel,
|
||||
});
|
||||
|
||||
final String title;
|
||||
final String buttonSemanticsLabel;
|
||||
final double total;
|
||||
final List<FinancialEntityCategoryView> financialItemViews;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
return Container(
|
||||
color: RallyColors.cardBackground,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
MergeSemantics(
|
||||
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(context).format(total),
|
||||
style: theme.textTheme.body2.copyWith(
|
||||
fontSize: 44 / reducedTextScale(context),
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
...financialItemViews.sublist(
|
||||
0, math.min(financialItemViews.length, 3)),
|
||||
FlatButton(
|
||||
child: Text(
|
||||
GalleryLocalizations.of(context).rallySeeAll,
|
||||
semanticsLabel: buttonSemanticsLabel,
|
||||
),
|
||||
textColor: Colors.white,
|
||||
onPressed: () {},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
54
gallery/lib/studies/rally/tabs/settings.dart
Normal file
54
gallery/lib/studies/rally/tabs/settings.dart
Normal file
@@ -0,0 +1,54 @@
|
||||
// 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 'package:flutter/material.dart';
|
||||
|
||||
import 'package:gallery/layout/adaptive.dart';
|
||||
import 'package:gallery/studies/rally/data.dart';
|
||||
import 'package:gallery/studies/rally/login.dart';
|
||||
|
||||
class SettingsView extends StatefulWidget {
|
||||
@override
|
||||
_SettingsViewState createState() => _SettingsViewState();
|
||||
}
|
||||
|
||||
class _SettingsViewState extends State<SettingsView> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final items = DummyDataService.getSettingsTitles(context)
|
||||
.map((title) => _SettingsItem(title))
|
||||
.toList();
|
||||
return Container(
|
||||
padding: EdgeInsets.only(top: isDisplayDesktop(context) ? 24 : 0),
|
||||
child: ListView(
|
||||
shrinkWrap: true,
|
||||
children: items,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _SettingsItem extends StatelessWidget {
|
||||
const _SettingsItem(this.title);
|
||||
|
||||
final String title;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return FlatButton(
|
||||
textColor: Colors.white,
|
||||
child: Container(
|
||||
alignment: AlignmentDirectional.centerStart,
|
||||
padding: EdgeInsets.symmetric(vertical: 24, horizontal: 12),
|
||||
child: Text(title),
|
||||
),
|
||||
onPressed: () {
|
||||
Navigator.push<void>(
|
||||
context,
|
||||
MaterialPageRoute<void>(builder: (context) => LoginPage()),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user