Commit 52795630 authored by Ian Hickson's avatar Ian Hickson Committed by GitHub

Keep-alive for widgets in lazy lists (#11010)

parent 57746f38
...@@ -130,8 +130,9 @@ abstract class RenderSliverFixedExtentBoxAdaptor extends RenderSliverMultiBoxAda ...@@ -130,8 +130,9 @@ abstract class RenderSliverFixedExtentBoxAdaptor extends RenderSliverMultiBoxAda
final int oldLastIndex = indexOf(lastChild); final int oldLastIndex = indexOf(lastChild);
final int leadingGarbage = (firstIndex - oldFirstIndex).clamp(0, childCount); final int leadingGarbage = (firstIndex - oldFirstIndex).clamp(0, childCount);
final int trailingGarbage = targetLastIndex == null ? 0 : (oldLastIndex - targetLastIndex).clamp(0, childCount); final int trailingGarbage = targetLastIndex == null ? 0 : (oldLastIndex - targetLastIndex).clamp(0, childCount);
if (leadingGarbage + trailingGarbage > 0) collectGarbage(leadingGarbage, trailingGarbage);
collectGarbage(leadingGarbage, trailingGarbage); } else {
collectGarbage(0, 0);
} }
if (firstChild == null) { if (firstChild == null) {
......
...@@ -507,8 +507,9 @@ class RenderSliverGrid extends RenderSliverMultiBoxAdaptor { ...@@ -507,8 +507,9 @@ class RenderSliverGrid extends RenderSliverMultiBoxAdaptor {
final int oldLastIndex = indexOf(lastChild); final int oldLastIndex = indexOf(lastChild);
final int leadingGarbage = (firstIndex - oldFirstIndex).clamp(0, childCount); final int leadingGarbage = (firstIndex - oldFirstIndex).clamp(0, childCount);
final int trailingGarbage = targetLastIndex == null ? 0 : (oldLastIndex - targetLastIndex).clamp(0, childCount); final int trailingGarbage = targetLastIndex == null ? 0 : (oldLastIndex - targetLastIndex).clamp(0, childCount);
if (leadingGarbage + trailingGarbage > 0) collectGarbage(leadingGarbage, trailingGarbage);
collectGarbage(leadingGarbage, trailingGarbage); } else {
collectGarbage(0, 0);
} }
final SliverGridGeometry firstChildGridGeometry = layout.getGeometryForChildIndex(firstIndex); final SliverGridGeometry firstChildGridGeometry = layout.getGeometryForChildIndex(firstIndex);
......
...@@ -111,8 +111,15 @@ class SliverMultiBoxAdaptorParentData extends SliverLogicalParentData with Conta ...@@ -111,8 +111,15 @@ class SliverMultiBoxAdaptorParentData extends SliverLogicalParentData with Conta
/// The index of this child according to the [RenderSliverBoxChildManager]. /// The index of this child according to the [RenderSliverBoxChildManager].
int index; int index;
/// Whether to keep the child alive even when it is no longer visible.
bool keepAlive = false;
/// Whether the widget is currently in the
/// [RenderSliverMultiBoxAdaptor._keepAliveBucket].
bool _keptAlive = false;
@override @override
String toString() => 'index=$index; ${super.toString()}'; String toString() => 'index=$index; ${keepAlive == true ? "keepAlive; " : ""}${super.toString()}';
} }
/// A sliver with multiple box children. /// A sliver with multiple box children.
...@@ -168,10 +175,15 @@ abstract class RenderSliverMultiBoxAdaptor extends RenderSliver ...@@ -168,10 +175,15 @@ abstract class RenderSliverMultiBoxAdaptor extends RenderSliver
RenderSliverBoxChildManager get childManager => _childManager; RenderSliverBoxChildManager get childManager => _childManager;
final RenderSliverBoxChildManager _childManager; final RenderSliverBoxChildManager _childManager;
/// The nodes being kept alive despite not being visible.
final Map<int, RenderBox> _keepAliveBucket = <int, RenderBox>{};
@override @override
void adoptChild(RenderObject child) { void adoptChild(RenderObject child) {
super.adoptChild(child); super.adoptChild(child);
childManager.didAdoptChild(child); final SliverMultiBoxAdaptorParentData childParentData = child.parentData;
if (!childParentData._keptAlive)
childManager.didAdoptChild(child);
} }
bool _debugAssertChildListLocked() => childManager.debugAssertChildListLocked(); bool _debugAssertChildListLocked() => childManager.debugAssertChildListLocked();
...@@ -192,64 +204,139 @@ abstract class RenderSliverMultiBoxAdaptor extends RenderSliver ...@@ -192,64 +204,139 @@ abstract class RenderSliverMultiBoxAdaptor extends RenderSliver
}); });
} }
@override
void remove(RenderBox child) {
final SliverMultiBoxAdaptorParentData childParentData = child.parentData;
if (!childParentData._keptAlive) {
super.remove(child);
return;
}
assert(_keepAliveBucket[childParentData.index] == child);
_keepAliveBucket.remove(childParentData.index);
dropChild(child);
}
@override
void removeAll() {
super.removeAll();
for (RenderBox child in _keepAliveBucket.values)
dropChild(child);
_keepAliveBucket.clear();
}
void _createOrObtainChild(int index, { RenderBox after }) {
invokeLayoutCallback<SliverConstraints>((SliverConstraints constraints) {
assert(constraints == this.constraints);
if (_keepAliveBucket.containsKey(index)) {
final RenderBox child = _keepAliveBucket.remove(index);
final SliverMultiBoxAdaptorParentData childParentData = child.parentData;
assert(childParentData._keptAlive);
dropChild(child);
child.parentData = childParentData;
insert(child, after: after);
childParentData._keptAlive = false;
} else {
_childManager.createChild(index, after: after);
}
});
}
void _destroyOrCacheChild(RenderBox child) {
final SliverMultiBoxAdaptorParentData childParentData = child.parentData;
if (childParentData.keepAlive) {
assert(!childParentData._keptAlive);
remove(child);
_keepAliveBucket[childParentData.index] = child;
child.parentData = childParentData;
super.adoptChild(child);
childParentData._keptAlive = true;
} else {
assert(child.parent == this);
_childManager.removeChild(child);
assert(child.parent == null);
}
}
@override
void attach(PipelineOwner owner) {
super.attach(owner);
for (RenderBox child in _keepAliveBucket.values)
child.attach(owner);
}
@override
void detach() {
super.detach();
for (RenderBox child in _keepAliveBucket.values)
child.detach();
}
@override
void redepthChildren() {
super.redepthChildren();
for (RenderBox child in _keepAliveBucket.values)
redepthChild(child);
}
@override
void visitChildren(RenderObjectVisitor visitor) {
super.visitChildren(visitor);
for (RenderBox child in _keepAliveBucket.values)
visitor(child);
}
/// Called during layout to create and add the child with the given index and /// Called during layout to create and add the child with the given index and
/// scroll offset. /// scroll offset.
/// ///
/// Calls [RenderSliverBoxChildManager.createChild] to actually create and add /// Calls [RenderSliverBoxChildManager.createChild] to actually create and add
/// the child. /// the child if necessary. The child may instead be obtained from a cache;
/// see [SliverMultiBoxAdaptorParentData.keepAlive].
/// ///
/// Returns false if createChild did not add any child, otherwise returns /// Returns false if there was no cached child and `createChild` did not add
/// true. /// any child, otherwise returns true.
/// ///
/// Does not layout the new child. /// Does not layout the new child.
/// ///
/// When this is called, there are no children, so no children can be removed /// When this is called, there are no visible children, so no children can be
/// during the call to createChild. No child should be added during that call /// removed during the call to `createChild`. No child should be added during
/// either, except for the one that is created and returned by createChild. /// that call either, except for the one that is created and returned by
/// `createChild`.
@protected @protected
bool addInitialChild({ int index: 0, double layoutOffset: 0.0 }) { bool addInitialChild({ int index: 0, double layoutOffset: 0.0 }) {
assert(_debugAssertChildListLocked()); assert(_debugAssertChildListLocked());
assert(firstChild == null); assert(firstChild == null);
bool result; _createOrObtainChild(index, after: null);
invokeLayoutCallback<SliverConstraints>((SliverConstraints constraints) { if (firstChild != null) {
assert(constraints == this.constraints); assert(firstChild == lastChild);
_childManager.createChild(index, after: null); assert(indexOf(firstChild) == index);
if (firstChild != null) { final SliverMultiBoxAdaptorParentData firstChildParentData = firstChild.parentData;
assert(firstChild == lastChild); firstChildParentData.layoutOffset = layoutOffset;
assert(indexOf(firstChild) == index); return true;
final SliverMultiBoxAdaptorParentData firstChildParentData = firstChild.parentData; }
firstChildParentData.layoutOffset = layoutOffset; childManager.setDidUnderflow(true);
result = true; return false;
} else {
childManager.setDidUnderflow(true);
result = false;
}
});
return result;
} }
/// Called during layout to create, add, and layout the child before /// Called during layout to create, add, and layout the child before
/// [firstChild]. /// [firstChild].
/// ///
/// Calls [RenderSliverBoxChildManager.createChild] to actually create and add /// Calls [RenderSliverBoxChildManager.createChild] to actually create and add
/// the child. /// the child if necessary. The child may instead be obtained from a cache;
/// see [SliverMultiBoxAdaptorParentData.keepAlive].
/// ///
/// Returns the new child or null if no child is created. /// Returns the new child or null if no child was obtained.
/// ///
/// The child that was previously the first child, as well as any subsequent /// The child that was previously the first child, as well as any subsequent
/// children, may be removed by this call if they have not yet been laid out /// children, may be removed by this call if they have not yet been laid out
/// during this layout pass. No child should be added during that call except /// during this layout pass. No child should be added during that call except
/// for the one that is created and returned by createChild. /// for the one that is created and returned by `createChild`.
@protected @protected
RenderBox insertAndLayoutLeadingChild(BoxConstraints childConstraints, { RenderBox insertAndLayoutLeadingChild(BoxConstraints childConstraints, {
bool parentUsesSize: false, bool parentUsesSize: false,
}) { }) {
assert(_debugAssertChildListLocked()); assert(_debugAssertChildListLocked());
final int index = indexOf(firstChild) - 1; final int index = indexOf(firstChild) - 1;
invokeLayoutCallback<SliverConstraints>((SliverConstraints constraints) { _createOrObtainChild(index, after: null);
assert(constraints == this.constraints);
_childManager.createChild(index, after: null);
});
if (indexOf(firstChild) == index) { if (indexOf(firstChild) == index) {
firstChild.layout(childConstraints, parentUsesSize: parentUsesSize); firstChild.layout(childConstraints, parentUsesSize: parentUsesSize);
return firstChild; return firstChild;
...@@ -262,7 +349,8 @@ abstract class RenderSliverMultiBoxAdaptor extends RenderSliver ...@@ -262,7 +349,8 @@ abstract class RenderSliverMultiBoxAdaptor extends RenderSliver
/// the given child. /// the given child.
/// ///
/// Calls [RenderSliverBoxChildManager.createChild] to actually create and add /// Calls [RenderSliverBoxChildManager.createChild] to actually create and add
/// the child. /// the child if necessary. The child may instead be obtained from a cache;
/// see [SliverMultiBoxAdaptorParentData.keepAlive].
/// ///
/// Returns the new child. It is the responsibility of the caller to configure /// Returns the new child. It is the responsibility of the caller to configure
/// the child's scroll offset. /// the child's scroll offset.
...@@ -277,13 +365,9 @@ abstract class RenderSliverMultiBoxAdaptor extends RenderSliver ...@@ -277,13 +365,9 @@ abstract class RenderSliverMultiBoxAdaptor extends RenderSliver
assert(_debugAssertChildListLocked()); assert(_debugAssertChildListLocked());
assert(after != null); assert(after != null);
final int index = indexOf(after) + 1; final int index = indexOf(after) + 1;
invokeLayoutCallback<SliverConstraints>((SliverConstraints constraints) { _createOrObtainChild(index, after: after);
assert(constraints == this.constraints);
_childManager.createChild(index, after: after);
});
final RenderBox child = childAfter(after); final RenderBox child = childAfter(after);
if (child != null && indexOf(child) == index) { if (child != null && indexOf(child) == index) {
assert(indexOf(child) == index);
child.layout(childConstraints, parentUsesSize: parentUsesSize); child.layout(childConstraints, parentUsesSize: parentUsesSize);
return child; return child;
} }
...@@ -293,19 +377,37 @@ abstract class RenderSliverMultiBoxAdaptor extends RenderSliver ...@@ -293,19 +377,37 @@ abstract class RenderSliverMultiBoxAdaptor extends RenderSliver
/// Called after layout with the number of children that can be garbage /// Called after layout with the number of children that can be garbage
/// collected at the head and tail of the child list. /// collected at the head and tail of the child list.
///
/// Children whose [SliverMultiBoxAdaptorParentData.keepAlive] property is
/// set to true will be removed to a cache instead of being dropped.
///
/// This method also collects any children that were previously kept alive but
/// are now no longer necessary. As such, it should be called every time
/// [performLayout] is run, even if the arguments are both zero.
@protected @protected
void collectGarbage(int leadingGarbage, int trailingGarbage) { void collectGarbage(int leadingGarbage, int trailingGarbage) {
assert(_debugAssertChildListLocked()); assert(_debugAssertChildListLocked());
assert(childCount >= leadingGarbage + trailingGarbage); assert(childCount >= leadingGarbage + trailingGarbage);
invokeLayoutCallback<SliverConstraints>((SliverConstraints constraints) { invokeLayoutCallback<SliverConstraints>((SliverConstraints constraints) {
while (leadingGarbage > 0) { while (leadingGarbage > 0) {
_childManager.removeChild(firstChild); _destroyOrCacheChild(firstChild);
leadingGarbage -= 1; leadingGarbage -= 1;
} }
while (trailingGarbage > 0) { while (trailingGarbage > 0) {
_childManager.removeChild(lastChild); _destroyOrCacheChild(lastChild);
trailingGarbage -= 1; trailingGarbage -= 1;
} }
// Ask the child manager to remove the children that are no longer being
// kept alive. (This should cause _keepAliveBucket to change, so we have
// to prepare our list ahead of time.)
_keepAliveBucket.values.where((RenderBox child) {
final SliverMultiBoxAdaptorParentData childParentData = child.parentData;
return !childParentData.keepAlive;
}).toList().forEach(_childManager.removeChild);
assert(_keepAliveBucket.values.where((RenderBox child) {
final SliverMultiBoxAdaptorParentData childParentData = child.parentData;
return !childParentData.keepAlive;
}).isEmpty);
}); });
} }
...@@ -442,4 +544,42 @@ abstract class RenderSliverMultiBoxAdaptor extends RenderSliver ...@@ -442,4 +544,42 @@ abstract class RenderSliverMultiBoxAdaptor extends RenderSliver
}); });
return true; return true;
} }
@override
String debugDescribeChildren(String prefix) {
StringBuffer result;
if (firstChild != null) {
result = new StringBuffer()
..write(prefix)
..write(' \u2502\n');
RenderBox child = firstChild;
while (child != lastChild) {
final SliverMultiBoxAdaptorParentData childParentData = child.parentData;
result.write(child.toStringDeep("$prefix \u251C\u2500child with index ${childParentData.index}: ", "$prefix \u2502"));
child = childParentData.nextSibling;
}
if (child != null) {
assert(child == lastChild);
final SliverMultiBoxAdaptorParentData childParentData = child.parentData;
if (_keepAliveBucket.isEmpty) {
result.write(child.toStringDeep("$prefix \u2514\u2500child with index ${childParentData.index}: ", "$prefix "));
} else {
result.write(child.toStringDeep("$prefix \u251C\u2500child with index ${childParentData.index}: ", "$prefix \u254E"));
}
}
}
if (_keepAliveBucket.isNotEmpty) {
result ??= new StringBuffer()
..write(prefix)
..write(' \u254E\n');
final List<int> indices = _keepAliveBucket.keys.toList()..sort();
final int lastIndex = indices.removeLast();
if (indices.isNotEmpty) {
for (int index in indices)
result.write(_keepAliveBucket[index].toStringDeep("$prefix \u251C\u2500child with index $index (kept alive offstage): ", "$prefix \u254E"));
}
result.write(_keepAliveBucket[lastIndex].toStringDeep("$prefix \u2514\u2500child with index $lastIndex (kept alive offstage): ", "$prefix "));
}
return result?.toString() ?? '';
}
} }
...@@ -1201,8 +1201,9 @@ class CustomSingleChildLayout extends SingleChildRenderObjectWidget { ...@@ -1201,8 +1201,9 @@ class CustomSingleChildLayout extends SingleChildRenderObjectWidget {
/// Meta data for identifying children in a [CustomMultiChildLayout]. /// Meta data for identifying children in a [CustomMultiChildLayout].
/// ///
/// The [MultiChildLayoutDelegate] hasChild, layoutChild, and positionChild /// The [MultiChildLayoutDelegate.hasChild],
/// methods use these identifiers. /// [MultiChildLayoutDelegate.layoutChild], and
/// [MultiChildLayoutDelegate.positionChild] methods use these identifiers.
class LayoutId extends ParentDataWidget<CustomMultiChildLayout> { class LayoutId extends ParentDataWidget<CustomMultiChildLayout> {
/// Marks a child with a layout identifier. /// Marks a child with a layout identifier.
/// ///
......
...@@ -514,6 +514,10 @@ class ListView extends BoxScrollView { ...@@ -514,6 +514,10 @@ class ListView extends BoxScrollView {
/// ///
/// It is usually more efficient to create children on demand using [new /// It is usually more efficient to create children on demand using [new
/// ListView.builder]. /// ListView.builder].
///
/// The `addRepaintBoundaries` argument corresponds to the
/// [SliverChildListDelegate.addRepaintBoundaries] property and must not be
/// null.
ListView({ ListView({
Key key, Key key,
Axis scrollDirection: Axis.vertical, Axis scrollDirection: Axis.vertical,
...@@ -524,8 +528,12 @@ class ListView extends BoxScrollView { ...@@ -524,8 +528,12 @@ class ListView extends BoxScrollView {
bool shrinkWrap: false, bool shrinkWrap: false,
EdgeInsets padding, EdgeInsets padding,
this.itemExtent, this.itemExtent,
bool addRepaintBoundaries: true,
List<Widget> children: const <Widget>[], List<Widget> children: const <Widget>[],
}) : childrenDelegate = new SliverChildListDelegate(children), super( }) : childrenDelegate = new SliverChildListDelegate(
children,
addRepaintBoundaries: addRepaintBoundaries,
), super(
key: key, key: key,
scrollDirection: scrollDirection, scrollDirection: scrollDirection,
reverse: reverse, reverse: reverse,
...@@ -554,6 +562,10 @@ class ListView extends BoxScrollView { ...@@ -554,6 +562,10 @@ class ListView extends BoxScrollView {
/// [ListView] itself is created, it is more efficient to use [new ListView]. /// [ListView] itself is created, it is more efficient to use [new ListView].
/// Even more efficient, however, is to create the instances on demand using /// Even more efficient, however, is to create the instances on demand using
/// this constructor's `itemBuilder` callback. /// this constructor's `itemBuilder` callback.
///
/// The `addRepaintBoundaries` argument corresponds to the
/// [SliverChildBuilderDelegate.addRepaintBoundaries] property and must not be
/// null.
ListView.builder({ ListView.builder({
Key key, Key key,
Axis scrollDirection: Axis.vertical, Axis scrollDirection: Axis.vertical,
...@@ -566,7 +578,12 @@ class ListView extends BoxScrollView { ...@@ -566,7 +578,12 @@ class ListView extends BoxScrollView {
this.itemExtent, this.itemExtent,
@required IndexedWidgetBuilder itemBuilder, @required IndexedWidgetBuilder itemBuilder,
int itemCount, int itemCount,
}) : childrenDelegate = new SliverChildBuilderDelegate(itemBuilder, childCount: itemCount), super( bool addRepaintBoundaries: true,
}) : childrenDelegate = new SliverChildBuilderDelegate(
itemBuilder,
childCount: itemCount,
addRepaintBoundaries: addRepaintBoundaries,
), super(
key: key, key: key,
scrollDirection: scrollDirection, scrollDirection: scrollDirection,
reverse: reverse, reverse: reverse,
...@@ -765,6 +782,10 @@ class GridView extends BoxScrollView { ...@@ -765,6 +782,10 @@ class GridView extends BoxScrollView {
/// [SliverGridDelegate]. /// [SliverGridDelegate].
/// ///
/// The [gridDelegate] argument must not be null. /// The [gridDelegate] argument must not be null.
///
/// The `addRepaintBoundaries` argument corresponds to the
/// [SliverChildListDelegate.addRepaintBoundaries] property and must not be
/// null.
GridView({ GridView({
Key key, Key key,
Axis scrollDirection: Axis.vertical, Axis scrollDirection: Axis.vertical,
...@@ -775,9 +796,13 @@ class GridView extends BoxScrollView { ...@@ -775,9 +796,13 @@ class GridView extends BoxScrollView {
bool shrinkWrap: false, bool shrinkWrap: false,
EdgeInsets padding, EdgeInsets padding,
@required this.gridDelegate, @required this.gridDelegate,
bool addRepaintBoundaries: true,
List<Widget> children: const <Widget>[], List<Widget> children: const <Widget>[],
}) : assert(gridDelegate != null), }) : assert(gridDelegate != null),
childrenDelegate = new SliverChildListDelegate(children), childrenDelegate = new SliverChildListDelegate(
children,
addRepaintBoundaries: addRepaintBoundaries,
),
super( super(
key: key, key: key,
scrollDirection: scrollDirection, scrollDirection: scrollDirection,
...@@ -802,6 +827,10 @@ class GridView extends BoxScrollView { ...@@ -802,6 +827,10 @@ class GridView extends BoxScrollView {
/// zero and less than `itemCount`. /// zero and less than `itemCount`.
/// ///
/// The [gridDelegate] argument must not be null. /// The [gridDelegate] argument must not be null.
///
/// The `addRepaintBoundaries` argument corresponds to the
/// [SliverChildBuilderDelegate.addRepaintBoundaries] property and must not be
/// null.
GridView.builder({ GridView.builder({
Key key, Key key,
Axis scrollDirection: Axis.vertical, Axis scrollDirection: Axis.vertical,
...@@ -814,8 +843,13 @@ class GridView extends BoxScrollView { ...@@ -814,8 +843,13 @@ class GridView extends BoxScrollView {
@required this.gridDelegate, @required this.gridDelegate,
@required IndexedWidgetBuilder itemBuilder, @required IndexedWidgetBuilder itemBuilder,
int itemCount, int itemCount,
bool addRepaintBoundaries: true,
}) : assert(gridDelegate != null), }) : assert(gridDelegate != null),
childrenDelegate = new SliverChildBuilderDelegate(itemBuilder, childCount: itemCount), childrenDelegate = new SliverChildBuilderDelegate(
itemBuilder,
childCount: itemCount,
addRepaintBoundaries: addRepaintBoundaries,
),
super( super(
key: key, key: key,
scrollDirection: scrollDirection, scrollDirection: scrollDirection,
...@@ -863,6 +897,10 @@ class GridView extends BoxScrollView { ...@@ -863,6 +897,10 @@ class GridView extends BoxScrollView {
/// ///
/// Uses a [SliverGridDelegateWithFixedCrossAxisCount] as the [gridDelegate]. /// Uses a [SliverGridDelegateWithFixedCrossAxisCount] as the [gridDelegate].
/// ///
/// The `addRepaintBoundaries` argument corresponds to the
/// [SliverChildListDelegate.addRepaintBoundaries] property and must not be
/// null.
///
/// See also: /// See also:
/// ///
/// * [new SliverGrid.count], the equivalent constructor for [SliverGrid]. /// * [new SliverGrid.count], the equivalent constructor for [SliverGrid].
...@@ -879,6 +917,7 @@ class GridView extends BoxScrollView { ...@@ -879,6 +917,7 @@ class GridView extends BoxScrollView {
double mainAxisSpacing: 0.0, double mainAxisSpacing: 0.0,
double crossAxisSpacing: 0.0, double crossAxisSpacing: 0.0,
double childAspectRatio: 1.0, double childAspectRatio: 1.0,
bool addRepaintBoundaries: true,
List<Widget> children: const <Widget>[], List<Widget> children: const <Widget>[],
}) : gridDelegate = new SliverGridDelegateWithFixedCrossAxisCount( }) : gridDelegate = new SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: crossAxisCount, crossAxisCount: crossAxisCount,
...@@ -886,7 +925,10 @@ class GridView extends BoxScrollView { ...@@ -886,7 +925,10 @@ class GridView extends BoxScrollView {
crossAxisSpacing: crossAxisSpacing, crossAxisSpacing: crossAxisSpacing,
childAspectRatio: childAspectRatio, childAspectRatio: childAspectRatio,
), ),
childrenDelegate = new SliverChildListDelegate(children), super( childrenDelegate = new SliverChildListDelegate(
children,
addRepaintBoundaries: addRepaintBoundaries,
), super(
key: key, key: key,
scrollDirection: scrollDirection, scrollDirection: scrollDirection,
reverse: reverse, reverse: reverse,
...@@ -902,6 +944,10 @@ class GridView extends BoxScrollView { ...@@ -902,6 +944,10 @@ class GridView extends BoxScrollView {
/// ///
/// Uses a [SliverGridDelegateWithMaxCrossAxisExtent] as the [gridDelegate]. /// Uses a [SliverGridDelegateWithMaxCrossAxisExtent] as the [gridDelegate].
/// ///
/// The `addRepaintBoundaries` argument corresponds to the
/// [SliverChildListDelegate.addRepaintBoundaries] property and must not be
/// null.
///
/// See also: /// See also:
/// ///
/// * [new SliverGrid.extent], the equivalent constructor for [SliverGrid]. /// * [new SliverGrid.extent], the equivalent constructor for [SliverGrid].
...@@ -918,6 +964,7 @@ class GridView extends BoxScrollView { ...@@ -918,6 +964,7 @@ class GridView extends BoxScrollView {
double mainAxisSpacing: 0.0, double mainAxisSpacing: 0.0,
double crossAxisSpacing: 0.0, double crossAxisSpacing: 0.0,
double childAspectRatio: 1.0, double childAspectRatio: 1.0,
bool addRepaintBoundaries: true,
List<Widget> children: const <Widget>[], List<Widget> children: const <Widget>[],
}) : gridDelegate = new SliverGridDelegateWithMaxCrossAxisExtent( }) : gridDelegate = new SliverGridDelegateWithMaxCrossAxisExtent(
maxCrossAxisExtent: maxCrossAxisExtent, maxCrossAxisExtent: maxCrossAxisExtent,
...@@ -925,7 +972,10 @@ class GridView extends BoxScrollView { ...@@ -925,7 +972,10 @@ class GridView extends BoxScrollView {
crossAxisSpacing: crossAxisSpacing, crossAxisSpacing: crossAxisSpacing,
childAspectRatio: childAspectRatio, childAspectRatio: childAspectRatio,
), ),
childrenDelegate = new SliverChildListDelegate(children), super( childrenDelegate = new SliverChildListDelegate(
children,
addRepaintBoundaries: addRepaintBoundaries,
), super(
key: key, key: key,
scrollDirection: scrollDirection, scrollDirection: scrollDirection,
reverse: reverse, reverse: reverse,
......
...@@ -115,8 +115,11 @@ abstract class SliverChildDelegate { ...@@ -115,8 +115,11 @@ abstract class SliverChildDelegate {
/// ///
/// Many slivers lazily construct their box children to avoid creating more /// Many slivers lazily construct their box children to avoid creating more
/// children than are visible through the [Viewport]. This delegate provides /// children than are visible through the [Viewport]. This delegate provides
/// children using an [IndexedWidgetBuilder] callback. The widgets returned from /// children using an [IndexedWidgetBuilder] callback, so that the children do
/// the builder callback are wrapped in [RepaintBoundary] widgets. /// not even have to be built until they are displayed.
///
/// The widgets returned from the builder callback are automatically wrapped in
/// [RepaintBoundary] widgets if [addRepaintBoundaries] is true (the default).
/// ///
/// See also: /// See also:
/// ///
...@@ -124,8 +127,15 @@ abstract class SliverChildDelegate { ...@@ -124,8 +127,15 @@ abstract class SliverChildDelegate {
/// of children. /// of children.
class SliverChildBuilderDelegate extends SliverChildDelegate { class SliverChildBuilderDelegate extends SliverChildDelegate {
/// Creates a delegate that supplies children for slivers using the given /// Creates a delegate that supplies children for slivers using the given
/// builder callback /// builder callback.
const SliverChildBuilderDelegate(this.builder, { this.childCount }); ///
/// The [builder] and [addRepaintBoundaries] arguments must not be null.
const SliverChildBuilderDelegate(
this.builder, {
this.childCount,
this.addRepaintBoundaries: true,
}) : assert(builder != null),
assert(addRepaintBoundaries != null);
/// Called to build children for the sliver. /// Called to build children for the sliver.
/// ///
...@@ -145,6 +155,17 @@ class SliverChildBuilderDelegate extends SliverChildDelegate { ...@@ -145,6 +155,17 @@ class SliverChildBuilderDelegate extends SliverChildDelegate {
/// [builder] returns null. /// [builder] returns null.
final int childCount; final int childCount;
/// Whether to wrap each child in a [RepaintBoundary].
///
/// Typically, children in a scrolling container are wrapped in repaint
/// boundaries so that they do not need to be repainted as the list scrolls.
/// If the children are easy to repaint (e.g., solid color blocks or a short
/// snippet of text), it might be more efficient to not add a repaint boundary
/// and simply repaint the children during scrolling.
///
/// Defaults to true.
final bool addRepaintBoundaries;
@override @override
Widget build(BuildContext context, int index) { Widget build(BuildContext context, int index) {
assert(builder != null); assert(builder != null);
...@@ -153,7 +174,7 @@ class SliverChildBuilderDelegate extends SliverChildDelegate { ...@@ -153,7 +174,7 @@ class SliverChildBuilderDelegate extends SliverChildDelegate {
final Widget child = builder(context, index); final Widget child = builder(context, index);
if (child == null) if (child == null)
return null; return null;
return new RepaintBoundary.wrap(child, index); return addRepaintBoundaries ? new RepaintBoundary.wrap(child, index) : child;
} }
@override @override
...@@ -183,6 +204,9 @@ class SliverChildBuilderDelegate extends SliverChildDelegate { ...@@ -183,6 +204,9 @@ class SliverChildBuilderDelegate extends SliverChildDelegate {
/// demand). For example, the body of a dialog box might fit both of these /// demand). For example, the body of a dialog box might fit both of these
/// conditions. /// conditions.
/// ///
/// The widgets in the given [children] list are automatically wrapped in
/// [RepaintBoundary] widgets if [addRepaintBoundaries] is true (the default).
///
/// See also: /// See also:
/// ///
/// * [SliverChildBuilderDelegate], which is a delegate that uses a builder /// * [SliverChildBuilderDelegate], which is a delegate that uses a builder
...@@ -190,7 +214,13 @@ class SliverChildBuilderDelegate extends SliverChildDelegate { ...@@ -190,7 +214,13 @@ class SliverChildBuilderDelegate extends SliverChildDelegate {
class SliverChildListDelegate extends SliverChildDelegate { class SliverChildListDelegate extends SliverChildDelegate {
/// Creates a delegate that supplies children for slivers using the given /// Creates a delegate that supplies children for slivers using the given
/// list. /// list.
const SliverChildListDelegate(this.children, { this.addRepaintBoundaries: true }); ///
/// The [children] and [addRepaintBoundaries] arguments must not be null.
const SliverChildListDelegate(
this.children, {
this.addRepaintBoundaries: true,
}) : assert(children != null),
assert(addRepaintBoundaries != null);
/// Whether to wrap each child in a [RepaintBoundary]. /// Whether to wrap each child in a [RepaintBoundary].
/// ///
...@@ -815,3 +845,44 @@ class SliverFillRemaining extends SingleChildRenderObjectWidget { ...@@ -815,3 +845,44 @@ class SliverFillRemaining extends SingleChildRenderObjectWidget {
@override @override
RenderSliverFillRemaining createRenderObject(BuildContext context) => new RenderSliverFillRemaining(); RenderSliverFillRemaining createRenderObject(BuildContext context) => new RenderSliverFillRemaining();
} }
/// Mark a child as needing to stay alive even when it's in a lazy list that
/// would otherwise remove it.
///
/// This widget is for use in [SliverMultiBoxAdaptorWidget]s, such as
/// [SliverGrid] or [SliverList].
class KeepAlive extends ParentDataWidget<SliverMultiBoxAdaptorWidget> {
/// Marks a child as needing to remain alive.
///
/// The [child] and [keepAlive] arguments must not be null.
KeepAlive({
Key key,
@required this.keepAlive,
@required Widget child,
}) : assert(child != null),
assert(keepAlive != null),
super(key: key, child: child);
/// Whether to keep the child alive.
///
/// If this is false, it is as if this widget was omitted.
final bool keepAlive;
@override
void applyParentData(RenderObject renderObject) {
assert(renderObject.parentData is SliverMultiBoxAdaptorParentData);
final SliverMultiBoxAdaptorParentData parentData = renderObject.parentData;
if (parentData.keepAlive != keepAlive) {
parentData.keepAlive = keepAlive;
final AbstractNode targetParent = renderObject.parent;
if (targetParent is RenderObject)
targetParent.markNeedsLayout();
}
}
@override
void debugFillDescription(List<String> description) {
super.debugFillDescription(description);
description.add('keepAlive: $keepAlive');
}
}
...@@ -244,4 +244,23 @@ void main() { ...@@ -244,4 +244,23 @@ void main() {
expect(inner.geometry.scrollOffsetCorrection, isNull); expect(inner.geometry.scrollOffsetCorrection, isNull);
}); });
test('SliverMultiBoxAdaptorParentData.toString', () {
final SliverMultiBoxAdaptorParentData candidate = new SliverMultiBoxAdaptorParentData();
expect(candidate.keepAlive, isFalse);
expect(candidate.index, isNull);
expect(candidate.toString(), 'index=null; layoutOffset=0.0');
candidate.keepAlive = null;
expect(candidate.toString(), 'index=null; layoutOffset=0.0');
candidate.keepAlive = true;
expect(candidate.toString(), 'index=null; keepAlive; layoutOffset=0.0');
candidate.keepAlive = false;
expect(candidate.toString(), 'index=null; layoutOffset=0.0');
candidate.index = 0;
expect(candidate.toString(), 'index=0; layoutOffset=0.0');
candidate.index = 1;
expect(candidate.toString(), 'index=1; layoutOffset=0.0');
candidate.index = -1;
expect(candidate.toString(), 'index=-1; layoutOffset=0.0');
});
} }
// Copyright 2017 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:io' show Platform;
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/widgets.dart';
class Leaf extends StatefulWidget {
Leaf({ Key key, this.index, this.child }) : super(key: key);
final int index;
final Widget child;
@override
_LeafState createState() => new _LeafState();
}
class _LeafState extends State<Leaf> {
bool _keepAlive = false;
void setKeepAlive(bool value) {
setState(() { _keepAlive = value; });
}
@override
Widget build(BuildContext context) {
return new KeepAlive(
keepAlive: _keepAlive,
child: widget.child,
);
}
}
List<Widget> generateList(Widget child) {
return new List<Widget>.generate(
100,
(int index) => new Leaf(
key: new GlobalObjectKey<_LeafState>(index),
index: index,
child: child,
),
growable: false,
);
}
void main() {
testWidgets('KeepAlive with ListView with itemExtent', (WidgetTester tester) async {
await tester.pumpWidget(new ListView(
addRepaintBoundaries: false,
itemExtent: 12.3, // about 50 widgets visible
children: generateList(const Placeholder()),
));
expect(find.byKey(const GlobalObjectKey<_LeafState>(3)), findsOneWidget);
expect(find.byKey(const GlobalObjectKey<_LeafState>(30)), findsOneWidget);
expect(find.byKey(const GlobalObjectKey<_LeafState>(59)), findsNothing);
expect(find.byKey(const GlobalObjectKey<_LeafState>(60)), findsNothing);
expect(find.byKey(const GlobalObjectKey<_LeafState>(61)), findsNothing);
expect(find.byKey(const GlobalObjectKey<_LeafState>(90)), findsNothing);
await tester.drag(find.byType(ListView), const Offset(0.0, -300.0)); // about 25 widgets' worth
await tester.pump();
expect(find.byKey(const GlobalObjectKey<_LeafState>(3)), findsNothing);
expect(find.byKey(const GlobalObjectKey<_LeafState>(30)), findsOneWidget);
expect(find.byKey(const GlobalObjectKey<_LeafState>(59)), findsOneWidget);
expect(find.byKey(const GlobalObjectKey<_LeafState>(60)), findsOneWidget);
expect(find.byKey(const GlobalObjectKey<_LeafState>(61)), findsOneWidget);
expect(find.byKey(const GlobalObjectKey<_LeafState>(90)), findsNothing);
const GlobalObjectKey<_LeafState>(60).currentState.setKeepAlive(true);
await tester.drag(find.byType(ListView), const Offset(0.0, 300.0)); // back to top
await tester.pump();
expect(find.byKey(const GlobalObjectKey<_LeafState>(3)), findsOneWidget);
expect(find.byKey(const GlobalObjectKey<_LeafState>(30)), findsOneWidget);
expect(find.byKey(const GlobalObjectKey<_LeafState>(59)), findsNothing);
expect(find.byKey(const GlobalObjectKey<_LeafState>(60)), findsOneWidget);
expect(find.byKey(const GlobalObjectKey<_LeafState>(61)), findsNothing);
expect(find.byKey(const GlobalObjectKey<_LeafState>(90)), findsNothing);
const GlobalObjectKey<_LeafState>(60).currentState.setKeepAlive(false);
await tester.pump();
expect(find.byKey(const GlobalObjectKey<_LeafState>(3)), findsOneWidget);
expect(find.byKey(const GlobalObjectKey<_LeafState>(30)), findsOneWidget);
expect(find.byKey(const GlobalObjectKey<_LeafState>(59)), findsNothing);
expect(find.byKey(const GlobalObjectKey<_LeafState>(60)), findsNothing);
expect(find.byKey(const GlobalObjectKey<_LeafState>(61)), findsNothing);
expect(find.byKey(const GlobalObjectKey<_LeafState>(90)), findsNothing);
});
testWidgets('KeepAlive with ListView without itemExtent', (WidgetTester tester) async {
await tester.pumpWidget(new ListView(
addRepaintBoundaries: false,
children: generateList(new Container(height: 12.3, child: const Placeholder())), // about 50 widgets visible
));
expect(find.byKey(const GlobalObjectKey<_LeafState>(3)), findsOneWidget);
expect(find.byKey(const GlobalObjectKey<_LeafState>(30)), findsOneWidget);
expect(find.byKey(const GlobalObjectKey<_LeafState>(59)), findsNothing);
expect(find.byKey(const GlobalObjectKey<_LeafState>(60)), findsNothing);
expect(find.byKey(const GlobalObjectKey<_LeafState>(61)), findsNothing);
expect(find.byKey(const GlobalObjectKey<_LeafState>(90)), findsNothing);
await tester.drag(find.byType(ListView), const Offset(0.0, -300.0)); // about 25 widgets' worth
await tester.pump();
expect(find.byKey(const GlobalObjectKey<_LeafState>(3)), findsNothing);
expect(find.byKey(const GlobalObjectKey<_LeafState>(30)), findsOneWidget);
expect(find.byKey(const GlobalObjectKey<_LeafState>(59)), findsOneWidget);
expect(find.byKey(const GlobalObjectKey<_LeafState>(60)), findsOneWidget);
expect(find.byKey(const GlobalObjectKey<_LeafState>(61)), findsOneWidget);
expect(find.byKey(const GlobalObjectKey<_LeafState>(90)), findsNothing);
const GlobalObjectKey<_LeafState>(60).currentState.setKeepAlive(true);
await tester.drag(find.byType(ListView), const Offset(0.0, 300.0)); // back to top
await tester.pump();
expect(find.byKey(const GlobalObjectKey<_LeafState>(3)), findsOneWidget);
expect(find.byKey(const GlobalObjectKey<_LeafState>(30)), findsOneWidget);
expect(find.byKey(const GlobalObjectKey<_LeafState>(59)), findsNothing);
expect(find.byKey(const GlobalObjectKey<_LeafState>(60)), findsOneWidget);
expect(find.byKey(const GlobalObjectKey<_LeafState>(61)), findsNothing);
expect(find.byKey(const GlobalObjectKey<_LeafState>(90)), findsNothing);
const GlobalObjectKey<_LeafState>(60).currentState.setKeepAlive(false);
await tester.pump();
expect(find.byKey(const GlobalObjectKey<_LeafState>(3)), findsOneWidget);
expect(find.byKey(const GlobalObjectKey<_LeafState>(30)), findsOneWidget);
expect(find.byKey(const GlobalObjectKey<_LeafState>(59)), findsNothing);
expect(find.byKey(const GlobalObjectKey<_LeafState>(60)), findsNothing);
expect(find.byKey(const GlobalObjectKey<_LeafState>(61)), findsNothing);
expect(find.byKey(const GlobalObjectKey<_LeafState>(90)), findsNothing);
});
testWidgets('KeepAlive with GridView', (WidgetTester tester) async {
await tester.pumpWidget(new GridView.count(
addRepaintBoundaries: false,
crossAxisCount: 2,
childAspectRatio: 400.0 / 24.6, // about 50 widgets visible
children: generateList(new Container(child: const Placeholder())),
));
expect(find.byKey(const GlobalObjectKey<_LeafState>(3)), findsOneWidget);
expect(find.byKey(const GlobalObjectKey<_LeafState>(30)), findsOneWidget);
expect(find.byKey(const GlobalObjectKey<_LeafState>(59)), findsNothing);
expect(find.byKey(const GlobalObjectKey<_LeafState>(60)), findsNothing);
expect(find.byKey(const GlobalObjectKey<_LeafState>(61)), findsNothing);
expect(find.byKey(const GlobalObjectKey<_LeafState>(90)), findsNothing);
await tester.drag(find.byType(GridView), const Offset(0.0, -300.0)); // about 25 widgets' worth
await tester.pump();
expect(find.byKey(const GlobalObjectKey<_LeafState>(3)), findsNothing);
expect(find.byKey(const GlobalObjectKey<_LeafState>(30)), findsOneWidget);
expect(find.byKey(const GlobalObjectKey<_LeafState>(59)), findsOneWidget);
expect(find.byKey(const GlobalObjectKey<_LeafState>(60)), findsOneWidget);
expect(find.byKey(const GlobalObjectKey<_LeafState>(61)), findsOneWidget);
expect(find.byKey(const GlobalObjectKey<_LeafState>(90)), findsNothing);
const GlobalObjectKey<_LeafState>(60).currentState.setKeepAlive(true);
await tester.drag(find.byType(GridView), const Offset(0.0, 300.0)); // back to top
await tester.pump();
expect(find.byKey(const GlobalObjectKey<_LeafState>(3)), findsOneWidget);
expect(find.byKey(const GlobalObjectKey<_LeafState>(30)), findsOneWidget);
expect(find.byKey(const GlobalObjectKey<_LeafState>(59)), findsNothing);
expect(find.byKey(const GlobalObjectKey<_LeafState>(60)), findsOneWidget);
expect(find.byKey(const GlobalObjectKey<_LeafState>(61)), findsNothing);
expect(find.byKey(const GlobalObjectKey<_LeafState>(90)), findsNothing);
const GlobalObjectKey<_LeafState>(60).currentState.setKeepAlive(false);
await tester.pump();
expect(find.byKey(const GlobalObjectKey<_LeafState>(3)), findsOneWidget);
expect(find.byKey(const GlobalObjectKey<_LeafState>(30)), findsOneWidget);
expect(find.byKey(const GlobalObjectKey<_LeafState>(59)), findsNothing);
expect(find.byKey(const GlobalObjectKey<_LeafState>(60)), findsNothing);
expect(find.byKey(const GlobalObjectKey<_LeafState>(61)), findsNothing);
expect(find.byKey(const GlobalObjectKey<_LeafState>(90)), findsNothing);
});
testWidgets('KeepAlive render tree description', (WidgetTester tester) async {
await tester.pumpWidget(new ListView(
addRepaintBoundaries: false,
itemExtent: 400.0, // 2 visible children
children: generateList(const Placeholder()),
));
// The important lines below are the ones marked with "<----"
expect(tester.binding.renderView.toStringDeep(), equalsIgnoringHashCodes(
'RenderView#00000\n'
' │ debug mode enabled - ${Platform.operatingSystem}\n'
' │ window size: Size(2400.0, 1800.0) (in physical pixels)\n'
' │ device pixel ratio: 3.0 (physical pixels per logical pixel)\n'
' │ configuration: Size(800.0, 600.0) at 3.0x (in logical pixels)\n'
' │\n'
' └─child: RenderRepaintBoundary#00000\n'
' │ creator: RepaintBoundary ←\n'
' │ NotificationListener<ScrollNotification> ←\n'
' │ GlowingOverscrollIndicator ← Scrollable ← ListView ← [root]\n'
' │ parentData: <none>\n'
' │ constraints: BoxConstraints(w=800.0, h=600.0)\n'
' │ layer: OffsetLayer#00000\n'
' │ size: Size(800.0, 600.0)\n'
' │ metrics: 0.0% useful (1 bad vs 0 good)\n'
' │ diagnosis: insufficient data to draw conclusion (less than five\n'
' │ repaints)\n'
' │\n'
' └─child: RenderCustomPaint#00000\n'
' │ creator: CustomPaint ← RepaintBoundary ←\n'
' │ NotificationListener<ScrollNotification> ←\n'
' │ GlowingOverscrollIndicator ← Scrollable ← ListView ← [root]\n'
' │ parentData: <none> (can use size)\n'
' │ constraints: BoxConstraints(w=800.0, h=600.0)\n'
' │ size: Size(800.0, 600.0)\n'
' │\n'
' └─child: RenderRepaintBoundary#00000\n'
' │ creator: RepaintBoundary ← CustomPaint ← RepaintBoundary ←\n'
' │ NotificationListener<ScrollNotification> ←\n'
' │ GlowingOverscrollIndicator ← Scrollable ← ListView ← [root]\n'
' │ parentData: <none> (can use size)\n'
' │ constraints: BoxConstraints(w=800.0, h=600.0)\n'
' │ layer: OffsetLayer#00000\n'
' │ size: Size(800.0, 600.0)\n'
' │ metrics: 0.0% useful (1 bad vs 0 good)\n'
' │ diagnosis: insufficient data to draw conclusion (less than five\n'
' │ repaints)\n'
' │\n'
' └─child: RenderSemanticsGestureHandler#00000\n'
' │ creator: _GestureSemantics ←\n'
' │ RawGestureDetector-[LabeledGlobalKey<RawGestureDetectorState>#00000]\n'
' │ ← RepaintBoundary ← CustomPaint ← RepaintBoundary ←\n'
' │ NotificationListener<ScrollNotification> ←\n'
' │ GlowingOverscrollIndicator ← Scrollable ← ListView ← [root]\n'
' │ parentData: <none> (can use size)\n'
' │ constraints: BoxConstraints(w=800.0, h=600.0)\n'
' │ semantic boundary\n'
' │ size: Size(800.0, 600.0)\n'
' │ gestures: horizontal scroll, vertical scroll\n'
' │\n'
' └─child: RenderPointerListener#00000\n'
' │ creator: Listener ← _GestureSemantics ←\n'
' │ RawGestureDetector-[LabeledGlobalKey<RawGestureDetectorState>#00000]\n'
' │ ← RepaintBoundary ← CustomPaint ← RepaintBoundary ←\n'
' │ NotificationListener<ScrollNotification> ←\n'
' │ GlowingOverscrollIndicator ← Scrollable ← ListView ← [root]\n'
' │ parentData: <none> (can use size)\n'
' │ constraints: BoxConstraints(w=800.0, h=600.0)\n'
' │ size: Size(800.0, 600.0)\n'
' │ behavior: opaque\n'
' │ listeners: down\n'
' │\n'
' └─child: RenderIgnorePointer#00000\n'
' │ creator: IgnorePointer-[GlobalKey#00000] ← Listener ←\n'
' │ _GestureSemantics ←\n'
' │ RawGestureDetector-[LabeledGlobalKey<RawGestureDetectorState>#00000]\n'
' │ ← RepaintBoundary ← CustomPaint ← RepaintBoundary ←\n'
' │ NotificationListener<ScrollNotification> ←\n'
' │ GlowingOverscrollIndicator ← Scrollable ← ListView ← [root]\n'
' │ parentData: <none> (can use size)\n'
' │ constraints: BoxConstraints(w=800.0, h=600.0)\n'
' │ size: Size(800.0, 600.0)\n'
' │ ignoring: false\n'
' │ ignoringSemantics: implicitly false\n'
' │\n'
' └─child: RenderViewport#00000\n'
' │ creator: Viewport ← _ScrollableScope ←\n'
' │ IgnorePointer-[GlobalKey#00000] ← Listener ← _GestureSemantics\n'
' │ ←\n'
' │ RawGestureDetector-[LabeledGlobalKey<RawGestureDetectorState>#00000]\n'
' │ ← RepaintBoundary ← CustomPaint ← RepaintBoundary ←\n'
' │ NotificationListener<ScrollNotification> ←\n'
' │ GlowingOverscrollIndicator ← Scrollable ← ⋯\n'
' │ parentData: <none> (can use size)\n'
' │ constraints: BoxConstraints(w=800.0, h=600.0)\n'
' │ layer: OffsetLayer#00000\n'
' │ size: Size(800.0, 600.0)\n'
' │ AxisDirection.down\n'
' │ offset: ScrollPositionWithSingleContext#00000(offset: 0.0, range:\n'
' │ 0.0..39400.0, viewport: 600.0, ScrollableState,\n'
' │ AlwaysScrollableScrollPhysics -> ClampingScrollPhysics,\n'
' │ IdleScrollActivity#00000, ScrollDirection.idle)\n'
' │ anchor: 0.0\n'
' │\n'
' └─center child: RenderSliverFixedExtentList#00000 relayoutBoundary=up1\n'
' │ creator: SliverFixedExtentList ← Viewport ← _ScrollableScope ←\n'
' │ IgnorePointer-[GlobalKey#00000] ← Listener ← _GestureSemantics\n'
' │ ←\n'
' │ RawGestureDetector-[LabeledGlobalKey<RawGestureDetectorState>#00000]\n'
' │ ← RepaintBoundary ← CustomPaint ← RepaintBoundary ←\n'
' │ NotificationListener<ScrollNotification> ←\n'
' │ GlowingOverscrollIndicator ← ⋯\n'
' │ parentData: paintOffset=Offset(0.0, 0.0) (can use size)\n'
' │ constraints: SliverConstraints(AxisDirection.down,\n'
' │ GrowthDirection.forward, ScrollDirection.idle, scrollOffset:\n'
' │ 0.0, remainingPaintExtent: 600.0, crossAxisExtent: 800.0,\n'
' │ viewportMainAxisExtent: 600.0)\n'
' │ geometry: SliverGeometry(scrollExtent: 40000.0, paintExtent:\n'
' │ 600.0, maxPaintExtent: 40000.0, hasVisualOverflow: true, )\n'
' │ currently live children: 0 to 1\n'
' │\n'
' ├─child with index 0: RenderLimitedBox#00000\n'
' │ │ creator: LimitedBox ← Placeholder ← KeepAlive ←\n'
' │ │ Leaf-[GlobalObjectKey<_LeafState> int#00000] ←\n'
' │ │ SliverFixedExtentList ← Viewport ← _ScrollableScope ←\n'
' │ │ IgnorePointer-[GlobalKey#00000] ← Listener ← _GestureSemantics\n'
' │ │ ←\n'
' │ │ RawGestureDetector-[LabeledGlobalKey<RawGestureDetectorState>#00000]\n'
' │ │ ← RepaintBoundary ← ⋯\n'
' │ │ parentData: index=0; layoutOffset=0.0\n'
' │ │ constraints: BoxConstraints(w=800.0, h=400.0)\n'
' │ │ size: Size(800.0, 400.0)\n'
' │ │ maxWidth: 400.0\n'
' │ │ maxHeight: 400.0\n'
' │ │\n'
' │ └─child: RenderCustomPaint#00000\n'
' │ creator: CustomPaint ← LimitedBox ← Placeholder ← KeepAlive ←\n'
' │ Leaf-[GlobalObjectKey<_LeafState> int#00000] ←\n'
' │ SliverFixedExtentList ← Viewport ← _ScrollableScope ←\n'
' │ IgnorePointer-[GlobalKey#00000] ← Listener ← _GestureSemantics\n'
' │ ←\n'
' │ RawGestureDetector-[LabeledGlobalKey<RawGestureDetectorState>#00000]\n'
' │ ← ⋯\n'
' │ parentData: <none> (can use size)\n'
' │ constraints: BoxConstraints(w=800.0, h=400.0)\n'
' │ size: Size(800.0, 400.0)\n'
' │\n'
' └─child with index 1: RenderLimitedBox#00000\n' // <----- no dashed line starts here
' │ creator: LimitedBox ← Placeholder ← KeepAlive ←\n'
' │ Leaf-[GlobalObjectKey<_LeafState> int#00000] ←\n'
' │ SliverFixedExtentList ← Viewport ← _ScrollableScope ←\n'
' │ IgnorePointer-[GlobalKey#00000] ← Listener ← _GestureSemantics\n'
' │ ←\n'
' │ RawGestureDetector-[LabeledGlobalKey<RawGestureDetectorState>#00000]\n'
' │ ← RepaintBoundary ← ⋯\n'
' │ parentData: index=1; layoutOffset=400.0\n'
' │ constraints: BoxConstraints(w=800.0, h=400.0)\n'
' │ size: Size(800.0, 400.0)\n'
' │ maxWidth: 400.0\n'
' │ maxHeight: 400.0\n'
' │\n'
' └─child: RenderCustomPaint#00000\n'
' creator: CustomPaint ← LimitedBox ← Placeholder ← KeepAlive ←\n'
' Leaf-[GlobalObjectKey<_LeafState> int#00000] ←\n'
' SliverFixedExtentList ← Viewport ← _ScrollableScope ←\n'
' IgnorePointer-[GlobalKey#00000] ← Listener ← _GestureSemantics\n'
' ←\n'
' RawGestureDetector-[LabeledGlobalKey<RawGestureDetectorState>#00000]\n'
' ← ⋯\n'
' parentData: <none> (can use size)\n'
' constraints: BoxConstraints(w=800.0, h=400.0)\n'
' size: Size(800.0, 400.0)\n'
'' // TODO(ianh): remove blank line
));
const GlobalObjectKey<_LeafState>(0).currentState.setKeepAlive(true);
await tester.drag(find.byType(ListView), const Offset(0.0, -1000.0));
await tester.pump();
const GlobalObjectKey<_LeafState>(3).currentState.setKeepAlive(true);
await tester.drag(find.byType(ListView), const Offset(0.0, -1000.0));
await tester.pump();
expect(tester.binding.renderView.toStringDeep(), equalsIgnoringHashCodes(
'RenderView#00000\n'
' │ debug mode enabled - ${Platform.operatingSystem}\n'
' │ window size: Size(2400.0, 1800.0) (in physical pixels)\n'
' │ device pixel ratio: 3.0 (physical pixels per logical pixel)\n'
' │ configuration: Size(800.0, 600.0) at 3.0x (in logical pixels)\n'
' │\n'
' └─child: RenderRepaintBoundary#00000\n'
' │ creator: RepaintBoundary ←\n'
' │ NotificationListener<ScrollNotification> ←\n'
' │ GlowingOverscrollIndicator ← Scrollable ← ListView ← [root]\n'
' │ parentData: <none>\n'
' │ constraints: BoxConstraints(w=800.0, h=600.0)\n'
' │ layer: OffsetLayer#00000\n'
' │ size: Size(800.0, 600.0)\n'
' │ metrics: 0.0% useful (1 bad vs 0 good)\n'
' │ diagnosis: insufficient data to draw conclusion (less than five\n'
' │ repaints)\n'
' │\n'
' └─child: RenderCustomPaint#00000\n'
' │ creator: CustomPaint ← RepaintBoundary ←\n'
' │ NotificationListener<ScrollNotification> ←\n'
' │ GlowingOverscrollIndicator ← Scrollable ← ListView ← [root]\n'
' │ parentData: <none> (can use size)\n'
' │ constraints: BoxConstraints(w=800.0, h=600.0)\n'
' │ size: Size(800.0, 600.0)\n'
' │\n'
' └─child: RenderRepaintBoundary#00000\n'
' │ creator: RepaintBoundary ← CustomPaint ← RepaintBoundary ←\n'
' │ NotificationListener<ScrollNotification> ←\n'
' │ GlowingOverscrollIndicator ← Scrollable ← ListView ← [root]\n'
' │ parentData: <none> (can use size)\n'
' │ constraints: BoxConstraints(w=800.0, h=600.0)\n'
' │ layer: OffsetLayer#00000\n'
' │ size: Size(800.0, 600.0)\n'
' │ metrics: 0.0% useful (1 bad vs 0 good)\n'
' │ diagnosis: insufficient data to draw conclusion (less than five\n'
' │ repaints)\n'
' │\n'
' └─child: RenderSemanticsGestureHandler#00000\n'
' │ creator: _GestureSemantics ←\n'
' │ RawGestureDetector-[LabeledGlobalKey<RawGestureDetectorState>#00000]\n'
' │ ← RepaintBoundary ← CustomPaint ← RepaintBoundary ←\n'
' │ NotificationListener<ScrollNotification> ←\n'
' │ GlowingOverscrollIndicator ← Scrollable ← ListView ← [root]\n'
' │ parentData: <none> (can use size)\n'
' │ constraints: BoxConstraints(w=800.0, h=600.0)\n'
' │ semantic boundary\n'
' │ size: Size(800.0, 600.0)\n'
' │ gestures: horizontal scroll, vertical scroll\n'
' │\n'
' └─child: RenderPointerListener#00000\n'
' │ creator: Listener ← _GestureSemantics ←\n'
' │ RawGestureDetector-[LabeledGlobalKey<RawGestureDetectorState>#00000]\n'
' │ ← RepaintBoundary ← CustomPaint ← RepaintBoundary ←\n'
' │ NotificationListener<ScrollNotification> ←\n'
' │ GlowingOverscrollIndicator ← Scrollable ← ListView ← [root]\n'
' │ parentData: <none> (can use size)\n'
' │ constraints: BoxConstraints(w=800.0, h=600.0)\n'
' │ size: Size(800.0, 600.0)\n'
' │ behavior: opaque\n'
' │ listeners: down\n'
' │\n'
' └─child: RenderIgnorePointer#00000\n'
' │ creator: IgnorePointer-[GlobalKey#00000] ← Listener ←\n'
' │ _GestureSemantics ←\n'
' │ RawGestureDetector-[LabeledGlobalKey<RawGestureDetectorState>#00000]\n'
' │ ← RepaintBoundary ← CustomPaint ← RepaintBoundary ←\n'
' │ NotificationListener<ScrollNotification> ←\n'
' │ GlowingOverscrollIndicator ← Scrollable ← ListView ← [root]\n'
' │ parentData: <none> (can use size)\n'
' │ constraints: BoxConstraints(w=800.0, h=600.0)\n'
' │ size: Size(800.0, 600.0)\n'
' │ ignoring: false\n'
' │ ignoringSemantics: implicitly false\n'
' │\n'
' └─child: RenderViewport#00000\n'
' │ creator: Viewport ← _ScrollableScope ←\n'
' │ IgnorePointer-[GlobalKey#00000] ← Listener ← _GestureSemantics\n'
' │ ←\n'
' │ RawGestureDetector-[LabeledGlobalKey<RawGestureDetectorState>#00000]\n'
' │ ← RepaintBoundary ← CustomPaint ← RepaintBoundary ←\n'
' │ NotificationListener<ScrollNotification> ←\n'
' │ GlowingOverscrollIndicator ← Scrollable ← ⋯\n'
' │ parentData: <none> (can use size)\n'
' │ constraints: BoxConstraints(w=800.0, h=600.0)\n'
' │ layer: OffsetLayer#00000\n'
' │ size: Size(800.0, 600.0)\n'
' │ AxisDirection.down\n'
' │ offset: ScrollPositionWithSingleContext#00000(offset: 2000.0,\n'
' │ range: 0.0..39400.0, viewport: 600.0, ScrollableState,\n'
' │ AlwaysScrollableScrollPhysics -> ClampingScrollPhysics,\n'
' │ IdleScrollActivity#00000, ScrollDirection.idle)\n'
' │ anchor: 0.0\n'
' │\n'
' └─center child: RenderSliverFixedExtentList#00000 relayoutBoundary=up1\n'
' │ creator: SliverFixedExtentList ← Viewport ← _ScrollableScope ←\n'
' │ IgnorePointer-[GlobalKey#00000] ← Listener ← _GestureSemantics\n'
' │ ←\n'
' │ RawGestureDetector-[LabeledGlobalKey<RawGestureDetectorState>#00000]\n'
' │ ← RepaintBoundary ← CustomPaint ← RepaintBoundary ←\n'
' │ NotificationListener<ScrollNotification> ←\n'
' │ GlowingOverscrollIndicator ← ⋯\n'
' │ parentData: paintOffset=Offset(0.0, 0.0) (can use size)\n'
' │ constraints: SliverConstraints(AxisDirection.down,\n'
' │ GrowthDirection.forward, ScrollDirection.idle, scrollOffset:\n'
' │ 2000.0, remainingPaintExtent: 600.0, crossAxisExtent: 800.0,\n'
' │ viewportMainAxisExtent: 600.0)\n'
' │ geometry: SliverGeometry(scrollExtent: 40000.0, paintExtent:\n'
' │ 600.0, maxPaintExtent: 40000.0, hasVisualOverflow: true, )\n'
' │ currently live children: 5 to 6\n'
' │\n'
' ├─child with index 5: RenderLimitedBox#00000\n' // <----- this is index 5, not 0
' │ │ creator: LimitedBox ← Placeholder ← KeepAlive ←\n'
' │ │ Leaf-[GlobalObjectKey<_LeafState> int#00000] ←\n'
' │ │ SliverFixedExtentList ← Viewport ← _ScrollableScope ←\n'
' │ │ IgnorePointer-[GlobalKey#00000] ← Listener ← _GestureSemantics\n'
' │ │ ←\n'
' │ │ RawGestureDetector-[LabeledGlobalKey<RawGestureDetectorState>#00000]\n'
' │ │ ← RepaintBoundary ← ⋯\n'
' │ │ parentData: index=5; layoutOffset=2000.0\n'
' │ │ constraints: BoxConstraints(w=800.0, h=400.0)\n'
' │ │ size: Size(800.0, 400.0)\n'
' │ │ maxWidth: 400.0\n'
' │ │ maxHeight: 400.0\n'
' │ │\n'
' │ └─child: RenderCustomPaint#00000\n'
' │ creator: CustomPaint ← LimitedBox ← Placeholder ← KeepAlive ←\n'
' │ Leaf-[GlobalObjectKey<_LeafState> int#00000] ←\n'
' │ SliverFixedExtentList ← Viewport ← _ScrollableScope ←\n'
' │ IgnorePointer-[GlobalKey#00000] ← Listener ← _GestureSemantics\n'
' │ ←\n'
' │ RawGestureDetector-[LabeledGlobalKey<RawGestureDetectorState>#00000]\n'
' │ ← ⋯\n'
' │ parentData: <none> (can use size)\n'
' │ constraints: BoxConstraints(w=800.0, h=400.0)\n'
' │ size: Size(800.0, 400.0)\n'
' │\n'
' ├─child with index 6: RenderLimitedBox#00000\n'
' ╎ │ creator: LimitedBox ← Placeholder ← KeepAlive ←\n' // <----- the line starts becoming dashed here
' ╎ │ Leaf-[GlobalObjectKey<_LeafState> int#00000] ←\n'
' ╎ │ SliverFixedExtentList ← Viewport ← _ScrollableScope ←\n'
' ╎ │ IgnorePointer-[GlobalKey#00000] ← Listener ← _GestureSemantics\n'
' ╎ │ ←\n'
' ╎ │ RawGestureDetector-[LabeledGlobalKey<RawGestureDetectorState>#00000]\n'
' ╎ │ ← RepaintBoundary ← ⋯\n'
' ╎ │ parentData: index=6; layoutOffset=2400.0\n'
' ╎ │ constraints: BoxConstraints(w=800.0, h=400.0)\n'
' ╎ │ size: Size(800.0, 400.0)\n'
' ╎ │ maxWidth: 400.0\n'
' ╎ │ maxHeight: 400.0\n'
' ╎ │\n'
' ╎ └─child: RenderCustomPaint#00000\n'
' ╎ creator: CustomPaint ← LimitedBox ← Placeholder ← KeepAlive ←\n'
' ╎ Leaf-[GlobalObjectKey<_LeafState> int#00000] ←\n'
' ╎ SliverFixedExtentList ← Viewport ← _ScrollableScope ←\n'
' ╎ IgnorePointer-[GlobalKey#00000] ← Listener ← _GestureSemantics\n'
' ╎ ←\n'
' ╎ RawGestureDetector-[LabeledGlobalKey<RawGestureDetectorState>#00000]\n'
' ╎ ← ⋯\n'
' ╎ parentData: <none> (can use size)\n'
' ╎ constraints: BoxConstraints(w=800.0, h=400.0)\n'
' ╎ size: Size(800.0, 400.0)\n'
' ╎\n'
' ├─child with index 0 (kept alive offstage): RenderLimitedBox#00000\n' // <----- this one is index 0 and is marked as being offstage
' ╎ │ creator: LimitedBox ← Placeholder ← KeepAlive ←\n'
' ╎ │ Leaf-[GlobalObjectKey<_LeafState> int#00000] ←\n'
' ╎ │ SliverFixedExtentList ← Viewport ← _ScrollableScope ←\n'
' ╎ │ IgnorePointer-[GlobalKey#00000] ← Listener ← _GestureSemantics\n'
' ╎ │ ←\n'
' ╎ │ RawGestureDetector-[LabeledGlobalKey<RawGestureDetectorState>#00000]\n'
' ╎ │ ← RepaintBoundary ← ⋯\n'
' ╎ │ parentData: index=0; keepAlive; layoutOffset=0.0\n'
' ╎ │ constraints: BoxConstraints(w=800.0, h=400.0)\n'
' ╎ │ size: Size(800.0, 400.0)\n'
' ╎ │ maxWidth: 400.0\n'
' ╎ │ maxHeight: 400.0\n'
' ╎ │\n'
' ╎ └─child: RenderCustomPaint#00000\n'
' ╎ creator: CustomPaint ← LimitedBox ← Placeholder ← KeepAlive ←\n'
' ╎ Leaf-[GlobalObjectKey<_LeafState> int#00000] ←\n'
' ╎ SliverFixedExtentList ← Viewport ← _ScrollableScope ←\n'
' ╎ IgnorePointer-[GlobalKey#00000] ← Listener ← _GestureSemantics\n'
' ╎ ←\n'
' ╎ RawGestureDetector-[LabeledGlobalKey<RawGestureDetectorState>#00000]\n'
' ╎ ← ⋯\n'
' ╎ parentData: <none> (can use size)\n'
' ╎ constraints: BoxConstraints(w=800.0, h=400.0)\n'
' ╎ size: Size(800.0, 400.0)\n'
' ╎\n' // <----- dashed line ends here
' └─child with index 3 (kept alive offstage): RenderLimitedBox#00000\n'
' │ creator: LimitedBox ← Placeholder ← KeepAlive ←\n'
' │ Leaf-[GlobalObjectKey<_LeafState> int#00000] ←\n'
' │ SliverFixedExtentList ← Viewport ← _ScrollableScope ←\n'
' │ IgnorePointer-[GlobalKey#00000] ← Listener ← _GestureSemantics\n'
' │ ←\n'
' │ RawGestureDetector-[LabeledGlobalKey<RawGestureDetectorState>#00000]\n'
' │ ← RepaintBoundary ← ⋯\n'
' │ parentData: index=3; keepAlive; layoutOffset=1200.0\n'
' │ constraints: BoxConstraints(w=800.0, h=400.0)\n'
' │ size: Size(800.0, 400.0)\n'
' │ maxWidth: 400.0\n'
' │ maxHeight: 400.0\n'
' │\n'
' └─child: RenderCustomPaint#00000\n'
' creator: CustomPaint ← LimitedBox ← Placeholder ← KeepAlive ←\n'
' Leaf-[GlobalObjectKey<_LeafState> int#00000] ←\n'
' SliverFixedExtentList ← Viewport ← _ScrollableScope ←\n'
' IgnorePointer-[GlobalKey#00000] ← Listener ← _GestureSemantics\n'
' ←\n'
' RawGestureDetector-[LabeledGlobalKey<RawGestureDetectorState>#00000]\n'
' ← ⋯\n'
' parentData: <none> (can use size)\n'
' constraints: BoxConstraints(w=800.0, h=400.0)\n'
' size: Size(800.0, 400.0)\n'
'' // TODO(ianh): remove blank line
));
});
}
...@@ -295,7 +295,7 @@ void main() { ...@@ -295,7 +295,7 @@ void main() {
' │ maxPaintExtent: 300.0, )\n' ' │ maxPaintExtent: 300.0, )\n'
' │ currently live children: 0 to 2\n' ' │ currently live children: 0 to 2\n'
' │\n' ' │\n'
' ├─child 1: RenderRepaintBoundary#00000 relayoutBoundary=up2\n' ' ├─child with index 0: RenderRepaintBoundary#00000 relayoutBoundary=up2\n'
' │ │ creator: RepaintBoundary-[<0>] ← SliverList ← Viewport ←\n' ' │ │ creator: RepaintBoundary-[<0>] ← SliverList ← Viewport ←\n'
' │ │ _ScrollableScope ← IgnorePointer-[GlobalKey#00000] ← Listener ←\n' ' │ │ _ScrollableScope ← IgnorePointer-[GlobalKey#00000] ← Listener ←\n'
' │ │ _GestureSemantics ←\n' ' │ │ _GestureSemantics ←\n'
...@@ -347,7 +347,7 @@ void main() { ...@@ -347,7 +347,7 @@ void main() {
' │ size: Size(800.0, 100.0)\n' ' │ size: Size(800.0, 100.0)\n'
' │ additionalConstraints: BoxConstraints(biggest)\n' ' │ additionalConstraints: BoxConstraints(biggest)\n'
' │\n' ' │\n'
' ├─child 2: RenderRepaintBoundary#00000 relayoutBoundary=up2\n' ' ├─child with index 1: RenderRepaintBoundary#00000 relayoutBoundary=up2\n'
' │ │ creator: RepaintBoundary-[<1>] ← SliverList ← Viewport ←\n' ' │ │ creator: RepaintBoundary-[<1>] ← SliverList ← Viewport ←\n'
' │ │ _ScrollableScope ← IgnorePointer-[GlobalKey#00000] ← Listener ←\n' ' │ │ _ScrollableScope ← IgnorePointer-[GlobalKey#00000] ← Listener ←\n'
' │ │ _GestureSemantics ←\n' ' │ │ _GestureSemantics ←\n'
...@@ -399,7 +399,7 @@ void main() { ...@@ -399,7 +399,7 @@ void main() {
' │ size: Size(800.0, 100.0)\n' ' │ size: Size(800.0, 100.0)\n'
' │ additionalConstraints: BoxConstraints(biggest)\n' ' │ additionalConstraints: BoxConstraints(biggest)\n'
' │\n' ' │\n'
' └─child 3: RenderRepaintBoundary#00000 relayoutBoundary=up2\n' ' └─child with index 2: RenderRepaintBoundary#00000 relayoutBoundary=up2\n'
' │ creator: RepaintBoundary-[<2>] ← SliverList ← Viewport ←\n' ' │ creator: RepaintBoundary-[<2>] ← SliverList ← Viewport ←\n'
' │ _ScrollableScope ← IgnorePointer-[GlobalKey#00000] ← Listener ←\n' ' │ _ScrollableScope ← IgnorePointer-[GlobalKey#00000] ← Listener ←\n'
' │ _GestureSemantics ←\n' ' │ _GestureSemantics ←\n'
......
...@@ -80,7 +80,7 @@ void main() { ...@@ -80,7 +80,7 @@ void main() {
' │ 600.0, maxPaintExtent: 12000.0, hasVisualOverflow: true, )\n' ' │ 600.0, maxPaintExtent: 12000.0, hasVisualOverflow: true, )\n'
' │ currently live children: 0 to 0\n' ' │ currently live children: 0 to 0\n'
' │\n' ' │\n'
' └─child 1: RenderRepaintBoundary#00000\n' ' └─child with index 0: RenderRepaintBoundary#00000\n'
' │ creator: RepaintBoundary-[<0>] ← SliverFillViewport ← Viewport ←\n' ' │ creator: RepaintBoundary-[<0>] ← SliverFillViewport ← Viewport ←\n'
' │ _ScrollableScope ← IgnorePointer-[GlobalKey#00000] ← Listener ←\n' ' │ _ScrollableScope ← IgnorePointer-[GlobalKey#00000] ← Listener ←\n'
' │ _GestureSemantics ←\n' ' │ _GestureSemantics ←\n'
......
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