Unverified Commit e810bee6 authored by Ian Hickson's avatar Ian Hickson Committed by GitHub

Fix scrolling of multiple SliverLists and SliverGrids (#14449)

* Fix scrolling of multiple SliverLists and SliverGrids

* Update sliver_fixed_extent_list.dart

* Update sliver_grid.dart
parent 3c379aaf
...@@ -84,9 +84,7 @@ class _ShrineGridLayout extends SliverGridLayout { ...@@ -84,9 +84,7 @@ class _ShrineGridLayout extends SliverGridLayout {
} }
@override @override
double estimateMaxScrollOffset(int childCount) { double computeMaxScrollOffset(int childCount) {
if (childCount == null)
return null;
if (childCount == 0) if (childCount == 0)
return 0.0; return 0.0;
final int rowCount = _rowAtIndex(childCount - 1) + 1; final int rowCount = _rowAtIndex(childCount - 1) + 1;
......
...@@ -87,6 +87,11 @@ abstract class RenderSliverFixedExtentBoxAdaptor extends RenderSliverMultiBoxAda ...@@ -87,6 +87,11 @@ abstract class RenderSliverFixedExtentBoxAdaptor extends RenderSliverMultiBoxAda
/// index. /// index.
/// ///
/// By default, defers to [RenderSliverBoxChildManager.estimateMaxScrollOffset]. /// By default, defers to [RenderSliverBoxChildManager.estimateMaxScrollOffset].
///
/// See also:
///
/// * [computeMaxScrollOffset], which is similar but must provide a precise
/// value.
@protected @protected
double estimateMaxScrollOffset(SliverConstraints constraints, { double estimateMaxScrollOffset(SliverConstraints constraints, {
int firstIndex, int firstIndex,
...@@ -103,6 +108,31 @@ abstract class RenderSliverFixedExtentBoxAdaptor extends RenderSliverMultiBoxAda ...@@ -103,6 +108,31 @@ abstract class RenderSliverFixedExtentBoxAdaptor extends RenderSliverMultiBoxAda
); );
} }
/// Called to obtain a precise measure of the total scrollable extents of this
/// object.
///
/// Must return the precise total distance from the start of the child with
/// the earliest possible index to the end of the child with the last possible
/// index.
///
/// This is used when no child is available for the index corresponding to the
/// current scroll offset, to determine the precise dimensions of the sliver.
/// It must return a precise value. It will not be called if the
/// [childManager] returns an infinite number of children for positive
/// indices.
///
/// By default, multiplies the [itemExtent] by the number of children reported
/// by [RenderSliverBoxChildManager.childCount].
///
/// See also:
///
/// * [estimateMaxScrollOffset], which is similar but may provide inaccurate
/// values.
@protected
double computeMaxScrollOffset(SliverConstraints constraints, double itemExtent) {
return childManager.childCount * itemExtent;
}
@override @override
void performLayout() { void performLayout() {
childManager.didStartLayout(); childManager.didStartLayout();
...@@ -137,8 +167,12 @@ abstract class RenderSliverFixedExtentBoxAdaptor extends RenderSliverMultiBoxAda ...@@ -137,8 +167,12 @@ abstract class RenderSliverFixedExtentBoxAdaptor extends RenderSliverMultiBoxAda
if (firstChild == null) { if (firstChild == null) {
if (!addInitialChild(index: firstIndex, layoutOffset: indexToLayoutOffset(itemExtent, firstIndex))) { if (!addInitialChild(index: firstIndex, layoutOffset: indexToLayoutOffset(itemExtent, firstIndex))) {
// There are no children. // There are either no children, or we are past the end of all our children.
geometry = SliverGeometry.zero; final double max = computeMaxScrollOffset(constraints, itemExtent);
geometry = new SliverGeometry(
scrollExtent: max,
maxPaintExtent: max,
);
childManager.didFinishLayout(); childManager.didFinishLayout();
return; return;
} }
......
...@@ -115,9 +115,11 @@ abstract class SliverGridLayout { ...@@ -115,9 +115,11 @@ abstract class SliverGridLayout {
/// The size and position of the child with the given index. /// The size and position of the child with the given index.
SliverGridGeometry getGeometryForChildIndex(int index); SliverGridGeometry getGeometryForChildIndex(int index);
/// An estimate of the scroll extent needed to fully display all the tiles if /// The scroll extent needed to fully display all the tiles if there are
/// there are `childCount` children in total. /// `childCount` children in total.
double estimateMaxScrollOffset(int childCount); ///
/// The child count will never be null.
double computeMaxScrollOffset(int childCount);
} }
/// A [SliverGridLayout] that uses equally sized and spaced tiles. /// A [SliverGridLayout] that uses equally sized and spaced tiles.
...@@ -221,9 +223,8 @@ class SliverGridRegularTileLayout extends SliverGridLayout { ...@@ -221,9 +223,8 @@ class SliverGridRegularTileLayout extends SliverGridLayout {
} }
@override @override
double estimateMaxScrollOffset(int childCount) { double computeMaxScrollOffset(int childCount) {
if (childCount == null) assert(childCount != null);
return null;
final int mainAxisCount = ((childCount - 1) ~/ crossAxisCount) + 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;
...@@ -539,10 +540,13 @@ class RenderSliverGrid extends RenderSliverMultiBoxAdaptor { ...@@ -539,10 +540,13 @@ class RenderSliverGrid extends RenderSliverMultiBoxAdaptor {
double trailingScrollOffset = firstChildGridGeometry.trailingScrollOffset; double trailingScrollOffset = firstChildGridGeometry.trailingScrollOffset;
if (firstChild == null) { if (firstChild == null) {
if (!addInitialChild(index: firstIndex, if (!addInitialChild(index: firstIndex, layoutOffset: firstChildGridGeometry.scrollOffset)) {
layoutOffset: firstChildGridGeometry.scrollOffset)) { // There are either no children, or we are past the end of all our children.
// There are no children. final double max = layout.computeMaxScrollOffset(childManager.childCount);
geometry = SliverGeometry.zero; geometry = new SliverGeometry(
scrollExtent: max,
maxPaintExtent: max,
);
childManager.didFinishLayout(); childManager.didFinishLayout();
return; return;
} }
......
...@@ -43,6 +43,10 @@ abstract class RenderSliverBoxChildManager { ...@@ -43,6 +43,10 @@ abstract class RenderSliverBoxChildManager {
/// the [RenderSliverMultiBoxAdaptor] object if they were not created during /// the [RenderSliverMultiBoxAdaptor] object if they were not created during
/// this frame and have not yet been updated during this frame. It is not /// this frame and have not yet been updated during this frame. It is not
/// valid to add any other children to this render object. /// valid to add any other children to this render object.
///
/// If this method does not create a child for a given `index` greater than or
/// equal to zero, then [computeMaxScrollOffset] must be able to return a
/// precise value.
void createChild(int index, { @required RenderBox after }); void createChild(int index, { @required RenderBox after });
/// Remove the given child from the child list. /// Remove the given child from the child list.
...@@ -68,6 +72,18 @@ abstract class RenderSliverBoxChildManager { ...@@ -68,6 +72,18 @@ abstract class RenderSliverBoxChildManager {
double trailingScrollOffset, double trailingScrollOffset,
}); });
/// Called to obtain a precise measure of the total number of children.
///
/// Must return the number that is one greater than the greatest `index` for
/// which `createChild` will actually create a child.
///
/// This is used when [createChild] cannot add a child for a positive `index`,
/// to determine the precise dimensions of the sliver. It must return an
/// accurate and precise non-null value. It will not be called if
/// [createChild] is always able to create a child (e.g. for an infinite
/// list).
int get childCount;
/// Called during [RenderSliverMultiBoxAdaptor.adoptChild]. /// Called during [RenderSliverMultiBoxAdaptor.adoptChild].
/// ///
/// Subclasses must ensure that the [SliverMultiBoxAdaptorParentData.index] /// Subclasses must ensure that the [SliverMultiBoxAdaptorParentData.index]
......
...@@ -41,7 +41,8 @@ abstract class SliverChildDelegate { ...@@ -41,7 +41,8 @@ abstract class SliverChildDelegate {
/// Returns the child with the given index. /// Returns the child with the given index.
/// ///
/// Should return null if asked to build a widget with a greater index than /// Should return null if asked to build a widget with a greater index than
/// exists. /// exists. If this returns null, [estimatedChildCount] must subsequently
/// return a precise non-null value.
/// ///
/// Subclasses typically override this function and wrap their children in /// Subclasses typically override this function and wrap their children in
/// [AutomaticKeepAlive] and [RepaintBoundary] widgets. /// [AutomaticKeepAlive] and [RepaintBoundary] widgets.
...@@ -54,6 +55,8 @@ abstract class SliverChildDelegate { ...@@ -54,6 +55,8 @@ abstract class SliverChildDelegate {
/// ///
/// Return null if there are an unbounded number of children or if it would /// Return null if there are an unbounded number of children or if it would
/// be too difficult to estimate the number of children. /// be too difficult to estimate the number of children.
///
/// This must return a precise number once [build] has returned null.
int get estimatedChildCount => null; int get estimatedChildCount => null;
/// Returns an estimate of the max scroll extent for all the children. /// Returns an estimate of the max scroll extent for all the children.
...@@ -336,8 +339,11 @@ abstract class SliverMultiBoxAdaptorWidget extends RenderObjectWidget { ...@@ -336,8 +339,11 @@ abstract class SliverMultiBoxAdaptorWidget extends RenderObjectWidget {
/// Subclasses should override this function if they have additional /// Subclasses should override this function if they have additional
/// information about their max scroll extent. /// information about their max scroll extent.
/// ///
/// The default implementation returns calls /// This is used by [SliverMultiBoxAdaptorElement] to implement part of the
/// [SliverChildDelegate.estimateMaxScrollOffset]. /// [RenderSliverBoxChildManager] API.
///
/// The default implementation defers to [delegate] via its
/// [SliverChildDelegate.estimateMaxScrollOffset] method.
double estimateMaxScrollOffset( double estimateMaxScrollOffset(
SliverConstraints constraints, SliverConstraints constraints,
int firstIndex, int firstIndex,
...@@ -592,7 +598,7 @@ class SliverGrid extends SliverMultiBoxAdaptorWidget { ...@@ -592,7 +598,7 @@ class SliverGrid extends SliverMultiBoxAdaptorWidget {
lastIndex, lastIndex,
leadingScrollOffset, leadingScrollOffset,
trailingScrollOffset, trailingScrollOffset,
) ?? gridDelegate.getLayout(constraints).estimateMaxScrollOffset(delegate.estimatedChildCount); ) ?? gridDelegate.getLayout(constraints).computeMaxScrollOffset(delegate.estimatedChildCount);
} }
} }
...@@ -765,7 +771,7 @@ class SliverMultiBoxAdaptorElement extends RenderObjectElement implements Render ...@@ -765,7 +771,7 @@ class SliverMultiBoxAdaptorElement extends RenderObjectElement implements Render
double leadingScrollOffset, double leadingScrollOffset,
double trailingScrollOffset, double trailingScrollOffset,
) { ) {
final int childCount = widget.delegate.estimatedChildCount; final int childCount = this.childCount;
if (childCount == null) if (childCount == null)
return double.INFINITY; return double.INFINITY;
if (lastIndex == childCount - 1) if (lastIndex == childCount - 1)
...@@ -797,6 +803,9 @@ class SliverMultiBoxAdaptorElement extends RenderObjectElement implements Render ...@@ -797,6 +803,9 @@ class SliverMultiBoxAdaptorElement extends RenderObjectElement implements Render
); );
} }
@override
int get childCount => widget.delegate.estimatedChildCount;
@override @override
void didStartLayout() { void didStartLayout() {
assert(debugAssertChildListLocked()); assert(debugAssertChildListLocked());
......
...@@ -52,6 +52,9 @@ class TestRenderSliverBoxChildManager extends RenderSliverBoxChildManager { ...@@ -52,6 +52,9 @@ class TestRenderSliverBoxChildManager extends RenderSliverBoxChildManager {
return children.length * (trailingScrollOffset - leadingScrollOffset) / (lastIndex - firstIndex + 1); return children.length * (trailingScrollOffset - leadingScrollOffset) / (lastIndex - firstIndex + 1);
} }
@override
int get childCount => children.length;
@override @override
void didAdoptChild(RenderBox child) { void didAdoptChild(RenderBox child) {
assert(_currentlyUpdatingChildIndex != null); assert(_currentlyUpdatingChildIndex != null);
......
...@@ -453,7 +453,4 @@ void main() { ...@@ -453,7 +453,4 @@ void main() {
expect(tester.getTopLeft(find.byKey(target)), const Offset(600.0, 0.0)); expect(tester.getTopLeft(find.byKey(target)), const Offset(600.0, 0.0));
expect(tester.getBottomRight(find.byKey(target)), const Offset(800.0, 200.0)); expect(tester.getBottomRight(find.byKey(target)), const Offset(800.0, 200.0));
}); });
// 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)
} }
...@@ -114,4 +114,85 @@ void main() { ...@@ -114,4 +114,85 @@ void main() {
const Offset(0.0, 600.0), const Offset(0.0, 600.0),
], <bool>[false, false, true, true, false]); ], <bool>[false, false, true, true, false]);
}); });
testWidgets('Multiple grids and lists', (WidgetTester tester) async {
await tester.pumpWidget(
new Center(
child: new SizedBox(
width: 44.4,
height: 60.0,
child: new Directionality(
textDirection: TextDirection.ltr,
child: new CustomScrollView(
slivers: <Widget>[
new SliverList(
delegate: new SliverChildListDelegate(
<Widget>[
new Container(height: 22.2, child: const Text('TOP')),
new Container(height: 22.2),
new Container(height: 22.2),
],
),
),
new SliverFixedExtentList(
itemExtent: 22.2,
delegate: new SliverChildListDelegate(
<Widget>[
new Container(),
new Container(child: const Text('A')),
new Container(),
],
),
),
new SliverGrid(
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
),
delegate: new SliverChildListDelegate(
<Widget>[
new Container(),
new Container(child: const Text('B')),
new Container(),
],
),
),
new SliverList(
delegate: new SliverChildListDelegate(
<Widget>[
new Container(height: 22.2),
new Container(height: 22.2),
new Container(height: 22.2, child: const Text('BOTTOM')),
],
),
),
],
),
),
),
),
);
final TestGesture gesture = await tester.startGesture(const Offset(400.0, 300.0));
expect(find.text('TOP'), findsOneWidget);
expect(find.text('A'), findsNothing);
expect(find.text('B'), findsNothing);
expect(find.text('BOTTOM'), findsNothing);
await gesture.moveBy(const Offset(0.0, -70.0));
await tester.pump();
expect(find.text('TOP'), findsNothing);
expect(find.text('A'), findsOneWidget);
expect(find.text('B'), findsNothing);
expect(find.text('BOTTOM'), findsNothing);
await gesture.moveBy(const Offset(0.0, -70.0));
await tester.pump();
expect(find.text('TOP'), findsNothing);
expect(find.text('A'), findsNothing);
expect(find.text('B'), findsOneWidget);
expect(find.text('BOTTOM'), findsNothing);
await gesture.moveBy(const Offset(0.0, -70.0));
await tester.pump();
expect(find.text('TOP'), findsNothing);
expect(find.text('A'), findsNothing);
expect(find.text('B'), findsNothing);
expect(find.text('BOTTOM'), findsOneWidget);
});
} }
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