CustomScrollView (#7881)

Also, use CustomScrollView in Shrine and fix a bug with one-line grids
not painting properly due to their reporiting zero paintExtent.
This commit is contained in:
Adam Barth 2017-02-03 22:52:25 -08:00 committed by GitHub
parent 667424659d
commit ff14f35d6d
7 changed files with 177 additions and 68 deletions

View file

@ -39,9 +39,9 @@ class OrderItem extends StatelessWidget {
height: 248.0,
child: new Hero(
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 Row(
@ -52,14 +52,14 @@ class OrderItem extends StatelessWidget {
child: new Icon(
Icons.info_outline,
size: 24.0,
color: const Color(0xFFFFE0E0)
)
)
color: const Color(0xFFFFE0E0),
),
),
),
new Expanded(
child: new Text(product.name, style: theme.featureTitleStyle)
)
]
child: new Text(product.name, style: theme.featureTitleStyle),
),
],
),
new Padding(
padding: const EdgeInsets.only(left: 56.0),
@ -75,8 +75,8 @@ class OrderItem extends StatelessWidget {
child: new Container(
decoration: new BoxDecoration(
border: new Border.all(
color: const Color(0xFFD9D9D9)
)
color: const Color(0xFFD9D9D9),
),
),
child: new DropdownButton<int>(
items: <int>[0, 1, 2, 3, 4, 5].map((int value) {
@ -84,33 +84,33 @@ class OrderItem extends StatelessWidget {
value: value,
child: new Padding(
padding: const EdgeInsets.only(left: 8.0),
child: new Text('Quantity $value', style: theme.quantityMenuStyle)
)
child: new Text('Quantity $value', style: theme.quantityMenuStyle),
),
);
}).toList(),
value: quantity,
onChanged: quantityChanged
)
)
)
onChanged: quantityChanged,
),
),
),
),
new SizedBox(height: 16.0),
new SizedBox(
height: 24.0,
child: new Align(
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 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> {
backgroundColor: const Color(0xFF16F0F0),
child: new Icon(
Icons.add_shopping_cart,
color: Colors.black
)
color: Colors.black,
),
),
body: new Block(
children: <Widget>[
new OrderItem(
product: config.order.product,
quantity: currentOrder.quantity,
quantityChanged: (int value) { updateOrder(quantity: value); }
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); },
),
new SizedBox(height: 24.0),
]),
),
new SizedBox(height: 24.0),
new FixedColumnCountGrid(
columnCount: 2,
rowSpacing: 8.0,
columnSpacing: 8.0,
new SliverPadding(
padding: const EdgeInsets.all(8.0),
tileAspectRatio: 160.0 / 216.0, // width/height
children: config.products
.where((Product product) => product != config.order.product)
.map((Product product) {
return new RepaintBoundary(
child: new Card(
elevation: 1,
child: new Image.asset(
product.imageAsset,
fit: ImageFit.contain
)
)
);
}).toList()
)
]
)
child: new SliverGrid(
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)
.map((Product product) {
return new Card(
elevation: 1,
child: new Image.asset(
product.imageAsset,
fit: ImageFit.contain,
),
);
}).toList(),
),
),
),
],
),
);
}
}
@ -220,7 +228,7 @@ class ShrineOrderRoute extends ShrinePageRoute<Order> {
ShrineOrderRoute({
this.order,
WidgetBuilder builder,
RouteSettings settings: const RouteSettings()
RouteSettings settings: const RouteSettings(),
}) : super(builder: builder, settings: settings) {
assert(order != null);
}

View file

@ -106,7 +106,7 @@ abstract class RenderSliverFixedExtentBoxAdaptor extends RenderSliverMultiBoxAda
trailingScrollOffset: trailingScrollOffset,
);
final double paintedExtent = calculatePaintOffset(
final double paintExtent = calculatePaintOffset(
constraints,
from: leadingScrollOffset,
to: trailingScrollOffset,
@ -114,7 +114,7 @@ abstract class RenderSliverFixedExtentBoxAdaptor extends RenderSliverMultiBoxAda
geometry = new SliverGeometry(
scrollExtent: estimatedMaxScrollOffset,
paintExtent: paintedExtent,
paintExtent: paintExtent,
maxPaintExtent: estimatedMaxScrollOffset,
// Conservative to avoid flickering away the clip during scroll.
hasVisualOverflow: lastIndex >= targetLastIndex || constraints.scrollOffset > 0.0,

View file

@ -52,6 +52,10 @@ class SliverGridGeometry {
/// scroll axis is horizontal, this extent is the child's height.
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
/// required size.
BoxConstraints getBoxConstraints(SliverConstraints constraints) {
@ -61,6 +65,16 @@ class SliverGridGeometry {
crossAxisExtent: crossAxisExtent,
);
}
@override
String toString() {
return 'SliverGridGeometry('
'scrollOffset: $scrollOffset, '
'crossAxisOffset: $crossAxisOffset, '
'mainAxisExtent: $mainAxisExtent, '
'crossAxisExtent: $crossAxisExtent'
')';
}
}
class SliverGridParentData extends SliverMultiBoxAdaptorParentData {
@ -329,7 +343,7 @@ class RenderSliverGrid extends RenderSliverMultiBoxAdaptor {
final SliverGridGeometry firstChildGridGeometry = _gridDelegate
.getGeometryForChildIndex(constraints, firstIndex);
double leadingScrollOffset = firstChildGridGeometry.scrollOffset;
double trailingScrollOffset = firstChildGridGeometry.scrollOffset;
double trailingScrollOffset = firstChildGridGeometry.trailingScrollOffset;
if (firstChild == null) {
if (!addInitialChild(index: firstIndex,
@ -352,8 +366,7 @@ class RenderSliverGrid extends RenderSliverMultiBoxAdaptor {
childParentData.crossAxisOffset = gridGeometry.crossAxisOffset;
assert(childParentData.index == index);
trailingChildWithLayout ??= child;
if (gridGeometry.scrollOffset > trailingScrollOffset)
trailingScrollOffset = gridGeometry.scrollOffset;
trailingScrollOffset = math.max(trailingScrollOffset, gridGeometry.trailingScrollOffset);
}
assert(childScrollOffset(firstChild) <= scrollOffset);
@ -389,8 +402,7 @@ class RenderSliverGrid extends RenderSliverMultiBoxAdaptor {
childParentData.layoutOffset = gridGeometry.scrollOffset;
childParentData.crossAxisOffset = gridGeometry.crossAxisOffset;
assert(childParentData.index == index);
if (gridGeometry.scrollOffset > trailingScrollOffset)
trailingScrollOffset = gridGeometry.scrollOffset;
trailingScrollOffset = math.max(trailingScrollOffset, gridGeometry.trailingScrollOffset);
}
final int lastIndex = indexOf(lastChild);
@ -407,7 +419,7 @@ class RenderSliverGrid extends RenderSliverMultiBoxAdaptor {
trailingScrollOffset: trailingScrollOffset,
);
final double paintedExtent = calculatePaintOffset(
final double paintExtent = calculatePaintOffset(
constraints,
from: leadingScrollOffset,
to: trailingScrollOffset,
@ -415,7 +427,7 @@ class RenderSliverGrid extends RenderSliverMultiBoxAdaptor {
geometry = new SliverGeometry(
scrollExtent: estimatedTotalExtent,
paintExtent: paintedExtent,
paintExtent: paintExtent,
maxPaintExtent: estimatedTotalExtent,
// Conservative to avoid complexity.
hasVisualOverflow: true,

View file

@ -197,14 +197,14 @@ class RenderSliverList extends RenderSliverMultiBoxAdaptor {
);
assert(estimatedMaxScrollOffset >= endScrollOffset - childScrollOffset(firstChild));
}
final double paintedExtent = calculatePaintOffset(
final double paintExtent = calculatePaintOffset(
constraints,
from: childScrollOffset(firstChild),
to: endScrollOffset,
);
geometry = new SliverGeometry(
scrollExtent: estimatedMaxScrollOffset,
paintExtent: paintedExtent,
paintExtent: paintExtent,
maxPaintExtent: estimatedMaxScrollOffset,
// Conservative to avoid flickering away the clip during scroll.
hasVisualOverflow: endScrollOffset > targetEndScrollOffset || constraints.scrollOffset > 0.0,

View file

@ -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 {
BoxScrollView({
Key key,

View file

@ -5,6 +5,7 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/widgets.dart';
import '../rendering/mock_canvas.dart';
import 'states.dart';
void main() {
@ -304,6 +305,29 @@ void main() {
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?
// (try it with the flutter_gallery Grid demo)
}

View file

@ -72,4 +72,47 @@ void main() {
Viewport2 viewport = tester.widget(find.byType(Viewport2));
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();
});
}