Commit ff14f35d authored by Adam Barth's avatar Adam Barth Committed by GitHub

CustomScrollView (#7881)

Also, use CustomScrollView in Shrine and fix a bug with one-line grids
not painting properly due to their reporiting zero paintExtent.
parent 66742465
...@@ -39,9 +39,9 @@ class OrderItem extends StatelessWidget { ...@@ -39,9 +39,9 @@ class OrderItem extends StatelessWidget {
height: 248.0, height: 248.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 SizedBox(height: 24.0), new SizedBox(height: 24.0),
new Row( new Row(
...@@ -52,14 +52,14 @@ class OrderItem extends StatelessWidget { ...@@ -52,14 +52,14 @@ class OrderItem extends StatelessWidget {
child: new Icon( child: new Icon(
Icons.info_outline, Icons.info_outline,
size: 24.0, size: 24.0,
color: const Color(0xFFFFE0E0) color: const Color(0xFFFFE0E0),
) ),
) ),
), ),
new Expanded( new Expanded(
child: new Text(product.name, style: theme.featureTitleStyle) child: new Text(product.name, style: theme.featureTitleStyle),
) ),
] ],
), ),
new Padding( new Padding(
padding: const EdgeInsets.only(left: 56.0), padding: const EdgeInsets.only(left: 56.0),
...@@ -75,8 +75,8 @@ class OrderItem extends StatelessWidget { ...@@ -75,8 +75,8 @@ class OrderItem extends StatelessWidget {
child: new Container( child: new Container(
decoration: new BoxDecoration( decoration: new BoxDecoration(
border: new Border.all( border: new Border.all(
color: const Color(0xFFD9D9D9) color: const Color(0xFFD9D9D9),
) ),
), ),
child: new DropdownButton<int>( child: new DropdownButton<int>(
items: <int>[0, 1, 2, 3, 4, 5].map((int value) { items: <int>[0, 1, 2, 3, 4, 5].map((int value) {
...@@ -84,33 +84,33 @@ class OrderItem extends StatelessWidget { ...@@ -84,33 +84,33 @@ class OrderItem extends StatelessWidget {
value: value, value: value,
child: new Padding( child: new Padding(
padding: const EdgeInsets.only(left: 8.0), padding: const EdgeInsets.only(left: 8.0),
child: new Text('Quantity $value', style: theme.quantityMenuStyle) child: new Text('Quantity $value', style: theme.quantityMenuStyle),
) ),
); );
}).toList(), }).toList(),
value: quantity, value: quantity,
onChanged: quantityChanged onChanged: quantityChanged,
) ),
) ),
) ),
), ),
new SizedBox(height: 16.0), new SizedBox(height: 16.0),
new SizedBox( new SizedBox(
height: 24.0, height: 24.0,
child: new Align( child: new Align(
alignment: FractionalOffset.bottomLeft, alignment: FractionalOffset.bottomLeft,
child: new Text(product.vendor.name, style: theme.vendorTitleStyle) child: new Text(product.vendor.name, style: theme.vendorTitleStyle),
) ),
), ),
new SizedBox(height: 16.0), new SizedBox(height: 16.0),
new Text(product.vendor.description, style: theme.vendorStyle), new Text(product.vendor.description, style: theme.vendorStyle),
new SizedBox(height: 24.0) new SizedBox(height: 24.0),
] ],
) ),
) ),
] ],
) ),
) ),
); );
} }
} }
...@@ -174,39 +174,47 @@ class _OrderPageState extends State<OrderPage> { ...@@ -174,39 +174,47 @@ class _OrderPageState extends State<OrderPage> {
backgroundColor: const Color(0xFF16F0F0), backgroundColor: const Color(0xFF16F0F0),
child: new Icon( child: new Icon(
Icons.add_shopping_cart, Icons.add_shopping_cart,
color: Colors.black color: Colors.black,
)
), ),
body: new Block( ),
children: <Widget>[ body: new CustomScrollView(
slivers: <Widget>[
new SliverList(
delegate: new SliverChildListDelegate(<Widget>[
new OrderItem( new OrderItem(
product: config.order.product, product: config.order.product,
quantity: currentOrder.quantity, quantity: currentOrder.quantity,
quantityChanged: (int value) { updateOrder(quantity: value); } quantityChanged: (int value) { updateOrder(quantity: value); },
), ),
new SizedBox(height: 24.0), new SizedBox(height: 24.0),
new FixedColumnCountGrid( ]),
columnCount: 2, ),
rowSpacing: 8.0, new SliverPadding(
columnSpacing: 8.0,
padding: const EdgeInsets.all(8.0), padding: const EdgeInsets.all(8.0),
tileAspectRatio: 160.0 / 216.0, // width/height child: new SliverGrid(
children: config.products gridDelegate: new SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
mainAxisSpacing: 8.0,
crossAxisSpacing: 8.0,
childAspectRatio: 160.0 / 216.0, // width/height
),
delegate: new SliverChildListDelegate(
config.products
.where((Product product) => product != config.order.product) .where((Product product) => product != config.order.product)
.map((Product product) { .map((Product product) {
return new RepaintBoundary( return new Card(
child: new Card(
elevation: 1, elevation: 1,
child: new Image.asset( child: new Image.asset(
product.imageAsset, product.imageAsset,
fit: ImageFit.contain fit: ImageFit.contain,
) ),
)
); );
}).toList() }).toList(),
) ),
] ),
) ),
],
),
); );
} }
} }
...@@ -220,7 +228,7 @@ class ShrineOrderRoute extends ShrinePageRoute<Order> { ...@@ -220,7 +228,7 @@ class ShrineOrderRoute extends ShrinePageRoute<Order> {
ShrineOrderRoute({ ShrineOrderRoute({
this.order, this.order,
WidgetBuilder builder, WidgetBuilder builder,
RouteSettings settings: const RouteSettings() RouteSettings settings: const RouteSettings(),
}) : super(builder: builder, settings: settings) { }) : super(builder: builder, settings: settings) {
assert(order != null); assert(order != null);
} }
......
...@@ -106,7 +106,7 @@ abstract class RenderSliverFixedExtentBoxAdaptor extends RenderSliverMultiBoxAda ...@@ -106,7 +106,7 @@ abstract class RenderSliverFixedExtentBoxAdaptor extends RenderSliverMultiBoxAda
trailingScrollOffset: trailingScrollOffset, trailingScrollOffset: trailingScrollOffset,
); );
final double paintedExtent = calculatePaintOffset( final double paintExtent = calculatePaintOffset(
constraints, constraints,
from: leadingScrollOffset, from: leadingScrollOffset,
to: trailingScrollOffset, to: trailingScrollOffset,
...@@ -114,7 +114,7 @@ abstract class RenderSliverFixedExtentBoxAdaptor extends RenderSliverMultiBoxAda ...@@ -114,7 +114,7 @@ abstract class RenderSliverFixedExtentBoxAdaptor extends RenderSliverMultiBoxAda
geometry = new SliverGeometry( geometry = new SliverGeometry(
scrollExtent: estimatedMaxScrollOffset, scrollExtent: estimatedMaxScrollOffset,
paintExtent: paintedExtent, paintExtent: paintExtent,
maxPaintExtent: estimatedMaxScrollOffset, maxPaintExtent: estimatedMaxScrollOffset,
// Conservative to avoid flickering away the clip during scroll. // Conservative to avoid flickering away the clip during scroll.
hasVisualOverflow: lastIndex >= targetLastIndex || constraints.scrollOffset > 0.0, hasVisualOverflow: lastIndex >= targetLastIndex || constraints.scrollOffset > 0.0,
......
...@@ -52,6 +52,10 @@ class SliverGridGeometry { ...@@ -52,6 +52,10 @@ class SliverGridGeometry {
/// scroll axis is horizontal, this extent is the child's height. /// scroll axis is horizontal, this extent is the child's height.
final double crossAxisExtent; final double crossAxisExtent;
/// The scroll offset of the trailing edge of the child relative to the
/// leading edge of the parent.
double get trailingScrollOffset => scrollOffset + mainAxisExtent;
/// Returns a tight [BoxConstraints] that forces the child to have the /// Returns a tight [BoxConstraints] that forces the child to have the
/// required size. /// required size.
BoxConstraints getBoxConstraints(SliverConstraints constraints) { BoxConstraints getBoxConstraints(SliverConstraints constraints) {
...@@ -61,6 +65,16 @@ class SliverGridGeometry { ...@@ -61,6 +65,16 @@ class SliverGridGeometry {
crossAxisExtent: crossAxisExtent, crossAxisExtent: crossAxisExtent,
); );
} }
@override
String toString() {
return 'SliverGridGeometry('
'scrollOffset: $scrollOffset, '
'crossAxisOffset: $crossAxisOffset, '
'mainAxisExtent: $mainAxisExtent, '
'crossAxisExtent: $crossAxisExtent'
')';
}
} }
class SliverGridParentData extends SliverMultiBoxAdaptorParentData { class SliverGridParentData extends SliverMultiBoxAdaptorParentData {
...@@ -329,7 +343,7 @@ class RenderSliverGrid extends RenderSliverMultiBoxAdaptor { ...@@ -329,7 +343,7 @@ class RenderSliverGrid extends RenderSliverMultiBoxAdaptor {
final SliverGridGeometry firstChildGridGeometry = _gridDelegate final SliverGridGeometry firstChildGridGeometry = _gridDelegate
.getGeometryForChildIndex(constraints, firstIndex); .getGeometryForChildIndex(constraints, firstIndex);
double leadingScrollOffset = firstChildGridGeometry.scrollOffset; double leadingScrollOffset = firstChildGridGeometry.scrollOffset;
double trailingScrollOffset = firstChildGridGeometry.scrollOffset; double trailingScrollOffset = firstChildGridGeometry.trailingScrollOffset;
if (firstChild == null) { if (firstChild == null) {
if (!addInitialChild(index: firstIndex, if (!addInitialChild(index: firstIndex,
...@@ -352,8 +366,7 @@ class RenderSliverGrid extends RenderSliverMultiBoxAdaptor { ...@@ -352,8 +366,7 @@ class RenderSliverGrid extends RenderSliverMultiBoxAdaptor {
childParentData.crossAxisOffset = gridGeometry.crossAxisOffset; childParentData.crossAxisOffset = gridGeometry.crossAxisOffset;
assert(childParentData.index == index); assert(childParentData.index == index);
trailingChildWithLayout ??= child; trailingChildWithLayout ??= child;
if (gridGeometry.scrollOffset > trailingScrollOffset) trailingScrollOffset = math.max(trailingScrollOffset, gridGeometry.trailingScrollOffset);
trailingScrollOffset = gridGeometry.scrollOffset;
} }
assert(childScrollOffset(firstChild) <= scrollOffset); assert(childScrollOffset(firstChild) <= scrollOffset);
...@@ -389,8 +402,7 @@ class RenderSliverGrid extends RenderSliverMultiBoxAdaptor { ...@@ -389,8 +402,7 @@ class RenderSliverGrid extends RenderSliverMultiBoxAdaptor {
childParentData.layoutOffset = gridGeometry.scrollOffset; childParentData.layoutOffset = gridGeometry.scrollOffset;
childParentData.crossAxisOffset = gridGeometry.crossAxisOffset; childParentData.crossAxisOffset = gridGeometry.crossAxisOffset;
assert(childParentData.index == index); assert(childParentData.index == index);
if (gridGeometry.scrollOffset > trailingScrollOffset) trailingScrollOffset = math.max(trailingScrollOffset, gridGeometry.trailingScrollOffset);
trailingScrollOffset = gridGeometry.scrollOffset;
} }
final int lastIndex = indexOf(lastChild); final int lastIndex = indexOf(lastChild);
...@@ -407,7 +419,7 @@ class RenderSliverGrid extends RenderSliverMultiBoxAdaptor { ...@@ -407,7 +419,7 @@ class RenderSliverGrid extends RenderSliverMultiBoxAdaptor {
trailingScrollOffset: trailingScrollOffset, trailingScrollOffset: trailingScrollOffset,
); );
final double paintedExtent = calculatePaintOffset( final double paintExtent = calculatePaintOffset(
constraints, constraints,
from: leadingScrollOffset, from: leadingScrollOffset,
to: trailingScrollOffset, to: trailingScrollOffset,
...@@ -415,7 +427,7 @@ class RenderSliverGrid extends RenderSliverMultiBoxAdaptor { ...@@ -415,7 +427,7 @@ class RenderSliverGrid extends RenderSliverMultiBoxAdaptor {
geometry = new SliverGeometry( geometry = new SliverGeometry(
scrollExtent: estimatedTotalExtent, scrollExtent: estimatedTotalExtent,
paintExtent: paintedExtent, paintExtent: paintExtent,
maxPaintExtent: estimatedTotalExtent, maxPaintExtent: estimatedTotalExtent,
// Conservative to avoid complexity. // Conservative to avoid complexity.
hasVisualOverflow: true, hasVisualOverflow: true,
......
...@@ -197,14 +197,14 @@ class RenderSliverList extends RenderSliverMultiBoxAdaptor { ...@@ -197,14 +197,14 @@ class RenderSliverList extends RenderSliverMultiBoxAdaptor {
); );
assert(estimatedMaxScrollOffset >= endScrollOffset - childScrollOffset(firstChild)); assert(estimatedMaxScrollOffset >= endScrollOffset - childScrollOffset(firstChild));
} }
final double paintedExtent = calculatePaintOffset( final double paintExtent = calculatePaintOffset(
constraints, constraints,
from: childScrollOffset(firstChild), from: childScrollOffset(firstChild),
to: endScrollOffset, to: endScrollOffset,
); );
geometry = new SliverGeometry( geometry = new SliverGeometry(
scrollExtent: estimatedMaxScrollOffset, scrollExtent: estimatedMaxScrollOffset,
paintExtent: paintedExtent, paintExtent: paintExtent,
maxPaintExtent: estimatedMaxScrollOffset, maxPaintExtent: estimatedMaxScrollOffset,
// Conservative to avoid flickering away the clip during scroll. // Conservative to avoid flickering away the clip during scroll.
hasVisualOverflow: endScrollOffset > targetEndScrollOffset || constraints.scrollOffset > 0.0, hasVisualOverflow: endScrollOffset > targetEndScrollOffset || constraints.scrollOffset > 0.0,
......
...@@ -82,6 +82,28 @@ abstract class ScrollView extends StatelessWidget { ...@@ -82,6 +82,28 @@ abstract class ScrollView extends StatelessWidget {
} }
} }
class CustomScrollView extends ScrollView {
CustomScrollView({
Key key,
Axis scrollDirection: Axis.vertical,
bool reverse: false,
ScrollPhysics physics,
bool shrinkWrap: false,
this.slivers: const <Widget>[],
}) : super(
key: key,
scrollDirection: scrollDirection,
reverse: reverse,
physics: physics,
shrinkWrap: shrinkWrap,
);
final List<Widget> slivers;
@override
List<Widget> buildSlivers(BuildContext context) => slivers;
}
abstract class BoxScrollView extends ScrollView { abstract class BoxScrollView extends ScrollView {
BoxScrollView({ BoxScrollView({
Key key, Key key,
......
...@@ -5,6 +5,7 @@ ...@@ -5,6 +5,7 @@
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import '../rendering/mock_canvas.dart';
import 'states.dart'; import 'states.dart';
void main() { void main() {
...@@ -304,6 +305,29 @@ void main() { ...@@ -304,6 +305,29 @@ void main() {
expect(find.text('4'), findsNothing); expect(find.text('4'), findsNothing);
}); });
testWidgets('One-line GridView paints', (WidgetTester tester) async {
const Color green = const Color(0xFF00FF00);
final Container container = new Container(
decoration: const BoxDecoration(
backgroundColor: green,
),
);
await tester.pumpWidget(new Center(
child: new SizedBox(
height: 200.0,
child: new GridView.count(
crossAxisCount: 2,
children: <Widget>[ container, container, container, container ],
),
),
));
expect(find.byType(GridView), paints..rect(color: green)..rect(color: green));
expect(find.byType(GridView), isNot(paints..rect(color: green)..rect(color: green)..rect(color: green)));
});
// TODO(ianh): can you tap a grid cell that is slightly off the bottom of the screen? // TODO(ianh): can you tap a grid cell that is slightly off the bottom of the screen?
// (try it with the flutter_gallery Grid demo) // (try it with the flutter_gallery Grid demo)
} }
...@@ -72,4 +72,47 @@ void main() { ...@@ -72,4 +72,47 @@ void main() {
Viewport2 viewport = tester.widget(find.byType(Viewport2)); Viewport2 viewport = tester.widget(find.byType(Viewport2));
expect(viewport.offset.pixels, equals(2400.0)); expect(viewport.offset.pixels, equals(2400.0));
}); });
testWidgets('CustomScrollView control test', (WidgetTester tester) async {
List<String> log = <String>[];
await tester.pumpWidget(new CustomScrollView(
slivers: <Widget>[
new SliverList(
delegate: new SliverChildListDelegate(
kStates.map<Widget>((String state) {
return new GestureDetector(
onTap: () {
log.add(state);
},
child: new Container(
height: 200.0,
decoration: const BoxDecoration(
backgroundColor: const Color(0xFF0000FF),
),
child: new Text(state),
),
);
}).toList(),
),
),
],
));
await tester.tap(find.text('Alabama'));
expect(log, equals(<String>['Alabama']));
log.clear();
expect(find.text('Nevada'), findsNothing);
await tester.scroll(find.text('Alabama'), const Offset(0.0, -4000.0));
await tester.pump();
expect(find.text('Alabama'), findsNothing);
expect(tester.getCenter(find.text('Massachusetts')), equals(const Point(400.0, 100.0)));
await tester.tap(find.text('Massachusetts'));
expect(log, equals(<String>['Massachusetts']));
log.clear();
});
} }
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