blob: d2cbf3b8883a73486b38ef80d98b62717f078d7c [file] [log] [blame]
// 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(
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;
}