// 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 'package:flutter/rendering.dart'; import 'package:meta/meta.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; final List<Product> _products = new List<Product>.from(allProducts()); final Map<Product, Order> _shoppingCart = <Product, Order>{}; const int _childrenPerBlock = 8; const int _rowsPerBlock = 5; int _minIndexInRow(int rowIndex) { final int blockIndex = rowIndex ~/ _rowsPerBlock; return const <int>[0, 2, 4, 6, 7][rowIndex % _rowsPerBlock] + blockIndex * _childrenPerBlock; } int _maxIndexInRow(int rowIndex) { final int blockIndex = rowIndex ~/ _rowsPerBlock; return const <int>[1, 3, 5, 6, 7][rowIndex % _rowsPerBlock] + blockIndex * _childrenPerBlock; } int _rowAtIndex(int index) { final int blockCount = index ~/ _childrenPerBlock; return const <int>[0, 0, 1, 1, 2, 2, 3, 4][index - blockCount * _childrenPerBlock] + blockCount * _rowsPerBlock; } int _columnAtIndex(int index) { return const <int>[0, 1, 0, 1, 0, 1, 0, 0][index % _childrenPerBlock]; } int _columnSpanAtIndex(int index) { return const <int>[1, 1, 1, 1, 1, 1, 2, 2][index % _childrenPerBlock]; } // The Shrine home page arranges the product cards into two columns. The card // on every 4th and 5th row spans two columns. class _ShrineGridLayout extends SliverGridLayout { const _ShrineGridLayout({ @required this.rowStride, @required this.columnStride, @required this.tileHeight, @required this.tileWidth, }); final double rowStride; final double columnStride; final double tileHeight; final double tileWidth; @override int getMinChildIndexForScrollOffset(double scrollOffset) { return _minIndexInRow(scrollOffset ~/ rowStride); } @override int getMaxChildIndexForScrollOffset(double scrollOffset) { return _maxIndexInRow(scrollOffset ~/ rowStride); } @override SliverGridGeometry getGeometryForChildIndex(int index) { final int row = _rowAtIndex(index); final int column = _columnAtIndex(index); final int columnSpan = _columnSpanAtIndex(index); return new SliverGridGeometry( scrollOffset: row * rowStride, crossAxisOffset: column * columnStride, mainAxisExtent: tileHeight, crossAxisExtent: tileWidth + (columnSpan - 1) * columnStride, ); } @override double computeMaxScrollOffset(int childCount) { if (childCount == 0) return 0.0; final int rowCount = _rowAtIndex(childCount - 1) + 1; final double rowSpacing = rowStride - tileHeight; return rowStride * rowCount - rowSpacing; } } class _ShrineGridDelegate extends SliverGridDelegate { static const double _spacing = 8.0; @override SliverGridLayout getLayout(SliverConstraints constraints) { final double tileWidth = (constraints.crossAxisExtent - _spacing) / 2.0; const double tileHeight = 40.0 + 144.0 + 40.0; return new _ShrineGridLayout( tileWidth: tileWidth, tileHeight: tileHeight, rowStride: tileHeight + _spacing, columnStride: tileWidth + _spacing, ); } @override bool shouldRelayout(covariant SliverGridDelegate oldDelegate) => false; } // Displays the Vendor's name and avatar. class _VendorItem extends StatelessWidget { const _VendorItem({ Key key, @required this.vendor }) : assert(vendor != null), super(key: key); 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( borderRadius: new BorderRadius.circular(12.0), child: new Image.asset( vendor.avatarAsset, package: vendor.avatarAssetPackage, fit: BoxFit.cover, ), ), ), const SizedBox(width: 8.0), new Expanded( child: new Text(vendor.name, style: ShrineTheme.of(context).vendorItemStyle), ), ], ), ); } } // Displays the product's price. If the product is in the shopping cart then the // background is highlighted. abstract class _PriceItem extends StatelessWidget { const _PriceItem({ Key key, @required this.product }) : assert(product != null), super(key: key); final Product product; Widget buildItem(BuildContext context, TextStyle style, EdgeInsets padding) { BoxDecoration decoration; if (_shoppingCart[product] != null) decoration = new BoxDecoration(color: ShrineTheme.of(context).priceHighlightColor); return new Container( padding: padding, decoration: decoration, child: new Text(product.priceString, style: style), ); } } class _ProductPriceItem extends _PriceItem { const _ProductPriceItem({ Key key, Product product }) : super(key: key, product: product); @override Widget build(BuildContext context) { return buildItem( context, ShrineTheme.of(context).priceStyle, const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0), ); } } class _FeaturePriceItem extends _PriceItem { const _FeaturePriceItem({ Key key, Product product }) : super(key: key, product: product); @override Widget build(BuildContext context) { return buildItem( context, ShrineTheme.of(context).featurePriceStyle, const EdgeInsets.symmetric(horizontal: 24.0, vertical: 16.0), ); } } class _HeadingLayout extends MultiChildLayoutDelegate { _HeadingLayout(); static const String price = 'price'; static const String image = 'image'; static const String title = 'title'; static const String description = 'description'; static const String vendor = 'vendor'; @override void performLayout(Size size) { final Size priceSize = layoutChild(price, new BoxConstraints.loose(size)); positionChild(price, new Offset(size.width - priceSize.width, 0.0)); final double halfWidth = size.width / 2.0; final double halfHeight = size.height / 2.0; const double halfUnit = unitSize / 2.0; const double margin = 16.0; final Size imageSize = layoutChild(image, new BoxConstraints.loose(size)); final double imageX = imageSize.width < halfWidth - halfUnit ? halfWidth / 2.0 - imageSize.width / 2.0 - halfUnit : halfWidth - imageSize.width; positionChild(image, new Offset(imageX, halfHeight - imageSize.height / 2.0)); final double maxTitleWidth = halfWidth + unitSize - margin; final BoxConstraints titleBoxConstraints = new BoxConstraints(maxWidth: maxTitleWidth); final Size titleSize = layoutChild(title, titleBoxConstraints); final double titleX = halfWidth - unitSize; final double titleY = halfHeight - titleSize.height; positionChild(title, new Offset(titleX, titleY)); final Size descriptionSize = layoutChild(description, titleBoxConstraints); final double descriptionY = titleY + titleSize.height + margin; positionChild(description, new Offset(titleX, descriptionY)); layoutChild(vendor, titleBoxConstraints); final double vendorY = descriptionY + descriptionSize.height + margin; positionChild(vendor, new Offset(titleX, vendorY)); } @override bool shouldRelayout(_HeadingLayout oldDelegate) => false; } // A card that highlights the "featured" catalog item. class _Heading extends StatelessWidget { _Heading({ Key key, @required this.product }) : assert(product != null), assert(product.featureTitle != null), assert(product.featureDescription != null), super(key: key); final Product product; @override Widget build(BuildContext context) { final Size screenSize = MediaQuery.of(context).size; final ShrineTheme theme = ShrineTheme.of(context); return new MergeSemantics( child: new SizedBox( height: screenSize.width > screenSize.height ? (screenSize.height - kToolbarHeight) * 0.85 : (screenSize.height - kToolbarHeight) * 0.70, child: new Container( decoration: new BoxDecoration( color: theme.cardBackgroundColor, border: new Border(bottom: new BorderSide(color: theme.dividerColor)), ), child: new CustomMultiChildLayout( delegate: new _HeadingLayout(), children: <Widget>[ new LayoutId( id: _HeadingLayout.price, child: new _FeaturePriceItem(product: product), ), new LayoutId( id: _HeadingLayout.image, child: new Image.asset( product.imageAsset, package: product.imageAssetPackage, fit: BoxFit.cover, ), ), new LayoutId( id: _HeadingLayout.title, child: new Text(product.featureTitle, style: theme.featureTitleStyle), ), new LayoutId( id: _HeadingLayout.description, child: new Text(product.featureDescription, style: theme.featureStyle), ), new LayoutId( id: _HeadingLayout.vendor, child: new _VendorItem(vendor: product.vendor), ), ], ), ), ), ); } } // A card that displays a product's image, price, and vendor. The _ProductItem // cards appear in a grid below the heading. class _ProductItem extends StatelessWidget { const _ProductItem({ Key key, @required this.product, this.onPressed }) : assert(product != null), super(key: key); final Product product; final VoidCallback onPressed; @override Widget build(BuildContext context) { return new MergeSemantics( child: new Card( child: new Stack( children: <Widget>[ new Column( children: <Widget>[ new Align( alignment: Alignment.centerRight, child: new _ProductPriceItem(product: product), ), new Container( width: 144.0, height: 144.0, padding: const EdgeInsets.symmetric(horizontal: 8.0), child: new Hero( tag: product.tag, child: new Image.asset( product.imageAsset, package: product.imageAssetPackage, fit: BoxFit.contain, ), ), ), new Padding( padding: const EdgeInsets.symmetric(horizontal: 8.0), child: new _VendorItem(vendor: product.vendor), ), ], ), new Material( type: MaterialType.transparency, child: new InkWell(onTap: onPressed), ), ], ), ), ); } } // The Shrine app's home page. Displays the featured item above a grid // of the product items. class ShrineHome extends StatefulWidget { @override _ShrineHomeState createState() => new _ShrineHomeState(); } class _ShrineHomeState extends State<ShrineHome> { static final GlobalKey<ScaffoldState> _scaffoldKey = new GlobalKey<ScaffoldState>(debugLabel: 'Shrine Home'); static final _ShrineGridDelegate gridDelegate = new _ShrineGridDelegate(); Future<Null> _showOrderPage(Product product) async { final Order order = _shoppingCart[product] ?? new Order(product: product); final Order completedOrder = await Navigator.push(context, new ShrineOrderRoute( order: order, builder: (BuildContext context) { return new OrderPage( order: order, products: _products, shoppingCart: _shoppingCart, ); } )); assert(completedOrder.product != null); if (completedOrder.quantity == 0) _shoppingCart.remove(completedOrder.product); } @override Widget build(BuildContext context) { final Product featured = _products.firstWhere((Product product) => product.featureDescription != null); return new ShrinePage( scaffoldKey: _scaffoldKey, products: _products, shoppingCart: _shoppingCart, body: new CustomScrollView( slivers: <Widget>[ new SliverToBoxAdapter(child: new _Heading(product: featured)), new SliverSafeArea( top: false, minimum: const EdgeInsets.all(16.0), sliver: new SliverGrid( gridDelegate: gridDelegate, delegate: new SliverChildListDelegate( _products.map((Product product) { return new _ProductItem( product: product, onPressed: () { _showOrderPage(product); }, ); }).toList(), ), ), ), ], ), ); } }