From 309b9f80103dbe61a67c11ed4b8cd3282ba775cc Mon Sep 17 00:00:00 2001 From: Hans Muller Date: Thu, 2 Jun 2016 14:23:20 -0700 Subject: [PATCH] Version 0.0 of a gallery demo of the Material Design "Shrine" app (#4327) --- dev/benchmarks/complex_layout/pubspec.yaml | 2 +- examples/flutter_gallery/flutter.yaml | 5 +- examples/flutter_gallery/lib/demo/all.dart | 1 + .../lib/demo/shrine/shrine_data.dart | 264 ++++++++++++++++ .../lib/demo/shrine/shrine_home.dart | 295 ++++++++++++++++++ .../lib/demo/shrine/shrine_order.dart | 227 ++++++++++++++ .../lib/demo/shrine/shrine_page.dart | 60 ++++ .../lib/demo/shrine/shrine_theme.dart | 47 +++ .../lib/demo/shrine/shrine_types.dart | 103 ++++++ .../flutter_gallery/lib/demo/shrine_demo.dart | 46 +++ examples/flutter_gallery/lib/gallery/app.dart | 1 + .../flutter_gallery/lib/gallery/home.dart | 1 + examples/flutter_gallery/pubspec.yaml | 2 +- .../flutter/lib/src/material/app_bar.dart | 2 +- packages/flutter/lib/src/material/card.dart | 10 +- .../flutter/lib/src/widgets/navigator.dart | 17 +- 16 files changed, 1070 insertions(+), 13 deletions(-) create mode 100644 examples/flutter_gallery/lib/demo/shrine/shrine_data.dart create mode 100644 examples/flutter_gallery/lib/demo/shrine/shrine_home.dart create mode 100644 examples/flutter_gallery/lib/demo/shrine/shrine_order.dart create mode 100644 examples/flutter_gallery/lib/demo/shrine/shrine_page.dart create mode 100644 examples/flutter_gallery/lib/demo/shrine/shrine_theme.dart create mode 100644 examples/flutter_gallery/lib/demo/shrine/shrine_types.dart create mode 100644 examples/flutter_gallery/lib/demo/shrine_demo.dart diff --git a/dev/benchmarks/complex_layout/pubspec.yaml b/dev/benchmarks/complex_layout/pubspec.yaml index 3f4be250a..45e2bd850 100644 --- a/dev/benchmarks/complex_layout/pubspec.yaml +++ b/dev/benchmarks/complex_layout/pubspec.yaml @@ -6,7 +6,7 @@ dependencies: path: ../../../packages/flutter flutter_driver: path: ../../../packages/flutter_driver - flutter_gallery_assets: '0.0.16' + flutter_gallery_assets: '0.0.18' dev_dependencies: flutter_test: diff --git a/examples/flutter_gallery/flutter.yaml b/examples/flutter_gallery/flutter.yaml index 7a2ca1199..3f1a057d1 100644 --- a/examples/flutter_gallery/flutter.yaml +++ b/examples/flutter_gallery/flutter.yaml @@ -37,5 +37,8 @@ assets: - packages/flutter_gallery_assets/landscape_9.jpg - packages/flutter_gallery_assets/landscape_10.jpg - packages/flutter_gallery_assets/landscape_11.jpg - - packages/flutter_gallery_assets/shadow.png - lib/gallery/example_code.dart +fonts: + - family: AbrilFatface + fonts: + - asset: packages/flutter_gallery_assets/shrine/fonts/abrilfatface/AbrilFatface-Regular.ttf diff --git a/examples/flutter_gallery/lib/demo/all.dart b/examples/flutter_gallery/lib/demo/all.dart index 56446e061..60217b2ed 100644 --- a/examples/flutter_gallery/lib/demo/all.dart +++ b/examples/flutter_gallery/lib/demo/all.dart @@ -25,6 +25,7 @@ export 'persistent_bottom_sheet_demo.dart'; export 'progress_indicator_demo.dart'; export 'scrollable_tabs_demo.dart'; export 'selection_controls_demo.dart'; +export 'shrine_demo.dart'; export 'slider_demo.dart'; export 'snack_bar_demo.dart'; export 'tabs_demo.dart'; diff --git a/examples/flutter_gallery/lib/demo/shrine/shrine_data.dart b/examples/flutter_gallery/lib/demo/shrine/shrine_data.dart new file mode 100644 index 000000000..70dac06a8 --- /dev/null +++ b/examples/flutter_gallery/lib/demo/shrine/shrine_data.dart @@ -0,0 +1,264 @@ +// Copyright 2016 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 'shrine_types.dart'; + +const Vendor _ali = const Vendor( + name: 'Ali’s shop', + avatarUrl: 'https://www.gstatic.com/angular/material-adaptive/shrine/ali-connors.png', + description: + 'Ali Connor’s makes custom goods for folks of all shapes and sizes ' + 'made by hand and sometimes by machine, but always with love and care. ' + 'Custom orders are available upon request if you need something extra special.' +); + +const Vendor _sandra = const Vendor( + name: 'Sandra’s shop', + avatarUrl: 'https://www.gstatic.com/angular/material-adaptive/shrine/sandra-adams.jpg', + description: + 'Sandra specializes in furniture, beauty and travel products with a classic vibe. ' + 'Custom orders are available if you’re looking for a certain color or material.' +); + +const Vendor _trevor = const Vendor( + name: 'Trevor’s shop', + avatarUrl: 'https://www.gstatic.com/angular/material-adaptive/shrine/zach.jpg', + description: + 'Trevor makes great stuff for awesome people like you. Super cool and extra ' + 'awesome all of his shop’s goods are handmade with love. Custom orders are ' + 'available upon request if you need something extra special.' +); + +const Vendor _peter = const Vendor( + name: 'Peter’s shop', + avatarUrl: 'https://www.gstatic.com/angular/material-adaptive/shrine/peter-carlsson.png', + description: + 'Peter makes great stuff for awesome people like you. Super cool and extra ' + 'awesome all of his shop’s goods are handmade with love. Custom orders are ' + 'available upon request if you need something extra special.' +); + +const Vendor _stella = const Vendor( + name: 'Stella’s shop', + avatarUrl: 'https://www.gstatic.com/angular/material-adaptive/shrine/16c477b.jpg', + description: + 'Stella sells awesome stuff at lovely prices. made by hand and sometimes by ' + 'machine, but always with love and care. Custom orders are available upon request ' + 'if you need something extra special.' +); + +const List _allProducts = const [ + const Product( + name: 'Vintage Bluetooth Radio', + imageUrl: 'https://www.gstatic.com/angular/material-adaptive/shrine/radio.png', + categories: const ['furniture', 'latest'], + price: 300.00, + vendor: _sandra, + description: + 'Isn’t it cool when things look old, but their not. Looks Old But Not makes ' + 'awesome vintage goods that are super smart. This ol’ radio just got an upgrade. ' + 'Connect to it with an app and jam out to some top forty.' + ), + const Product( + name: 'Sunglasses', + imageUrl: 'https://www.gstatic.com/angular/material-adaptive/shrine/sunnies.png', + categories: const ['travel', 'fashion', 'beauty'], + price: 70.00, + vendor: _trevor, + description: + 'Be an optimist. Carry Sunglasses with you at all times. All Tints and ' + 'Shades products come with polarized lenses and super duper UV protection ' + 'so you can look at the sun for however long you want. Sunglasses make you ' + 'look cool, wear them.' + ), + const Product( + name: 'Clock', + imageUrl: 'https://www.gstatic.com/angular/material-adaptive/shrine/clock.png', + categories: const ['furniture'], + price: 120.00, + vendor: _trevor, + description: + 'Timekeeper Co makes clocks that tell time precisely. Clock is ' + 'very simple to use, set the time using your phone, hang it, and viola! ' + 'You’ll never be late again.' + ), + const Product( + name: 'Red popsicle', + imageUrl: 'https://www.gstatic.com/angular/material-adaptive/shrine/popsicle.png', + categories: const ['food', 'fashion'], + price: 300.00, + vendor: _stella, + description: + 'Looks can be deceiving. This red popsicle comes in a wide variety of ' + 'flavors, including strawberry, that burst as soon as they hit your mouth. ' + 'Red popsicles melt slowly, so savor the flavor.' + ), + const Product( + name: 'Folding Chair', + imageUrl: 'https://www.gstatic.com/angular/material-adaptive/shrine/lawn_chair.png', + categories: const ['furniture'], + price: 63.00, + vendor: _stella, + description: + 'Leave the tunnel and the rain is fallin amazing things happen when you wait' + ), + const Product( + name: 'Green comfort chair', + imageUrl: 'https://www.gstatic.com/angular/material-adaptive/shrine/chair.png', + categories: const ['furniture'], + price: 36.00, + vendor: _ali, + description: + 'Leave the tunnel and the rain is fallin amazing things happen when you wait' + ), + const Product( + name: 'Better wearing heels', + imageUrl: 'https://www.gstatic.com/angular/material-adaptive/shrine/heels.png', + categories: const ['fashion'], + price: 125.00, + vendor: _peter, + description: + 'Leave the tunnel and the rain is fallin amazing things happen when you wait' + ), + const Product( + name: 'Green Slip-ons', + imageUrl: 'https://www.gstatic.com/angular/material-adaptive/shrine/green-shoes.png', + categories: const ['travel', 'fashion'], + price: 75.00, + vendor: _sandra, + description: + 'Feetsy has been making extraordinary slip-ons for decades. With each pair ' + 'of shoes purchased Feetsy donates a pair to those in need. Buy yourself a pair, ' + 'buy someone else a pair. Very Comfortable.' + ), + const Product( + name: 'Teapot', + imageUrl: 'https://www.gstatic.com/angular/material-adaptive/shrine/teapot.png', + categories: const ['furniture', 'fashion'], + price: 210.00, + vendor: _trevor, + featureTitle: 'Beautiful little teapot', + featureDescription: + 'Leave the tunnel and the rain is fallin amazing things happen when you wait', + description: + 'Impress your guests with Teapot by Kitchen Stuff. Teapot holds extremely ' + 'hot liquids and pours them from the spout. Use the handle, shown on the left, ' + 'so your fingers don’t get burnt while pouring.' + ), + const Product( + name: 'Blue suede shoes', + imageUrl: 'https://www.gstatic.com/angular/material-adaptive/shrine/chucks.png', + categories: const ['travel', 'fashion'], + price: 89.00, + vendor: _trevor, + description: + 'Who needs pants when you have shoes! Blue suede shoes were meant to go ' + 'dancing in, so you may want to pick up a few of these. These things are stylish.' + ), + const Product( + name: 'Dipped Brush', + imageUrl: 'https://www.gstatic.com/angular/material-adaptive/shrine/brush.png', + categories: const ['fashion', 'beauty'], + price: 25.00, + vendor: _stella, + description: + 'WeDipIt does it again. This handle dipped 4 inch brush is a perfect for ' + 'painting 4 inch lines, or coloring in big areas with paint. Just be sure you ' + 'don’t drop it in a bucket of red paint, then it won’t look dipped anymore.' + ), + const Product( + name: 'Perfect Goldfish Bowl', + imageUrl: 'https://www.gstatic.com/angular/material-adaptive/shrine/fish_bowl.png', + categories: const ['latest', 'furniture'], + price: 25.00, + vendor: _ali, + description: + 'The Perfect Bowl Co makes the best bowls for just about anything you can ' + 'think of. This Perfect Goldfish Bowl holds water and fish perfectly. Looks ' + 'great in living rooms. Keep out of reach from cats.' + ), + const Product( + name: 'Red Lipstick Set', + imageUrl: 'https://www.gstatic.com/angular/material-adaptive/shrine/lipstick.png', + categories: const ['fashion', 'beauty'], + price: 25.00, + vendor: _sandra, + description: + 'Trying to find the perfect shade to match your mood? Try no longer. Red ' + 'Lipstick Set by StickLips has you covered for those nights when you need ' + 'to get out, or even if you’re just headed to work.' + ), + const Product( + name: 'Backpack', + imageUrl: 'https://www.gstatic.com/angular/material-adaptive/shrine/backpack.png', + categories: const ['travel', 'fashion'], + price: 25.00, + vendor: _peter, + description: + 'This backpack by Bags ‘n’ stuff can hold just about anything: a laptop, ' + 'a pen, a protractor, notebooks, small animals, plugs for your devices, ' + 'sunglasses, gym clothes, shoes, gloves, two kittens, and even lunch!' + ), + const Product( + name: 'Half Shield Helmet', + imageUrl: 'https://www.gstatic.com/angular/material-adaptive/shrine/helmet.png', + categories: const ['travel', 'fashion', 'latest'], + price: 25.00, + vendor: _ali, + description: + 'Half Shield is the right helmet for those warm summer days on the road. ' + 'Dot approved, these helmets have been rigorously tested. Keep that noggin ' + 'protected.' + ), + const Product( + name: 'Beachball', + imageUrl: 'https://www.gstatic.com/angular/material-adaptive/shrine/beachball.png', + categories: const ['latest'], + price: 17.00, + vendor: _peter, + description: + 'Are you at a baseball game and feeling bored? At a pool party and looking ' + 'for a laugh? Do you need something to take your anger out on? Beachball, ' + 'by inflatable fun, is the perfect outlet.' + ), + const Product( + name: 'Old Binoculars', + imageUrl: 'https://www.gstatic.com/angular/material-adaptive/shrine/binoculars.png', + categories: const ['travel', 'fashion', 'latest'], + price: 25.00, + vendor: _stella, + description: + 'These Binoculars by See Through are amazing and can make things that are ' + 'really far away seem like they’re right in front of you. Bring them to the ' + 'beach. Now you can buy the cheap seats at the big game and feel like you’re ' + 'right in the action.' + ), + const Product( + name: 'Lime Flippers', + imageUrl: 'https://www.gstatic.com/angular/material-adaptive/shrine/flippers.png', + categories: const ['travel', 'fashion', 'beauty'], + price: 25.00, + vendor: _peter, + description: + 'Flippers are a nice tool to have when you’re being chased by an oversized ' + 'sea turtle. Never get caught again with these fast water shoes. You’re like ' + 'a fish, but more graceful.' + ), + const Product( + name: 'Surfboard', + imageUrl: 'https://www.gstatic.com/angular/material-adaptive/shrine/surfboard.png', + categories: const [ 'travel', 'latest'], + price: 25.00, + vendor: _stella, + description: + 'Who says you can’t walk on water? With Surfboard, by Surfboard Supply, ' + 'you can fly on water. This beast is fast and handles like a porsche. ' + 'Hang Ten Bro!' + ) +]; + +List allProducts() { + assert(_allProducts.every((Product product) => product.isValid())); + return new List.unmodifiable(_allProducts); +} diff --git a/examples/flutter_gallery/lib/demo/shrine/shrine_home.dart b/examples/flutter_gallery/lib/demo/shrine/shrine_home.dart new file mode 100644 index 000000000..44d58cb5b --- /dev/null +++ b/examples/flutter_gallery/lib/demo/shrine/shrine_home.dart @@ -0,0 +1,295 @@ +// Copyright 2016 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 'dart:async'; +import 'dart:collection' show HashSet; + +import 'package:flutter/material.dart'; + +import 'shrine_data.dart'; +import 'shrine_order.dart'; +import 'shrine_page.dart'; +import 'shrine_theme.dart'; +import 'shrine_types.dart'; + +const double unitSize = kToolBarHeight; + +Map shoppingCart = {}; + +/// Displays the Vendor's name and avatar. +class VendorItem extends StatelessWidget { + VendorItem({ Key key, this.vendor }) : super(key: key) { + assert(vendor != null); + } + + final Vendor vendor; + + @override + Widget build(BuildContext context) { + return new SizedBox( + height: 24.0, + child: new Row( + children: [ + new SizedBox( + width: 24.0, + child: new ClipRRect( + xRadius: 12.0, + yRadius: 12.0, + child: new NetworkImage( + fit: ImageFit.cover, + src: vendor.avatarUrl + ) + ) + ), + new SizedBox(width: 8.0), + new Flexible( + child: new Text(vendor.name, style: ShrineTheme.of(context).vendorItemStyle) + ) + ] + ) + ); + } +} + +/// Displays the product's price. If the product is in the shopping cart the background +/// is highlighted. +class PriceItem extends StatelessWidget { + PriceItem({ Key key, this.product }) : super(key: key) { + assert(product != null); + } + + final Product product; + + @override + Widget build(BuildContext context) { + BoxDecoration decoration; + if (shoppingCart[product] != null) + decoration = new BoxDecoration(backgroundColor: const Color(0xFFFFE0E0)); + + return new Container( + padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0), + decoration: decoration, + child: new Text(product.priceString, style: ShrineTheme.of(context).priceStyle) + ); + } +} + +/// Layout the main left and right elements of a FeatureItem. +class FeatureLayout extends MultiChildLayoutDelegate { + FeatureLayout(); + + static final String left = 'left'; + static final String right = 'right'; + + // Horizontally: the feature product image appears on the left and + // occupies 50% of the available width; the feature product's + // description apepars on the right and occupies 50% of the available + // width + unitSize. The left and right widgets overlap and the right + // widget is stacked on top. + @override + void performLayout(Size size) { + final double halfWidth = size.width / 2.0; + layoutChild(left, new BoxConstraints.tightFor(width: halfWidth, height: size.height)); + positionChild(left, Offset.zero); + layoutChild(right, new BoxConstraints.expand(width: halfWidth + unitSize, height: size.height)); + positionChild(right, new Offset(halfWidth - unitSize, 0.0)); + } + + @override + bool shouldRelayout(FeatureLayout oldDelegate) => false; +} + +/// A card that highlights the "featured" catalog item. +class FeatureItem extends StatelessWidget { + FeatureItem({ Key key, this.product }) : super(key: key) { + assert(product.featureTitle != null); + assert(product.featureDescription != null); + } + + final Product product; + + @override + Widget build(BuildContext context) { + final ShrineTheme theme = ShrineTheme.of(context); + return new AspectRatio( + aspectRatio: 3.0 / 3.5, + child: new Material( + type: MaterialType.card, + elevation: 1, + child: new Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + new SizedBox( + height: unitSize, + child: new Align( + alignment: FractionalOffset.topRight, + child: new PriceItem(product: product) + ) + ), + new Flexible( + child: new CustomMultiChildLayout( + delegate: new FeatureLayout(), + children: [ + new LayoutId( + id: FeatureLayout.left, + child: new ClipRect( + child: new OverflowBox( + minWidth: 340.0, + maxWidth: 340.0, + minHeight: 340.0, + maxHeight: 340.0, + alignment: FractionalOffset.topRight, + child: new NetworkImage( + fit: ImageFit.cover, + src: product.imageUrl + ) + ) + ) + ), + new LayoutId( + id: FeatureLayout.right, + child: new Padding( + padding: const EdgeInsets.only(right: 16.0), + child: new Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + new Padding( + padding: const EdgeInsets.only(top: 18.0), + child: new Text(product.featureTitle, style: theme.featureTitleStyle) + ), + new Padding( + padding: const EdgeInsets.symmetric(vertical: 16.0), + child: new Text(product.featureDescription, style: theme.featureStyle) + ), + new VendorItem(vendor: product.vendor) + ] + ) + ) + ) + ] + ) + ) + ] + ) + ) + ); + } +} + +/// A card that displays a product's image, price, and vendor. +class ProductItem extends StatelessWidget { + ProductItem({ Key key, this.product, this.onPressed }) : super(key: key) { + assert(product != null); + } + + final Product product; + final VoidCallback onPressed; + + @override + Widget build(BuildContext context) { + return new Card( + child: new Padding( + padding: const EdgeInsets.all(8.0), + child: new Column( + children: [ + new Align( + alignment: FractionalOffset.centerRight, + child: new PriceItem(product: product) + ), + new SizedBox( + width: 144.0, + height: 144.0, + child: new Stack( + children: [ + new Hero( + tag: productHeroTag, + key: new ObjectKey(product), + child: new NetworkImage( + fit: ImageFit.contain, + src: product.imageUrl + ) + ), + new Material( + color: Theme.of(context).canvasColor.withAlpha(0x00), + child: new InkWell(onTap: onPressed) + ), + ] + ) + ), + new VendorItem(vendor: product.vendor) + ] + ) + ) + ); + } +} + +/// The Shrine app's home page. Displays the featured item above all of the +/// product items arranged in two columns. +class ShrineHome extends StatefulWidget { + @override + _ShrineHomeState createState() => new _ShrineHomeState(); +} + +class _ShrineHomeState extends State { + final List _products = allProducts(); + + void handleCompletedOrder(Order completedOrder) { + assert(completedOrder.product != null); + if (completedOrder.inCart && completedOrder.quantity > 0) + shoppingCart[completedOrder.product] = completedOrder; + else + shoppingCart[completedOrder.product] = null; + } + + void showOrderPage(Product product) { + final Order order = shoppingCart[product] ?? new Order(product: product); + final Completer completer = new Completer(); + final Key productKey = new ObjectKey(product); + final Set mostValuableKeys = new HashSet(); + mostValuableKeys.add(productKey); + Navigator.push(context, new ShrineOrderRoute( + order: order, + settings: new RouteSettings(mostValuableKeys: mostValuableKeys), + completer: completer, + builder: (BuildContext context) { + return new OrderPage( + order: order, + products: _products + ); + } + )); + completer.future.then(handleCompletedOrder); + } + + @override + Widget build(BuildContext context) { + final Product featured = _products.firstWhere((Product product) => product.featureDescription != null); + return new ShrinePage( + body: new ScrollableViewport( + child: new Column( + children: [ + new Container( + margin: new EdgeInsets.only(bottom: 8.0), + child: new FeatureItem(product: featured) + ), + new FixedColumnCountGrid( + columnCount: 2, + rowSpacing: 8.0, + columnSpacing: 8.0, + padding: const EdgeInsets.all(8.0), + tileAspectRatio: 160.0 / 216.0, // width/height + children: _products.map((Product product) { + return new ProductItem( + product: product, + onPressed: () { showOrderPage(product); } + ); + }).toList() + ) + ] + ) + ) + ); + } +} diff --git a/examples/flutter_gallery/lib/demo/shrine/shrine_order.dart b/examples/flutter_gallery/lib/demo/shrine/shrine_order.dart new file mode 100644 index 000000000..5a5d5bb26 --- /dev/null +++ b/examples/flutter_gallery/lib/demo/shrine/shrine_order.dart @@ -0,0 +1,227 @@ +// Copyright 2016 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 'dart:async'; + +import 'package:flutter/material.dart'; + +import '../shrine_demo.dart' show ShrinePageRoute; +import 'shrine_page.dart'; +import 'shrine_theme.dart'; +import 'shrine_types.dart'; + +/// Describes a product and vendor in detail, supports specifying +/// a order quantity (0-5). Appears at the top of the OrderPage. +class OrderItem extends StatelessWidget { + OrderItem({ Key key, this.product, this.quantity, this.quantityChanged }) : super(key: key) { + assert(product != null); + assert(quantity != null && quantity >= 0 && quantity <= 5); + } + + final Product product; + final int quantity; + final ValueChanged quantityChanged; + + @override + Widget build(BuildContext context) { + final ShrineTheme theme = ShrineTheme.of(context); + return new Material( + type: MaterialType.card, + elevation: 0, + child: new Padding( + padding: const EdgeInsets.only(left: 16.0, top: 18.0, right: 16.0, bottom: 24.0), + child: new Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + new Padding( + padding: const EdgeInsets.only(left: 56.0), + child: new SizedBox( + width: 248.0, + height: 248.0, + child: new Hero( + tag: productHeroTag, + child: new NetworkImage( + fit: ImageFit.contain, + src: product.imageUrl + ) + ) + ) + ), + new SizedBox(height: 24.0), + new Row( + children: [ + new Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: new Center( + child: new Icon( + icon: Icons.info_outline, + size: 24.0, + color: const Color(0xFFFFE0E0) + ) + ) + ), + new Flexible( + child: new Text(product.name, style: theme.featureTitleStyle) + ) + ] + ), + new Padding( + padding: const EdgeInsets.only(left: 56.0), + child: new Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + new SizedBox(height: 24.0), + new Text(product.description, style: theme.featureStyle), + new SizedBox(height: 16.0), + new Padding( + padding: const EdgeInsets.only(top: 8.0, bottom: 8.0, right: 88.0), + child: new DropDownButtonHideUnderline( + child: new Container( + decoration: new BoxDecoration( + border: new Border.all( + color: const Color(0xFFD9D9D9) + ) + ), + child: new DropDownButton( + items: [0, 1, 2, 3, 4, 5].map((int value) { + return new DropDownMenuItem( + value: value, + child: new Text('Quantity $value', style: theme.quantityMenuStyle) + ); + }).toList(), + value: quantity, + onChanged: quantityChanged + ) + ) + ) + ), + new SizedBox(height: 16.0), + new SizedBox( + height: 24.0, + child: new Align( + alignment: FractionalOffset.bottomLeft, + child: new Text(product.vendor.name, style: theme.vendorTitleStyle) + ) + ), + new SizedBox(height: 16.0), + new Text(product.vendor.description, style: theme.vendorStyle), + new SizedBox(height: 24.0) + ] + ) + ) + ] + ) + ) + ); + } +} + +class OrderPage extends StatefulWidget { + OrderPage({ Key key, this.order, this.products }) : super(key: key) { + assert(order != null); + assert(products != null && products.length > 0); + } + + final Order order; + final List products; + + @override + _OrderPageState createState() => new _OrderPageState(); +} + +/// Displays a product's OrderItem above photos of all of the other products +/// arranged in two columns. Enables the user to specify a quantity and add an +/// order to the shopping cart. +class _OrderPageState extends State { + final GlobalKey scaffoldKey = new GlobalKey(debugLabel: 'Order Page'); + + Order get currentOrder => ShrineOrderRoute.of(context).order; + + set currentOrder(Order value) { + ShrineOrderRoute.of(context).order = value; + } + + void updateOrder({ int quantity, bool inCart }) { + Order newOrder = currentOrder.copyWith(quantity: quantity, inCart: inCart); + if (currentOrder != newOrder) { + setState(() { + currentOrder = newOrder; + }); + } + } + + void showSnackBarMessage(String message) { + scaffoldKey.currentState.showSnackBar(new SnackBar(content: new Text(message))); + } + + @override + Widget build(BuildContext context) { + return new ShrinePage( + scaffoldKey: scaffoldKey, + floatingActionButton: new FloatingActionButton( + onPressed: () { + updateOrder(inCart: true); + showSnackBarMessage('There are ${currentOrder.quantity} items in the shopping cart'); + }, + backgroundColor: const Color(0xFF16F0F0), + child: new Icon( + icon: Icons.add_shopping_cart, + color: Colors.black + ) + ), + body: new Block( + children: [ + new OrderItem( + product: config.order.product, + quantity: currentOrder.quantity, + quantityChanged: (int value) { updateOrder(quantity: value); } + ), + new SizedBox(height: 24.0), + new FixedColumnCountGrid( + columnCount: 2, + rowSpacing: 8.0, + columnSpacing: 8.0, + padding: const EdgeInsets.all(8.0), + tileAspectRatio: 160.0 / 216.0, // width/height + children: config.products + .where((Product product) => product != config.order.product) + .map((Product product) { + return new Card( + elevation: 0, + child: new NetworkImage( + fit: ImageFit.contain, + src: product.imageUrl + ) + ); + }).toList() + ) + ] + ) + ); + + } +} + +/// Displays a full-screen modal OrderPage. +/// +/// The order field will be replaced each time the user reconfigures the order. +/// When the user backs out of this route the completer's value will be the +/// final value of the order field. +class ShrineOrderRoute extends ShrinePageRoute { + ShrineOrderRoute({ + this.order, + WidgetBuilder builder, + Completer completer, + RouteSettings settings: const RouteSettings() + }) : super(builder: builder, completer: completer, settings: settings) { + assert(order != null); + } + + Order order; + + @override + Order get currentResult => order; + + static ShrineOrderRoute of(BuildContext context) => ModalRoute.of(context); +} diff --git a/examples/flutter_gallery/lib/demo/shrine/shrine_page.dart b/examples/flutter_gallery/lib/demo/shrine/shrine_page.dart new file mode 100644 index 000000000..4a5d5b89c --- /dev/null +++ b/examples/flutter_gallery/lib/demo/shrine/shrine_page.dart @@ -0,0 +1,60 @@ +// Copyright 2016 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 'shrine_theme.dart'; + +enum ShrineAction { + sortByPrice, + sortByProduct, + emptyCart +} + +/// Defines the Scaffold, AppBar, etc that the demo pages have in common. +class ShrinePage extends StatelessWidget { + ShrinePage({ Key key, this.scaffoldKey, this.body, this.floatingActionButton }) : super(key: key); + + final Key scaffoldKey; + final Widget body; + final Widget floatingActionButton; + + @override + Widget build(BuildContext context) { + return new Scaffold( + key: scaffoldKey, + appBar: new AppBar( + title: new Center( + child: new Text('SHRINE', style: ShrineTheme.of(context).appBarTitleStyle) + ), + backgroundColor: Theme.of(context).canvasColor, + actions: [ // TODO(hansmuller): implement the actions. + new IconButton( + icon: Icons.shopping_cart, + tooltip: 'Shopping cart', + onPressed: () { /* activate the button for now */ } + ), + new PopupMenuButton( + itemBuilder: (BuildContext context) => >[ + new PopupMenuItem( + value: ShrineAction.sortByPrice, + child: new Text('Sort by price') + ), + new PopupMenuItem( + value: ShrineAction.sortByProduct, + child: new Text('Sort by product') + ), + new PopupMenuItem( + value: ShrineAction.emptyCart, + child: new Text('Empty shopping cart') + ) + ] + ) + ] + ), + floatingActionButton: floatingActionButton, + body: body + ); + } +} diff --git a/examples/flutter_gallery/lib/demo/shrine/shrine_theme.dart b/examples/flutter_gallery/lib/demo/shrine/shrine_theme.dart new file mode 100644 index 000000000..fd3a92a33 --- /dev/null +++ b/examples/flutter_gallery/lib/demo/shrine/shrine_theme.dart @@ -0,0 +1,47 @@ +// Copyright 2016 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'; + +class ShrineStyle extends TextStyle { + const ShrineStyle.roboto(double size, FontWeight weight, Color color) + : super(inherit: false, color: color, fontSize: size, fontWeight: weight, textBaseline: TextBaseline.alphabetic); + + const ShrineStyle.abrilFatface(double size, FontWeight weight, Color color) + : super(inherit: false, color: color, fontFamily: 'AbrilFatface', fontSize: size, fontWeight: weight, textBaseline: TextBaseline.alphabetic); +} + +TextStyle robotoRegular12(Color color) => new ShrineStyle.roboto(12.0, FontWeight.w500, color); +TextStyle robotoLight12(Color color) => new ShrineStyle.roboto(12.0, FontWeight.w300, color); +TextStyle robotoRegular14(Color color) => new ShrineStyle.roboto(14.0, FontWeight.w500, color); +TextStyle robotoMedium14(Color color) => new ShrineStyle.roboto(14.0, FontWeight.w600, color); +TextStyle robotoLight14(Color color) => new ShrineStyle.roboto(14.0, FontWeight.w300, color); +TextStyle robotoRegular20(Color color) => new ShrineStyle.roboto(20.0, FontWeight.w500, color); +TextStyle abrilFatfaceRegular24(Color color) => new ShrineStyle.abrilFatface(24.0, FontWeight.w500, color); +TextStyle abrilFatfaceRegular34(Color color) => new ShrineStyle.abrilFatface(34.0, FontWeight.w500, color); + +/// The TextStyles and Colors used for titles, labels, and descriptions. This +/// InheritedWidget is shared by all of the routes and widgets created for +/// the Shrine app. +class ShrineTheme extends InheritedWidget { + ShrineTheme({ Key key, Widget child }) : super(key: key, child: child) { + assert(child != null); + } + + final TextStyle appBarTitleStyle = robotoRegular20(Colors.black87); + final TextStyle vendorItemStyle = robotoRegular12(const Color(0xFF81959D)); + final TextStyle priceStyle = robotoRegular14(Colors.black87); + final TextStyle featureTitleStyle = abrilFatfaceRegular34(Colors.black87); + final TextStyle featureStyle = robotoLight14(Colors.black54); + final TextStyle orderTitleStyle = abrilFatfaceRegular24(Colors.black87); + final TextStyle orderStyle = robotoLight14(Colors.black54); + final TextStyle vendorTitleStyle = robotoMedium14(Colors.black87); + final TextStyle vendorStyle = robotoLight14(Colors.black54); + final TextStyle quantityMenuStyle = robotoLight14(Colors.black54); + + static ShrineTheme of(BuildContext context) => context.inheritFromWidgetOfExactType(ShrineTheme); + + @override + bool updateShouldNotify(ShrineTheme old) => false; +} diff --git a/examples/flutter_gallery/lib/demo/shrine/shrine_types.dart b/examples/flutter_gallery/lib/demo/shrine/shrine_types.dart new file mode 100644 index 000000000..b0f862afb --- /dev/null +++ b/examples/flutter_gallery/lib/demo/shrine/shrine_types.dart @@ -0,0 +1,103 @@ +// Copyright 2016 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 'dart:ui' show hashValues; + +const String productHeroTag = 'Product'; + +class Vendor { + const Vendor({ + this.name, + this.description, + this.avatarUrl + }); + + final String name; + final String description; + final String avatarUrl; + + bool isValid() { + return name != null && + description != null && + avatarUrl != null; + } + + @override + String toString() => 'Vendor($name)'; +} + +class Product { + const Product({ + this.name, + this.description, + this.featureTitle, + this.featureDescription, + this.imageUrl, + this.categories, + this.price, + this.vendor + }); + + final String name; + final String description; + final String featureTitle; + final String featureDescription; + final String imageUrl; + final List categories; + final double price; + final Vendor vendor; + + String get priceString => '\$${price.floor()}'; + + bool isValid() { + return name != null && + description != null && + imageUrl != null && + categories != null && + categories.length > 0 && + price != null && + vendor.isValid(); + } + + @override + String toString() => 'Product($name)'; +} + +class Order { + Order({ this.product, this.quantity: 1, this.inCart: false }) { + assert(product != null); + assert(quantity != null && quantity >= 0); + assert(inCart != null); + } + + final Product product; + final int quantity; + final bool inCart; + + Order copyWith({ Product product, int quantity, bool inCart }) { + return new Order( + product: product ?? this.product, + quantity: quantity ?? this.quantity, + inCart: inCart ?? this.inCart + ); + } + + @override + bool operator ==(dynamic other) { + if (identical(this, other)) + return true; + if (other.runtimeType != runtimeType) + return false; + final Order typedOther = other; + return product == typedOther.product && + quantity == typedOther.quantity && + inCart == typedOther.inCart; + } + + @override + int get hashCode => hashValues(product, quantity, inCart); + + @override + String toString() => 'Order($product, quantity=$quantity, inCart=$inCart)'; +} diff --git a/examples/flutter_gallery/lib/demo/shrine_demo.dart b/examples/flutter_gallery/lib/demo/shrine_demo.dart new file mode 100644 index 000000000..f283f05b3 --- /dev/null +++ b/examples/flutter_gallery/lib/demo/shrine_demo.dart @@ -0,0 +1,46 @@ +// Copyright 2016 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 'dart:async'; + +import 'package:flutter/material.dart'; + +import 'shrine/shrine_home.dart' show ShrineHome; +import 'shrine/shrine_theme.dart' show ShrineTheme; + +// This code would ordinarily be part of the MaterialApp's home. It's being +// used by the ShrineDemo and by each route pushed from there because this +// isn't a standalone app with its own main() and MaterialApp. +Widget buildShrine(Widget child) { + return new Theme( + data: new ThemeData(primarySwatch: Colors.grey), + child: new IconTheme( + data: new IconThemeData(color: const Color(0xFF707070)), + child: new ShrineTheme( + child: child + ) + ) + ); +} + +// In a standalone version of this app, MaterialPageRoute could be used directly. +class ShrinePageRoute extends MaterialPageRoute { + ShrinePageRoute({ + WidgetBuilder builder, + Completer completer, + RouteSettings settings: const RouteSettings() + }) : super(builder: builder, completer: completer, settings: settings); + + @override + Widget buildPage(BuildContext context, Animation animation, Animation forwardAnimation) { + return buildShrine(super.buildPage(context, animation, forwardAnimation)); + } +} + +class ShrineDemo extends StatelessWidget { + static const String routeName = '/shrine'; // Used by the Gallery app. + + @override + Widget build(BuildContext context) => buildShrine(new ShrineHome()); +} diff --git a/examples/flutter_gallery/lib/gallery/app.dart b/examples/flutter_gallery/lib/gallery/app.dart index 1a1d44451..deaee4be8 100644 --- a/examples/flutter_gallery/lib/gallery/app.dart +++ b/examples/flutter_gallery/lib/gallery/app.dart @@ -9,6 +9,7 @@ import '../demo/all.dart'; import 'home.dart'; final Map kRoutes = { + ShrineDemo.routeName: (BuildContext context) => new ShrineDemo(), WeatherDemo.routeName: (BuildContext context) => new WeatherDemo(), FitnessDemo.routeName: (BuildContext context) => new FitnessDemo(), Calculator.routeName: (BuildContext context) => new Calculator(), diff --git a/examples/flutter_gallery/lib/gallery/home.dart b/examples/flutter_gallery/lib/gallery/home.dart index d34261272..9f14308f6 100644 --- a/examples/flutter_gallery/lib/gallery/home.dart +++ b/examples/flutter_gallery/lib/gallery/home.dart @@ -71,6 +71,7 @@ class GalleryHomeState extends State { leading: new Icon(icon: Icons.star), title: new Text('Demos'), children: [ + new GalleryItem(title: 'Shrine', routeName: ShrineDemo.routeName), new GalleryItem(title: 'Weather', routeName: WeatherDemo.routeName), new GalleryItem(title: 'Fitness', routeName: FitnessDemo.routeName), new GalleryItem(title: 'Calculator', routeName: Calculator.routeName), diff --git a/examples/flutter_gallery/pubspec.yaml b/examples/flutter_gallery/pubspec.yaml index 5756ac401..5497ee8ce 100644 --- a/examples/flutter_gallery/pubspec.yaml +++ b/examples/flutter_gallery/pubspec.yaml @@ -10,7 +10,7 @@ dependencies: path: ../../packages/flutter_sprites flutter_markdown: path: ../../packages/flutter_markdown - flutter_gallery_assets: '0.0.16' + flutter_gallery_assets: '0.0.18' dev_dependencies: test: any # flutter_test provides the version constraints diff --git a/packages/flutter/lib/src/material/app_bar.dart b/packages/flutter/lib/src/material/app_bar.dart index ef23529cd..7b126f7fb 100644 --- a/packages/flutter/lib/src/material/app_bar.dart +++ b/packages/flutter/lib/src/material/app_bar.dart @@ -183,7 +183,7 @@ class AppBar extends StatelessWidget { final double statusBarHeight = MediaQuery.of(context).padding.top; final ThemeData theme = Theme.of(context); - IconThemeData iconTheme = theme.primaryIconTheme; + IconThemeData iconTheme = IconTheme.of(context) ?? theme.primaryIconTheme; TextStyle centerStyle = textTheme?.title ?? theme.primaryTextTheme.title; TextStyle sideStyle = textTheme?.body1 ?? theme.primaryTextTheme.body1; diff --git a/packages/flutter/lib/src/material/card.dart b/packages/flutter/lib/src/material/card.dart index 2749a1950..bf263b67f 100644 --- a/packages/flutter/lib/src/material/card.dart +++ b/packages/flutter/lib/src/material/card.dart @@ -17,8 +17,9 @@ class Card extends StatelessWidget { /// Creates a material design card. const Card({ Key key, - this.child, - this.color + this.color, + this.elevation: 2, + this.child }) : super(key: key); /// The widget below this widget in the tree. @@ -27,6 +28,9 @@ class Card extends StatelessWidget { /// The color of material used for this card. final Color color; + /// The following elevations have defined shadows: 1, 2, 3, 4, 6, 8, 9, 12, 16, 24 + final int elevation; + @override Widget build(BuildContext context) { return new Container( @@ -34,7 +38,7 @@ class Card extends StatelessWidget { child: new Material( color: color, type: MaterialType.card, - elevation: 2, + elevation: elevation, child: child ) ); diff --git a/packages/flutter/lib/src/widgets/navigator.dart b/packages/flutter/lib/src/widgets/navigator.dart index 9402dcde1..bd1439946 100644 --- a/packages/flutter/lib/src/widgets/navigator.dart +++ b/packages/flutter/lib/src/widgets/navigator.dart @@ -32,6 +32,10 @@ abstract class Route { /// Called after install() when the route is pushed onto the navigator. void didPush() { } + /// When this route is popped (see [Navigator.pop]) if the result isn't + /// specified or if it's null, this value will be used instead. + T get currentResult => null; + /// Called after install() when the route replaced another in the navigator. void didReplace(Route oldRoute) { } @@ -427,7 +431,7 @@ class NavigatorState extends State { assert(route._navigator == this); bool debugPredictedWouldPop; assert(() { debugPredictedWouldPop = !route.willHandlePopInternally; return true; }); - if (route.didPop(result)) { + if (route.didPop(result ?? route.currentResult)) { assert(debugPredictedWouldPop); if (_history.length > 1) { setState(() { @@ -564,11 +568,12 @@ class NavigatorTransaction { /// (if any) is notified using its didPop() method, and the previous route is /// notified using [Route.didChangeNext]. /// - /// If non-null, [result] will be used as the result of the route. Routes - /// such as dialogs or popup menus typically use this mechanism to return the - /// value selected by the user to the widget that created their route. The - /// type of [result], if provided, must match the type argument of the class - /// of the current route. (In practice, this is usually "dynamic".) + /// If non-null, [result] will be used as the result of the route, otherwise + /// the route's [Route.currentValue] will be used. Routes such as dialogs or + /// popup menus typically use this mechanism to return the value selected by + /// the user to the widget that created their route. The type of [result], + /// if provided, must match the type argument of the class of the current + /// route. (In practice, this is usually "dynamic".) /// /// Returns true if a route was popped; returns false if there are no further /// previous routes.