diff --git a/LICENSE b/LICENSE index 972bb2edb..57e60e3ab 100644 --- a/LICENSE +++ b/LICENSE @@ -25,3 +25,302 @@ // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +-------------------------------------------------------------------------------- +Copyright (c) 2014, Vaibhav Singh (design) and Rosetta Type Foundry s.r.o. (post-production). + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. + +-------------------------------------------------------------------------------- + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + 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. diff --git a/material_studies/rally/.gitignore b/material_studies/rally/.gitignore new file mode 100644 index 000000000..07488ba61 --- /dev/null +++ b/material_studies/rally/.gitignore @@ -0,0 +1,70 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# Visual Studio Code related +.vscode/ + +# Flutter/Dart/Pub related +**/doc/api/ +.dart_tool/ +.flutter-plugins +.packages +.pub-cache/ +.pub/ +/build/ + +# Android related +**/android/**/gradle-wrapper.jar +**/android/.gradle +**/android/captures/ +**/android/gradlew +**/android/gradlew.bat +**/android/local.properties +**/android/**/GeneratedPluginRegistrant.java + +# iOS/XCode related +**/ios/**/*.mode1v3 +**/ios/**/*.mode2v3 +**/ios/**/*.moved-aside +**/ios/**/*.pbxuser +**/ios/**/*.perspectivev3 +**/ios/**/*sync/ +**/ios/**/.sconsign.dblite +**/ios/**/.tags* +**/ios/**/.vagrant/ +**/ios/**/DerivedData/ +**/ios/**/Icon? +**/ios/**/Pods/ +**/ios/**/.symlinks/ +**/ios/**/profile +**/ios/**/xcuserdata +**/ios/.generated/ +**/ios/Flutter/App.framework +**/ios/Flutter/Flutter.framework +**/ios/Flutter/Generated.xcconfig +**/ios/Flutter/app.flx +**/ios/Flutter/app.zip +**/ios/Flutter/flutter_assets/ +**/ios/ServiceDefinitions.json +**/ios/Runner/GeneratedPluginRegistrant.* + +# Exceptions to above rules. +!**/ios/**/default.mode1v3 +!**/ios/**/default.mode2v3 +!**/ios/**/default.pbxuser +!**/ios/**/default.perspectivev3 +!/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages diff --git a/material_studies/rally/README.md b/material_studies/rally/README.md new file mode 100644 index 000000000..a1007b537 --- /dev/null +++ b/material_studies/rally/README.md @@ -0,0 +1,37 @@ +# Rally + +A Flutter sample app based on the Material study Rally (a hypothetical, personal finance app). It +showcases custom tabs, custom painted widgets, and custom animations. + +For info on the Rally Material Study, see: https://material.io/design/material-studies/rally.html + +## Goals + +* Show how to customize a tab bar. +* Show how to create reusable custom widgets with composition and custom painting. +* Show how to create an app with tabs and child navigation screens. + +## The important bits + +### `/charts/*` + +These are the custom charts. The circle chart and line chart are custom painters, +while the single vertical bar chart is a simple composition of boxes. + +### `/sections/*` + +These are the main sections for the tab views and the child screen with the details and line chart. +The financial entity is a reusable screen that is the base for the accounts, bills, and budgets +screens. + +## Questions/issues + +If you have a general question about any of the techniques you see in +the sample, the best places to go are: + +* [The FlutterDev Google Group](https://groups.google.com/forum/#!forum/flutter-dev) +* [The Flutter Gitter channel](https://gitter.im/flutter/flutter) +* [StackOverflow](https://stackoverflow.com/questions/tagged/flutter) + +If you run into an issue with the sample itself, please file an issue +in the [main Flutter repo](https://github.com/flutter/flutter/issues). diff --git a/material_studies/rally/analysis_options.yaml b/material_studies/rally/analysis_options.yaml new file mode 100644 index 000000000..b9a01f119 --- /dev/null +++ b/material_studies/rally/analysis_options.yaml @@ -0,0 +1,30 @@ +include: package:pedantic/analysis_options.1.7.0.yaml + +analyzer: + strong-mode: + implicit-casts: false + implicit-dynamic: false + +linter: + rules: + - avoid_types_on_closure_parameters + - avoid_void_async + - await_only_futures + - camel_case_types + - cancel_subscriptions + - close_sinks + - constant_identifier_names + - control_flow_in_finally + - empty_statements + - hash_and_equals + - implementation_imports + - non_constant_identifier_names + - package_api_docs + - package_names + - package_prefixed_library_names + - test_types_in_equals + - throw_in_finally + - unnecessary_brace_in_string_interps + - unnecessary_getters_setters + - unnecessary_new + - unnecessary_statements diff --git a/material_studies/rally/assets/logo.png b/material_studies/rally/assets/logo.png new file mode 100644 index 000000000..c718f3860 Binary files /dev/null and b/material_studies/rally/assets/logo.png differ diff --git a/material_studies/rally/assets/thumb.png b/material_studies/rally/assets/thumb.png new file mode 100644 index 000000000..4a495f08c Binary files /dev/null and b/material_studies/rally/assets/thumb.png differ diff --git a/material_studies/rally/fonts/Eczar-Bold.ttf b/material_studies/rally/fonts/Eczar-Bold.ttf new file mode 100755 index 000000000..175210250 Binary files /dev/null and b/material_studies/rally/fonts/Eczar-Bold.ttf differ diff --git a/material_studies/rally/fonts/Eczar-Regular.ttf b/material_studies/rally/fonts/Eczar-Regular.ttf new file mode 100755 index 000000000..8eb92d975 Binary files /dev/null and b/material_studies/rally/fonts/Eczar-Regular.ttf differ diff --git a/material_studies/rally/fonts/Eczar-SemiBold.ttf b/material_studies/rally/fonts/Eczar-SemiBold.ttf new file mode 100755 index 000000000..2132b24c0 Binary files /dev/null and b/material_studies/rally/fonts/Eczar-SemiBold.ttf differ diff --git a/material_studies/rally/fonts/RobotoCondensed-Bold.ttf b/material_studies/rally/fonts/RobotoCondensed-Bold.ttf new file mode 100755 index 000000000..8c7a08be0 Binary files /dev/null and b/material_studies/rally/fonts/RobotoCondensed-Bold.ttf differ diff --git a/material_studies/rally/fonts/RobotoCondensed-Light.ttf b/material_studies/rally/fonts/RobotoCondensed-Light.ttf new file mode 100755 index 000000000..67e84089e Binary files /dev/null and b/material_studies/rally/fonts/RobotoCondensed-Light.ttf differ diff --git a/material_studies/rally/fonts/RobotoCondensed-Regular.ttf b/material_studies/rally/fonts/RobotoCondensed-Regular.ttf new file mode 100755 index 000000000..533e3999c Binary files /dev/null and b/material_studies/rally/fonts/RobotoCondensed-Regular.ttf differ diff --git a/material_studies/rally/lib/app.dart b/material_studies/rally/lib/app.dart new file mode 100644 index 000000000..964090a7e --- /dev/null +++ b/material_studies/rally/lib/app.dart @@ -0,0 +1,85 @@ +// 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/widgets.dart'; + +import 'package:rally/colors.dart'; +import 'package:rally/home.dart'; +import 'package: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': (context) => LoginPage()}, + ); + } + + ThemeData _buildRallyTheme() { + final ThemeData base = ThemeData.dark(); + return ThemeData( + scaffoldBackgroundColor: RallyColors.primaryBackground, + primaryColor: RallyColors.primaryBackground, + textTheme: _buildRallyTextTheme(base.textTheme), + inputDecorationTheme: InputDecorationTheme( + labelStyle: + 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/material_studies/rally/lib/charts/line_chart.dart b/material_studies/rally/lib/charts/line_chart.dart new file mode 100644 index 000000000..87e6c9d8e --- /dev/null +++ b/material_studies/rally/lib/charts/line_chart.dart @@ -0,0 +1,193 @@ +// 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/rendering.dart'; +import 'package:flutter/widgets.dart'; + +import 'package:rally/colors.dart'; +import 'package:rally/data.dart'; + +class RallyLineChart extends StatelessWidget { + RallyLineChart({this.events = const []}) : assert(events != null); + + final List events; + + @override + Widget build(BuildContext context) { + return CustomPaint(painter: RallyLineChartPainter(context, events)); + } +} + +class RallyLineChartPainter extends CustomPainter { + RallyLineChartPainter(this.context, this.events); + + final BuildContext context; + + // 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.0; // minAmount is assumed to be 0.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.0; + + @override + void paint(Canvas canvas, Size size) { + double ticksTop = size.height - space * 5; + double labelsTop = size.height - space * 2; + _drawLine( + canvas, + Rect.fromLTWH(0.0, 0.0, size.width, ticksTop), + ); + _drawXAxisTicks( + canvas, + Rect.fromLTWH(0.0, ticksTop, size.width, labelsTop - ticksTop), + ); + _drawXAxisLabels( + canvas, + Rect.fromLTWH(0.0, labelsTop, size.width, size.height - labelsTop), + ); + } + + @override + bool shouldRepaint(CustomPainter oldDelegate) { + return false; + } + + void _drawLine(Canvas canvas, Rect rect) { + final linePaint = Paint() + ..color = RallyColors.accountColor(2) + ..style = PaintingStyle.stroke + ..strokeWidth = 2.0; + + // 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.0; + + // Try changing this value between 1, 7, 15, etc. + int smoothing = 7; + + // Align the points with equal deltas (1 day) as a cumulative sum. + int startMillis = startDate.millisecondsSinceEpoch; + final points = [ + Offset(0.0, (maxAmount - lastAmount) / maxAmount * rect.height) + ]; + for (int i = 0; i < numDays + smoothing; i++) { + int endMillis = startMillis + millisInDay * 1; + final filteredEvents = events.where((e) { + return startMillis <= e.date.millisecondsSinceEpoch && + e.date.millisecondsSinceEpoch <= endMillis; + }).toList(); + lastAmount += filteredEvents.fold(0.0, (sum, e) => sum + e.amount); + double x = i / numDays * rect.width; + 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) { + double x1 = points[i].dx; + double y1 = points[i].dy; + double x2 = (x1 + points[i + smoothing].dx) / 2; + 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) { + double dayTop = (rect.top + rect.bottom) / 2; + for (int i = 0; i < numDays; i++) { + double x = rect.width / numDays * i; + canvas.drawRect( + Rect.fromPoints( + Offset(x, i % 7 == tickShift ? rect.top : dayTop), + Offset(x, rect.bottom), + ), + Paint() + ..style = PaintingStyle.stroke + ..strokeWidth = 1.0 + ..color = RallyColors.gray25, + ); + } + } + + /// Set X-axis labels under the X-axis increment markers. + void _drawXAxisLabels(Canvas canvas, Rect rect) { + final selectedLabelStyle = Theme.of(context).textTheme.body1.copyWith( + fontWeight: FontWeight.w700, + ); + final unselectedLabelStyle = Theme.of(context).textTheme.body1.copyWith( + fontWeight: FontWeight.w700, + color: RallyColors.gray25, + ); + + final 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 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 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/material_studies/rally/lib/charts/pie_chart.dart b/material_studies/rally/lib/charts/pie_chart.dart new file mode 100644 index 000000000..87ef02a20 --- /dev/null +++ b/material_studies/rally/lib/charts/pie_chart.dart @@ -0,0 +1,239 @@ +// 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/rendering.dart'; +import 'package:flutter/widgets.dart'; + +import 'package:rally/colors.dart'; +import 'package:rally/data.dart'; +import 'package:rally/formatters.dart'; + +/// A colored piece of the [RallyPieChart]. +class RallyPieChartSegment { + final Color color; + final double value; + + const RallyPieChartSegment({this.color, this.value}); +} + +List buildSegmentsFromAccountItems( + List items) { + return List.generate( + items.length, + (i) => RallyPieChartSegment( + color: RallyColors.accountColor(i), + value: items[i].primaryAmount, + ), + ); +} + +List buildSegmentsFromBillItems(List items) { + return List.generate( + items.length, + (i) => RallyPieChartSegment( + color: RallyColors.billColor(i), + value: items[i].primaryAmount, + ), + ); +} + +List buildSegmentsFromBudgetItems( + List items) { + return List.generate( + items.length, + (i) => 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 { + RallyPieChart( + {this.heroLabel, this.heroAmount, this.wholeAmount, this.segments}); + + final String heroLabel; + final double heroAmount; + final double wholeAmount; + final List segments; + + _RallyPieChartState createState() => _RallyPieChartState(); +} + +class _RallyPieChartState extends State + with SingleTickerProviderStateMixin { + AnimationController controller; + Animation animation; + + @override + initState() { + super.initState(); + controller = AnimationController( + duration: const Duration(milliseconds: 600), vsync: this); + animation = CurvedAnimation( + parent: TweenSequence(>[ + TweenSequenceItem(tween: Tween(begin: 0.0, end: 0.0), weight: 1.0), + TweenSequenceItem(tween: Tween(begin: 0.0, end: 1.0), weight: 1.5), + ]).animate(controller), + curve: Curves.decelerate); + controller.forward(); + } + + dispose() { + controller.dispose(); + super.dispose(); + } + + Widget build(BuildContext context) { + return _AnimatedRallyPieChart( + animation: animation, + centerLabel: widget.heroLabel, + centerAmount: widget.heroAmount, + total: widget.wholeAmount, + segments: widget.segments, + ); + } +} + +class _AnimatedRallyPieChart extends AnimatedWidget { + _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; + + Widget build(BuildContext context) { + final labelTextStyle = Theme.of(context) + .textTheme + .body1 + .copyWith(fontSize: 14.0, letterSpacing: 0.5); + + return DecoratedBox( + decoration: _RallyPieChartOutlineDecoration( + maxFraction: animation.value, total: total, segments: segments), + child: SizedBox( + height: 300.0, + child: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + centerLabel, + style: labelTextStyle, + ), + Text( + Formatters.usdWithSign.format(centerAmount), + style: Theme.of(context).textTheme.headline, + ), + ], + ), + ), + ), + ); + } +} + +class _RallyPieChartOutlineDecoration extends Decoration { + _RallyPieChartOutlineDecoration( + {this.maxFraction, this.total, this.segments}); + + final double maxFraction; + final double total; + final List segments; + + @override + BoxPainter createBoxPainter([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; + + @override + void paint(Canvas canvas, Offset offset, ImageConfiguration configuration) { + // Create two padded rects to draw arcs in: one for colored arcs and one for + // inner bg arc. + const double strokeWidth = 4.0; + final outerRadius = + min(configuration.size.width, configuration.size.height) / 2; + final outerRect = Rect.fromCircle( + center: configuration.size.center(Offset.zero), + radius: outerRadius - strokeWidth * 3.0); + final innerRect = Rect.fromCircle( + center: configuration.size.center(Offset.zero), + radius: outerRadius - strokeWidth * 4.0); + + // Paint each arc with spacing. + double cummulativeSpace = 0.0; + double cummulativeTotal = 0.0; + const double wholeRadians = (2.0 * pi); + const double spaceRadians = wholeRadians / 180.0; + final wholeMinusSpacesRadians = + wholeRadians - (segments.length * spaceRadians); + for (RallyPieChartSegment segment in segments) { + final paint = Paint()..color = segment.color; + final start = maxFraction * + ((cummulativeTotal / wholeAmount * wholeMinusSpacesRadians) + + cummulativeSpace) - + pi / 2.0; + final sweep = + maxFraction * (segment.value / wholeAmount * wholeMinusSpacesRadians); + canvas.drawArc(outerRect, start, sweep, true, paint); + cummulativeTotal += segment.value; + cummulativeSpace += spaceRadians; + } + + // Paint any remaining space black (e.g. budget amount remaining). + double remaining = wholeAmount - cummulativeTotal; + if (remaining > 0) { + final paint = Paint()..color = Colors.black; + final start = maxFraction * + ((cummulativeTotal / wholeAmount * wholeMinusSpacesRadians) + + spaceRadians * segments.length) - + pi / 2.0; + final sweep = maxFraction * + (remaining / wholeAmount * wholeMinusSpacesRadians - spaceRadians); + canvas.drawArc(outerRect, start, sweep, true, paint); + } + + // Paint a smaller inner circle to cover the painted arcs, so they are + // display as segments. + Paint bgPaint = Paint()..color = RallyColors.primaryBackground; + canvas.drawArc(innerRect, 0.0, 2.0 * pi, true, bgPaint); + } +} diff --git a/material_studies/rally/lib/charts/vertical_fraction_bar.dart b/material_studies/rally/lib/charts/vertical_fraction_bar.dart new file mode 100644 index 000000000..81b7308f8 --- /dev/null +++ b/material_studies/rally/lib/charts/vertical_fraction_bar.dart @@ -0,0 +1,45 @@ +// 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/widgets.dart'; + +class VerticalFractionBar extends StatelessWidget { + VerticalFractionBar({this.color, this.fraction}); + + final Color color; + final double fraction; + + @override + Widget build(BuildContext context) { + return SizedBox( + height: 32.0, + width: 4.0, + child: Column( + children: [ + SizedBox( + height: (1 - fraction) * 32.0, + child: Container( + color: Colors.black, + ), + ), + SizedBox( + height: fraction * 32.0, + child: Container(color: color), + ), + ], + ), + ); + } +} diff --git a/material_studies/rally/lib/colors.dart b/material_studies/rally/lib/colors.dart new file mode 100644 index 000000000..0c6b4d721 --- /dev/null +++ b/material_studies/rally/lib/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 'package:flutter/material.dart'; + +/// 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 accountColors = [ + Color(0xFF005D57), + Color(0xFF04B97F), + Color(0xFF37EFBA), + Color(0xFF007D51), + ]; + + static const billColors = [ + Color(0xFFFFDC78), + Color(0xFFFF6951), + Color(0xFFFFD7D0), + Color(0xFFFFAC12), + ]; + + static const budgetColors = [ + Color(0xFFB2F2FF), + Color(0xFFB15DFF), + Color(0xFF72DEFF), + Color(0xFF0082FB), + ]; + + static const gray = Color(0xFFD8D8D8); + static const gray60 = Color(0x99D8D8D8); + static const gray25 = Color(0x40D8D8D8); + static const white60 = Color(0x99FFFFFF); + static const primaryBackground = Color(0xFF33333D); + static const inputBackground = Color(0xFF26282F); + static const 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/material_studies/rally/lib/data.dart b/material_studies/rally/lib/data.dart new file mode 100644 index 000000000..c31daa1ac --- /dev/null +++ b/material_studies/rally/lib/data.dart @@ -0,0 +1,223 @@ +// 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) { + return items.fold( + 0, + (sum, next) => sum + next.primaryAmount, + ); +} + +/// Calculates the sum of the primary amounts of a list of [BillData]. +double sumBillDataPrimaryAmount(List items) { + return items.fold( + 0, + (sum, next) => sum + next.primaryAmount, + ); +} + +/// Calculates the sum of the primary amounts of a list of [BudgetData]. +double sumBudgetDataPrimaryAmount(List items) { + return items.fold( + 0, + (sum, next) => sum + next.primaryAmount, + ); +} + +/// Calculates the sum of the amounts used of a list of [BudgetData]. +double sumBudgetDataAmountUsed(List items) { + return items.fold( + 0.0, + (sum, next) => sum + next.amountUsed, + ); +} + +/// 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 [ + AccountData( + name: 'Checking', + primaryAmount: 2215.13, + accountNumber: '1234561234', + ), + AccountData( + name: 'Home Savings', + primaryAmount: 8678.88, + accountNumber: '8888885678', + ), + AccountData( + name: 'Car Savings', + primaryAmount: 987.48, + accountNumber: '8888889012', + ), + AccountData( + name: 'Vacation', + primaryAmount: 253.0, + 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 [ + BillData( + name: 'RedPay Credit', + primaryAmount: 45.36, + dueDate: 'Jan 29', + ), + BillData( + name: 'Rent', + primaryAmount: 1200.0, + dueDate: 'Feb 9', + ), + BillData( + name: 'TabFine Credit', + primaryAmount: 87.33, + dueDate: 'Feb 22', + ), + BillData( + name: 'ABC Loans', + primaryAmount: 400.0, + dueDate: 'Feb 29', + ), + ]; + } + + static List getBudgetDataList() { + return [ + BudgetData( + name: 'Coffee Shops', + primaryAmount: 70.0, + amountUsed: 45.49, + ), + BudgetData( + name: 'Groceries', + primaryAmount: 170.0, + amountUsed: 16.45, + ), + BudgetData( + name: 'Restaurants', + primaryAmount: 170.0, + amountUsed: 123.25, + ), + BudgetData( + name: 'Clothing', + primaryAmount: 70.0, + amountUsed: 19.45, + ), + ]; + } + + static List getSettingsTitles() { + return [ + 'Manage Accounts', + 'Tax Documents', + 'Passcode and Touch ID', + 'Notifications', + 'Personal Information', + 'Paperless Settings', + 'Find ATMs', + 'Help', + ]; + } +} diff --git a/material_studies/rally/lib/finance.dart b/material_studies/rally/lib/finance.dart new file mode 100644 index 000000000..ab82dd51e --- /dev/null +++ b/material_studies/rally/lib/finance.dart @@ -0,0 +1,344 @@ +// 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/widgets.dart'; + +import 'package:rally/charts/pie_chart.dart'; +import 'package:rally/charts/line_chart.dart'; +import 'package:rally/charts/vertical_fraction_bar.dart'; +import 'package:rally/colors.dart'; +import 'package:rally/data.dart'; +import 'package:rally/formatters.dart'; + +class FinancialEntityView extends StatelessWidget { + FinancialEntityView({ + this.heroLabel, + this.heroAmount, + this.wholeAmount, + this.segments, + this.financialEntityCards, + }) : assert(segments.length == financialEntityCards.length); + + /// The amounts to assign each item. + /// + /// This list must have the same length as [colors]. + 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.0, + child: Container( + color: 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: (context) => FinancialEntityCategoryDetailsPage(), + ), + ); + }, + child: SizedBox( + height: 68, + child: Column( + children: [ + Expanded( + child: Row( + children: [ + Padding( + padding: 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.0), + ), + Text( + subtitle, + style: Theme.of(context) + .textTheme + .body1 + .copyWith(color: RallyColors.gray60), + ), + ], + ), + Spacer(), + Text( + '\$ ' + Formatters.usd.format(amount), + style: Theme.of(context) + .textTheme + .body2 + .copyWith(fontSize: 20.0, color: RallyColors.gray), + ), + SizedBox(width: 32.0, child: suffix), + ], + ), + ), + Divider( + height: 1, + indent: 16, + endIndent: 16, + color: Color(0xAA282828), + ), + ], + ), + ), + ); + } +} + +/// Data model for [FinancialEntityCategoryView]. +class FinancialEntityCategoryModel { + final Color indicatorColor; + final double indicatorFraction; + final String title; + final String subtitle; + final double usdAmount; + final Widget suffix; + + const FinancialEntityCategoryModel( + this.indicatorColor, + this.indicatorFraction, + this.title, + this.subtitle, + this.usdAmount, + this.suffix, + ); +} + +FinancialEntityCategoryView buildFinancialEntityFromAccountData( + AccountData model, + int i, +) { + return FinancialEntityCategoryView( + suffix: Icon(Icons.chevron_right, color: Colors.grey), + title: model.name, + subtitle: '• • • • • • ${model.accountNumber.substring(6)}', + indicatorColor: RallyColors.accountColor(i), + indicatorFraction: 1.0, + amount: model.primaryAmount, + ); +} + +FinancialEntityCategoryView buildFinancialEntityFromBillData( + BillData model, + int i, +) { + return FinancialEntityCategoryView( + suffix: Icon(Icons.chevron_right, color: Colors.grey), + title: model.name, + subtitle: model.dueDate, + indicatorColor: RallyColors.billColor(i), + indicatorFraction: 1.0, + amount: model.primaryAmount, + ); +} + +FinancialEntityCategoryView buildFinancialEntityFromBudgetData( + BudgetData item, + int i, + BuildContext context, +) { + return FinancialEntityCategoryView( + suffix: Text(' LEFT', + style: Theme.of(context) + .textTheme + .body1 + .copyWith(color: RallyColors.gray60, fontSize: 10.0)), + title: item.name, + subtitle: Formatters.usdWithSign.format(item.amountUsed) + + ' / ' + + Formatters.usdWithSign.format(item.primaryAmount), + indicatorColor: RallyColors.budgetColor(i), + indicatorFraction: item.amountUsed / item.primaryAmount, + amount: item.primaryAmount - item.amountUsed, + ); +} + +List buildAccountDataListViews( + List items) { + return List.generate( + items.length, (i) => buildFinancialEntityFromAccountData(items[i], i)); +} + +List buildBillDataListViews(List items) { + return List.generate( + items.length, (i) => buildFinancialEntityFromBillData(items[i], i)); +} + +List buildBudgetDataListViews( + List items, BuildContext context) { + return [ + for (var 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((i) => _DetailedEventCard( + title: i.title, + subtitle: Formatters.date.format(i.date), + amount: i.amount, + )) + .toList(); + + return Scaffold( + appBar: AppBar( + elevation: 0.0, + centerTitle: true, + title: Text( + 'Checking', + style: Theme.of(context).textTheme.body1.copyWith(fontSize: 18.0), + ), + ), + body: Column(children: [ + SizedBox( + height: 200.0, + 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) { + return FlatButton( + onPressed: () {}, + child: SizedBox( + height: 68.0, + child: Column( + children: [ + SizedBox( + height: 67.0, + child: Row( + children: [ + Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: Theme.of(context) + .textTheme + .body1 + .copyWith(fontSize: 16.0), + ), + Text( + subtitle, + style: Theme.of(context) + .textTheme + .body1 + .copyWith(color: RallyColors.gray60), + ) + ], + ), + Spacer(), + Text( + '\$${Formatters.usd.format(amount)}', + style: Theme.of(context) + .textTheme + .body2 + .copyWith(fontSize: 20.0, color: RallyColors.gray), + ), + ], + ), + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: SizedBox( + height: 1.0, + child: Container( + color: Color(0xAA282828), + ), + ), + ) + ], + ), + ), + ); + } +} diff --git a/material_studies/rally/lib/formatters.dart b/material_studies/rally/lib/formatters.dart new file mode 100644 index 000000000..3d6f081c1 --- /dev/null +++ b/material_studies/rally/lib/formatters.dart @@ -0,0 +1,21 @@ +// 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'; + +class Formatters { + static final NumberFormat usd = NumberFormat.currency(name: ''); + static final NumberFormat usdWithSign = NumberFormat.currency(name: '\$'); + static final DateFormat date = DateFormat('MM-dd-yy'); +} diff --git a/material_studies/rally/lib/home.dart b/material_studies/rally/lib/home.dart new file mode 100644 index 000000000..bbb12bff2 --- /dev/null +++ b/material_studies/rally/lib/home.dart @@ -0,0 +1,221 @@ +// 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/widgets.dart'; +import 'package:rally/tabs/accounts.dart'; +import 'package:rally/tabs/bills.dart'; +import 'package:rally/tabs/budgets.dart'; +import 'package:rally/tabs/overview.dart'; +import 'package:rally/tabs/settings.dart'; + +const int tabCount = 5; + +class HomePage extends StatefulWidget { + @override + _HomePageState createState() => _HomePageState(); +} + +class _HomePageState extends State + with SingleTickerProviderStateMixin { + TabController _tabController; + + _HomePageState() { + _tabController = TabController(length: tabCount, vsync: this); + } + + @override + void initState() { + super.initState(); + print('_HomePageState initState'); + + _tabController.addListener(() { + if (_tabController.indexIsChanging && + _tabController.previousIndex != _tabController.index) { + setState(() {}); + } + }); + } + + @override + dispose() { + _tabController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final 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 removes the tab indicator. + indicatorColor: Colors.transparent, + ), + ), + Expanded( + child: TabBarView( + controller: _tabController, + children: _buildTabViews(), + ), + ) + ], + ), + ), + ); + } + + List _buildTabs(ThemeData theme) { + return [ + _buildTab(theme, Icons.pie_chart, 'OVERVIEW', 0), + _buildTab(theme, Icons.attach_money, 'ACCOUNTS', 1), + _buildTab(theme, Icons.money_off, 'BILLS', 2), + _buildTab(theme, Icons.table_chart, 'BUDGETS', 3), + _buildTab(theme, Icons.settings, 'SETTINGS', 4), + ]; + } + + List _buildTabViews() { + return [ + OverviewView(), + AccountsView(), + BillsView(), + BudgetsView(), + SettingsView(), + ]; + } + + Widget _buildTab( + ThemeData theme, + IconData iconData, + String title, + int index, + ) { + return _RallyTab( + theme.textTheme.button, + Icon(iconData), + title, + _tabController.index == index, + ); + } +} + +class _RallyTab extends StatefulWidget { + final TextStyle style; + final Text titleText; + final Icon icon; + final bool isExpanded; + + _RallyTab(TextStyle style, Icon icon, String title, bool isExpanded) + : this.style = style, + this.titleText = Text(title, style: style), + this.icon = icon, + this.isExpanded = isExpanded; + + _RallyTabState createState() => _RallyTabState(); +} + +class _RallyTabState extends State<_RallyTab> + with SingleTickerProviderStateMixin { + Animation _titleSizeAnimation; + Animation _titleFadeAnimation; + Animation _iconFadeAnimation; + AnimationController _controller; + + @override + initState() { + super.initState(); + _controller = AnimationController( + duration: 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.0; + } + } + + @override + void didUpdateWidget(_RallyTab oldWidget) { + super.didUpdateWidget(oldWidget); + if (widget.isExpanded) { + _controller.forward(); + } else { + _controller.reverse(); + } + } + + 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; + final 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 + dispose() { + _controller.dispose(); + super.dispose(); + } +} diff --git a/material_studies/rally/lib/login.dart b/material_studies/rally/lib/login.dart new file mode 100644 index 000000000..bf41a1071 --- /dev/null +++ b/material_studies/rally/lib/login.dart @@ -0,0 +1,68 @@ +// 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 _usernameController = TextEditingController(); + final _passwordController = TextEditingController(); + + @override + Widget build(BuildContext context) { + return Scaffold( + body: SafeArea( + child: GestureDetector( + onTap: () { + Navigator.pop(context); + }, + child: ListView( + padding: EdgeInsets.symmetric(horizontal: 24), + children: [ + Padding( + padding: const EdgeInsets.symmetric(vertical: 64), + child: SizedBox( + height: 160, + child: Image.asset('assets/logo.png'), + ), + ), + TextField( + controller: _usernameController, + decoration: InputDecoration( + labelText: 'Username', + ), + ), + SizedBox(height: 12), + TextField( + controller: _passwordController, + decoration: InputDecoration( + labelText: 'Password', + ), + obscureText: true, + ), + SizedBox( + height: 120, + child: Image.asset('assets/thumb.png'), + ), + ], + ), + ), + ), + ); + } +} diff --git a/material_studies/rally/lib/main.dart b/material_studies/rally/lib/main.dart new file mode 100644 index 000000000..c52246f5c --- /dev/null +++ b/material_studies/rally/lib/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/material.dart'; +import 'package:rally/app.dart'; + +void main() => runApp(RallyApp()); diff --git a/material_studies/rally/lib/tabs/accounts.dart b/material_studies/rally/lib/tabs/accounts.dart new file mode 100644 index 000000000..85797dd08 --- /dev/null +++ b/material_studies/rally/lib/tabs/accounts.dart @@ -0,0 +1,37 @@ +// 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/widgets.dart'; + +import 'package:rally/data.dart'; +import 'package:rally/finance.dart'; +import 'package: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) { + double balanceTotal = sumAccountDataPrimaryAmount(items); + return FinancialEntityView( + heroLabel: 'Total', + heroAmount: balanceTotal, + segments: buildSegmentsFromAccountItems(items), + wholeAmount: balanceTotal, + financialEntityCards: buildAccountDataListViews(items), + ); + } +} diff --git a/material_studies/rally/lib/tabs/bills.dart b/material_studies/rally/lib/tabs/bills.dart new file mode 100644 index 000000000..80ec924ca --- /dev/null +++ b/material_studies/rally/lib/tabs/bills.dart @@ -0,0 +1,43 @@ +// 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/widgets.dart'; + +import 'package:rally/data.dart'; +import 'package:rally/finance.dart'; +import 'package: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) { + double dueTotal = sumBillDataPrimaryAmount(items); + return FinancialEntityView( + heroLabel: 'Due', + heroAmount: dueTotal, + segments: buildSegmentsFromBillItems(items), + wholeAmount: dueTotal, + financialEntityCards: buildBillDataListViews(items), + ); + } +} diff --git a/material_studies/rally/lib/tabs/budgets.dart b/material_studies/rally/lib/tabs/budgets.dart new file mode 100644 index 000000000..ccac509be --- /dev/null +++ b/material_studies/rally/lib/tabs/budgets.dart @@ -0,0 +1,43 @@ +// 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/widgets.dart'; + +import 'package:rally/charts/pie_chart.dart'; +import 'package:rally/data.dart'; +import 'package: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) { + double capTotal = sumBudgetDataPrimaryAmount(items); + double usedTotal = sumBudgetDataAmountUsed(items); + return FinancialEntityView( + heroLabel: 'Left', + heroAmount: capTotal - usedTotal, + segments: buildSegmentsFromBudgetItems(items), + wholeAmount: capTotal, + financialEntityCards: buildBudgetDataListViews(items, context), + ); + } +} diff --git a/material_studies/rally/lib/tabs/overview.dart b/material_studies/rally/lib/tabs/overview.dart new file mode 100644 index 000000000..7861fc0d3 --- /dev/null +++ b/material_studies/rally/lib/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/widgets.dart'; + +import 'package:rally/colors.dart'; +import 'package:rally/data.dart'; +import 'package:rally/finance.dart'; +import 'package: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 accountDataList = DummyDataService.getAccountDataList(); + final billDataList = DummyDataService.getBillDataList(); + final budgetDataList = DummyDataService.getBudgetDataList(); + + return Padding( + padding: EdgeInsets.symmetric(horizontal: 16, vertical: 8), + child: ListView(children: [ + _AlertsView(), + SizedBox(height: 16), + _FinancialView( + title: 'Accounts', + total: sumAccountDataPrimaryAmount(accountDataList), + financialItemViews: buildAccountDataListViews(accountDataList), + ), + SizedBox(height: 16), + _FinancialView( + title: 'Bills', + total: sumBillDataPrimaryAmount(billDataList), + financialItemViews: buildBillDataListViews(billDataList), + ), + SizedBox(height: 16), + _FinancialView( + title: 'Budgets', + total: sumBudgetDataPrimaryAmount(budgetDataList), + financialItemViews: buildBudgetDataListViews(budgetDataList, context), + ), + SizedBox(height: 16), + ]), + ); + } +} + +class _AlertsView extends StatelessWidget { + @override + Widget build(BuildContext context) { + return Container( + padding: EdgeInsets.only(left: 16, top: 4, bottom: 4), + color: RallyColors.cardBackground, + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text('Alerts'), + FlatButton( + onPressed: () {}, + child: Text('SEE ALL'), + textColor: Colors.white, + ), + ], + ), + Container(color: RallyColors.primaryBackground, height: 1), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + 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 { + _FinancialView({this.title, this.total, this.financialItemViews}); + + final String title; + final double total; + final List financialItemViews; + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + return Container( + color: RallyColors.cardBackground, + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Padding( + padding: EdgeInsets.all(16), + child: Text(title), + ), + Padding( + padding: EdgeInsets.only(left: 16, right: 16), + child: Text( + Formatters.usdWithSign.format(total), + style: theme.textTheme.body2.copyWith( + fontSize: 44.0, + fontWeight: FontWeight.w600, + ), + ), + ), + ...financialItemViews.sublist(0, min(financialItemViews.length, 3)), + FlatButton( + child: Text('SEE ALL'), + textColor: Colors.white, + onPressed: () {}, + ), + ], + ), + ); + } +} diff --git a/material_studies/rally/lib/tabs/settings.dart b/material_studies/rally/lib/tabs/settings.dart new file mode 100644 index 000000000..757ef92e7 --- /dev/null +++ b/material_studies/rally/lib/tabs/settings.dart @@ -0,0 +1,54 @@ +// 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/widgets.dart'; + +import 'package:rally/data.dart'; + +class SettingsView extends StatefulWidget { + @override + _SettingsViewState createState() => _SettingsViewState(); +} + +class _SettingsViewState extends State { + List items = DummyDataService.getSettingsTitles() + .map((title) => _SettingsItem(title)) + .toList(); + + @override + Widget build(BuildContext context) { + return ListView(children: items); + } +} + +class _SettingsItem extends StatelessWidget { + _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: () {}, + ); + } +} diff --git a/material_studies/rally/pubspec.yaml b/material_studies/rally/pubspec.yaml new file mode 100644 index 000000000..9c2a993ad --- /dev/null +++ b/material_studies/rally/pubspec.yaml @@ -0,0 +1,39 @@ +name: rally +description: Rally +version: 1.0.0+1 + +environment: + sdk: ">=2.2.2 <3.0.0" + +dependencies: + flutter: + sdk: flutter + cupertino_icons: ^0.1.2 + intl: ^0.15.7 + +dev_dependencies: + flutter_test: + sdk: flutter + +flutter: + uses-material-design: true + assets: + - assets/logo.png + - assets/thumb.png + fonts: + - family: Roboto Condensed + fonts: + - asset: fonts/RobotoCondensed-Light.ttf + weight: 400 + - asset: fonts/RobotoCondensed-Regular.ttf + weight: 500 + - asset: fonts/RobotoCondensed-Bold.ttf + weight: 700 + - family: Eczar + fonts: + - asset: fonts/Eczar-Regular.ttf + weight: 400 + - asset: fonts/Eczar-SemiBold.ttf + weight: 600 + - asset: fonts/Eczar-Bold.ttf + weight: 700 diff --git a/material_studies/rally/test/widget_test.dart b/material_studies/rally/test/widget_test.dart new file mode 100644 index 000000000..eac78b4f8 --- /dev/null +++ b/material_studies/rally/test/widget_test.dart @@ -0,0 +1,18 @@ +// This is a basic Flutter widget test. +// +// To perform an interaction with a widget in your test, use the WidgetTester +// utility that Flutter provides. For example, you can send tap and scroll +// gestures. You can also use WidgetTester to find child widgets in the widget +// tree, read text, and verify that the values of widget properties are correct. + +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import 'package:rally/app.dart'; + +void main() { + testWidgets('Smoke test', (tester) async { + await tester.pumpWidget(RallyApp()); + expect(find.byType(MaterialApp), findsOneWidget); + }); +} diff --git a/travis_script.sh b/travis_script.sh index 4eb40cf86..da0347c3a 100755 --- a/travis_script.sh +++ b/travis_script.sh @@ -22,6 +22,7 @@ declare -a PROJECT_NAMES=( "platform_view_swift" \ "provider_counter" \ "provider_shopper" \ + "material_studies/rally" \ "material_studies/shrine" \ "veggieseasons" \ )