Commit 0bcecef5 authored by Adam Barth's avatar Adam Barth Committed by GitHub

Convert ShrineHome to use CustomScrollView (#7887)

This patch converts the Shrine home page to using a sliver-based grid.
This required using a CustomScrollView to mix the block at the top with
the grid below.
parent 921c0fa5
...@@ -5,6 +5,8 @@ ...@@ -5,6 +5,8 @@
import 'dart:async'; import 'dart:async';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:meta/meta.dart';
import 'shrine_data.dart'; import 'shrine_data.dart';
import 'shrine_order.dart'; import 'shrine_order.dart';
...@@ -17,46 +19,99 @@ const double unitSize = kToolbarHeight; ...@@ -17,46 +19,99 @@ const double unitSize = kToolbarHeight;
final List<Product> _products = new List<Product>.from(allProducts()); final List<Product> _products = new List<Product>.from(allProducts());
final Map<Product, Order> _shoppingCart = <Product, Order>{}; 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 // The Shrine home page arranges the product cards into two columns. The card
// on every 4th and 5th row spans two columns. // on every 4th and 5th row spans two columns.
class ShrineGridDelegate extends GridDelegate { class ShrineGridLayout extends SliverGridLayout {
int _rowAtIndex(int index) { const ShrineGridLayout({
final int n = index ~/ 8; @required this.rowStride,
return const <int>[0, 0, 1, 1, 2, 2, 3, 4][index - n * 8] + n * 5; @required this.columnStride,
} @required this.tileHeight,
@required this.tileWidth,
});
final double rowStride;
final double columnStride;
final double tileHeight;
final double tileWidth;
int _columnAtIndex(int index) { @override
return const <int>[0, 1, 0, 1, 0, 1, 0, 0][index % 8]; int getMinChildIndexForScrollOffset(double scrollOffset) {
return _minIndexInRow(scrollOffset ~/ rowStride);
} }
int _columnSpanAtIndex(int index) { @override
return const <int>[1, 1, 1, 1, 1, 1, 2, 2][index % 8]; int getMaxChildIndexForScrollOffset(double scrollOffset) {
return _maxIndexInRow(scrollOffset ~/ rowStride);
} }
@override @override
GridSpecification getGridSpecification(BoxConstraints constraints, int childCount) { SliverGridGeometry getGeometryForChildIndex(int index) {
assert(childCount >= 0); final int row = _rowAtIndex(index);
return new GridSpecification.fromRegularTiles( final int column = _columnAtIndex(index);
tileWidth: constraints.maxWidth / 2.0 - 8.0, final int columnSpan = _columnSpanAtIndex(index);
// height = ProductPriceItem + product image + VendorItem return new SliverGridGeometry(
tileHeight: 40.0 + 144.0 + 40.0, scrollOffset: row * rowStride,
columnCount: 2, crossAxisOffset: column * columnStride,
rowCount: childCount == 0 ? 0 : _rowAtIndex(childCount - 1) + 1, mainAxisExtent: tileHeight,
rowSpacing: 8.0, crossAxisExtent: tileWidth + (columnSpan - 1) * columnStride,
columnSpacing: 8.0
); );
} }
@override @override
GridChildPlacement getChildPlacement(GridSpecification specification, int index, Object placementData) { double estimateMaxScrollOffset(int childCount) {
assert(index >= 0); if (childCount == null)
return new GridChildPlacement( return null;
column: _columnAtIndex(index), if (childCount == 0)
row: _rowAtIndex(index), return 0.0;
columnSpan: _columnSpanAtIndex(index), final int rowCount = _rowAtIndex(childCount - 1) + 1;
rowSpan: 1 final double rowSpacing = rowStride - tileHeight;
return rowStride * rowCount - rowSpacing;
}
}
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(
tileWidth: tileWidth,
tileHeight: tileHeight,
rowStride: tileHeight + _kSpacing,
columnStride: tileWidth + _kSpacing,
); );
} }
@override
bool shouldRelayout(@checked SliverGridDelegate oldDelegate) => false;
} }
/// Displays the Vendor's name and avatar. /// Displays the Vendor's name and avatar.
...@@ -77,15 +132,15 @@ class VendorItem extends StatelessWidget { ...@@ -77,15 +132,15 @@ class VendorItem extends StatelessWidget {
width: 24.0, width: 24.0,
child: new ClipRRect( child: new ClipRRect(
borderRadius: new BorderRadius.circular(12.0), borderRadius: new BorderRadius.circular(12.0),
child: new Image.asset(vendor.avatarAsset, fit: ImageFit.cover) child: new Image.asset(vendor.avatarAsset, fit: ImageFit.cover),
) ),
), ),
new SizedBox(width: 8.0), new SizedBox(width: 8.0),
new Expanded( new Expanded(
child: new Text(vendor.name, style: ShrineTheme.of(context).vendorItemStyle) child: new Text(vendor.name, style: ShrineTheme.of(context).vendorItemStyle),
) ),
] ],
) ),
); );
} }
} }
...@@ -107,7 +162,7 @@ abstract class PriceItem extends StatelessWidget { ...@@ -107,7 +162,7 @@ abstract class PriceItem extends StatelessWidget {
return new Container( return new Container(
padding: padding, padding: padding,
decoration: decoration, decoration: decoration,
child: new Text(product.priceString, style: style) child: new Text(product.priceString, style: style),
); );
} }
} }
...@@ -120,7 +175,7 @@ class ProductPriceItem extends PriceItem { ...@@ -120,7 +175,7 @@ class ProductPriceItem extends PriceItem {
return buildItem( return buildItem(
context, context,
ShrineTheme.of(context).priceStyle, ShrineTheme.of(context).priceStyle,
const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0) const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
); );
} }
} }
...@@ -133,7 +188,7 @@ class FeaturePriceItem extends PriceItem { ...@@ -133,7 +188,7 @@ class FeaturePriceItem extends PriceItem {
return buildItem( return buildItem(
context, context,
ShrineTheme.of(context).featurePriceStyle, ShrineTheme.of(context).featurePriceStyle,
const EdgeInsets.symmetric(horizontal: 24.0, vertical: 16.0) const EdgeInsets.symmetric(horizontal: 24.0, vertical: 16.0),
); );
} }
} }
...@@ -180,7 +235,7 @@ class FeatureItem extends StatelessWidget { ...@@ -180,7 +235,7 @@ class FeatureItem extends StatelessWidget {
child: new Container( child: new Container(
decoration: new BoxDecoration( decoration: new BoxDecoration(
backgroundColor: theme.cardBackgroundColor, backgroundColor: theme.cardBackgroundColor,
border: new Border(bottom: new BorderSide(color: theme.dividerColor)) border: new Border(bottom: new BorderSide(color: theme.dividerColor)),
), ),
child: new Column( child: new Column(
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.stretch,
...@@ -189,8 +244,8 @@ class FeatureItem extends StatelessWidget { ...@@ -189,8 +244,8 @@ class FeatureItem extends StatelessWidget {
height: unitSize, height: unitSize,
child: new Align( child: new Align(
alignment: FractionalOffset.topRight, alignment: FractionalOffset.topRight,
child: new FeaturePriceItem(product: product) child: new FeaturePriceItem(product: product),
) ),
), ),
new Expanded( new Expanded(
child: new CustomMultiChildLayout( child: new CustomMultiChildLayout(
...@@ -205,9 +260,9 @@ class FeatureItem extends StatelessWidget { ...@@ -205,9 +260,9 @@ class FeatureItem extends StatelessWidget {
minHeight: 340.0, minHeight: 340.0,
maxHeight: 340.0, maxHeight: 340.0,
alignment: FractionalOffset.topRight, alignment: FractionalOffset.topRight,
child: new Image.asset(product.imageAsset, fit: ImageFit.cover) child: new Image.asset(product.imageAsset, fit: ImageFit.cover),
) ),
) ),
), ),
new LayoutId( new LayoutId(
id: FeatureLayout.right, id: FeatureLayout.right,
...@@ -218,23 +273,23 @@ class FeatureItem extends StatelessWidget { ...@@ -218,23 +273,23 @@ class FeatureItem extends StatelessWidget {
children: <Widget>[ children: <Widget>[
new Padding( new Padding(
padding: const EdgeInsets.only(top: 18.0), padding: const EdgeInsets.only(top: 18.0),
child: new Text(product.featureTitle, style: theme.featureTitleStyle) child: new Text(product.featureTitle, style: theme.featureTitleStyle),
), ),
new Padding( new Padding(
padding: const EdgeInsets.symmetric(vertical: 16.0), padding: const EdgeInsets.symmetric(vertical: 16.0),
child: new Text(product.featureDescription, style: theme.featureStyle) child: new Text(product.featureDescription, style: theme.featureStyle),
), ),
new VendorItem(vendor: product.vendor) new VendorItem(vendor: product.vendor),
] ],
) ),
) ),
) ),
] ],
) ),
) ),
] ],
) ),
) ),
); );
} }
} }
...@@ -257,7 +312,7 @@ class ProductItem extends StatelessWidget { ...@@ -257,7 +312,7 @@ class ProductItem extends StatelessWidget {
children: <Widget>[ children: <Widget>[
new Align( new Align(
alignment: FractionalOffset.centerRight, alignment: FractionalOffset.centerRight,
child: new ProductPriceItem(product: product) child: new ProductPriceItem(product: product),
), ),
new Container( new Container(
width: 144.0, width: 144.0,
...@@ -265,21 +320,21 @@ class ProductItem extends StatelessWidget { ...@@ -265,21 +320,21 @@ class ProductItem extends StatelessWidget {
padding: const EdgeInsets.symmetric(horizontal: 8.0), padding: const EdgeInsets.symmetric(horizontal: 8.0),
child: new Hero( child: new Hero(
tag: product.tag, tag: product.tag,
child: new Image.asset(product.imageAsset, fit: ImageFit.contain) child: new Image.asset(product.imageAsset, fit: ImageFit.contain),
) ),
), ),
new Padding( new Padding(
padding: const EdgeInsets.symmetric(horizontal: 8.0), padding: const EdgeInsets.symmetric(horizontal: 8.0),
child: new VendorItem(vendor: product.vendor) child: new VendorItem(vendor: product.vendor),
) ),
] ],
), ),
new Material( new Material(
type: MaterialType.transparency, type: MaterialType.transparency,
child: new InkWell(onTap: onPressed) child: new InkWell(onTap: onPressed),
), ),
] ],
) ),
); );
} }
} }
...@@ -293,7 +348,7 @@ class ShrineHome extends StatefulWidget { ...@@ -293,7 +348,7 @@ class ShrineHome extends StatefulWidget {
class _ShrineHomeState extends State<ShrineHome> { class _ShrineHomeState extends State<ShrineHome> {
static final GlobalKey<ScaffoldState> scaffoldKey = new GlobalKey<ScaffoldState>(debugLabel: 'Shrine Home'); static final GlobalKey<ScaffoldState> scaffoldKey = new GlobalKey<ScaffoldState>(debugLabel: 'Shrine Home');
static final GridDelegate gridDelegate = new ShrineGridDelegate(); 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 order = _shoppingCart[product] ?? new Order(product: product);
...@@ -303,7 +358,7 @@ class _ShrineHomeState extends State<ShrineHome> { ...@@ -303,7 +358,7 @@ class _ShrineHomeState extends State<ShrineHome> {
return new OrderPage( return new OrderPage(
order: order, order: order,
products: _products, products: _products,
shoppingCart: _shoppingCart shoppingCart: _shoppingCart,
); );
} }
)); ));
...@@ -319,29 +374,27 @@ class _ShrineHomeState extends State<ShrineHome> { ...@@ -319,29 +374,27 @@ class _ShrineHomeState extends State<ShrineHome> {
scaffoldKey: scaffoldKey, scaffoldKey: scaffoldKey,
products: _products, products: _products,
shoppingCart: _shoppingCart, shoppingCart: _shoppingCart,
body: new ScrollableViewport( body: new CustomScrollView(
child: new RepaintBoundary( slivers: <Widget>[
child: new Column( new SliverToBoxAdapter(
children: <Widget>[ child: new FeatureItem(product: featured),
new FeatureItem(product: featured), ),
new Padding( new SliverPadding(
padding: const EdgeInsets.all(16.0), padding: const EdgeInsets.all(16.0),
child: new CustomGrid( child: new SliverGrid(
delegate: gridDelegate, gridDelegate: gridDelegate,
children: _products.map((Product product) { delegate: new SliverChildListDelegate(
return new RepaintBoundary( _products.map((Product product) {
child: new ProductItem( return new ProductItem(
product: product, product: product,
onPressed: () { showOrderPage(product); } onPressed: () { showOrderPage(product); },
) );
); }).toList(),
}).toList() ),
) ),
) ),
] ],
) ),
)
)
); );
} }
} }
...@@ -24,10 +24,10 @@ import 'sliver_multi_box_adaptor.dart'; ...@@ -24,10 +24,10 @@ import 'sliver_multi_box_adaptor.dart';
class SliverGridGeometry { class SliverGridGeometry {
/// Creates an object that describes the placement of a child in a [RenderSliverGrid]. /// Creates an object that describes the placement of a child in a [RenderSliverGrid].
const SliverGridGeometry({ const SliverGridGeometry({
this.scrollOffset, @required this.scrollOffset,
this.crossAxisOffset, @required this.crossAxisOffset,
this.mainAxisExtent, @required this.mainAxisExtent,
this.crossAxisExtent, @required this.crossAxisExtent,
}); });
/// The scroll offset of the leading edge of the child relative to the leading /// The scroll offset of the leading edge of the child relative to the leading
...@@ -195,7 +195,7 @@ class SliverGridRegularTileLayout extends SliverGridLayout { ...@@ -195,7 +195,7 @@ class SliverGridRegularTileLayout extends SliverGridLayout {
double estimateMaxScrollOffset(int childCount) { double estimateMaxScrollOffset(int childCount) {
if (childCount == null) if (childCount == null)
return null; return null;
final int mainAxisCount = ((childCount - 1) / crossAxisCount).floor() + 1; final int mainAxisCount = ((childCount - 1) ~/ crossAxisCount) + 1;
final double mainAxisSpacing = mainAxisStride - childMainAxisExtent; final double mainAxisSpacing = mainAxisStride - childMainAxisExtent;
return mainAxisStride * mainAxisCount - mainAxisSpacing; return mainAxisStride * mainAxisCount - mainAxisSpacing;
} }
......
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