mirror of
https://github.com/flutter/flutter.git
synced 2025-06-03 00:51:18 +00:00
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.
This commit is contained in:
parent
921c0fa5e4
commit
0bcecef5de
@ -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;
|
|||||||
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,
|
||||||
|
});
|
||||||
|
|
||||||
int _columnAtIndex(int index) {
|
final double rowStride;
|
||||||
return const <int>[0, 1, 0, 1, 0, 1, 0, 0][index % 8];
|
final double columnStride;
|
||||||
}
|
final double tileHeight;
|
||||||
|
final double tileWidth;
|
||||||
|
|
||||||
int _columnSpanAtIndex(int index) {
|
@override
|
||||||
return const <int>[1, 1, 1, 1, 1, 1, 2, 2][index % 8];
|
int getMinChildIndexForScrollOffset(double scrollOffset) {
|
||||||
|
return _minIndexInRow(scrollOffset ~/ rowStride);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
GridSpecification getGridSpecification(BoxConstraints constraints, int childCount) {
|
int getMaxChildIndexForScrollOffset(double scrollOffset) {
|
||||||
assert(childCount >= 0);
|
return _maxIndexInRow(scrollOffset ~/ rowStride);
|
||||||
return new GridSpecification.fromRegularTiles(
|
}
|
||||||
tileWidth: constraints.maxWidth / 2.0 - 8.0,
|
|
||||||
// height = ProductPriceItem + product image + VendorItem
|
@override
|
||||||
tileHeight: 40.0 + 144.0 + 40.0,
|
SliverGridGeometry getGeometryForChildIndex(int index) {
|
||||||
columnCount: 2,
|
final int row = _rowAtIndex(index);
|
||||||
rowCount: childCount == 0 ? 0 : _rowAtIndex(childCount - 1) + 1,
|
final int column = _columnAtIndex(index);
|
||||||
rowSpacing: 8.0,
|
final int columnSpan = _columnSpanAtIndex(index);
|
||||||
columnSpacing: 8.0
|
return new SliverGridGeometry(
|
||||||
|
scrollOffset: row * rowStride,
|
||||||
|
crossAxisOffset: column * columnStride,
|
||||||
|
mainAxisExtent: tileHeight,
|
||||||
|
crossAxisExtent: tileWidth + (columnSpan - 1) * columnStride,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@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 {
|
|||||||
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 {
|
|||||||
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 {
|
|||||||
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 {
|
|||||||
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 {
|
|||||||
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 {
|
|||||||
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 {
|
|||||||
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 {
|
|||||||
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 {
|
|||||||
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 {
|
|||||||
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 {
|
|||||||
|
|
||||||
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> {
|
|||||||
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> {
|
|||||||
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';
|
|||||||
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 {
|
|||||||
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;
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user