SliverChildDelegate should know which children are live (#9073)

This patch adds a notification to SliverChildDelegate that says which
children are alive after each layout. The delegate can use this
information to optimize it's underlying model of the children (e.g., by
discarding models for children that are far outside the live range).

Fixes #9045
This commit is contained in:
Adam Barth 2017-03-29 22:54:03 -07:00 committed by GitHub
parent abfee824cd
commit 0e43e581d1
6 changed files with 96 additions and 6 deletions

View File

@ -103,7 +103,7 @@ abstract class RenderSliverFixedExtentBoxAdaptor extends RenderSliverMultiBoxAda
@override
void performLayout() {
assert(childManager.debugAssertChildListLocked());
childManager.didStartLayout();
childManager.setDidUnderflow(false);
final double itemExtent = this.itemExtent;
@ -136,6 +136,7 @@ abstract class RenderSliverFixedExtentBoxAdaptor extends RenderSliverMultiBoxAda
if (!addInitialChild(index: firstIndex, layoutOffset: indexToLayoutOffset(itemExtent, firstIndex))) {
// There are no children.
geometry = SliverGeometry.zero;
childManager.didFinishLayout();
return;
}
}
@ -206,7 +207,7 @@ abstract class RenderSliverFixedExtentBoxAdaptor extends RenderSliverMultiBoxAda
|| constraints.scrollOffset > 0.0,
);
assert(childManager.debugAssertChildListLocked());
childManager.didFinishLayout();
}
}

View File

@ -477,7 +477,7 @@ class RenderSliverGrid extends RenderSliverMultiBoxAdaptor {
@override
void performLayout() {
assert(childManager.debugAssertChildListLocked());
childManager.didStartLayout();
childManager.setDidUnderflow(false);
final double scrollOffset = constraints.scrollOffset;
@ -510,6 +510,7 @@ class RenderSliverGrid extends RenderSliverMultiBoxAdaptor {
layoutOffset: firstChildGridGeometry.scrollOffset)) {
// There are no children.
geometry = SliverGeometry.zero;
childManager.didFinishLayout();
return;
}
}
@ -587,6 +588,6 @@ class RenderSliverGrid extends RenderSliverMultiBoxAdaptor {
hasVisualOverflow: true,
);
assert(childManager.debugAssertChildListLocked());
childManager.didFinishLayout();
}
}

View File

@ -43,7 +43,7 @@ class RenderSliverList extends RenderSliverMultiBoxAdaptor {
@override
void performLayout() {
assert(childManager.debugAssertChildListLocked());
childManager.didStartLayout();
childManager.setDidUnderflow(false);
final double scrollOffset = constraints.scrollOffset;
@ -77,6 +77,7 @@ class RenderSliverList extends RenderSliverMultiBoxAdaptor {
if (!addInitialChild()) {
// There are no children.
geometry = SliverGeometry.zero;
childManager.didFinishLayout();
return;
}
}
@ -241,6 +242,6 @@ class RenderSliverList extends RenderSliverMultiBoxAdaptor {
hasVisualOverflow: endScrollOffset > targetEndScrollOffset || constraints.scrollOffset > 0.0,
);
assert(childManager.debugAssertChildListLocked());
childManager.didFinishLayout();
}
}

View File

@ -87,6 +87,13 @@ abstract class RenderSliverBoxChildManager {
/// affect the visible contents of the [RenderSliverMultiBoxAdaptor].
void setDidUnderflow(bool value);
/// Called at the beginning of layout to indicate that layout is about to
/// occur.
void didStartLayout() { }
/// Called at the end of layout to indicate that layout is now complete.
void didFinishLayout() { }
/// In debug mode, asserts that this manager is not expecting any
/// modifications to the [RenderSliverMultiBoxAdaptor]'s child list.
///

View File

@ -31,6 +31,13 @@ abstract class SliverChildDelegate {
/// be too difficult to estimate the number of children.
int get estimatedChildCount => null;
/// Returns an estimate of the max scroll extent for all the children.
///
/// Subclasses should override this function if they have additional
/// information about their max scroll extent.
///
/// The default implementation returns null, which causes the caller to
/// extrapolate the max scroll offset from the given parameters.
double estimateMaxScrollOffset(
int firstIndex,
int lastIndex,
@ -38,6 +45,16 @@ abstract class SliverChildDelegate {
double trailingScrollOffset,
) => null;
/// Called at the end of layout to indicate that layout is now complete.
///
/// The `firstIndex` argument is the index of the first child that was
/// included in the current layout. The `lastIndex` argument is the index of
/// the last child that was included in the current layout.
///
/// Useful for subclasses that which to track which children are included in
/// the underlying render tree.
void didFinishLayout(int firstIndex, int lastIndex) {}
bool shouldRebuild(covariant SliverChildDelegate oldDelegate);
@override
@ -415,6 +432,19 @@ class SliverMultiBoxAdaptorElement extends RenderObjectElement implements Render
);
}
@override
void didStartLayout() {
assert(debugAssertChildListLocked());
}
@override
void didFinishLayout() {
assert(debugAssertChildListLocked());
final int firstIndex = _childElements.firstKey() ?? 0;
final int lastIndex = _childElements.lastKey() ?? 0;
widget.delegate.didFinishLayout(firstIndex, lastIndex);
}
int _currentlyUpdatingChildIndex;
@override

View File

@ -5,6 +5,17 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/widgets.dart';
class TestSliverChildListDelegate extends SliverChildListDelegate {
TestSliverChildListDelegate(List<Widget> children) : super(children);
final List<String> log = <String>[];
@override
void didFinishLayout(int firstIndex, int lastIndex) {
log.add('didFinishLayout firstIndex=$firstIndex lastIndex=$lastIndex');
}
}
void main() {
testWidgets('ListView default control', (WidgetTester tester) async {
await tester.pumpWidget(new Center(child: new ListView(itemExtent: 100.0)));
@ -163,4 +174,43 @@ void main() {
expect(find.text('0'), findsOneWidget);
expect(find.text('19'), findsOneWidget);
});
testWidgets('didFinishLayout has correct indicies', (WidgetTester tester) async {
final TestSliverChildListDelegate delegate = new TestSliverChildListDelegate(
new List<Widget>.generate(20, (int i) {
return new Container(
child: new Text('$i'),
);
})
);
await tester.pumpWidget(
new ListView.custom(
itemExtent: 110.0,
childrenDelegate: delegate,
),
);
expect(delegate.log, equals(<String>['didFinishLayout firstIndex=0 lastIndex=5']));
delegate.log.clear();
await tester.pumpWidget(
new ListView.custom(
itemExtent: 210.0,
childrenDelegate: delegate,
),
);
expect(delegate.log, equals(<String>['didFinishLayout firstIndex=0 lastIndex=2']));
delegate.log.clear();
await tester.drag(find.byType(ListView), const Offset(0.0, -600.0));
expect(delegate.log, isEmpty);
await tester.pump();
expect(delegate.log, equals(<String>['didFinishLayout firstIndex=2 lastIndex=5']));
delegate.log.clear();
});
}