Unverified Commit 0c80ed6d authored by Kate Lovett's avatar Kate Lovett Committed by GitHub

Keep alive support for 2D scrolling (#131641)

Fixes https://github.com/flutter/flutter/issues/126297

This adds support for keep alive to the 2D scrolling foundation. The TwoDimensionalChildBuilderDelegate and TwoDimensionalChildListDelegate will both add automatic keep alives to their children, matching the convention from SliverChildDelegates. The TwoDimensionalViewportParentData now incorporates keep alive and which is managed by the RenderTwoDimensionalViewport.
parent 9156f6b5
...@@ -395,8 +395,8 @@ class SliverChildBuilderDelegate extends SliverChildDelegate { ...@@ -395,8 +395,8 @@ class SliverChildBuilderDelegate extends SliverChildDelegate {
/// {@template flutter.widgets.SliverChildBuilderDelegate.addAutomaticKeepAlives} /// {@template flutter.widgets.SliverChildBuilderDelegate.addAutomaticKeepAlives}
/// Whether to wrap each child in an [AutomaticKeepAlive]. /// Whether to wrap each child in an [AutomaticKeepAlive].
/// ///
/// Typically, children in lazy list are wrapped in [AutomaticKeepAlive] /// Typically, lazily laid out children are wrapped in [AutomaticKeepAlive]
/// widgets so that children can use [KeepAliveNotification]s to preserve /// widgets so that the children can use [KeepAliveNotification]s to preserve
/// their state when they would otherwise be garbage collected off-screen. /// their state when they would otherwise be garbage collected off-screen.
/// ///
/// This feature (and [addRepaintBoundaries]) must be disabled if the children /// This feature (and [addRepaintBoundaries]) must be disabled if the children
...@@ -863,7 +863,6 @@ Widget _createErrorWidget(Object exception, StackTrace stackTrace) { ...@@ -863,7 +863,6 @@ Widget _createErrorWidget(Object exception, StackTrace stackTrace) {
return ErrorWidget.builder(details); return ErrorWidget.builder(details);
} }
// TODO(Piinks): Come back and add keep alive support, https://github.com/flutter/flutter/issues/126297
/// A delegate that supplies children for scrolling in two dimensions. /// A delegate that supplies children for scrolling in two dimensions.
/// ///
/// A [TwoDimensionalScrollView] lazily constructs its box children to avoid /// A [TwoDimensionalScrollView] lazily constructs its box children to avoid
...@@ -929,10 +928,11 @@ class TwoDimensionalChildBuilderDelegate extends TwoDimensionalChildDelegate { ...@@ -929,10 +928,11 @@ class TwoDimensionalChildBuilderDelegate extends TwoDimensionalChildDelegate {
/// Creates a delegate that supplies children for a [TwoDimensionalScrollView] /// Creates a delegate that supplies children for a [TwoDimensionalScrollView]
/// using the given builder callback. /// using the given builder callback.
TwoDimensionalChildBuilderDelegate({ TwoDimensionalChildBuilderDelegate({
this.addRepaintBoundaries = true,
required this.builder, required this.builder,
int? maxXIndex, int? maxXIndex,
int? maxYIndex, int? maxYIndex,
this.addRepaintBoundaries = true,
this.addAutomaticKeepAlives = true,
}) : assert(maxYIndex == null || maxYIndex >= 0), }) : assert(maxYIndex == null || maxYIndex >= 0),
assert(maxXIndex == null || maxXIndex >= 0), assert(maxXIndex == null || maxXIndex >= 0),
_maxYIndex = maxYIndex, _maxYIndex = maxYIndex,
...@@ -1028,6 +1028,9 @@ class TwoDimensionalChildBuilderDelegate extends TwoDimensionalChildDelegate { ...@@ -1028,6 +1028,9 @@ class TwoDimensionalChildBuilderDelegate extends TwoDimensionalChildDelegate {
/// {@macro flutter.widgets.SliverChildBuilderDelegate.addRepaintBoundaries} /// {@macro flutter.widgets.SliverChildBuilderDelegate.addRepaintBoundaries}
final bool addRepaintBoundaries; final bool addRepaintBoundaries;
/// {@macro flutter.widgets.SliverChildBuilderDelegate.addAutomaticKeepAlives}
final bool addAutomaticKeepAlives;
@override @override
Widget? build(BuildContext context, ChildVicinity vicinity) { Widget? build(BuildContext context, ChildVicinity vicinity) {
// If we have exceeded explicit upper bounds, return null. // If we have exceeded explicit upper bounds, return null.
...@@ -1050,6 +1053,9 @@ class TwoDimensionalChildBuilderDelegate extends TwoDimensionalChildDelegate { ...@@ -1050,6 +1053,9 @@ class TwoDimensionalChildBuilderDelegate extends TwoDimensionalChildDelegate {
if (addRepaintBoundaries) { if (addRepaintBoundaries) {
child = RepaintBoundary(child: child); child = RepaintBoundary(child: child);
} }
if (addAutomaticKeepAlives) {
child = AutomaticKeepAlive(child: _SelectionKeepAlive(child: child));
}
return child; return child;
} }
...@@ -1095,6 +1101,7 @@ class TwoDimensionalChildListDelegate extends TwoDimensionalChildDelegate { ...@@ -1095,6 +1101,7 @@ class TwoDimensionalChildListDelegate extends TwoDimensionalChildDelegate {
/// null. /// null.
TwoDimensionalChildListDelegate({ TwoDimensionalChildListDelegate({
this.addRepaintBoundaries = true, this.addRepaintBoundaries = true,
this.addAutomaticKeepAlives = true,
required this.children, required this.children,
}); });
...@@ -1114,6 +1121,9 @@ class TwoDimensionalChildListDelegate extends TwoDimensionalChildDelegate { ...@@ -1114,6 +1121,9 @@ class TwoDimensionalChildListDelegate extends TwoDimensionalChildDelegate {
/// {@macro flutter.widgets.SliverChildBuilderDelegate.addRepaintBoundaries} /// {@macro flutter.widgets.SliverChildBuilderDelegate.addRepaintBoundaries}
final bool addRepaintBoundaries; final bool addRepaintBoundaries;
/// {@macro flutter.widgets.SliverChildBuilderDelegate.addAutomaticKeepAlives}
final bool addAutomaticKeepAlives;
@override @override
Widget? build(BuildContext context, ChildVicinity vicinity) { Widget? build(BuildContext context, ChildVicinity vicinity) {
// If we have exceeded explicit upper bounds, return null. // If we have exceeded explicit upper bounds, return null.
...@@ -1128,7 +1138,9 @@ class TwoDimensionalChildListDelegate extends TwoDimensionalChildDelegate { ...@@ -1128,7 +1138,9 @@ class TwoDimensionalChildListDelegate extends TwoDimensionalChildDelegate {
if (addRepaintBoundaries) { if (addRepaintBoundaries) {
child = RepaintBoundary(child: child); child = RepaintBoundary(child: child);
} }
if (addAutomaticKeepAlives) {
child = AutomaticKeepAlive(child: _SelectionKeepAlive(child: child));
}
return child; return child;
} }
......
...@@ -391,7 +391,7 @@ class _TwoDimensionalViewportElement extends RenderObjectElement ...@@ -391,7 +391,7 @@ class _TwoDimensionalViewportElement extends RenderObjectElement
/// RenderTwoDimensionalViewport override the paint method, the [paintOffset] /// RenderTwoDimensionalViewport override the paint method, the [paintOffset]
/// should be used to position the child in the viewport in order to account for /// should be used to position the child in the viewport in order to account for
/// a reversed [AxisDirection] in one or both dimensions. /// a reversed [AxisDirection] in one or both dimensions.
class TwoDimensionalViewportParentData extends ParentData { class TwoDimensionalViewportParentData extends ParentData with KeepAliveParentDataMixin {
/// The offset at which to paint the child in the parent's coordinate system. /// The offset at which to paint the child in the parent's coordinate system.
/// ///
/// This [Offset] represents the top left corner of the child of the /// This [Offset] represents the top left corner of the child of the
...@@ -472,14 +472,18 @@ class TwoDimensionalViewportParentData extends ParentData { ...@@ -472,14 +472,18 @@ class TwoDimensionalViewportParentData extends ParentData {
/// position the children instead of [layoutOffset]. /// position the children instead of [layoutOffset].
Offset? paintOffset; Offset? paintOffset;
@override
bool get keptAlive => keepAlive && !isVisible;
@override @override
String toString() { String toString() {
return 'vicinity=$vicinity; ' return 'vicinity=$vicinity; '
'layoutOffset=$layoutOffset; ' 'layoutOffset=$layoutOffset; '
'paintOffset=$paintOffset; ' 'paintOffset=$paintOffset; '
'${_paintExtent == null '${_paintExtent == null
? 'not visible ' ? 'not visible; '
: '${!isVisible ? 'not ' : ''}visible - paintExtent=$_paintExtent'}'; : '${!isVisible ? 'not ' : ''}visible - paintExtent=$_paintExtent; '}'
'${keepAlive ? "keepAlive; " : ""}';
} }
} }
...@@ -493,9 +497,7 @@ class TwoDimensionalViewportParentData extends ParentData { ...@@ -493,9 +497,7 @@ class TwoDimensionalViewportParentData extends ParentData {
/// ///
/// Subclasses should not override [performLayout], as it handles housekeeping /// Subclasses should not override [performLayout], as it handles housekeeping
/// on either side of the call to [layoutChildSequence]. /// on either side of the call to [layoutChildSequence].
// TODO(Piinks): Two follow up changes: // TODO(Piinks): ensureVisible https://github.com/flutter/flutter/issues/126299
// - Keep alive https://github.com/flutter/flutter/issues/126297
// - ensureVisible https://github.com/flutter/flutter/issues/126299
abstract class RenderTwoDimensionalViewport extends RenderBox implements RenderAbstractViewport { abstract class RenderTwoDimensionalViewport extends RenderBox implements RenderAbstractViewport {
/// Initializes fields for subclasses. /// Initializes fields for subclasses.
/// ///
...@@ -527,7 +529,12 @@ abstract class RenderTwoDimensionalViewport extends RenderBox implements RenderA ...@@ -527,7 +529,12 @@ abstract class RenderTwoDimensionalViewport extends RenderBox implements RenderA
_delegate = delegate, _delegate = delegate,
_mainAxis = mainAxis, _mainAxis = mainAxis,
_cacheExtent = cacheExtent ?? RenderAbstractViewport.defaultCacheExtent, _cacheExtent = cacheExtent ?? RenderAbstractViewport.defaultCacheExtent,
_clipBehavior = clipBehavior; _clipBehavior = clipBehavior {
assert(() {
_debugDanglingKeepAlives = <RenderBox>[];
return true;
}());
}
/// Which part of the content inside the viewport should be visible in the /// Which part of the content inside the viewport should be visible in the
/// horizontal axis. /// horizontal axis.
...@@ -674,6 +681,16 @@ abstract class RenderTwoDimensionalViewport extends RenderBox implements RenderA ...@@ -674,6 +681,16 @@ abstract class RenderTwoDimensionalViewport extends RenderBox implements RenderA
} }
final TwoDimensionalChildManager _childManager; final TwoDimensionalChildManager _childManager;
final Map<ChildVicinity, RenderBox> _children = <ChildVicinity, RenderBox>{};
/// Children that have been laid out (or re-used) during the course of
/// performLayout, used to update the keep alive bucket at the end of
/// performLayout.
final Map<ChildVicinity, RenderBox> _activeChildrenForLayoutPass = <ChildVicinity, RenderBox>{};
/// The nodes being kept alive despite not being visible.
final Map<ChildVicinity, RenderBox> _keepAliveBucket = <ChildVicinity, RenderBox>{};
late List<RenderBox> _debugDanglingKeepAlives;
bool _hasVisualOverflow = false; bool _hasVisualOverflow = false;
final LayerHandle<ClipRectLayer> _clipRectLayer = LayerHandle<ClipRectLayer>(); final LayerHandle<ClipRectLayer> _clipRectLayer = LayerHandle<ClipRectLayer>();
...@@ -683,7 +700,6 @@ abstract class RenderTwoDimensionalViewport extends RenderBox implements RenderA ...@@ -683,7 +700,6 @@ abstract class RenderTwoDimensionalViewport extends RenderBox implements RenderA
@override @override
bool get sizedByParent => true; bool get sizedByParent => true;
final Map<ChildVicinity, RenderBox> _children = <ChildVicinity, RenderBox>{};
// Keeps track of the upper and lower bounds of ChildVicinity indices when // Keeps track of the upper and lower bounds of ChildVicinity indices when
// subclasses call buildOrObtainChildFor during layoutChildSequence. These // subclasses call buildOrObtainChildFor during layoutChildSequence. These
// values are used to sort children in accordance with the mainAxis for // values are used to sort children in accordance with the mainAxis for
...@@ -788,6 +804,9 @@ abstract class RenderTwoDimensionalViewport extends RenderBox implements RenderA ...@@ -788,6 +804,9 @@ abstract class RenderTwoDimensionalViewport extends RenderBox implements RenderA
for (final RenderBox child in _children.values) { for (final RenderBox child in _children.values) {
child.attach(owner); child.attach(owner);
} }
for (final RenderBox child in _keepAliveBucket.values) {
child.attach(owner);
}
} }
@override @override
...@@ -799,6 +818,9 @@ abstract class RenderTwoDimensionalViewport extends RenderBox implements RenderA ...@@ -799,6 +818,9 @@ abstract class RenderTwoDimensionalViewport extends RenderBox implements RenderA
for (final RenderBox child in _children.values) { for (final RenderBox child in _children.values) {
child.detach(); child.detach();
} }
for (final RenderBox child in _keepAliveBucket.values) {
child.detach();
}
} }
@override @override
...@@ -806,6 +828,7 @@ abstract class RenderTwoDimensionalViewport extends RenderBox implements RenderA ...@@ -806,6 +828,7 @@ abstract class RenderTwoDimensionalViewport extends RenderBox implements RenderA
for (final RenderBox child in _children.values) { for (final RenderBox child in _children.values) {
child.redepthChildren(); child.redepthChildren();
} }
_keepAliveBucket.values.forEach(redepthChild);
} }
@override @override
...@@ -815,6 +838,7 @@ abstract class RenderTwoDimensionalViewport extends RenderBox implements RenderA ...@@ -815,6 +838,7 @@ abstract class RenderTwoDimensionalViewport extends RenderBox implements RenderA
visitor(child); visitor(child);
child = parentDataOf(child)._nextSibling; child = parentDataOf(child)._nextSibling;
} }
_keepAliveBucket.values.forEach(visitor);
} }
@override @override
...@@ -824,11 +848,14 @@ abstract class RenderTwoDimensionalViewport extends RenderBox implements RenderA ...@@ -824,11 +848,14 @@ abstract class RenderTwoDimensionalViewport extends RenderBox implements RenderA
RenderBox? child = _firstChild; RenderBox? child = _firstChild;
while (child != null) { while (child != null) {
final TwoDimensionalViewportParentData childParentData = parentDataOf(child); final TwoDimensionalViewportParentData childParentData = parentDataOf(child);
// TODO(Piinks): When ensure visible is supported, remove this isVisible
// condition.
if (childParentData.isVisible) { if (childParentData.isVisible) {
visitor(child); visitor(child);
} }
child = childParentData._nextSibling; child = childParentData._nextSibling;
} }
// Do not visit children in [_keepAliveBucket].
} }
@override @override
...@@ -959,6 +986,7 @@ abstract class RenderTwoDimensionalViewport extends RenderBox implements RenderA ...@@ -959,6 +986,7 @@ abstract class RenderTwoDimensionalViewport extends RenderBox implements RenderA
void performLayout() { void performLayout() {
_firstChild = null; _firstChild = null;
_lastChild = null; _lastChild = null;
_activeChildrenForLayoutPass.clear();
_childManager._startLayout(); _childManager._startLayout();
// Subclass lays out children. // Subclass lays out children.
...@@ -967,15 +995,35 @@ abstract class RenderTwoDimensionalViewport extends RenderBox implements RenderA ...@@ -967,15 +995,35 @@ abstract class RenderTwoDimensionalViewport extends RenderBox implements RenderA
assert(_debugCheckContentDimensions()); assert(_debugCheckContentDimensions());
_didResize = false; _didResize = false;
_needsDelegateRebuild = false; _needsDelegateRebuild = false;
_cacheKeepAlives();
invokeLayoutCallback<BoxConstraints>((BoxConstraints _) { invokeLayoutCallback<BoxConstraints>((BoxConstraints _) {
_childManager._endLayout(); _childManager._endLayout();
assert(_debugOrphans?.isEmpty ?? true); assert(_debugOrphans?.isEmpty ?? true);
assert(_debugDanglingKeepAlives.isEmpty);
// Ensure we are not keeping anything alive that should not be any longer.
assert(_keepAliveBucket.values.where((RenderBox child) {
return !parentDataOf(child).keepAlive;
}).isEmpty);
// Organize children in paint order and complete parent data after // Organize children in paint order and complete parent data after
// un-used children are disposed of by the childManager. // un-used children are disposed of by the childManager.
_reifyChildren(); _reifyChildren();
}); });
} }
void _cacheKeepAlives() {
final List<RenderBox> remainingChildren = _children.values.toSet().difference(
_activeChildrenForLayoutPass.values.toSet()
).toList();
for (final RenderBox child in remainingChildren) {
final TwoDimensionalViewportParentData childParentData = parentDataOf(child);
if (childParentData.keepAlive) {
_keepAliveBucket[childParentData.vicinity] = child;
// Let the child manager know we intend to keep this.
_childManager._reuseChild(childParentData.vicinity);
}
}
}
// Ensures all children have a layoutOffset, sets paintExtent & paintOffset, // Ensures all children have a layoutOffset, sets paintExtent & paintOffset,
// and arranges children in paint order. // and arranges children in paint order.
void _reifyChildren() { void _reifyChildren() {
...@@ -1082,12 +1130,20 @@ abstract class RenderTwoDimensionalViewport extends RenderBox implements RenderA ...@@ -1082,12 +1130,20 @@ abstract class RenderTwoDimensionalViewport extends RenderBox implements RenderA
return true; return true;
} }
/// Returns the child for a given [ChildVicinity]. /// Returns the child for a given [ChildVicinity], should be called during
/// [layoutChildSequence] in order to instantiate or retrieve children.
/// ///
/// This method will build the child if it has not been already, or will reuse /// This method will build the child if it has not been already, or will reuse
/// it if it already exists. /// it if it already exists, whether it was part of the previous frame or kept
/// alive.
///
/// Children for the given [ChildVicinity] will be inserted into the active
/// children list, and so should be visible, or contained within the
/// [cacheExtent].
RenderBox? buildOrObtainChildFor(ChildVicinity vicinity) { RenderBox? buildOrObtainChildFor(ChildVicinity vicinity) {
assert(vicinity != ChildVicinity.invalid); assert(vicinity != ChildVicinity.invalid);
// This should only be called during layout.
assert(debugDoingThisLayout);
if (_leadingXIndex == null || _trailingXIndex == null || _leadingXIndex == null || _trailingYIndex == null) { if (_leadingXIndex == null || _trailingXIndex == null || _leadingXIndex == null || _trailingYIndex == null) {
// First child of this layout pass. Set leading and trailing trackers. // First child of this layout pass. Set leading and trailing trackers.
_leadingXIndex = vicinity.xIndex; _leadingXIndex = vicinity.xIndex;
...@@ -1107,11 +1163,12 @@ abstract class RenderTwoDimensionalViewport extends RenderBox implements RenderA ...@@ -1107,11 +1163,12 @@ abstract class RenderTwoDimensionalViewport extends RenderBox implements RenderA
_leadingYIndex = math.min(vicinity.yIndex, _leadingYIndex!); _leadingYIndex = math.min(vicinity.yIndex, _leadingYIndex!);
_trailingYIndex = math.max(vicinity.yIndex, _trailingYIndex!); _trailingYIndex = math.max(vicinity.yIndex, _trailingYIndex!);
} }
if (_needsDelegateRebuild || !_children.containsKey(vicinity)) { if (_needsDelegateRebuild || (!_children.containsKey(vicinity) && !_keepAliveBucket.containsKey(vicinity))) {
invokeLayoutCallback<BoxConstraints>((BoxConstraints _) { invokeLayoutCallback<BoxConstraints>((BoxConstraints _) {
_childManager._buildChild(vicinity); _childManager._buildChild(vicinity);
}); });
} else { } else {
_keepAliveBucket.remove(vicinity);
_childManager._reuseChild(vicinity); _childManager._reuseChild(vicinity);
} }
if (!_children.containsKey(vicinity)) { if (!_children.containsKey(vicinity)) {
...@@ -1122,6 +1179,7 @@ abstract class RenderTwoDimensionalViewport extends RenderBox implements RenderA ...@@ -1122,6 +1179,7 @@ abstract class RenderTwoDimensionalViewport extends RenderBox implements RenderA
assert(_children.containsKey(vicinity)); assert(_children.containsKey(vicinity));
final RenderBox child = _children[vicinity]!; final RenderBox child = _children[vicinity]!;
_activeChildrenForLayoutPass[vicinity] = child;
parentDataOf(child).vicinity = vicinity; parentDataOf(child).vicinity = vicinity;
return child; return child;
} }
...@@ -1304,24 +1362,60 @@ abstract class RenderTwoDimensionalViewport extends RenderBox implements RenderA ...@@ -1304,24 +1362,60 @@ abstract class RenderTwoDimensionalViewport extends RenderBox implements RenderA
void _insertChild(RenderBox child, ChildVicinity slot) { void _insertChild(RenderBox child, ChildVicinity slot) {
assert(_debugTrackOrphans(newOrphan: _children[slot])); assert(_debugTrackOrphans(newOrphan: _children[slot]));
assert(!_keepAliveBucket.containsValue(child));
_children[slot] = child; _children[slot] = child;
adoptChild(child); adoptChild(child);
} }
void _moveChild(RenderBox child, {required ChildVicinity from, required ChildVicinity to}) { void _moveChild(RenderBox child, {required ChildVicinity from, required ChildVicinity to}) {
final TwoDimensionalViewportParentData childParentData = parentDataOf(child);
if (!childParentData.keptAlive) {
if (_children[from] == child) { if (_children[from] == child) {
_children.remove(from); _children.remove(from);
} }
assert(_debugTrackOrphans(newOrphan: _children[to], noLongerOrphan: child)); assert(_debugTrackOrphans(newOrphan: _children[to], noLongerOrphan: child));
_children[to] = child; _children[to] = child;
return;
}
// If the child in the bucket is not current child, that means someone has
// already moved and replaced current child, and we cannot remove this
// child.
if (_keepAliveBucket[childParentData.vicinity] == child) {
_keepAliveBucket.remove(childParentData.vicinity);
}
assert(() {
_debugDanglingKeepAlives.remove(child);
return true;
}());
// If there is an existing child in the new slot, that mean that child
// will be moved to other index. In other cases, the existing child should
// have been removed by _removeChild. Thus, it is ok to overwrite it.
assert(() {
if (_keepAliveBucket.containsKey(childParentData.vicinity)) {
_debugDanglingKeepAlives.add(_keepAliveBucket[childParentData.vicinity]!);
}
return true;
}());
_keepAliveBucket[childParentData.vicinity] = child;
} }
void _removeChild(RenderBox child, ChildVicinity slot) { void _removeChild(RenderBox child, ChildVicinity slot) {
final TwoDimensionalViewportParentData childParentData = parentDataOf(child);
if (!childParentData.keptAlive) {
if (_children[slot] == child) { if (_children[slot] == child) {
_children.remove(slot); _children.remove(slot);
} }
assert(_debugTrackOrphans(noLongerOrphan: child)); assert(_debugTrackOrphans(noLongerOrphan: child));
dropChild(child); dropChild(child);
return;
}
assert(_keepAliveBucket[childParentData.vicinity] == child);
assert(() {
_debugDanglingKeepAlives.remove(child);
return true;
}());
_keepAliveBucket.remove(childParentData.vicinity);
dropChild(child);
} }
List<RenderBox>? _debugOrphans; List<RenderBox>? _debugOrphans;
......
...@@ -393,18 +393,18 @@ class RenderSimpleListTableViewport extends RenderTwoDimensionalViewport { ...@@ -393,18 +393,18 @@ class RenderSimpleListTableViewport extends RenderTwoDimensionalViewport {
final TwoDimensionalChildListDelegate listDelegate = delegate as TwoDimensionalChildListDelegate; final TwoDimensionalChildListDelegate listDelegate = delegate as TwoDimensionalChildListDelegate;
final int rowCount; final int rowCount;
final int columnCount; final int columnCount;
rowCount = listDelegate.children.length - 1; rowCount = listDelegate.children.length;
columnCount = listDelegate.children[0].length - 1; columnCount = listDelegate.children[0].length;
final int leadingColumn = math.max((horizontalPixels / 200).floor(), 0); final int leadingColumn = math.max((horizontalPixels / 200).floor(), 0);
final int leadingRow = math.max((verticalPixels / 200).floor(), 0); final int leadingRow = math.max((verticalPixels / 200).floor(), 0);
final int trailingColumn = math.min( final int trailingColumn = math.min(
((horizontalPixels + viewportDimension.width) / 200).ceil(), ((horizontalPixels + viewportDimension.width) / 200).ceil(),
columnCount, columnCount - 1,
); );
final int trailingRow = math.min( final int trailingRow = math.min(
((verticalPixels + viewportDimension.height) / 200).ceil(), ((verticalPixels + viewportDimension.height) / 200).ceil(),
rowCount, rowCount - 1,
); );
double xLayoutOffset = (leadingColumn * 200) - horizontalOffset.pixels; double xLayoutOffset = (leadingColumn * 200) - horizontalOffset.pixels;
...@@ -420,7 +420,51 @@ class RenderSimpleListTableViewport extends RenderTwoDimensionalViewport { ...@@ -420,7 +420,51 @@ class RenderSimpleListTableViewport extends RenderTwoDimensionalViewport {
} }
xLayoutOffset += 200; xLayoutOffset += 200;
} }
verticalOffset.applyContentDimensions(0, 200 * 100 - viewportDimension.height);
horizontalOffset.applyContentDimensions(0, 200 * 100 - viewportDimension.width); verticalOffset.applyContentDimensions(
0.0,
math.max(200 * rowCount - viewportDimension.height, 0.0),
);
horizontalOffset.applyContentDimensions(
0,
math.max(200 * columnCount - viewportDimension.width, 0.0),
);
}
}
class KeepAliveCheckBox extends StatefulWidget {
const KeepAliveCheckBox({ super.key });
@override
KeepAliveCheckBoxState createState() => KeepAliveCheckBoxState();
}
class KeepAliveCheckBoxState extends State<KeepAliveCheckBox> with AutomaticKeepAliveClientMixin {
bool checkValue = false;
@override
bool get wantKeepAlive => _wantKeepAlive;
bool _wantKeepAlive = false;
set wantKeepAlive(bool value) {
if (_wantKeepAlive != value) {
_wantKeepAlive = value;
updateKeepAlive();
}
}
@override
Widget build(BuildContext context) {
super.build(context);
return Checkbox(
value: checkValue,
onChanged: (bool? value) {
if (checkValue != value) {
setState(() {
checkValue = value!;
wantKeepAlive = value;
});
}
},
);
} }
} }
...@@ -212,6 +212,172 @@ void main() { ...@@ -212,6 +212,172 @@ void main() {
testWidgets('shouldRebuild', (WidgetTester tester) async { testWidgets('shouldRebuild', (WidgetTester tester) async {
expect(builderDelegate.shouldRebuild(builderDelegate), isTrue); expect(builderDelegate.shouldRebuild(builderDelegate), isTrue);
}, variant: TargetPlatformVariant.all()); }, variant: TargetPlatformVariant.all());
testWidgets('builder delegate supports automatic keep alive - default true', (WidgetTester tester) async {
const ChildVicinity firstCell = ChildVicinity(xIndex: 0, yIndex: 0);
final ScrollController verticalController = ScrollController();
final UniqueKey checkBoxKey = UniqueKey();
final TwoDimensionalChildBuilderDelegate builderDelegate = TwoDimensionalChildBuilderDelegate(
maxXIndex: 5,
maxYIndex: 5,
builder: (BuildContext context, ChildVicinity vicinity) {
return SizedBox.square(
dimension: 200,
child: Center(child: vicinity == firstCell
? KeepAliveCheckBox(key: checkBoxKey)
: Text('R${vicinity.xIndex}:C${vicinity.yIndex}')
),
);
}
);
await tester.pumpWidget(simpleBuilderTest(
delegate: builderDelegate,
verticalDetails: ScrollableDetails.vertical(controller: verticalController),
));
await tester.pumpAndSettle();
expect(verticalController.hasClients, isTrue);
expect(verticalController.position.pixels, 0.0);
expect(find.byKey(checkBoxKey), findsOneWidget);
expect(
tester.state<KeepAliveCheckBoxState>(find.byKey(checkBoxKey)).checkValue,
isFalse,
);
expect(
tester.state<KeepAliveCheckBoxState>(find.byKey(checkBoxKey)).wantKeepAlive,
isFalse,
);
// Scroll away, disposing of the checkbox.
verticalController.jumpTo(verticalController.position.maxScrollExtent);
await tester.pump();
expect(verticalController.position.pixels, 600.0);
expect(find.byKey(checkBoxKey), findsNothing);
// Bring back into view, still unchecked, not kept alive.
verticalController.jumpTo(0.0);
await tester.pump();
expect(verticalController.position.pixels, 0.0);
expect(find.byKey(checkBoxKey), findsOneWidget);
await tester.tap(find.byKey(checkBoxKey));
await tester.pumpAndSettle();
expect(
tester.state<KeepAliveCheckBoxState>(find.byKey(checkBoxKey)).checkValue,
isTrue,
);
expect(
tester.state<KeepAliveCheckBoxState>(find.byKey(checkBoxKey)).wantKeepAlive,
isTrue,
);
// Scroll away again, checkbox should be kept alive now.
verticalController.jumpTo(verticalController.position.maxScrollExtent);
await tester.pump();
expect(verticalController.position.pixels, 600.0);
expect(find.byKey(checkBoxKey), findsOneWidget);
expect(
tester.state<KeepAliveCheckBoxState>(find.byKey(checkBoxKey)).checkValue,
isTrue,
);
expect(
tester.state<KeepAliveCheckBoxState>(find.byKey(checkBoxKey)).wantKeepAlive,
isTrue,
);
// Bring back into view, still checked, after being kept alive.
verticalController.jumpTo(0.0);
await tester.pump();
expect(verticalController.position.pixels, 0.0);
expect(find.byKey(checkBoxKey), findsOneWidget);
expect(
tester.state<KeepAliveCheckBoxState>(find.byKey(checkBoxKey)).checkValue,
isTrue,
);
expect(
tester.state<KeepAliveCheckBoxState>(find.byKey(checkBoxKey)).wantKeepAlive,
isTrue,
);
});
testWidgets('builder delegate will not add automatic keep alives', (WidgetTester tester) async {
const ChildVicinity firstCell = ChildVicinity(xIndex: 0, yIndex: 0);
final ScrollController verticalController = ScrollController();
final UniqueKey checkBoxKey = UniqueKey();
final TwoDimensionalChildBuilderDelegate builderDelegate = TwoDimensionalChildBuilderDelegate(
maxXIndex: 5,
maxYIndex: 5,
addAutomaticKeepAlives: false, // No keeping alive this time
builder: (BuildContext context, ChildVicinity vicinity) {
return SizedBox.square(
dimension: 200,
child: Center(child: vicinity == firstCell
? KeepAliveCheckBox(key: checkBoxKey)
: Text('R${vicinity.xIndex}:C${vicinity.yIndex}')
),
);
}
);
await tester.pumpWidget(simpleBuilderTest(
delegate: builderDelegate,
verticalDetails: ScrollableDetails.vertical(controller: verticalController),
));
await tester.pumpAndSettle();
expect(verticalController.hasClients, isTrue);
expect(verticalController.position.pixels, 0.0);
expect(find.byKey(checkBoxKey), findsOneWidget);
expect(
tester.state<KeepAliveCheckBoxState>(find.byKey(checkBoxKey)).checkValue,
isFalse,
);
expect(
tester.state<KeepAliveCheckBoxState>(find.byKey(checkBoxKey)).wantKeepAlive,
isFalse,
);
// Scroll away, disposing of the checkbox.
verticalController.jumpTo(verticalController.position.maxScrollExtent);
await tester.pump();
expect(verticalController.position.pixels, 600.0);
expect(find.byKey(checkBoxKey), findsNothing);
// Bring back into view, still unchecked, not kept alive.
verticalController.jumpTo(0.0);
await tester.pump();
expect(verticalController.position.pixels, 0.0);
expect(find.byKey(checkBoxKey), findsOneWidget);
await tester.tap(find.byKey(checkBoxKey));
await tester.pumpAndSettle();
expect(
tester.state<KeepAliveCheckBoxState>(find.byKey(checkBoxKey)).checkValue,
isTrue,
);
expect(
tester.state<KeepAliveCheckBoxState>(find.byKey(checkBoxKey)).wantKeepAlive,
isTrue,
);
// Scroll away again, checkbox should not be kept alive since the
// delegate did not add automatic keep alive.
verticalController.jumpTo(verticalController.position.maxScrollExtent);
await tester.pump();
expect(verticalController.position.pixels, 600.0);
expect(find.byKey(checkBoxKey), findsNothing);
// Bring back into view, not checked, having not been kept alive.
verticalController.jumpTo(0.0);
await tester.pump();
expect(verticalController.position.pixels, 0.0);
expect(find.byKey(checkBoxKey), findsOneWidget);
expect(
tester.state<KeepAliveCheckBoxState>(find.byKey(checkBoxKey)).checkValue,
isFalse,
);
expect(
tester.state<KeepAliveCheckBoxState>(find.byKey(checkBoxKey)).wantKeepAlive,
isFalse,
);
});
}); });
group('TwoDimensionalChildListDelegate', () { group('TwoDimensionalChildListDelegate', () {
...@@ -338,6 +504,174 @@ void main() { ...@@ -338,6 +504,174 @@ void main() {
expect(delegate.shouldRebuild(oldDelegate), isTrue); expect(delegate.shouldRebuild(oldDelegate), isTrue);
}, variant: TargetPlatformVariant.all()); }, variant: TargetPlatformVariant.all());
}); });
testWidgets('list delegate supports automatic keep alive - default true', (WidgetTester tester) async {
final UniqueKey checkBoxKey = UniqueKey();
final Widget originCell = SizedBox.square(
dimension: 200,
child: Center(child: KeepAliveCheckBox(key: checkBoxKey)
),
);
const Widget otherCell = SizedBox.square(dimension: 200);
final ScrollController verticalController = ScrollController();
final TwoDimensionalChildListDelegate listDelegate = TwoDimensionalChildListDelegate(
children: <List<Widget>>[
<Widget>[originCell, otherCell, otherCell, otherCell, otherCell],
<Widget>[otherCell, otherCell, otherCell, otherCell, otherCell],
<Widget>[otherCell, otherCell, otherCell, otherCell, otherCell],
<Widget>[otherCell, otherCell, otherCell, otherCell, otherCell],
<Widget>[otherCell, otherCell, otherCell, otherCell, otherCell],
],
);
await tester.pumpWidget(simpleListTest(
delegate: listDelegate,
verticalDetails: ScrollableDetails.vertical(controller: verticalController),
));
await tester.pumpAndSettle();
expect(verticalController.hasClients, isTrue);
expect(verticalController.position.pixels, 0.0);
expect(find.byKey(checkBoxKey), findsOneWidget);
expect(
tester.state<KeepAliveCheckBoxState>(find.byKey(checkBoxKey)).checkValue,
isFalse,
);
expect(
tester.state<KeepAliveCheckBoxState>(find.byKey(checkBoxKey)).wantKeepAlive,
isFalse,
);
// Scroll away, disposing of the checkbox.
verticalController.jumpTo(verticalController.position.maxScrollExtent);
await tester.pump();
expect(verticalController.position.pixels, 400.0);
expect(find.byKey(checkBoxKey), findsNothing);
// Bring back into view, still unchecked, not kept alive.
verticalController.jumpTo(0.0);
await tester.pump();
expect(verticalController.position.pixels, 0.0);
expect(find.byKey(checkBoxKey), findsOneWidget);
await tester.tap(find.byKey(checkBoxKey));
await tester.pumpAndSettle();
expect(
tester.state<KeepAliveCheckBoxState>(find.byKey(checkBoxKey)).checkValue,
isTrue,
);
expect(
tester.state<KeepAliveCheckBoxState>(find.byKey(checkBoxKey)).wantKeepAlive,
isTrue,
);
// Scroll away again, checkbox should be kept alive now.
verticalController.jumpTo(verticalController.position.maxScrollExtent);
await tester.pump();
expect(verticalController.position.pixels, 400.0);
expect(find.byKey(checkBoxKey), findsOneWidget);
expect(
tester.state<KeepAliveCheckBoxState>(find.byKey(checkBoxKey)).checkValue,
isTrue,
);
expect(
tester.state<KeepAliveCheckBoxState>(find.byKey(checkBoxKey)).wantKeepAlive,
isTrue,
);
// Bring back into view, still checked, after being kept alive.
verticalController.jumpTo(0.0);
await tester.pump();
expect(verticalController.position.pixels, 0.0);
expect(find.byKey(checkBoxKey), findsOneWidget);
expect(
tester.state<KeepAliveCheckBoxState>(find.byKey(checkBoxKey)).checkValue,
isTrue,
);
expect(
tester.state<KeepAliveCheckBoxState>(find.byKey(checkBoxKey)).wantKeepAlive,
isTrue,
);
});
testWidgets('list delegate will not add automatic keep alives', (WidgetTester tester) async {
final UniqueKey checkBoxKey = UniqueKey();
final Widget originCell = SizedBox.square(
dimension: 200,
child: Center(child: KeepAliveCheckBox(key: checkBoxKey)
),
);
const Widget otherCell = SizedBox.square(dimension: 200);
final ScrollController verticalController = ScrollController();
final TwoDimensionalChildListDelegate listDelegate = TwoDimensionalChildListDelegate(
addAutomaticKeepAlives: false,
children: <List<Widget>>[
<Widget>[originCell, otherCell, otherCell, otherCell, otherCell],
<Widget>[otherCell, otherCell, otherCell, otherCell, otherCell],
<Widget>[otherCell, otherCell, otherCell, otherCell, otherCell],
<Widget>[otherCell, otherCell, otherCell, otherCell, otherCell],
<Widget>[otherCell, otherCell, otherCell, otherCell, otherCell],
],
);
await tester.pumpWidget(simpleListTest(
delegate: listDelegate,
verticalDetails: ScrollableDetails.vertical(controller: verticalController),
));
await tester.pumpAndSettle();
expect(verticalController.hasClients, isTrue);
expect(verticalController.position.pixels, 0.0);
expect(find.byKey(checkBoxKey), findsOneWidget);
expect(
tester.state<KeepAliveCheckBoxState>(find.byKey(checkBoxKey)).checkValue,
isFalse,
);
expect(
tester.state<KeepAliveCheckBoxState>(find.byKey(checkBoxKey)).wantKeepAlive,
isFalse,
);
// Scroll away, disposing of the checkbox.
verticalController.jumpTo(verticalController.position.maxScrollExtent);
await tester.pump();
expect(verticalController.position.pixels, 400.0);
expect(find.byKey(checkBoxKey), findsNothing);
// Bring back into view, still unchecked, not kept alive.
verticalController.jumpTo(0.0);
await tester.pump();
expect(verticalController.position.pixels, 0.0);
expect(find.byKey(checkBoxKey), findsOneWidget);
await tester.tap(find.byKey(checkBoxKey));
await tester.pumpAndSettle();
expect(
tester.state<KeepAliveCheckBoxState>(find.byKey(checkBoxKey)).checkValue,
isTrue,
);
expect(
tester.state<KeepAliveCheckBoxState>(find.byKey(checkBoxKey)).wantKeepAlive,
isTrue,
);
// Scroll away again, checkbox should not be kept alive since the
// delegate did not add automatic keep alive.
verticalController.jumpTo(verticalController.position.maxScrollExtent);
await tester.pump();
expect(verticalController.position.pixels, 400.0);
expect(find.byKey(checkBoxKey), findsNothing);
// Bring back into view, not checked, having not been kept alive.
verticalController.jumpTo(0.0);
await tester.pump();
expect(verticalController.position.pixels, 0.0);
expect(find.byKey(checkBoxKey), findsOneWidget);
expect(
tester.state<KeepAliveCheckBoxState>(find.byKey(checkBoxKey)).checkValue,
isFalse,
);
expect(
tester.state<KeepAliveCheckBoxState>(find.byKey(checkBoxKey)).wantKeepAlive,
isFalse,
);
});
}); });
group('TwoDimensionalScrollable', () { group('TwoDimensionalScrollable', () {
...@@ -1025,7 +1359,7 @@ void main() { ...@@ -1025,7 +1359,7 @@ void main() {
expect( expect(
parentData.toString(), parentData.toString(),
'vicinity=(xIndex: 10, yIndex: 10); layoutOffset=Offset(20.0, 20.0); ' 'vicinity=(xIndex: 10, yIndex: 10); layoutOffset=Offset(20.0, 20.0); '
'paintOffset=Offset(20.0, 20.0); not visible ', 'paintOffset=Offset(20.0, 20.0); not visible; ',
); );
}); });
......
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