From 9192f672521f8fcdd24f60ff44f7f730817d75c8 Mon Sep 17 00:00:00 2001 From: Hans Muller Date: Mon, 27 Mar 2017 15:07:24 -0700 Subject: [PATCH] Shrine gallery demo: support for landscape layout (#9025) --- .../lib/demo/shrine/shrine_home.dart | 189 ++++++------ .../lib/demo/shrine/shrine_order.dart | 287 +++++++++++------- 2 files changed, 275 insertions(+), 201 deletions(-) diff --git a/examples/flutter_gallery/lib/demo/shrine/shrine_home.dart b/examples/flutter_gallery/lib/demo/shrine/shrine_home.dart index 44340e5325f..dabd182d195 100644 --- a/examples/flutter_gallery/lib/demo/shrine/shrine_home.dart +++ b/examples/flutter_gallery/lib/demo/shrine/shrine_home.dart @@ -47,8 +47,8 @@ int _columnSpanAtIndex(int index) { // 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({ +class _ShrineGridLayout extends SliverGridLayout { + const _ShrineGridLayout({ @required this.rowStride, @required this.columnStride, @required this.tileHeight, @@ -95,14 +95,14 @@ class ShrineGridLayout extends SliverGridLayout { } } -class ShrineGridDelegate extends SliverGridDelegate { +class _ShrineGridDelegate extends SliverGridDelegate { static const double _kSpacing = 8.0; @override SliverGridLayout getLayout(SliverConstraints constraints) { final double tileWidth = (constraints.crossAxisExtent - _kSpacing) / 2.0; final double tileHeight = 40.0 + 144.0 + 40.0; - return new ShrineGridLayout( + return new _ShrineGridLayout( tileWidth: tileWidth, tileHeight: tileHeight, rowStride: tileHeight + _kSpacing, @@ -114,9 +114,9 @@ class ShrineGridDelegate extends SliverGridDelegate { bool shouldRelayout(covariant SliverGridDelegate oldDelegate) => false; } -/// Displays the Vendor's name and avatar. -class VendorItem extends StatelessWidget { - VendorItem({ Key key, this.vendor }) : super(key: key) { +// Displays the Vendor's name and avatar. +class _VendorItem extends StatelessWidget { + _VendorItem({ Key key, this.vendor }) : super(key: key) { assert(vendor != null); } @@ -145,10 +145,10 @@ class VendorItem extends StatelessWidget { } } -/// Displays the product's price. If the product is in the shopping cart the background -/// is highlighted. -abstract class PriceItem extends StatelessWidget { - PriceItem({ Key key, this.product }) : super(key: key) { +// Displays the product's price. If the product is in the shopping cart then the +// background is highlighted. +abstract class _PriceItem extends StatelessWidget { + _PriceItem({ Key key, this.product }) : super(key: key) { assert(product != null); } @@ -167,8 +167,8 @@ abstract class PriceItem extends StatelessWidget { } } -class ProductPriceItem extends PriceItem { - ProductPriceItem({ Key key, Product product }) : super(key: key, product: product); +class _ProductPriceItem extends _PriceItem { + _ProductPriceItem({ Key key, Product product }) : super(key: key, product: product); @override Widget build(BuildContext context) { @@ -180,8 +180,8 @@ class ProductPriceItem extends PriceItem { } } -class FeaturePriceItem extends PriceItem { - FeaturePriceItem({ Key key, Product product }) : super(key: key, product: product); +class _FeaturePriceItem extends _PriceItem { + _FeaturePriceItem({ Key key, Product product }) : super(key: key, product: product); @override Widget build(BuildContext context) { @@ -193,34 +193,54 @@ class FeaturePriceItem extends PriceItem { } } -/// Layout the main left and right elements of a FeatureItem. -class FeatureLayout extends MultiChildLayoutDelegate { - FeatureLayout(); +class _HeadingLayout extends MultiChildLayoutDelegate { + _HeadingLayout(); - static final String left = 'left'; - static final String right = 'right'; + static final String price = 'price'; + static final String image = 'image'; + static final String title = 'title'; + static final String description = 'description'; + static final String vendor = 'vendor'; - // 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 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; - 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)); + final double halfHeight = size.height / 2.0; + final 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(FeatureLayout oldDelegate) => false; + bool shouldRelayout(_HeadingLayout oldDelegate) => false; } -/// A card that highlights the "featured" catalog item. -class FeatureItem extends StatelessWidget { - FeatureItem({ Key key, this.product }) : super(key: key) { +// A card that highlights the "featured" catalog item. +class _Heading extends StatelessWidget { + _Heading({ Key key, this.product }) : super(key: key) { assert(product.featureTitle != null); assert(product.featureDescription != null); } @@ -229,63 +249,39 @@ class FeatureItem extends StatelessWidget { @override Widget build(BuildContext context) { + final Size screenSize = MediaQuery.of(context).size; final ShrineTheme theme = ShrineTheme.of(context); - return new AspectRatio( - aspectRatio: 3.0 / 3.5, + return new SizedBox( + height: screenSize.width > screenSize.height + ? (screenSize.height - kToolbarHeight) * 0.85 + : (screenSize.height - kToolbarHeight) * 0.70, child: new Container( decoration: new BoxDecoration( backgroundColor: theme.cardBackgroundColor, border: new Border(bottom: new BorderSide(color: theme.dividerColor)), ), - child: new Column( - crossAxisAlignment: CrossAxisAlignment.stretch, + child: new CustomMultiChildLayout( + delegate: new _HeadingLayout(), children: [ - new SizedBox( - height: unitSize, - child: new Align( - alignment: FractionalOffset.topRight, - child: new FeaturePriceItem(product: product), - ), + new LayoutId( + id: _HeadingLayout.price, + child: new _FeaturePriceItem(product: product), ), - new Expanded( - 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 Image.asset(product.imageAsset, fit: BoxFit.cover), - ), - ), - ), - 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), - ], - ), - ), - ), - ], - ), + new LayoutId( + id: _HeadingLayout.image, + child: new Image.asset(product.imageAsset, 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), ), ], ), @@ -294,9 +290,10 @@ class FeatureItem extends StatelessWidget { } } -/// 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) { +// 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 { + _ProductItem({ Key key, this.product, this.onPressed }) : super(key: key) { assert(product != null); } @@ -312,7 +309,7 @@ class ProductItem extends StatelessWidget { children: [ new Align( alignment: FractionalOffset.centerRight, - child: new ProductPriceItem(product: product), + child: new _ProductPriceItem(product: product), ), new Container( width: 144.0, @@ -325,7 +322,7 @@ class ProductItem extends StatelessWidget { ), new Padding( padding: const EdgeInsets.symmetric(horizontal: 8.0), - child: new VendorItem(vendor: product.vendor), + child: new _VendorItem(vendor: product.vendor), ), ], ), @@ -339,18 +336,18 @@ class ProductItem extends StatelessWidget { } } -/// The Shrine app's home page. Displays the featured item above all of the -/// product items arranged in two columns. +// 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 { - static final GlobalKey scaffoldKey = new GlobalKey(debugLabel: 'Shrine Home'); - static final ShrineGridDelegate gridDelegate = new ShrineGridDelegate(); + static final GlobalKey _scaffoldKey = new GlobalKey(debugLabel: 'Shrine Home'); + static final _ShrineGridDelegate gridDelegate = new _ShrineGridDelegate(); - Future showOrderPage(Product product) async { + Future _showOrderPage(Product product) async { final Order order = _shoppingCart[product] ?? new Order(product: product); final Order completedOrder = await Navigator.push(context, new ShrineOrderRoute( order: order, @@ -371,13 +368,13 @@ class _ShrineHomeState extends State { Widget build(BuildContext context) { final Product featured = _products.firstWhere((Product product) => product.featureDescription != null); return new ShrinePage( - scaffoldKey: scaffoldKey, + scaffoldKey: _scaffoldKey, products: _products, shoppingCart: _shoppingCart, body: new CustomScrollView( slivers: [ new SliverToBoxAdapter( - child: new FeatureItem(product: featured), + child: new _Heading(product: featured), ), new SliverPadding( padding: const EdgeInsets.all(16.0), @@ -385,9 +382,9 @@ class _ShrineHomeState extends State { gridDelegate: gridDelegate, delegate: new SliverChildListDelegate( _products.map((Product product) { - return new ProductItem( + return new _ProductItem( product: product, - onPressed: () { showOrderPage(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 index 992374a93a1..cd9ff7f7ea1 100644 --- a/examples/flutter_gallery/lib/demo/shrine/shrine_order.dart +++ b/examples/flutter_gallery/lib/demo/shrine/shrine_order.dart @@ -9,10 +9,133 @@ 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) { +// Displays the product title's, description, and order quantity dropdown. +class _ProductItem extends StatelessWidget { + _ProductItem({ Key key, this.product, this.quantity, this.onChanged }) : super(key: key) { + assert(product != null); + assert(quantity != null); + assert(onChanged != null); + } + + final Product product; + final int quantity; + final ValueChanged onChanged; + + @override + Widget build(BuildContext context) { + final ShrineTheme theme = ShrineTheme.of(context); + return new Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + new Text(product.name, style: theme.featureTitleStyle), + const SizedBox(height: 24.0), + new Text(product.description, style: theme.featureStyle), + const 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 Padding( + padding: const EdgeInsets.only(left: 8.0), + child: new Text('Quantity $value', style: theme.quantityMenuStyle), + ), + ); + }).toList(), + value: quantity, + onChanged: onChanged, + ), + ), + ), + ), + ], + ); + } +} + +// Vendor name and description +class _VendorItem extends StatelessWidget { + _VendorItem({ Key key, this.vendor }) : super(key: key) { + assert(vendor != null); + } + + final Vendor vendor; + + @override + Widget build(BuildContext context) { + final ShrineTheme theme = ShrineTheme.of(context); + return new Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + new SizedBox( + height: 24.0, + child: new Align( + alignment: FractionalOffset.bottomLeft, + child: new Text(vendor.name, style: theme.vendorTitleStyle), + ), + ), + const SizedBox(height: 16.0), + new Text(vendor.description, style: theme.vendorStyle), + ], + ); + } +} + +// Layout the order page's heading: the product's image, the +// title/description/dropdown product item, and the vendor item. +class _HeadingLayout extends MultiChildLayoutDelegate { + _HeadingLayout(); + + static final String image = 'image'; + static final String icon = 'icon'; + static final String product = 'product'; + static final String vendor = 'vendor'; + + @override + void performLayout(Size size) { + const double margin = 56.0; + final bool landscape = size.width > size.height; + final double imageWidth = (landscape ? size.width / 2.0 : size.width) - margin * 2.0; + final BoxConstraints imageConstraints = new BoxConstraints(maxHeight: 224.0, maxWidth: imageWidth); + final Size imageSize = layoutChild(image, imageConstraints); + final double imageY = 0.0; + positionChild(image, new Offset(margin, imageY)); + + final double productWidth = landscape ? size.width / 2.0 : size.width - margin; + final BoxConstraints productConstraints = new BoxConstraints(maxWidth: productWidth); + final Size productSize = layoutChild(product, productConstraints); + final double productX = landscape ? size.width / 2.0 : margin; + final double productY = landscape ? 0.0 : imageY + imageSize.height + 16.0; + positionChild(product, new Offset(productX, productY)); + + final Size iconSize = layoutChild(icon, new BoxConstraints.loose(size)); + positionChild(icon, new Offset(productX - iconSize.width - 16.0, productY + 8.0)); + + final double vendorWidth = landscape ? size.width - margin : productWidth; + layoutChild(vendor, new BoxConstraints(maxWidth: vendorWidth)); + final double vendorX = landscape ? margin : productX; + final double vendorY = productY + productSize.height + 16.0; + positionChild(vendor, new Offset(vendorX, vendorY)); + } + + @override + bool shouldRelayout(_HeadingLayout oldDelegate) => true; +} + +// Describes a product and vendor in detail, supports specifying +// a order quantity (0-5). Appears at the top of the OrderPage. +class _Heading extends StatelessWidget { + _Heading({ Key key, this.product, this.quantity, this.quantityChanged }) : super(key: key) { assert(product != null); assert(quantity != null && quantity >= 0 && quantity <= 5); } @@ -23,92 +146,50 @@ class OrderItem extends StatelessWidget { @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, + final Size screenSize = MediaQuery.of(context).size; + return new SizedBox( + height: (screenSize.height - kToolbarHeight) * 1.35, + child: 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 CustomMultiChildLayout( + delegate: new _HeadingLayout(), + children: [ + new LayoutId( + id: _HeadingLayout.image, child: new Hero( tag: product.tag, - child: new Image.asset(product.imageAsset, fit: BoxFit.contain), + child: new Image.asset( + product.imageAsset, + fit: BoxFit.contain, + alignment: FractionalOffset.center, + ), ), ), - ), - const SizedBox(height: 24.0), - new Row( - children: [ - new Padding( - padding: const EdgeInsets.symmetric(horizontal: 16.0), - child: new Center( - child: new Icon( - Icons.info_outline, - size: 24.0, - color: const Color(0xFFFFE0E0), - ), - ), + new LayoutId( + id: _HeadingLayout.icon, + child: new Icon( + Icons.info_outline, + size: 24.0, + color: const Color(0xFFFFE0E0), ), - new Expanded( - child: new Text(product.name, style: theme.featureTitleStyle), - ), - ], - ), - new Padding( - padding: const EdgeInsets.only(left: 56.0), - child: new Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - const SizedBox(height: 24.0), - new Text(product.description, style: theme.featureStyle), - const 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 Padding( - padding: const EdgeInsets.only(left: 8.0), - child: new Text('Quantity $value', style: theme.quantityMenuStyle), - ), - ); - }).toList(), - value: quantity, - onChanged: quantityChanged, - ), - ), - ), - ), - const SizedBox(height: 16.0), - new SizedBox( - height: 24.0, - child: new Align( - alignment: FractionalOffset.bottomLeft, - child: new Text(product.vendor.name, style: theme.vendorTitleStyle), - ), - ), - const SizedBox(height: 16.0), - new Text(product.vendor.description, style: theme.vendorStyle), - const SizedBox(height: 24.0), - ], ), - ), - ], + new LayoutId( + id: _HeadingLayout.product, + child: new _ProductItem( + product: product, + quantity: quantity, + onChanged: quantityChanged, + ), + ), + new LayoutId( + id: _HeadingLayout.vendor, + child: new _VendorItem(vendor: product.vendor), + ), + ], + ), ), ), ); @@ -130,9 +211,9 @@ class OrderPage extends StatefulWidget { _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. +// Displays a product's heading 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 { GlobalKey scaffoldKey; @@ -185,24 +266,20 @@ class _OrderPageState extends State { ), body: new CustomScrollView( slivers: [ - new SliverList( - delegate: new SliverChildListDelegate([ - new OrderItem( - product: config.order.product, - quantity: currentOrder.quantity, - quantityChanged: (int value) { updateOrder(quantity: value); }, - ), - const SizedBox(height: 24.0), - ]), + new SliverToBoxAdapter( + child: new _Heading( + product: config.order.product, + quantity: currentOrder.quantity, + quantityChanged: (int value) { updateOrder(quantity: value); }, + ), ), new SliverPadding( - padding: const EdgeInsets.all(8.0), + padding: const EdgeInsets.fromLTRB(8.0, 32.0, 8.0, 8.0), sliver: new SliverGrid( - gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: 2, + gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent( + maxCrossAxisExtent: 248.0, mainAxisSpacing: 8.0, crossAxisSpacing: 8.0, - childAspectRatio: 160.0 / 216.0, // width/height ), delegate: new SliverChildListDelegate( config.products @@ -225,11 +302,11 @@ class _OrderPageState extends State { } } -/// 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. +// 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,