Version 0.0 of a gallery demo of the Material Design "Shrine" app (#4327)

This commit is contained in:
Hans Muller 2016-06-02 14:23:20 -07:00
parent a464052191
commit 309b9f8010
16 changed files with 1070 additions and 13 deletions

View File

@ -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:

View File

@ -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

View File

@ -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';

View File

@ -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: 'Alis shop',
avatarUrl: 'https://www.gstatic.com/angular/material-adaptive/shrine/ali-connors.png',
description:
'Ali Connors 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: 'Sandras 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 youre looking for a certain color or material.'
);
const Vendor _trevor = const Vendor(
name: 'Trevors 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 shops goods are handmade with love. Custom orders are '
'available upon request if you need something extra special.'
);
const Vendor _peter = const Vendor(
name: 'Peters 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 shops goods are handmade with love. Custom orders are '
'available upon request if you need something extra special.'
);
const Vendor _stella = const Vendor(
name: 'Stellas 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<Product> _allProducts = const <Product> [
const Product(
name: 'Vintage Bluetooth Radio',
imageUrl: 'https://www.gstatic.com/angular/material-adaptive/shrine/radio.png',
categories: const <String>['furniture', 'latest'],
price: 300.00,
vendor: _sandra,
description:
'Isnt 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 <String>['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 <String>['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! '
'Youll never be late again.'
),
const Product(
name: 'Red popsicle',
imageUrl: 'https://www.gstatic.com/angular/material-adaptive/shrine/popsicle.png',
categories: const <String>['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 <String>['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 <String>['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 <String>['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 <String>['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 <String>['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 dont get burnt while pouring.'
),
const Product(
name: 'Blue suede shoes',
imageUrl: 'https://www.gstatic.com/angular/material-adaptive/shrine/chucks.png',
categories: const <String>['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 <String>['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 '
'dont drop it in a bucket of red paint, then it wont look dipped anymore.'
),
const Product(
name: 'Perfect Goldfish Bowl',
imageUrl: 'https://www.gstatic.com/angular/material-adaptive/shrine/fish_bowl.png',
categories: const <String>['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 <String>['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 youre just headed to work.'
),
const Product(
name: 'Backpack',
imageUrl: 'https://www.gstatic.com/angular/material-adaptive/shrine/backpack.png',
categories: const <String>['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 <String>['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 <String>['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 <String>['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 theyre right in front of you. Bring them to the '
'beach. Now you can buy the cheap seats at the big game and feel like youre '
'right in the action.'
),
const Product(
name: 'Lime Flippers',
imageUrl: 'https://www.gstatic.com/angular/material-adaptive/shrine/flippers.png',
categories: const <String>['travel', 'fashion', 'beauty'],
price: 25.00,
vendor: _peter,
description:
'Flippers are a nice tool to have when youre being chased by an oversized '
'sea turtle. Never get caught again with these fast water shoes. Youre like '
'a fish, but more graceful.'
),
const Product(
name: 'Surfboard',
imageUrl: 'https://www.gstatic.com/angular/material-adaptive/shrine/surfboard.png',
categories: const <String>[ 'travel', 'latest'],
price: 25.00,
vendor: _stella,
description:
'Who says you cant 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<Product> allProducts() {
assert(_allProducts.every((Product product) => product.isValid()));
return new List<Product>.unmodifiable(_allProducts);
}

View File

@ -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<Product, Order> shoppingCart = <Product, Order>{};
/// 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: <Widget>[
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: <Widget>[
new SizedBox(
height: unitSize,
child: new Align(
alignment: FractionalOffset.topRight,
child: new PriceItem(product: product)
)
),
new Flexible(
child: new CustomMultiChildLayout(
delegate: new FeatureLayout(),
children: <Widget>[
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: <Widget>[
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: <Widget>[
new Align(
alignment: FractionalOffset.centerRight,
child: new PriceItem(product: product)
),
new SizedBox(
width: 144.0,
height: 144.0,
child: new Stack(
children: <Widget>[
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<ShrineHome> {
final List<Product> _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<Order> completer = new Completer<Order>();
final Key productKey = new ObjectKey(product);
final Set<Key> mostValuableKeys = new HashSet<Key>();
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: <Widget>[
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()
)
]
)
)
);
}
}

View File

@ -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<int> 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: <Widget>[
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: <Widget>[
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: <Widget>[
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<int>(
items: <int>[0, 1, 2, 3, 4, 5].map((int value) {
return new DropDownMenuItem<int>(
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<Product> 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<OrderPage> {
final GlobalKey<ScaffoldState> scaffoldKey = new GlobalKey<ScaffoldState>(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: <Widget>[
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<Order> {
ShrineOrderRoute({
this.order,
WidgetBuilder builder,
Completer<Order> 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);
}

View File

@ -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: <Widget>[ // TODO(hansmuller): implement the actions.
new IconButton(
icon: Icons.shopping_cart,
tooltip: 'Shopping cart',
onPressed: () { /* activate the button for now */ }
),
new PopupMenuButton<ShrineAction>(
itemBuilder: (BuildContext context) => <PopupMenuItem<ShrineAction>>[
new PopupMenuItem<ShrineAction>(
value: ShrineAction.sortByPrice,
child: new Text('Sort by price')
),
new PopupMenuItem<ShrineAction>(
value: ShrineAction.sortByProduct,
child: new Text('Sort by product')
),
new PopupMenuItem<ShrineAction>(
value: ShrineAction.emptyCart,
child: new Text('Empty shopping cart')
)
]
)
]
),
floatingActionButton: floatingActionButton,
body: body
);
}
}

View File

@ -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;
}

View File

@ -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<String> 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)';
}

View File

@ -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<T> could be used directly.
class ShrinePageRoute<T> extends MaterialPageRoute<T> {
ShrinePageRoute({
WidgetBuilder builder,
Completer<T> completer,
RouteSettings settings: const RouteSettings()
}) : super(builder: builder, completer: completer, settings: settings);
@override
Widget buildPage(BuildContext context, Animation<double> animation, Animation<double> 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());
}

View File

@ -9,6 +9,7 @@ import '../demo/all.dart';
import 'home.dart';
final Map<String, WidgetBuilder> kRoutes = <String, WidgetBuilder>{
ShrineDemo.routeName: (BuildContext context) => new ShrineDemo(),
WeatherDemo.routeName: (BuildContext context) => new WeatherDemo(),
FitnessDemo.routeName: (BuildContext context) => new FitnessDemo(),
Calculator.routeName: (BuildContext context) => new Calculator(),

View File

@ -71,6 +71,7 @@ class GalleryHomeState extends State<GalleryHome> {
leading: new Icon(icon: Icons.star),
title: new Text('Demos'),
children: <Widget>[
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),

View File

@ -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

View File

@ -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;

View File

@ -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
)
);

View File

@ -32,6 +32,10 @@ abstract class Route<T> {
/// 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<dynamic> oldRoute) { }
@ -427,7 +431,7 @@ class NavigatorState extends State<Navigator> {
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.