Commit 9192f672 authored by Hans Muller's avatar Hans Muller Committed by GitHub

Shrine gallery demo: support for landscape layout (#9025)

parent 23981f59
......@@ -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: <Widget>[
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: <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 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: <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),
],
),
),
),
],
),
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: <Widget>[
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<ShrineHome> {
static final GlobalKey<ScaffoldState> scaffoldKey = new GlobalKey<ScaffoldState>(debugLabel: 'Shrine Home');
static final ShrineGridDelegate gridDelegate = new ShrineGridDelegate();
static final GlobalKey<ScaffoldState> _scaffoldKey = new GlobalKey<ScaffoldState>(debugLabel: 'Shrine Home');
static final _ShrineGridDelegate gridDelegate = new _ShrineGridDelegate();
Future<Null> showOrderPage(Product product) async {
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,
......@@ -371,13 +368,13 @@ class _ShrineHomeState extends State<ShrineHome> {
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: <Widget>[
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<ShrineHome> {
gridDelegate: gridDelegate,
delegate: new SliverChildListDelegate(
_products.map((Product product) {
return new ProductItem(
return new _ProductItem(
product: product,
onPressed: () { showOrderPage(product); },
onPressed: () { _showOrderPage(product); },
);
}).toList(),
),
......
......@@ -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<int> onChanged;
@override
Widget build(BuildContext context) {
final ShrineTheme theme = ShrineTheme.of(context);
return new Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
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<int>(
items: <int>[0, 1, 2, 3, 4, 5].map((int value) {
return new DropdownMenuItem<int>(
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: <Widget>[
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: <Widget>[
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: <Widget>[
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: <Widget>[
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 LayoutId(
id: _HeadingLayout.product,
child: new _ProductItem(
product: product,
quantity: quantity,
onChanged: quantityChanged,
),
],
),
new Padding(
padding: const EdgeInsets.only(left: 56.0),
child: new Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
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<int>(
items: <int>[0, 1, 2, 3, 4, 5].map((int value) {
return new DropdownMenuItem<int>(
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.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<OrderPage> {
GlobalKey<ScaffoldState> scaffoldKey;
......@@ -185,24 +266,20 @@ class _OrderPageState extends State<OrderPage> {
),
body: new CustomScrollView(
slivers: <Widget>[
new SliverList(
delegate: new SliverChildListDelegate(<Widget>[
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<OrderPage> {
}
}
/// 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<Order> {
ShrineOrderRoute({
this.order,
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment