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
final int oldLastIndex = indexOf(lastChild);
final int leadingGarbage = (firstIndex - oldFirstIndex).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) {
......
......@@ -507,8 +507,9 @@ class RenderSliverGrid extends RenderSliverMultiBoxAdaptor {
final int oldLastIndex = indexOf(lastChild);
final int leadingGarbage = (firstIndex - oldFirstIndex).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);
......
......@@ -1201,8 +1201,9 @@ class CustomSingleChildLayout extends SingleChildRenderObjectWidget {
/// Meta data for identifying children in a [CustomMultiChildLayout].
///
/// The [MultiChildLayoutDelegate] hasChild, layoutChild, and positionChild
/// methods use these identifiers.
/// The [MultiChildLayoutDelegate.hasChild],
/// [MultiChildLayoutDelegate.layoutChild], and
/// [MultiChildLayoutDelegate.positionChild] methods use these identifiers.
class LayoutId extends ParentDataWidget<CustomMultiChildLayout> {
/// Marks a child with a layout identifier.
///
......
......@@ -514,6 +514,10 @@ class ListView extends BoxScrollView {
///
/// It is usually more efficient to create children on demand using [new
/// ListView.builder].
///
/// The `addRepaintBoundaries` argument corresponds to the
/// [SliverChildListDelegate.addRepaintBoundaries] property and must not be
/// null.
ListView({
Key key,
Axis scrollDirection: Axis.vertical,
......@@ -524,8 +528,12 @@ class ListView extends BoxScrollView {
bool shrinkWrap: false,
EdgeInsets padding,
this.itemExtent,
bool addRepaintBoundaries: true,
List<Widget> children: const <Widget>[],
}) : childrenDelegate = new SliverChildListDelegate(children), super(
}) : childrenDelegate = new SliverChildListDelegate(
children,
addRepaintBoundaries: addRepaintBoundaries,
), super(
key: key,
scrollDirection: scrollDirection,
reverse: reverse,
......@@ -554,6 +562,10 @@ class ListView extends BoxScrollView {
/// [ListView] itself is created, it is more efficient to use [new ListView].
/// Even more efficient, however, is to create the instances on demand using
/// this constructor's `itemBuilder` callback.
///
/// The `addRepaintBoundaries` argument corresponds to the
/// [SliverChildBuilderDelegate.addRepaintBoundaries] property and must not be
/// null.
ListView.builder({
Key key,
Axis scrollDirection: Axis.vertical,
......@@ -566,7 +578,12 @@ class ListView extends BoxScrollView {
this.itemExtent,
@required IndexedWidgetBuilder itemBuilder,
int itemCount,
}) : childrenDelegate = new SliverChildBuilderDelegate(itemBuilder, childCount: itemCount), super(
bool addRepaintBoundaries: true,
}) : childrenDelegate = new SliverChildBuilderDelegate(
itemBuilder,
childCount: itemCount,
addRepaintBoundaries: addRepaintBoundaries,
), super(
key: key,
scrollDirection: scrollDirection,
reverse: reverse,
......@@ -765,6 +782,10 @@ class GridView extends BoxScrollView {
/// [SliverGridDelegate].
///
/// The [gridDelegate] argument must not be null.
///
/// The `addRepaintBoundaries` argument corresponds to the
/// [SliverChildListDelegate.addRepaintBoundaries] property and must not be
/// null.
GridView({
Key key,
Axis scrollDirection: Axis.vertical,
......@@ -775,9 +796,13 @@ class GridView extends BoxScrollView {
bool shrinkWrap: false,
EdgeInsets padding,
@required this.gridDelegate,
bool addRepaintBoundaries: true,
List<Widget> children: const <Widget>[],
}) : assert(gridDelegate != null),
childrenDelegate = new SliverChildListDelegate(children),
childrenDelegate = new SliverChildListDelegate(
children,
addRepaintBoundaries: addRepaintBoundaries,
),
super(
key: key,
scrollDirection: scrollDirection,
......@@ -802,6 +827,10 @@ class GridView extends BoxScrollView {
/// zero and less than `itemCount`.
///
/// The [gridDelegate] argument must not be null.
///
/// The `addRepaintBoundaries` argument corresponds to the
/// [SliverChildBuilderDelegate.addRepaintBoundaries] property and must not be
/// null.
GridView.builder({
Key key,
Axis scrollDirection: Axis.vertical,
......@@ -814,8 +843,13 @@ class GridView extends BoxScrollView {
@required this.gridDelegate,
@required IndexedWidgetBuilder itemBuilder,
int itemCount,
bool addRepaintBoundaries: true,
}) : assert(gridDelegate != null),
childrenDelegate = new SliverChildBuilderDelegate(itemBuilder, childCount: itemCount),
childrenDelegate = new SliverChildBuilderDelegate(
itemBuilder,
childCount: itemCount,
addRepaintBoundaries: addRepaintBoundaries,
),
super(
key: key,
scrollDirection: scrollDirection,
......@@ -863,6 +897,10 @@ class GridView extends BoxScrollView {
///
/// Uses a [SliverGridDelegateWithFixedCrossAxisCount] as the [gridDelegate].
///
/// The `addRepaintBoundaries` argument corresponds to the
/// [SliverChildListDelegate.addRepaintBoundaries] property and must not be
/// null.
///
/// See also:
///
/// * [new SliverGrid.count], the equivalent constructor for [SliverGrid].
......@@ -879,6 +917,7 @@ class GridView extends BoxScrollView {
double mainAxisSpacing: 0.0,
double crossAxisSpacing: 0.0,
double childAspectRatio: 1.0,
bool addRepaintBoundaries: true,
List<Widget> children: const <Widget>[],
}) : gridDelegate = new SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: crossAxisCount,
......@@ -886,7 +925,10 @@ class GridView extends BoxScrollView {
crossAxisSpacing: crossAxisSpacing,
childAspectRatio: childAspectRatio,
),
childrenDelegate = new SliverChildListDelegate(children), super(
childrenDelegate = new SliverChildListDelegate(
children,
addRepaintBoundaries: addRepaintBoundaries,
), super(
key: key,
scrollDirection: scrollDirection,
reverse: reverse,
......@@ -902,6 +944,10 @@ class GridView extends BoxScrollView {
///
/// Uses a [SliverGridDelegateWithMaxCrossAxisExtent] as the [gridDelegate].
///
/// The `addRepaintBoundaries` argument corresponds to the
/// [SliverChildListDelegate.addRepaintBoundaries] property and must not be
/// null.
///
/// See also:
///
/// * [new SliverGrid.extent], the equivalent constructor for [SliverGrid].
......@@ -918,6 +964,7 @@ class GridView extends BoxScrollView {
double mainAxisSpacing: 0.0,
double crossAxisSpacing: 0.0,
double childAspectRatio: 1.0,
bool addRepaintBoundaries: true,
List<Widget> children: const <Widget>[],
}) : gridDelegate = new SliverGridDelegateWithMaxCrossAxisExtent(
maxCrossAxisExtent: maxCrossAxisExtent,
......@@ -925,7 +972,10 @@ class GridView extends BoxScrollView {
crossAxisSpacing: crossAxisSpacing,
childAspectRatio: childAspectRatio,
),
childrenDelegate = new SliverChildListDelegate(children), super(
childrenDelegate = new SliverChildListDelegate(
children,
addRepaintBoundaries: addRepaintBoundaries,
), super(
key: key,
scrollDirection: scrollDirection,
reverse: reverse,
......
......@@ -115,8 +115,11 @@ abstract class SliverChildDelegate {
///
/// Many slivers lazily construct their box children to avoid creating more
/// children than are visible through the [Viewport]. This delegate provides
/// children using an [IndexedWidgetBuilder] callback. The widgets returned from
/// the builder callback are wrapped in [RepaintBoundary] widgets.
/// children using an [IndexedWidgetBuilder] callback, so that the children do
/// 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:
///
......@@ -124,8 +127,15 @@ abstract class SliverChildDelegate {
/// of children.
class SliverChildBuilderDelegate extends SliverChildDelegate {
/// Creates a delegate that supplies children for slivers using the given
/// builder callback
const SliverChildBuilderDelegate(this.builder, { this.childCount });
/// builder callback.
///
/// 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.
///
......@@ -145,6 +155,17 @@ class SliverChildBuilderDelegate extends SliverChildDelegate {
/// [builder] returns null.
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
Widget build(BuildContext context, int index) {
assert(builder != null);
......@@ -153,7 +174,7 @@ class SliverChildBuilderDelegate extends SliverChildDelegate {
final Widget child = builder(context, index);
if (child == null)
return null;
return new RepaintBoundary.wrap(child, index);
return addRepaintBoundaries ? new RepaintBoundary.wrap(child, index) : child;
}
@override
......@@ -183,6 +204,9 @@ class SliverChildBuilderDelegate extends SliverChildDelegate {
/// demand). For example, the body of a dialog box might fit both of these
/// conditions.
///
/// The widgets in the given [children] list are automatically wrapped in
/// [RepaintBoundary] widgets if [addRepaintBoundaries] is true (the default).
///
/// See also:
///
/// * [SliverChildBuilderDelegate], which is a delegate that uses a builder
......@@ -190,7 +214,13 @@ class SliverChildBuilderDelegate extends SliverChildDelegate {
class SliverChildListDelegate extends SliverChildDelegate {
/// Creates a delegate that supplies children for slivers using the given
/// 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].
///
......@@ -815,3 +845,44 @@ class SliverFillRemaining extends SingleChildRenderObjectWidget {
@override
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() {
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');
});
}
This diff is collapsed.
......@@ -295,7 +295,7 @@ void main() {
' │ maxPaintExtent: 300.0, )\n'
' │ currently live children: 0 to 2\n'
' │\n'
' ├─child 1: RenderRepaintBoundary#00000 relayoutBoundary=up2\n'
' ├─child with index 0: RenderRepaintBoundary#00000 relayoutBoundary=up2\n'
' │ │ creator: RepaintBoundary-[<0>] ← SliverList ← Viewport ←\n'
' │ │ _ScrollableScope ← IgnorePointer-[GlobalKey#00000] ← Listener ←\n'
' │ │ _GestureSemantics ←\n'
......@@ -347,7 +347,7 @@ void main() {
' │ size: Size(800.0, 100.0)\n'
' │ additionalConstraints: BoxConstraints(biggest)\n'
' │\n'
' ├─child 2: RenderRepaintBoundary#00000 relayoutBoundary=up2\n'
' ├─child with index 1: RenderRepaintBoundary#00000 relayoutBoundary=up2\n'
' │ │ creator: RepaintBoundary-[<1>] ← SliverList ← Viewport ←\n'
' │ │ _ScrollableScope ← IgnorePointer-[GlobalKey#00000] ← Listener ←\n'
' │ │ _GestureSemantics ←\n'
......@@ -399,7 +399,7 @@ void main() {
' │ size: Size(800.0, 100.0)\n'
' │ additionalConstraints: BoxConstraints(biggest)\n'
' │\n'
' └─child 3: RenderRepaintBoundary#00000 relayoutBoundary=up2\n'
' └─child with index 2: RenderRepaintBoundary#00000 relayoutBoundary=up2\n'
' │ creator: RepaintBoundary-[<2>] ← SliverList ← Viewport ←\n'
' │ _ScrollableScope ← IgnorePointer-[GlobalKey#00000] ← Listener ←\n'
' │ _GestureSemantics ←\n'
......
......@@ -80,7 +80,7 @@ void main() {
' │ 600.0, maxPaintExtent: 12000.0, hasVisualOverflow: true, )\n'
' │ currently live children: 0 to 0\n'
' │\n'
' └─child 1: RenderRepaintBoundary#00000\n'
' └─child with index 0: RenderRepaintBoundary#00000\n'
' │ creator: RepaintBoundary-[<0>] ← SliverFillViewport ← Viewport ←\n'
' │ _ScrollableScope ← IgnorePointer-[GlobalKey#00000] ← Listener ←\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