diff --git a/README.md b/README.md index a85425c264..0233d57b03 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ Codelabs MDC-101 through MDC-104 will guide you through building and integrating The starter and completed code is in the various branches of this repo. ## Getting Started -Visit the [Google codelabs site](https://codelabs.developers.google.com/), or [codelabs.developers.google.com/codelabs/mdc-101-flutter](https://codelabs.developers.google.com/codelabs/mdc-101-flutter), to follow along the guided steps. +Visit the [Google codelabs site](https://codelabs.developers.google.com/), or [codelabs.developers.google.com/codelabs/mdc-104-flutter](https://codelabs.developers.google.com/codelabs/mdc-104-flutter), to follow along the guided steps. ## Support diff --git a/mdc_100_series/lib/app.dart b/mdc_100_series/lib/app.dart index e46448369f..9213000518 100644 --- a/mdc_100_series/lib/app.dart +++ b/mdc_100_series/lib/app.dart @@ -14,39 +14,111 @@ import 'package:flutter/material.dart'; +import 'backdrop.dart'; +import 'colors.dart'; import 'home.dart'; import 'login.dart'; +import 'category_menu_page.dart'; +import 'model/product.dart'; +import 'supplemental/cut_corners_border.dart'; + +class ShrineApp extends StatefulWidget { + @override + _ShrineAppState createState() => _ShrineAppState(); +} + +class _ShrineAppState extends State { + Category _currentCategory = Category.all; -// TODO: Convert ShrineApp to stateful widget (104) -class ShrineApp extends StatelessWidget { - @override Widget build(BuildContext context) { return MaterialApp( title: 'Shrine', - // TODO: Change home: to a Backdrop with a HomePage frontLayer (104) - home: HomePage(), - // TODO: Make currentCategory field take _currentCategory (104) - // TODO: Pass _currentCategory for frontLayer (104) - // TODO: Change backLayer field value to CategoryMenuPage (104) + home: Backdrop( + currentCategory: _currentCategory, + frontLayer: HomePage(category: _currentCategory), + backLayer: CategoryMenuPage( + currentCategory: _currentCategory, + onCategoryTap: _onCategoryTap, + ), + frontTitle: Text('SHRINE'), + backTitle: Text('MENU'), + ), initialRoute: '/login', onGenerateRoute: _getRoute, - // TODO: Add a theme (103) + theme: _kShrineTheme, ); } - Route _getRoute(RouteSettings settings) { - if (settings.name != '/login') { - return null; - } + /// Function to call when a [Category] is tapped. + void _onCategoryTap(Category category) { + setState(() { + _currentCategory = category; + }); + } +} - return MaterialPageRoute( - settings: settings, - builder: (BuildContext context) => LoginPage(), - fullscreenDialog: true, - ); +Route _getRoute(RouteSettings settings) { + if (settings.name != '/login') { + return null; } + + return MaterialPageRoute( + settings: settings, + builder: (BuildContext context) => LoginPage(), + fullscreenDialog: true, + ); +} + +final ThemeData _kShrineTheme = _buildShrineTheme(); + +IconThemeData _customIconTheme(IconThemeData original) { + return original.copyWith(color: kShrineBrown900); } -// TODO: Build a Shrine Theme (103) -// TODO: Build a Shrine Text Theme (103) +ThemeData _buildShrineTheme() { + final ThemeData base = ThemeData.light(); + return base.copyWith( + accentColor: kShrineBrown900, + primaryColor: kShrinePink100, + buttonColor: kShrinePink100, + scaffoldBackgroundColor: kShrineBackgroundWhite, + cardColor: kShrineBackgroundWhite, + textSelectionColor: kShrinePink100, + errorColor: kShrineErrorRed, + buttonTheme: ButtonThemeData( + textTheme: ButtonTextTheme.accent, + ), + primaryIconTheme: base.iconTheme.copyWith(color: kShrineBrown900), + inputDecorationTheme: InputDecorationTheme( + border: CutCornersBorder(), + ), + textTheme: _buildShrineTextTheme(base.textTheme), + primaryTextTheme: _buildShrineTextTheme(base.primaryTextTheme), + accentTextTheme: _buildShrineTextTheme(base.accentTextTheme), + iconTheme: _customIconTheme(base.iconTheme), + ); +} + +TextTheme _buildShrineTextTheme(TextTheme base) { + return base.copyWith( + headline: base.headline.copyWith( + fontWeight: FontWeight.w500, + ), + title: base.title.copyWith( + fontSize: 18.0 + ), + caption: base.caption.copyWith( + fontWeight: FontWeight.w400, + fontSize: 14.0, + ), + body2: base.body2.copyWith( + fontWeight: FontWeight.w500, + fontSize: 16.0, + ), + ).apply( + fontFamily: 'Rubik', + displayColor: kShrineBrown900, + bodyColor: kShrineBrown900, + ); +} diff --git a/mdc_100_series/lib/backdrop.dart b/mdc_100_series/lib/backdrop.dart new file mode 100644 index 0000000000..250c4cea74 --- /dev/null +++ b/mdc_100_series/lib/backdrop.dart @@ -0,0 +1,294 @@ +// Copyright 2018-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:meta/meta.dart'; + +import 'model/product.dart'; +import 'login.dart'; + +double _kFlingVelocity = 2.0; + +class _FrontLayer extends StatelessWidget { + const _FrontLayer({ + 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(46.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 Function onPress; + final Widget frontTitle; + final Widget backTitle; + + const _BackdropTitle({ + Key key, + Listenable listenable, + this.onPress, + @required this.frontTitle, + @required this.backTitle, + }) : assert(frontTitle != null), + assert(backTitle != null), + 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, + child: Row(children: [ + // branded icon + SizedBox( + width: 72.0, + child: IconButton( + padding: EdgeInsets.only(right: 8.0), + onPressed: this.onPress, + icon: Stack(children: [ + Opacity( + opacity: animation.value, + child: ImageIcon(AssetImage('assets/slanted_menu.png')), + ), + FractionalTranslation( + translation: Tween( + begin: Offset.zero, + end: Offset(1.0, 0.0), + ).evaluate(animation), + child: ImageIcon(AssetImage('assets/diamond.png')), + )]), + ), + ), + // Here, we do a custom cross fade between backTitle and frontTitle. + // This makes a smooth animation between the two texts. + Stack( + children: [ + Opacity( + opacity: CurvedAnimation( + parent: ReverseAnimation(animation), + curve: Interval(0.5, 1.0), + ).value, + child: FractionalTranslation( + translation: Tween( + begin: Offset.zero, + end: Offset(0.5, 0.0), + ).evaluate(animation), + child: Semantics( + label: 'hide categories menu', + child: ExcludeSemantics(child: backTitle) + ), + ), + ), + Opacity( + opacity: CurvedAnimation( + parent: animation, + curve: Interval(0.5, 1.0), + ).value, + child: FractionalTranslation( + translation: Tween( + begin: Offset(-0.25, 0.0), + end: Offset.zero, + ).evaluate(animation), + child: Semantics( + label: 'show categories menu', + child: ExcludeSemantics(child: frontTitle) + ), + ), + ), + ], + ) + ]), + ); + } +} + +/// Builds a Backdrop. +/// +/// A Backdrop widget has two layers, front and back. The front layer is shown +/// by default, and slides down to show the back layer, from which a user +/// can make a selection. The user can also configure the titles for when the +/// front or back layer is showing. +class Backdrop extends StatefulWidget { + final Category currentCategory; + final Widget frontLayer; + final Widget backLayer; + final Widget frontTitle; + final Widget backTitle; + + const Backdrop({ + @required this.currentCategory, + @required this.frontLayer, + @required this.backLayer, + @required this.frontTitle, + @required this.backTitle, + }) : assert(currentCategory != null), + assert(frontLayer != null), + assert(backLayer != 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) { + _toggleBackdropLayerVisibility(); + } else if (!_frontLayerVisible) { + _controller.fling(velocity: _kFlingVelocity); + } + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + bool get _frontLayerVisible { + final AnimationStatus status = _controller.status; + return status == AnimationStatus.completed || + status == AnimationStatus.forward; + } + + void _toggleBackdropLayerVisibility() { + _controller.fling( + velocity: _frontLayerVisible ? -_kFlingVelocity : _kFlingVelocity); + } + + Widget _buildStack(BuildContext context, BoxConstraints constraints) { + double layerTitleHeight = 48.0; + final Size layerSize = constraints.biggest; + final double layerTop = layerSize.height - layerTitleHeight; + + Animation layerAnimation = RelativeRectTween( + begin: RelativeRect.fromLTRB( + 0.0, layerTop, 0.0, layerTop - layerSize.height), + end: RelativeRect.fromLTRB(0.0, 0.0, 0.0, 0.0), + ).animate(_controller.view); + + return Stack( + key: _backdropKey, + children: [ + ExcludeSemantics( + child: widget.backLayer, + excluding: _frontLayerVisible, + ), + PositionedTransition( + rect: layerAnimation, + child: _FrontLayer( + onTap: _toggleBackdropLayerVisibility, + child: widget.frontLayer, + ), + ), + ], + ); + } + + @override + Widget build(BuildContext context) { + var appBar = AppBar( + brightness: Brightness.light, + elevation: 0.0, + titleSpacing: 0.0, + title: _BackdropTitle( + listenable: _controller.view, + onPress: _toggleBackdropLayerVisibility, + frontTitle: widget.frontTitle, + backTitle: widget.backTitle, + ), + actions: [ + new IconButton( + icon: Icon( + Icons.search, + semanticLabel: 'login', + ), + onPressed: () { + Navigator.push( + context, + MaterialPageRoute(builder: (BuildContext context) => LoginPage()), + ); + }, + ), + new IconButton( + icon: Icon( + Icons.tune, + semanticLabel: 'login', + ), + onPressed: () { + Navigator.push( + context, + MaterialPageRoute(builder: (BuildContext context) => LoginPage()), + ); + }, + ), + ], + ); + return Scaffold( + appBar: appBar, + body: LayoutBuilder( + builder: _buildStack, + ), + ); + } +} diff --git a/mdc_100_series/lib/category_menu_page.dart b/mdc_100_series/lib/category_menu_page.dart new file mode 100644 index 0000000000..189e46bbe3 --- /dev/null +++ b/mdc_100_series/lib/category_menu_page.dart @@ -0,0 +1,82 @@ +// Copyright 2018-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:meta/meta.dart'; + +import 'colors.dart'; +import 'model/product.dart'; + +class CategoryMenuPage extends StatelessWidget { + final Category currentCategory; + final ValueChanged onCategoryTap; + final List _categories = Category.values; + + const CategoryMenuPage({ + Key key, + @required this.currentCategory, + @required this.onCategoryTap, + }) : assert(currentCategory != null), + assert(onCategoryTap != null); + + Widget _buildCategory(Category category, BuildContext context) { + final categoryString = + category.toString().replaceAll('Category.', '').toUpperCase(); + final ThemeData theme = Theme.of(context); + return GestureDetector( + onTap: () => onCategoryTap(category), + child: category == currentCategory + ? Column( + children: [ + SizedBox(height: 16.0), + Text( + categoryString, + style: theme.textTheme.body2, + textAlign: TextAlign.center, + ), + SizedBox(height: 14.0), + Container( + width: 70.0, + height: 2.0, + color: kShrinePink400, + ), + ], + ) + : Padding( + padding: EdgeInsets.symmetric(vertical: 16.0), + child: Text( + categoryString, + style: theme.textTheme.body2.copyWith( + color: kShrineBrown900.withAlpha(153) + ), + textAlign: TextAlign.center, + ), + ), + ); + } + + @override + Widget build(BuildContext context) { + return Center( + child: Container( + padding: EdgeInsets.only(top: 40.0), + color: kShrinePink100, + child: ListView( + children: _categories + .map((Category c) => _buildCategory(c, context)) + .toList()), + ), + ); + } +} diff --git a/mdc_100_series/lib/colors.dart b/mdc_100_series/lib/colors.dart new file mode 100644 index 0000000000..b1ecd49a9d --- /dev/null +++ b/mdc_100_series/lib/colors.dart @@ -0,0 +1,27 @@ +// Copyright 2018-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'; + +const kShrinePink50 = const Color(0xFFFEEAE6); +const kShrinePink100 = const Color(0xFFFEDBD0); +const kShrinePink300 = const Color(0xFFFBB8AC); +const kShrinePink400 = const Color(0xFFEAA4A4); + +const kShrineBrown900 = const Color(0xFF442B2D); + +const kShrineErrorRed = const Color(0xFFC5032B); + +const kShrineSurfaceWhite = const Color(0xFFFFFBFA); +const kShrineBackgroundWhite = Colors.white; \ No newline at end of file diff --git a/mdc_100_series/lib/home.dart b/mdc_100_series/lib/home.dart index e5b3637878..e44aeeb197 100644 --- a/mdc_100_series/lib/home.dart +++ b/mdc_100_series/lib/home.dart @@ -14,19 +14,17 @@ import 'package:flutter/material.dart'; +import 'model/products_repository.dart'; +import 'model/product.dart'; +import 'supplemental/asymmetric_view.dart'; + class HomePage extends StatelessWidget { - // TODO: Make a collection of cards (102) - // TODO: Add a variable for Category (104) + final Category category; + + const HomePage({this.category: Category.all}); + @override Widget build(BuildContext context) { - // TODO: Return an AsymmetricView (104) - // TODO: Pass Category variable to AsymmetricView (104) - return Scaffold( - // TODO: Add app bar (102) - // TODO: Add a grid view (102) - body: Center( - child: Text('You did it!'), - ), - ); + return AsymmetricView(products: ProductsRepository.loadProducts(category)); } } diff --git a/mdc_100_series/lib/login.dart b/mdc_100_series/lib/login.dart index 188438a4ca..6e19cea59c 100644 --- a/mdc_100_series/lib/login.dart +++ b/mdc_100_series/lib/login.dart @@ -14,13 +14,17 @@ import 'package:flutter/material.dart'; +import 'colors.dart'; + class LoginPage extends StatefulWidget { @override _LoginPageState createState() => _LoginPageState(); } class _LoginPageState extends State { - // TODO: Add text editing controllers (101) + final _usernameController = TextEditingController(); + final _passwordController = TextEditingController(); + @override Widget build(BuildContext context) { return Scaffold( @@ -33,15 +37,56 @@ class _LoginPageState extends State { children: [ Image.asset('assets/diamond.png'), SizedBox(height: 16.0), - Text('SHRINE'), + Text( + 'SHRINE', + style: Theme.of(context).textTheme.headline, + ), ], ), SizedBox(height: 120.0), - // TODO: Wrap Username with PrimaryColorOverride (103) - // TODO: Remove filled: true values (103) - // TODO: Wrap Password with PrimaryColorOverride (103) - // TODO: Add TextField widgets (101) - // TODO: Add button bar (101) + AccentColorOverride( + color: kShrineBrown900, + child: TextField( + controller: _usernameController, + decoration: InputDecoration( + labelText: 'Username', + ), + ), + ), + SizedBox(height: 12.0), + AccentColorOverride( + color: kShrineBrown900, + child: TextField( + controller: _passwordController, + decoration: InputDecoration( + labelText: 'Password', + ), + ), + ), + ButtonBar( + children: [ + FlatButton( + child: Text('CANCEL'), + shape: BeveledRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(7.0)), + ), + onPressed: () { + _usernameController.clear(); + _passwordController.clear(); + }, + ), + RaisedButton( + child: Text('NEXT'), + elevation: 8.0, + shape: BeveledRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(7.0)), + ), + onPressed: () { + Navigator.pop(context); + }, + ), + ], + ), ], ), ), @@ -49,4 +94,18 @@ class _LoginPageState extends State { } } -// TODO: Add PrimaryColorOverride (103) +class AccentColorOverride extends StatelessWidget { + const AccentColorOverride({Key key, this.color, this.child}) + : super(key: key); + + final Color color; + final Widget child; + + @override + Widget build(BuildContext context) { + return Theme( + child: child, + data: Theme.of(context).copyWith(accentColor: color), + ); + } +} diff --git a/mdc_100_series/lib/model/data.dart b/mdc_100_series/lib/model/data.dart deleted file mode 100755 index c19397f642..0000000000 --- a/mdc_100_series/lib/model/data.dart +++ /dev/null @@ -1,293 +0,0 @@ -// Copyright 2018-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 'product.dart'; - -List getProducts(Category category) { - const allProducts = [ - Product( - category: Category.accessories, - id: 0, - isFeatured: true, - name: 'Vagabond sack', - price: 120, - ), - Product( - category: Category.accessories, - id: 1, - isFeatured: true, - name: 'Stella sunglasses', - price: 58, - ), - Product( - category: Category.accessories, - id: 2, - isFeatured: false, - name: 'Whitney belt', - price: 35, - ), - Product( - category: Category.accessories, - id: 3, - isFeatured: true, - name: 'Garden strand', - price: 98, - ), - Product( - category: Category.accessories, - id: 4, - isFeatured: false, - name: 'Strut earrings', - price: 34, - ), - Product( - category: Category.accessories, - id: 5, - isFeatured: false, - name: 'Varsity socks', - price: 12, - ), - Product( - category: Category.accessories, - id: 6, - isFeatured: false, - name: 'Weave keyring', - price: 16, - ), - Product( - category: Category.accessories, - id: 7, - isFeatured: true, - name: 'Gatsby hat', - price: 40, - ), - Product( - category: Category.accessories, - id: 8, - isFeatured: true, - name: 'Shrug bag', - price: 198, - ), - Product( - category: Category.home, - id: 9, - isFeatured: true, - name: 'Gilt desk trio', - price: 58, - ), - Product( - category: Category.home, - id: 10, - isFeatured: false, - name: 'Copper wire rack', - price: 18, - ), - Product( - category: Category.home, - id: 11, - isFeatured: false, - name: 'Soothe ceramic set', - price: 28, - ), - Product( - category: Category.home, - id: 12, - isFeatured: false, - name: 'Hurrahs tea set', - price: 34, - ), - Product( - category: Category.home, - id: 13, - isFeatured: true, - name: 'Blue stone mug', - price: 18, - ), - Product( - category: Category.home, - id: 14, - isFeatured: true, - name: 'Rainwater tray', - price: 27, - ), - Product( - category: Category.home, - id: 15, - isFeatured: true, - name: 'Chambray napkins', - price: 16, - ), - Product( - category: Category.home, - id: 16, - isFeatured: true, - name: 'Succulent planters', - price: 16, - ), - Product( - category: Category.home, - id: 17, - isFeatured: false, - name: 'Quartet table', - price: 175, - ), - Product( - category: Category.home, - id: 18, - isFeatured: true, - name: 'Kitchen quattro', - price: 129, - ), - Product( - category: Category.clothing, - id: 19, - isFeatured: false, - name: 'Clay sweater', - price: 48, - ), - Product( - category: Category.clothing, - id: 20, - isFeatured: false, - name: 'Sea tunic', - price: 45, - ), - Product( - category: Category.clothing, - id: 21, - isFeatured: false, - name: 'Plaster tunic', - price: 38, - ), - Product( - category: Category.clothing, - id: 22, - isFeatured: false, - name: 'White pinstripe shirt', - price: 70, - ), - Product( - category: Category.clothing, - id: 23, - isFeatured: false, - name: 'Chambray shirt', - price: 70, - ), - Product( - category: Category.clothing, - id: 24, - isFeatured: true, - name: 'Seabreeze sweater', - price: 60, - ), - Product( - category: Category.clothing, - id: 25, - isFeatured: false, - name: 'Gentry jacket', - price: 178, - ), - Product( - category: Category.clothing, - id: 26, - isFeatured: false, - name: 'Navy trousers', - price: 74, - ), - Product( - category: Category.clothing, - id: 27, - isFeatured: true, - name: 'Walter henley (white)', - price: 38, - ), - Product( - category: Category.clothing, - id: 28, - isFeatured: true, - name: 'Surf and perf shirt', - price: 48, - ), - Product( - category: Category.clothing, - id: 29, - isFeatured: true, - name: 'Ginger scarf', - price: 98, - ), - Product( - category: Category.clothing, - id: 30, - isFeatured: true, - name: 'Ramona crossover', - price: 68, - ), - Product( - category: Category.clothing, - id: 31, - isFeatured: false, - name: 'Chambray shirt', - price: 38, - ), - Product( - category: Category.clothing, - id: 32, - isFeatured: false, - name: 'Classic white collar', - price: 58, - ), - Product( - category: Category.clothing, - id: 33, - isFeatured: true, - name: 'Cerise scallop tee', - price: 42, - ), - Product( - category: Category.clothing, - id: 34, - isFeatured: false, - name: 'Shoulder rolls tee', - price: 27, - ), - Product( - category: Category.clothing, - id: 35, - isFeatured: false, - name: 'Grey slouch tank', - price: 24, - ), - Product( - category: Category.clothing, - id: 36, - isFeatured: false, - name: 'Sunshirt dress', - price: 58, - ), - Product( - category: Category.clothing, - id: 37, - isFeatured: true, - name: 'Fine lines tee', - 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 index 5df0ad197b..abca50ab83 100755 --- 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/model/products_repository.dart b/mdc_100_series/lib/model/products_repository.dart new file mode 100755 index 0000000000..a1824e8907 --- /dev/null +++ b/mdc_100_series/lib/model/products_repository.dart @@ -0,0 +1,295 @@ +// Copyright 2018-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 'product.dart'; + +class ProductsRepository { + static List loadProducts(Category category) { + const allProducts = [ + Product( + category: Category.accessories, + id: 0, + isFeatured: true, + name: 'Vagabond sack', + price: 120, + ), + Product( + category: Category.accessories, + id: 1, + isFeatured: true, + name: 'Stella sunglasses', + price: 58, + ), + Product( + category: Category.accessories, + id: 2, + isFeatured: false, + name: 'Whitney belt', + price: 35, + ), + Product( + category: Category.accessories, + id: 3, + isFeatured: true, + name: 'Garden strand', + price: 98, + ), + Product( + category: Category.accessories, + id: 4, + isFeatured: false, + name: 'Strut earrings', + price: 34, + ), + Product( + category: Category.accessories, + id: 5, + isFeatured: false, + name: 'Varsity socks', + price: 12, + ), + Product( + category: Category.accessories, + id: 6, + isFeatured: false, + name: 'Weave keyring', + price: 16, + ), + Product( + category: Category.accessories, + id: 7, + isFeatured: true, + name: 'Gatsby hat', + price: 40, + ), + Product( + category: Category.accessories, + id: 8, + isFeatured: true, + name: 'Shrug bag', + price: 198, + ), + Product( + category: Category.home, + id: 9, + isFeatured: true, + name: 'Gilt desk trio', + price: 58, + ), + Product( + category: Category.home, + id: 10, + isFeatured: false, + name: 'Copper wire rack', + price: 18, + ), + Product( + category: Category.home, + id: 11, + isFeatured: false, + name: 'Soothe ceramic set', + price: 28, + ), + Product( + category: Category.home, + id: 12, + isFeatured: false, + name: 'Hurrahs tea set', + price: 34, + ), + Product( + category: Category.home, + id: 13, + isFeatured: true, + name: 'Blue stone mug', + price: 18, + ), + Product( + category: Category.home, + id: 14, + isFeatured: true, + name: 'Rainwater tray', + price: 27, + ), + Product( + category: Category.home, + id: 15, + isFeatured: true, + name: 'Chambray napkins', + price: 16, + ), + Product( + category: Category.home, + id: 16, + isFeatured: true, + name: 'Succulent planters', + price: 16, + ), + Product( + category: Category.home, + id: 17, + isFeatured: false, + name: 'Quartet table', + price: 175, + ), + Product( + category: Category.home, + id: 18, + isFeatured: true, + name: 'Kitchen quattro', + price: 129, + ), + Product( + category: Category.clothing, + id: 19, + isFeatured: false, + name: 'Clay sweater', + price: 48, + ), + Product( + category: Category.clothing, + id: 20, + isFeatured: false, + name: 'Sea tunic', + price: 45, + ), + Product( + category: Category.clothing, + id: 21, + isFeatured: false, + name: 'Plaster tunic', + price: 38, + ), + Product( + category: Category.clothing, + id: 22, + isFeatured: false, + name: 'White pinstripe shirt', + price: 70, + ), + Product( + category: Category.clothing, + id: 23, + isFeatured: false, + name: 'Chambray shirt', + price: 70, + ), + Product( + category: Category.clothing, + id: 24, + isFeatured: true, + name: 'Seabreeze sweater', + price: 60, + ), + Product( + category: Category.clothing, + id: 25, + isFeatured: false, + name: 'Gentry jacket', + price: 178, + ), + Product( + category: Category.clothing, + id: 26, + isFeatured: false, + name: 'Navy trousers', + price: 74, + ), + Product( + category: Category.clothing, + id: 27, + isFeatured: true, + name: 'Walter henley (white)', + price: 38, + ), + Product( + category: Category.clothing, + id: 28, + isFeatured: true, + name: 'Surf and perf shirt', + price: 48, + ), + Product( + category: Category.clothing, + id: 29, + isFeatured: true, + name: 'Ginger scarf', + price: 98, + ), + Product( + category: Category.clothing, + id: 30, + isFeatured: true, + name: 'Ramona crossover', + price: 68, + ), + Product( + category: Category.clothing, + id: 31, + isFeatured: false, + name: 'Chambray shirt', + price: 38, + ), + Product( + category: Category.clothing, + id: 32, + isFeatured: false, + name: 'Classic white collar', + price: 58, + ), + Product( + category: Category.clothing, + id: 33, + isFeatured: true, + name: 'Cerise scallop tee', + price: 42, + ), + Product( + category: Category.clothing, + id: 34, + isFeatured: false, + name: 'Shoulder rolls tee', + price: 27, + ), + Product( + category: Category.clothing, + id: 35, + isFeatured: false, + name: 'Grey slouch tank', + price: 24, + ), + Product( + category: Category.clothing, + id: 36, + isFeatured: false, + name: 'Sunshirt dress', + price: 58, + ), + Product( + category: Category.clothing, + id: 37, + isFeatured: true, + name: 'Fine lines tee', + 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/supplemental/product_columns.dart b/mdc_100_series/lib/supplemental/product_columns.dart index ad6be75006..f6a96eccc7 100644 --- a/mdc_100_series/lib/supplemental/product_columns.dart +++ b/mdc_100_series/lib/supplemental/product_columns.dart @@ -29,40 +29,40 @@ class TwoProductCardColumn extends StatelessWidget { Widget build(BuildContext context) { return LayoutBuilder( builder: (BuildContext context, BoxConstraints constraints) { - const spacerHeight = 44.0; + const spacerHeight = 44.0; - double heightOfCards = (constraints.biggest.height - spacerHeight) / 2.0; - double heightOfImages = heightOfCards - ProductCard.kTextBoxHeight; - // TODO: Change imageAspectRatio calculation (104) - double imageAspectRatio = constraints.biggest.width / heightOfImages; + double heightOfCards = (constraints.biggest.height - spacerHeight) / 2.0; + double heightOfImages = heightOfCards - ProductCard.kTextBoxHeight; + double imageAspectRatio = + (heightOfImages >= 0.0 && constraints.biggest.width > heightOfImages) + ? constraints.biggest.width / heightOfImages + : 33 / 49; - // TODO: Replace Column with a ListView (104) - return Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Padding( - padding: EdgeInsetsDirectional.only(start: 28.0), - child: top != null - ? ProductCard( - imageAspectRatio: imageAspectRatio, - product: top, - ) - : SizedBox( - height: heightOfCards, - ), - ), - SizedBox(height: spacerHeight), - Padding( - padding: EdgeInsetsDirectional.only(end: 28.0), - child: ProductCard( - imageAspectRatio: imageAspectRatio, - product: bottom, - ), - ), - ], - ); - }); + return ListView( + physics: ClampingScrollPhysics(), + children: [ + Padding( + padding: EdgeInsetsDirectional.only(start: 28.0), + child: top != null + ? ProductCard( + imageAspectRatio: imageAspectRatio, + product: top, + ) + : SizedBox( + height: heightOfCards > 0 ? heightOfCards : spacerHeight, + ), + ), + SizedBox(height: spacerHeight), + Padding( + padding: EdgeInsetsDirectional.only(end: 28.0), + child: ProductCard( + imageAspectRatio: imageAspectRatio, + product: bottom, + ), + ), + ], + ); + }); } } @@ -73,16 +73,16 @@ class OneProductCardColumn extends StatelessWidget { @override Widget build(BuildContext context) { - // TODO: Replace Column with a ListView (104) - return Column( - mainAxisAlignment: MainAxisAlignment.end, + return ListView( + reverse: true, + physics: ClampingScrollPhysics(), children: [ - ProductCard( - product: product, - ), SizedBox( height: 40.0, ), + ProductCard( + product: product, + ), ], ); } diff --git a/mdc_100_series/pubspec.yaml b/mdc_100_series/pubspec.yaml index 2bbb1312d3..1ee3ead872 100644 --- a/mdc_100_series/pubspec.yaml +++ b/mdc_100_series/pubspec.yaml @@ -1,5 +1,5 @@ name: Shrine -description: Learn the basics of using Material Components by building a simple app with core components. +description: Take your design up a notch and learn to use our advanced component backdrop menu. dependencies: flutter: @@ -17,6 +17,7 @@ flutter: uses-material-design: true assets: - assets/diamond.png + - assets/slanted_menu.png - packages/shrine_images/0-0.jpg - packages/shrine_images/1-0.jpg - packages/shrine_images/2-0.jpg @@ -55,3 +56,9 @@ flutter: - packages/shrine_images/35-0.jpg - packages/shrine_images/36-0.jpg - packages/shrine_images/37-0.jpg + fonts: + - family: Rubik + fonts: + - asset: fonts/Rubik-Regular.ttf + - asset: fonts/Rubik-Medium.ttf + weight: 500