diff --git a/mdc_100_series/lib/app.dart b/mdc_100_series/lib/app.dart index 37d7380d7c..287c1e5e7e 100644 --- a/mdc_100_series/lib/app.dart +++ b/mdc_100_series/lib/app.dart @@ -14,38 +14,68 @@ import 'package:flutter/material.dart'; +import 'backdrop.dart'; import 'colors.dart'; import 'home.dart'; import 'login.dart'; +import 'menu_page.dart'; +import 'model/product.dart'; import 'supplemental/cut_corners_border.dart'; -class ShrineApp extends StatelessWidget { +class ShrineApp extends StatefulWidget { + @override + _ShrineAppState createState() => _ShrineAppState(); +} + +class _ShrineAppState extends State { + Category _currentCategory = Category.all; @override Widget build(BuildContext context) { return MaterialApp( title: 'Shrine', - home: HomePage(), + home: Backdrop( + currentCategory: _currentCategory, + frontPanel: HomePage(category: _currentCategory), + backPanel: MenuPage( + currentCategory: _currentCategory, + onCategoryTap: _onCategoryTap, + ), + frontTitle: Text('SHRINE'), + backTitle: Text('MENU'), + ), + initialRoute: '/login', onGenerateRoute: _getRoute, theme: _kShrineTheme, ); } - Route _getRoute(RouteSettings settings) { - if (settings.name == '/login') { - return new MaterialPageRoute( - settings: settings, - builder: (BuildContext context) => LoginPage(), - fullscreenDialog: true, - ); - } + /// Function to call when a [Category] is tapped. + void _onCategoryTap(Category category) { + setState(() { + _currentCategory = category; + }); + } +} - return null; +Route _getRoute(RouteSettings settings) { + if (settings.name == '/login') { + return MaterialPageRoute( + settings: settings, + builder: (BuildContext context) => LoginPage(), + fullscreenDialog: true, + ); } + + return null; } final ThemeData _kShrineTheme = _buildShrineTheme(); +IconThemeData _customIconTheme(IconThemeData original) { + return original.copyWith(color: kShrineBrown900); +} + ThemeData _buildShrineTheme() { final ThemeData base = ThemeData.light(); return base.copyWith( @@ -68,6 +98,7 @@ ThemeData _buildShrineTheme() { textTheme: _buildShrineTextTheme(base.textTheme), primaryTextTheme: _buildShrineTextTheme(base.primaryTextTheme), accentTextTheme: _buildShrineTextTheme(base.accentTextTheme), + iconTheme: _customIconTheme(base.iconTheme), ); } @@ -83,6 +114,10 @@ TextTheme _buildShrineTextTheme(TextTheme base) { fontWeight: FontWeight.w400, fontSize: 14.0, ), + body2: base.body2.copyWith( + fontWeight: FontWeight.w500, + fontSize: 16.0, + ), ).apply( fontFamily: 'Rubik', displayColor: kShrineBrown900, diff --git a/mdc_100_series/lib/backdrop.dart b/mdc_100_series/lib/backdrop.dart new file mode 100644 index 0000000000..17a0dbbd96 --- /dev/null +++ b/mdc_100_series/lib/backdrop.dart @@ -0,0 +1,242 @@ +// Copyright 2018 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/material.dart'; +import 'package:meta/meta.dart'; + +import 'model/product.dart'; +import 'login.dart'; + +const double _kFlingVelocity = 2.0; + +class _BackdropPanel extends StatelessWidget { + const _BackdropPanel({ + Key key, + this.onTap, + this.child, + }) : super(key: key); + + final VoidCallback onTap; + final Widget child; + + @override + Widget build(BuildContext context) { + return Material( + elevation: 16.0, + shape: BeveledRectangleBorder( + borderRadius: BorderRadius.only(topLeft: Radius.circular(64.0)), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: onTap, + child: Container( + height: 40.0, + alignment: AlignmentDirectional.centerStart, + ), + ), + Expanded( + child: child, + ), + ], + ), + ); + } +} + +class _BackdropTitle extends AnimatedWidget { + final Widget frontTitle; + final Widget backTitle; + + const _BackdropTitle({ + Key key, + Listenable listenable, + this.frontTitle, + this.backTitle, + }) : super(key: key, listenable: listenable); + + @override + Widget build(BuildContext context) { + final Animation animation = this.listenable; + return DefaultTextStyle( + style: Theme.of(context).primaryTextTheme.title, + softWrap: false, + overflow: TextOverflow.ellipsis, + // Here, we do a custom cross fade between backTitle and frontTitle. + // This makes a smooth animation between the two texts. + child: Stack( + children: [ + Opacity( + opacity: CurvedAnimation( + parent: ReverseAnimation(animation), + curve: Interval(0.5, 1.0), + ).value, + child: backTitle, + ), + Opacity( + opacity: CurvedAnimation( + parent: animation, + curve: Interval(0.5, 1.0), + ).value, + child: frontTitle, + ), + ], + ), + ); + } +} + +/// Builds a Backdrop. +/// +/// A Backdrop widget has two panels, front and back. The front panel is shown +/// by default, and slides down to show the back panel, from which a user +/// can make a selection. The user can also configure the titles for when the +/// front or back panel is showing. +class Backdrop extends StatefulWidget { + final Category currentCategory; + final Widget frontPanel; + final Widget backPanel; + final Widget frontTitle; + final Widget backTitle; + + const Backdrop({ + @required this.currentCategory, + @required this.frontPanel, + @required this.backPanel, + @required this.frontTitle, + @required this.backTitle, + }) : assert(currentCategory != null), + assert(frontPanel != null), + assert(backPanel != null), + assert(frontTitle != null), + assert(backTitle != null); + + @override + _BackdropState createState() => _BackdropState(); +} + +class _BackdropState extends State + with SingleTickerProviderStateMixin { + final GlobalKey _backdropKey = GlobalKey(debugLabel: 'Backdrop'); + AnimationController _controller; + + @override + void initState() { + super.initState(); + _controller = AnimationController( + duration: Duration(milliseconds: 300), + value: 1.0, + vsync: this, + ); + } + + @override + void didUpdateWidget(Backdrop old) { + super.didUpdateWidget(old); + if (widget.currentCategory != old.currentCategory) { + setState(() { + _controller.fling( + velocity: + _backdropPanelVisible ? -_kFlingVelocity : _kFlingVelocity); + }); + } else if (!_backdropPanelVisible) { + setState(() { + _controller.fling(velocity: _kFlingVelocity); + }); + } + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + bool get _backdropPanelVisible { + final AnimationStatus status = _controller.status; + return status == AnimationStatus.completed || + status == AnimationStatus.forward; + } + + void _toggleBackdropPanelVisibility() { + _controller.fling( + velocity: _backdropPanelVisible ? -_kFlingVelocity : _kFlingVelocity); + } + + Widget _buildStack(BuildContext context, BoxConstraints constraints) { + const double panelTitleHeight = 48.0; + final Size panelSize = constraints.biggest; + final double panelTop = panelSize.height - panelTitleHeight; + + Animation panelAnimation = RelativeRectTween( + begin: RelativeRect.fromLTRB( + 0.0, panelTop, 0.0, panelTop - panelSize.height), + end: RelativeRect.fromLTRB(0.0, 0.0, 0.0, 0.0), + ).animate(_controller.view); + + return Container( + key: _backdropKey, + child: Stack( + children: [ + widget.backPanel, + PositionedTransition( + rect: panelAnimation, + child: _BackdropPanel( + onTap: _toggleBackdropPanelVisibility, + child: widget.frontPanel, + ), + ), + ], + ), + ); + } + + @override + Widget build(BuildContext context) { + var appBar = AppBar( + brightness: Brightness.light, + elevation: 0.0, + leading: IconButton( + onPressed: _toggleBackdropPanelVisibility, + icon: AnimatedIcon( + icon: AnimatedIcons.close_menu, + progress: _controller.view, + ), + ), + title: _BackdropTitle( + listenable: _controller.view, + frontTitle: widget.frontTitle, + backTitle: widget.backTitle, + ), + actions: [ + new IconButton( + icon: const Icon(Icons.search), + onPressed: () { + Navigator.push( + context, + MaterialPageRoute(builder: (BuildContext context) => LoginPage()), + ); + }, + ), + new IconButton( + icon: const Icon(Icons.tune), + onPressed: () { + Navigator.push( + context, + MaterialPageRoute(builder: (BuildContext context) => LoginPage()), + ); + }, + ), + ], + ); + return Scaffold( + appBar: appBar, + body: LayoutBuilder( + builder: _buildStack, + ), + ); + } +} diff --git a/mdc_100_series/lib/home.dart b/mdc_100_series/lib/home.dart index c1a8df2e2c..bf28747bcf 100644 --- a/mdc_100_series/lib/home.dart +++ b/mdc_100_series/lib/home.dart @@ -14,38 +14,17 @@ import 'package:flutter/material.dart'; +import 'model/product.dart'; import 'model/data.dart'; import 'supplemental/asymmetric_view.dart'; class HomePage extends StatelessWidget { + final Category category; + + const HomePage({this.category: Category.all}); + @override Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - brightness: Brightness.light, - leading: IconButton( - icon: Icon(Icons.menu), - onPressed: () { - print('Menu button'); - }, - ), - title: Text('SHRINE'), - actions: [ - IconButton( - icon: Icon(Icons.search), - onPressed: () { - print('Search button'); - }, - ), - IconButton( - icon: Icon(Icons.tune), - onPressed: () { - print('Filter button'); - }, - ), - ], - ), - body: AsymmetricView(products: getAllProducts()), - ); + return AsymmetricView(products: getProducts(category)); } } diff --git a/mdc_100_series/lib/menu_page.dart b/mdc_100_series/lib/menu_page.dart new file mode 100644 index 0000000000..a2a85fcb1d --- /dev/null +++ b/mdc_100_series/lib/menu_page.dart @@ -0,0 +1,68 @@ +import 'package:flutter/material.dart'; +import 'package:meta/meta.dart'; + +import 'model/product.dart'; +import 'colors.dart'; + +class MenuPage extends StatelessWidget { + final Category currentCategory; + final ValueChanged onCategoryTap; + final List _categories = Category.values; + + const MenuPage({ + Key key, + @required this.currentCategory, + this.onCategoryTap, + }) : assert(currentCategory != null); + + Widget _buildCategory(Category category, BuildContext context) { + var categoryString = + category.toString().replaceAll('Category.', '').toUpperCase(); + return GestureDetector( + onTap: () => onCategoryTap(category), + child: category == currentCategory + ? Column( + children: [ + SizedBox(height: 16.0), + Text( + categoryString, + style: Theme.of(context).textTheme.body2, + textAlign: TextAlign.center, + ), + SizedBox(height: 14.0), + Container( + width: 70.0, + height: 2.0, + color: Color(0xFFEAA4A4), + ), + ], + ) + : Container( + padding: EdgeInsets.symmetric(vertical: 16.0), + child: Text( + categoryString, + style: Theme.of(context).textTheme.body2.copyWith( + color: kShrineBrown900.withAlpha(153), + ), + textAlign: TextAlign.center, + ), + ), + ); + } + + @override + Widget build(BuildContext context) { + var menuItems = []; + _categories.forEach((Category c) { + menuItems.add(_buildCategory(c, context)); + }); + + return Center( + child: Container( + padding: EdgeInsets.only(top: 40.0), + color: kShrinePink100, + child: ListView(children: menuItems), + ), + ); + } +} diff --git a/mdc_100_series/lib/model/data.dart b/mdc_100_series/lib/model/data.dart old mode 100755 new mode 100644 index bc64bf4c23..4c3a4aee16 --- a/mdc_100_series/lib/model/data.dart +++ b/mdc_100_series/lib/model/data.dart @@ -14,8 +14,8 @@ import 'product.dart'; -List getAllProducts() { - return const [ +List getProducts(Category category) { + const allProducts = [ Product( category: Category.accessories, id: 0, @@ -283,4 +283,11 @@ List getAllProducts() { price: 58, ), ]; + if (category == Category.all) { + return allProducts; + } else { + return allProducts.where((Product p) { + return p.category == category; + }).toList(); + } } diff --git a/mdc_100_series/lib/model/product.dart b/mdc_100_series/lib/model/product.dart old mode 100755 new mode 100644 index 5df0ad197b..abca50ab83 --- a/mdc_100_series/lib/model/product.dart +++ b/mdc_100_series/lib/model/product.dart @@ -39,5 +39,5 @@ class Product { String get assetPackage => 'shrine_images'; @override - String toString() => "$name (id=$id)"; + String toString() => '$name (id=$id)'; } diff --git a/mdc_100_series/lib/supplemental/product_columns.dart b/mdc_100_series/lib/supplemental/product_columns.dart index 51523042fd..8f3ec376dc 100644 --- a/mdc_100_series/lib/supplemental/product_columns.dart +++ b/mdc_100_series/lib/supplemental/product_columns.dart @@ -33,11 +33,12 @@ class TwoProductCardColumn extends StatelessWidget { double heightOfCards = (constraints.biggest.height - spacerHeight) / 2.0; double heightOfImages = heightOfCards - ProductCard.kTextBoxHeight; - double imageAspectRatio = constraints.biggest.width / heightOfImages; + double imageAspectRatio = + (heightOfImages >= 0.0 && constraints.biggest.width > heightOfImages) + ? constraints.biggest.width / heightOfImages + : 33 / 49; - return Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, + return ListView( children: [ Container( padding: EdgeInsetsDirectional.only(start: 28.0), @@ -71,15 +72,15 @@ class OneProductCardColumn extends StatelessWidget { @override Widget build(BuildContext context) { - return Column( - mainAxisAlignment: MainAxisAlignment.end, + return ListView( + reverse: true, children: [ - ProductCard( - product: product, - ), SizedBox( height: 40.0, ), + ProductCard( + product: product, + ), ], ); } diff --git a/mdc_100_series/mdc_100_series.iml b/mdc_100_series/mdc_100_series.iml index 485a35d430..104f7fb6b8 100644 --- a/mdc_100_series/mdc_100_series.iml +++ b/mdc_100_series/mdc_100_series.iml @@ -1,17 +1,22 @@ + + + + + + - - - + + - \ No newline at end of file