// Copyright 2014 The Flutter 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:math' as math; import 'package:flutter/foundation.dart' show clampDouble; import 'package:flutter/gestures.dart' show DragStartBehavior; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart' show ViewportOffset; // BUILDER DELEGATE --- final TwoDimensionalChildBuilderDelegate builderDelegate = TwoDimensionalChildBuilderDelegate( maxXIndex: 5, maxYIndex: 5, builder: (BuildContext context, ChildVicinity vicinity) { return Container( key: ValueKey<ChildVicinity>(vicinity), color: vicinity.xIndex.isEven && vicinity.yIndex.isEven ? Colors.amber[100] : (vicinity.xIndex.isOdd && vicinity.yIndex.isOdd ? Colors.blueAccent[100] : null), height: 200, width: 200, child: Center(child: Text('R${vicinity.xIndex}:C${vicinity.yIndex}')), ); } ); // Creates a simple 2D table of 200x200 squares with a builder delegate. Widget simpleBuilderTest({ Axis mainAxis = Axis.vertical, bool? primary, ScrollableDetails? verticalDetails, ScrollableDetails? horizontalDetails, TwoDimensionalChildBuilderDelegate? delegate, double? cacheExtent, DiagonalDragBehavior? diagonalDrag, Clip? clipBehavior, String? restorationID, bool useCacheExtent = false, bool applyDimensions = true, bool forgetToLayoutChild = false, bool setLayoutOffset = true, }) { return MaterialApp( theme: ThemeData(useMaterial3: false), restorationScopeId: restorationID, home: Scaffold( body: SimpleBuilderTableView( mainAxis: mainAxis, verticalDetails: verticalDetails ?? const ScrollableDetails.vertical(), horizontalDetails: horizontalDetails ?? const ScrollableDetails.horizontal(), cacheExtent: cacheExtent, useCacheExtent: useCacheExtent, diagonalDragBehavior: diagonalDrag ?? DiagonalDragBehavior.none, clipBehavior: clipBehavior ?? Clip.hardEdge, delegate: delegate ?? builderDelegate, applyDimensions: applyDimensions, forgetToLayoutChild: forgetToLayoutChild, setLayoutOffset: setLayoutOffset, ), ), ); } class SimpleBuilderTableView extends TwoDimensionalScrollView { const SimpleBuilderTableView({ super.key, super.primary, super.mainAxis = Axis.vertical, super.verticalDetails = const ScrollableDetails.vertical(), super.horizontalDetails = const ScrollableDetails.horizontal(), required TwoDimensionalChildBuilderDelegate delegate, super.cacheExtent, super.diagonalDragBehavior = DiagonalDragBehavior.none, super.dragStartBehavior = DragStartBehavior.start, super.keyboardDismissBehavior = ScrollViewKeyboardDismissBehavior.manual, super.clipBehavior = Clip.hardEdge, this.useCacheExtent = false, this.applyDimensions = true, this.forgetToLayoutChild = false, this.setLayoutOffset = true, }) : super(delegate: delegate); // Piped through for testing in RenderTwoDimensionalViewport final bool useCacheExtent; final bool applyDimensions; final bool forgetToLayoutChild; final bool setLayoutOffset; @override Widget buildViewport(BuildContext context, ViewportOffset verticalOffset, ViewportOffset horizontalOffset) { return SimpleBuilderTableViewport( horizontalOffset: horizontalOffset, horizontalAxisDirection: horizontalDetails.direction, verticalOffset: verticalOffset, verticalAxisDirection: verticalDetails.direction, mainAxis: mainAxis, delegate: delegate as TwoDimensionalChildBuilderDelegate, cacheExtent: cacheExtent, clipBehavior: clipBehavior, useCacheExtent: useCacheExtent, applyDimensions: applyDimensions, forgetToLayoutChild: forgetToLayoutChild, setLayoutOffset: setLayoutOffset, ); } } class SimpleBuilderTableViewport extends TwoDimensionalViewport { const SimpleBuilderTableViewport({ super.key, required super.verticalOffset, required super.verticalAxisDirection, required super.horizontalOffset, required super.horizontalAxisDirection, required TwoDimensionalChildBuilderDelegate delegate, required super.mainAxis, super.cacheExtent, super.clipBehavior = Clip.hardEdge, this.useCacheExtent = false, this.applyDimensions = true, this.forgetToLayoutChild = false, this.setLayoutOffset = true, }) : super(delegate: delegate); // Piped through for testing in RenderTwoDimensionalViewport final bool useCacheExtent; final bool applyDimensions; final bool forgetToLayoutChild; final bool setLayoutOffset; @override RenderTwoDimensionalViewport createRenderObject(BuildContext context) { return RenderSimpleBuilderTableViewport( horizontalOffset: horizontalOffset, horizontalAxisDirection: horizontalAxisDirection, verticalOffset: verticalOffset, verticalAxisDirection: verticalAxisDirection, mainAxis: mainAxis, delegate: delegate as TwoDimensionalChildBuilderDelegate, childManager: context as TwoDimensionalChildManager, cacheExtent: cacheExtent, clipBehavior: clipBehavior, useCacheExtent: useCacheExtent, applyDimensions: applyDimensions, forgetToLayoutChild: forgetToLayoutChild, setLayoutOffset: setLayoutOffset, ); } @override void updateRenderObject(BuildContext context, RenderSimpleBuilderTableViewport renderObject) { renderObject ..horizontalOffset = horizontalOffset ..horizontalAxisDirection = horizontalAxisDirection ..verticalOffset = verticalOffset ..verticalAxisDirection = verticalAxisDirection ..mainAxis = mainAxis ..delegate = delegate ..cacheExtent = cacheExtent ..clipBehavior = clipBehavior; } } class RenderSimpleBuilderTableViewport extends RenderTwoDimensionalViewport { RenderSimpleBuilderTableViewport({ required super.horizontalOffset, required super.horizontalAxisDirection, required super.verticalOffset, required super.verticalAxisDirection, required TwoDimensionalChildBuilderDelegate delegate, required super.mainAxis, required super.childManager, super.cacheExtent, super.clipBehavior = Clip.hardEdge, this.applyDimensions = true, this.setLayoutOffset = true, this.useCacheExtent = false, this.forgetToLayoutChild = false, }) : super(delegate: delegate); // These are to test conditions to validate subclass implementations after // layoutChildSequence final bool applyDimensions; final bool setLayoutOffset; final bool useCacheExtent; final bool forgetToLayoutChild; RenderBox? testGetChildFor(ChildVicinity vicinity) => getChildFor(vicinity); @override TestExtendedParentData parentDataOf(RenderBox child) { return child.parentData! as TestExtendedParentData; } @override void setupParentData(RenderBox child) { if (child.parentData is! TestExtendedParentData) { child.parentData = TestExtendedParentData(); } } @override void layoutChildSequence() { // Really simple table implementation for testing. // Every child is 200x200 square final double horizontalPixels = horizontalOffset.pixels; final double verticalPixels = verticalOffset.pixels; final double viewportWidth = viewportDimension.width + (useCacheExtent ? cacheExtent : 0.0); final double viewportHeight = viewportDimension.height + (useCacheExtent ? cacheExtent : 0.0); final TwoDimensionalChildBuilderDelegate builderDelegate = delegate as TwoDimensionalChildBuilderDelegate; final int maxRowIndex; final int maxColumnIndex; maxRowIndex = builderDelegate.maxYIndex ?? 5; maxColumnIndex = builderDelegate.maxXIndex ?? 5; final int leadingColumn = math.max((horizontalPixels / 200).floor(), 0); final int leadingRow = math.max((verticalPixels / 200).floor(), 0); final int trailingColumn = math.min( ((horizontalPixels + viewportWidth) / 200).ceil(), maxColumnIndex, ); final int trailingRow = math.min( ((verticalPixels + viewportHeight) / 200).ceil(), maxRowIndex, ); double xLayoutOffset = (leadingColumn * 200) - horizontalOffset.pixels; for (int column = leadingColumn; column <= trailingColumn; column++) { double yLayoutOffset = (leadingRow * 200) - verticalOffset.pixels; for (int row = leadingRow; row <= trailingRow; row++) { final ChildVicinity vicinity = ChildVicinity(xIndex: column, yIndex: row); final RenderBox child = buildOrObtainChildFor(vicinity)!; if (!forgetToLayoutChild) { child.layout(constraints.tighten(width: 200.0, height: 200.0)); } if (setLayoutOffset) { parentDataOf(child).layoutOffset = Offset(xLayoutOffset, yLayoutOffset); } yLayoutOffset += 200; } xLayoutOffset += 200; } if (applyDimensions) { final double verticalExtent = 200 * (maxRowIndex + 1); verticalOffset.applyContentDimensions( 0.0, clampDouble(verticalExtent - viewportDimension.height, 0.0, double.infinity), ); final double horizontalExtent = 200 * (maxColumnIndex + 1); horizontalOffset.applyContentDimensions( 0.0, clampDouble(horizontalExtent - viewportDimension.width, 0.0, double.infinity), ); } } } // LIST DELEGATE --- final List<List<Widget>> children = List<List<Widget>>.generate( 100, (int xIndex) { return List<Widget>.generate( 100, (int yIndex) { return Container( color: xIndex.isEven && yIndex.isEven ? Colors.amber[100] : (xIndex.isOdd && yIndex.isOdd ? Colors.blueAccent[100] : null), height: 200, width: 200, child: Center(child: Text('R$xIndex:C$yIndex')), ); }, ); }, ); // Builds a simple 2D table of 200x200 squares with a list delegate. Widget simpleListTest({ Axis mainAxis = Axis.vertical, bool? primary, ScrollableDetails? verticalDetails, ScrollableDetails? horizontalDetails, TwoDimensionalChildListDelegate? delegate, double? cacheExtent, DiagonalDragBehavior? diagonalDrag, Clip? clipBehavior, }) { return MaterialApp( theme: ThemeData(useMaterial3: true), home: Scaffold( body: SimpleListTableView( mainAxis: mainAxis, verticalDetails: verticalDetails ?? const ScrollableDetails.vertical(), horizontalDetails: horizontalDetails ?? const ScrollableDetails.horizontal(), cacheExtent: cacheExtent, diagonalDragBehavior: diagonalDrag ?? DiagonalDragBehavior.none, clipBehavior: clipBehavior ?? Clip.hardEdge, delegate: delegate ?? TwoDimensionalChildListDelegate(children: children), ), ), ); } class SimpleListTableView extends TwoDimensionalScrollView { const SimpleListTableView({ super.key, super.primary, super.mainAxis = Axis.vertical, super.verticalDetails = const ScrollableDetails.vertical(), super.horizontalDetails = const ScrollableDetails.horizontal(), required TwoDimensionalChildListDelegate delegate, super.cacheExtent, super.diagonalDragBehavior = DiagonalDragBehavior.none, super.dragStartBehavior = DragStartBehavior.start, super.keyboardDismissBehavior = ScrollViewKeyboardDismissBehavior.manual, super.clipBehavior = Clip.hardEdge, }) : super(delegate: delegate); @override Widget buildViewport(BuildContext context, ViewportOffset verticalOffset, ViewportOffset horizontalOffset) { return SimpleListTableViewport( horizontalOffset: horizontalOffset, horizontalAxisDirection: horizontalDetails.direction, verticalOffset: verticalOffset, verticalAxisDirection: verticalDetails.direction, mainAxis: mainAxis, delegate: delegate as TwoDimensionalChildListDelegate, cacheExtent: cacheExtent, clipBehavior: clipBehavior, ); } } class SimpleListTableViewport extends TwoDimensionalViewport { const SimpleListTableViewport({ super.key, required super.verticalOffset, required super.verticalAxisDirection, required super.horizontalOffset, required super.horizontalAxisDirection, required TwoDimensionalChildListDelegate delegate, required super.mainAxis, super.cacheExtent, super.clipBehavior = Clip.hardEdge, }) : super(delegate: delegate); @override RenderTwoDimensionalViewport createRenderObject(BuildContext context) { return RenderSimpleListTableViewport( horizontalOffset: horizontalOffset, horizontalAxisDirection: horizontalAxisDirection, verticalOffset: verticalOffset, verticalAxisDirection: verticalAxisDirection, mainAxis: mainAxis, delegate: delegate as TwoDimensionalChildListDelegate, childManager: context as TwoDimensionalChildManager, cacheExtent: cacheExtent, clipBehavior: clipBehavior, ); } @override void updateRenderObject(BuildContext context, RenderSimpleListTableViewport renderObject) { renderObject ..horizontalOffset = horizontalOffset ..horizontalAxisDirection = horizontalAxisDirection ..verticalOffset = verticalOffset ..verticalAxisDirection = verticalAxisDirection ..mainAxis = mainAxis ..delegate = delegate ..cacheExtent = cacheExtent ..clipBehavior = clipBehavior; } } class RenderSimpleListTableViewport extends RenderTwoDimensionalViewport { RenderSimpleListTableViewport({ required super.horizontalOffset, required super.horizontalAxisDirection, required super.verticalOffset, required super.verticalAxisDirection, required TwoDimensionalChildListDelegate delegate, required super.mainAxis, required super.childManager, super.cacheExtent, super.clipBehavior = Clip.hardEdge, }) : super(delegate: delegate); @override void layoutChildSequence() { // Really simple table implementation for testing. // Every child is 200x200 square final double horizontalPixels = horizontalOffset.pixels; final double verticalPixels = verticalOffset.pixels; final TwoDimensionalChildListDelegate listDelegate = delegate as TwoDimensionalChildListDelegate; final int rowCount; final int columnCount; rowCount = listDelegate.children.length; columnCount = listDelegate.children[0].length; final int leadingColumn = math.max((horizontalPixels / 200).floor(), 0); final int leadingRow = math.max((verticalPixels / 200).floor(), 0); final int trailingColumn = math.min( ((horizontalPixels + viewportDimension.width) / 200).ceil(), columnCount - 1, ); final int trailingRow = math.min( ((verticalPixels + viewportDimension.height) / 200).ceil(), rowCount - 1, ); double xLayoutOffset = (leadingColumn * 200) - horizontalOffset.pixels; for (int column = leadingColumn; column <= trailingColumn; column++) { double yLayoutOffset = (leadingRow * 200) - verticalOffset.pixels; for (int row = leadingRow; row <= trailingRow; row++) { final ChildVicinity vicinity = ChildVicinity(xIndex: column, yIndex: row); final RenderBox child = buildOrObtainChildFor(vicinity)!; child.layout(constraints.tighten(width: 200.0, height: 200.0)); parentDataOf(child).layoutOffset = Offset(xLayoutOffset, yLayoutOffset); yLayoutOffset += 200; } xLayoutOffset += 200; } 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; }); } }, ); } } // TwoDimensionalViewportParentData already mixes in KeepAliveParentDataMixin, // and so should be compatible with both the KeepAlive and // TestParentDataWidget ParentDataWidgets. // This ParentData is set up above as part of the // RenderSimpleBuilderTableViewport for testing. class TestExtendedParentData extends TwoDimensionalViewportParentData { int? testValue; } class TestParentDataWidget extends ParentDataWidget<TestExtendedParentData> { const TestParentDataWidget({ super.key, required super.child, this.testValue, }); final int? testValue; @override void applyParentData(RenderObject renderObject) { assert(renderObject.parentData is TestExtendedParentData); final TestExtendedParentData parentData = renderObject.parentData! as TestExtendedParentData; parentData.testValue = testValue; } @override Type get debugTypicalAncestorWidgetClass => SimpleBuilderTableViewport; }