Sliver Cross Axis Group (#123862)
This widget implements the ability to place slivers side by side in a single ScrollView so that they scroll together. The design document for `SliverCrossAxisGroup` can be found [here](https://docs.google.com/document/d/1e2bdLSYV_Dq2h8aHpF8mda67aOmZocPiMyjCcTTZhTg/edit?resourcekey=0-Xj2X2XA3CAFae22Sv3hAiA). Fixes #56756.
This commit is contained in:
parent
2282eb3682
commit
1a0b03cb25
@ -0,0 +1,88 @@
|
||||
// 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 'package:flutter/material.dart';
|
||||
|
||||
void main() => runApp(const SliverCrossAxisGroupExampleApp());
|
||||
|
||||
class SliverCrossAxisGroupExampleApp extends StatelessWidget {
|
||||
const SliverCrossAxisGroupExampleApp({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MaterialApp(
|
||||
home: Scaffold(
|
||||
appBar: AppBar(title: const Text('SliverCrossAxisGroup Sample')),
|
||||
body: const SliverCrossAxisGroupExample(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class SliverCrossAxisGroupExample extends StatelessWidget {
|
||||
const SliverCrossAxisGroupExample({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return CustomScrollView(
|
||||
slivers: <Widget>[
|
||||
SliverCrossAxisGroup(
|
||||
slivers: <Widget>[
|
||||
SliverList.builder(
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
return Container(
|
||||
color: index.isEven ? Colors.amber[300] : Colors.blue[300],
|
||||
height: 100.0,
|
||||
child: Center(
|
||||
child: Text(
|
||||
'Item $index',
|
||||
style: const TextStyle(fontSize: 24),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
itemCount: 5,
|
||||
),
|
||||
SliverConstrainedCrossAxis(
|
||||
maxExtent: 200,
|
||||
sliver: SliverList.builder(
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
return Container(
|
||||
color: index.isEven ? Colors.green[300] : Colors.red[300],
|
||||
height: 100.0,
|
||||
child: Center(
|
||||
child: Text(
|
||||
'Item ${index + 5}',
|
||||
style: const TextStyle(fontSize: 24),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
itemCount: 5,
|
||||
),
|
||||
),
|
||||
SliverCrossAxisExpanded(
|
||||
flex: 2,
|
||||
sliver: SliverList.builder(
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
return Container(
|
||||
color: index.isEven ? Colors.purple[300] : Colors.orange[300],
|
||||
height: 100.0,
|
||||
child: Center(
|
||||
child: Text(
|
||||
'Item ${index + 10}',
|
||||
style: const TextStyle(fontSize: 24),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
itemCount: 5,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,43 @@
|
||||
// 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 'package:flutter/material.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
import 'package:flutter_api_samples/widgets/sliver/sliver_cross_axis_group.0.dart'
|
||||
as example;
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
void main() {
|
||||
testWidgets('SliverCrossAxisGroup example', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(
|
||||
const example.SliverCrossAxisGroupExampleApp(),
|
||||
);
|
||||
|
||||
final RenderSliverCrossAxisGroup renderSliverGroup = tester.renderObject(find.byType(SliverCrossAxisGroup));
|
||||
expect(renderSliverGroup, isNotNull);
|
||||
|
||||
final double crossAxisExtent = renderSliverGroup.constraints.crossAxisExtent;
|
||||
|
||||
final List<RenderSliverList> renderSliverLists = tester.renderObjectList<RenderSliverList>(find.byType(SliverList)).toList();
|
||||
final RenderSliverList firstList = renderSliverLists[0];
|
||||
final RenderSliverList secondList = renderSliverLists[1];
|
||||
final RenderSliverList thirdList = renderSliverLists[2];
|
||||
|
||||
final double expectedFirstExtent = (crossAxisExtent - 200) / 3;
|
||||
const double expectedSecondExtent = 200;
|
||||
final double expectedThirdExtent = 2 * (crossAxisExtent - 200) / 3;
|
||||
expect(firstList.constraints.crossAxisExtent, equals(expectedFirstExtent));
|
||||
expect(secondList.constraints.crossAxisExtent, equals(expectedSecondExtent));
|
||||
expect(thirdList.constraints.crossAxisExtent, equals(expectedThirdExtent));
|
||||
|
||||
// Also check that the paint offsets are correct.
|
||||
final RenderSliverConstrainedCrossAxis renderConstrained = tester.renderObject<RenderSliverConstrainedCrossAxis>(
|
||||
find.byType(SliverConstrainedCrossAxis)
|
||||
);
|
||||
|
||||
expect((firstList.parentData! as SliverPhysicalParentData).paintOffset.dx, equals(0));
|
||||
expect((renderConstrained.parentData! as SliverPhysicalParentData).paintOffset.dx, equals(expectedFirstExtent));
|
||||
expect((thirdList.parentData! as SliverPhysicalParentData).paintOffset.dx, equals(expectedFirstExtent + expectedSecondExtent));
|
||||
});
|
||||
}
|
@ -61,6 +61,7 @@ export 'src/rendering/sliver.dart';
|
||||
export 'src/rendering/sliver_fill.dart';
|
||||
export 'src/rendering/sliver_fixed_extent_list.dart';
|
||||
export 'src/rendering/sliver_grid.dart';
|
||||
export 'src/rendering/sliver_group.dart';
|
||||
export 'src/rendering/sliver_list.dart';
|
||||
export 'src/rendering/sliver_multi_box_adaptor.dart';
|
||||
export 'src/rendering/sliver_padding.dart';
|
||||
|
@ -762,6 +762,8 @@ class SliverGeometry with Diagnosticable {
|
||||
///
|
||||
/// * [SliverConstrainedCrossAxis] for an example of a sliver which takes up
|
||||
/// a smaller cross axis extent than the provided constraint.
|
||||
/// * [SliverCrossAxisGroup] for an example of a sliver which makes use of this
|
||||
/// [crossAxisExtent] to lay out their children.
|
||||
final double? crossAxisExtent;
|
||||
|
||||
/// Asserts that this geometry is internally consistent.
|
||||
@ -1004,7 +1006,20 @@ class SliverPhysicalParentData extends ParentData {
|
||||
|
||||
/// The [crossAxisFlex] factor to use for this sliver child.
|
||||
///
|
||||
/// If used outside of a [SliverCrossAxisGroup] widget, this value has no meaning.
|
||||
///
|
||||
/// If null or zero, the child is inflexible and determines its own size in the cross axis.
|
||||
/// If non-zero, the amount of space the child can occupy in the cross axis is
|
||||
/// determined by dividing the free space (after placing the inflexible children)
|
||||
/// according to the flex factors of the flexible children.
|
||||
///
|
||||
/// This value is only used by the [SliverCrossAxisGroup] widget to determine
|
||||
/// how to allocate its [SliverConstraints.crossAxisExtent] to its children.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [SliverCrossAxisGroup], which lays out multiple slivers along the
|
||||
/// cross axis direction.
|
||||
int? crossAxisFlex;
|
||||
|
||||
/// Apply the [paintOffset] to the given [transform].
|
||||
|
170
packages/flutter/lib/src/rendering/sliver_group.dart
Normal file
170
packages/flutter/lib/src/rendering/sliver_group.dart
Normal file
@ -0,0 +1,170 @@
|
||||
// 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:vector_math/vector_math_64.dart';
|
||||
|
||||
import 'object.dart';
|
||||
import 'sliver.dart';
|
||||
|
||||
/// A sliver that places multiple sliver children in a linear array along the cross
|
||||
/// axis.
|
||||
///
|
||||
/// Since the extent of the viewport in the cross axis direction is finite,
|
||||
/// this extent will be divided up and allocated to the children slivers.
|
||||
///
|
||||
/// The algorithm for dividing up the cross axis extent is as follows.
|
||||
/// Every widget has a [SliverPhysicalParentData.crossAxisFlex] value associated with them.
|
||||
/// First, lay out all of the slivers with flex of 0 or null, in which case the slivers themselves will
|
||||
/// figure out how much cross axis extent to take up. For example, [SliverConstrainedCrossAxis]
|
||||
/// is an example of a widget which sets its own flex to 0. Then [RenderSliverCrossAxisGroup] will
|
||||
/// divide up the remaining space to all the remaining children proportionally
|
||||
/// to each child's flex factor. By default, children of [SliverCrossAxisGroup]
|
||||
/// are setup to have a flex factor of 1, but a different flex factor can be
|
||||
/// specified via the [SliverCrossAxisExpanded] widgets.
|
||||
class RenderSliverCrossAxisGroup extends RenderSliver with ContainerRenderObjectMixin<RenderSliver, SliverPhysicalContainerParentData> {
|
||||
@override
|
||||
void setupParentData(RenderObject child) {
|
||||
if (child.parentData is! SliverPhysicalContainerParentData) {
|
||||
child.parentData = SliverPhysicalContainerParentData();
|
||||
(child.parentData! as SliverPhysicalParentData).crossAxisFlex = 1;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
double childMainAxisPosition(RenderSliver child) => 0.0;
|
||||
|
||||
@override
|
||||
double childCrossAxisPosition(RenderSliver child) {
|
||||
switch (constraints.axisDirection) {
|
||||
case AxisDirection.up:
|
||||
case AxisDirection.down:
|
||||
return (child.parentData! as SliverPhysicalParentData).paintOffset.dx;
|
||||
case AxisDirection.left:
|
||||
case AxisDirection.right:
|
||||
return (child.parentData! as SliverPhysicalParentData).paintOffset.dy;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void performLayout() {
|
||||
// Iterate through each sliver.
|
||||
// Get the parent's dimensions.
|
||||
final double crossAxisExtent = constraints.crossAxisExtent;
|
||||
assert(crossAxisExtent.isFinite);
|
||||
|
||||
// First, layout each child with flex == 0 or null.
|
||||
int totalFlex = 0;
|
||||
double remainingExtent = crossAxisExtent;
|
||||
RenderSliver? child = firstChild;
|
||||
while (child != null) {
|
||||
final SliverPhysicalParentData childParentData = child.parentData! as SliverPhysicalParentData;
|
||||
final int flex = childParentData.crossAxisFlex ?? 0;
|
||||
if (flex == 0) {
|
||||
// If flex is 0 or null, then the child sliver must provide their own crossAxisExtent.
|
||||
assert(_assertOutOfExtent(remainingExtent));
|
||||
child.layout(constraints.copyWith(crossAxisExtent: remainingExtent), parentUsesSize: true);
|
||||
final double? childCrossAxisExtent = child.geometry!.crossAxisExtent;
|
||||
assert(childCrossAxisExtent != null);
|
||||
remainingExtent = math.max(0.0, remainingExtent - childCrossAxisExtent!);
|
||||
} else {
|
||||
totalFlex += flex;
|
||||
}
|
||||
child = childAfter(child);
|
||||
}
|
||||
final double extentPerFlexValue = remainingExtent / totalFlex;
|
||||
|
||||
child = firstChild;
|
||||
double offset = 0.0;
|
||||
|
||||
// At this point, all slivers with constrained cross axis should already be laid out.
|
||||
// Layout the rest and keep track of the child geometry with greatest scrollExtent.
|
||||
geometry = SliverGeometry.zero;
|
||||
while (child != null) {
|
||||
final SliverPhysicalParentData childParentData = child.parentData! as SliverPhysicalParentData;
|
||||
final int flex = childParentData.crossAxisFlex ?? 0;
|
||||
double childExtent;
|
||||
if (flex != 0) {
|
||||
childExtent = extentPerFlexValue * flex;
|
||||
assert(_assertOutOfExtent(childExtent));
|
||||
child.layout(constraints.copyWith(
|
||||
crossAxisExtent: extentPerFlexValue * flex,
|
||||
), parentUsesSize: true);
|
||||
} else {
|
||||
childExtent = child.geometry!.crossAxisExtent!;
|
||||
}
|
||||
// Set child parent data.
|
||||
switch (constraints.axis) {
|
||||
case Axis.vertical:
|
||||
childParentData.paintOffset = Offset(offset, 0.0);
|
||||
case Axis.horizontal:
|
||||
childParentData.paintOffset = Offset(0.0, offset);
|
||||
}
|
||||
offset += childExtent;
|
||||
if (geometry!.scrollExtent < child.geometry!.scrollExtent) {
|
||||
geometry = child.geometry;
|
||||
}
|
||||
child = childAfter(child);
|
||||
}
|
||||
|
||||
// Set the geometry with the proper crossAxisExtent.
|
||||
geometry = geometry!.copyWith(crossAxisExtent: constraints.crossAxisExtent);
|
||||
}
|
||||
|
||||
@override
|
||||
void paint(PaintingContext context, Offset offset) {
|
||||
RenderSliver? child = firstChild;
|
||||
|
||||
while (child != null) {
|
||||
final SliverPhysicalParentData childParentData = child.parentData! as SliverPhysicalParentData;
|
||||
context.paintChild(child, offset + childParentData.paintOffset);
|
||||
child = childAfter(child);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void applyPaintTransform(RenderSliver child, Matrix4 transform) {
|
||||
final SliverPhysicalParentData childParentData = child.parentData! as SliverPhysicalParentData;
|
||||
childParentData.applyPaintTransform(transform);
|
||||
}
|
||||
|
||||
@override
|
||||
bool hitTestChildren(SliverHitTestResult result, {required double mainAxisPosition, required double crossAxisPosition}) {
|
||||
RenderSliver? child = lastChild;
|
||||
while (child != null) {
|
||||
final bool isHit = result.addWithAxisOffset(
|
||||
mainAxisPosition: mainAxisPosition,
|
||||
crossAxisPosition: crossAxisPosition,
|
||||
paintOffset: null,
|
||||
mainAxisOffset: childMainAxisPosition(child),
|
||||
crossAxisOffset: childCrossAxisPosition(child),
|
||||
hitTest: child.hitTest,
|
||||
);
|
||||
if (isHit) {
|
||||
return true;
|
||||
}
|
||||
child = childBefore(child);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
bool _assertOutOfExtent(double extent) {
|
||||
if(extent <= 0.0) {
|
||||
throw FlutterError.fromParts(<DiagnosticsNode>[
|
||||
ErrorSummary('SliverCrossAxisGroup ran out of extent before child could be laid out.'),
|
||||
ErrorDescription(
|
||||
'SliverCrossAxisGroup lays out any slivers with a constrained cross '
|
||||
'axis before laying out those which expand. In this case, cross axis '
|
||||
'extent was used up before the next sliver could be laid out.'
|
||||
),
|
||||
ErrorHint(
|
||||
'Make sure that the total amount of extent allocated by constrained '
|
||||
'child slivers does not exceed the cross axis extent that is available '
|
||||
'for the SliverCrossAxisGroup.'
|
||||
),
|
||||
]);
|
||||
}
|
||||
return true;
|
||||
}
|
@ -1375,12 +1375,21 @@ class KeepAlive extends ParentDataWidget<KeepAliveParentDataMixin> {
|
||||
/// This is useful when you want to apply a custom cross-axis extent constraint
|
||||
/// to a sliver child, as slivers typically consume the full cross axis extent.
|
||||
///
|
||||
/// This widget also sets its parent data's [SliverPhysicalParentData.crossAxisFlex]
|
||||
/// to 0, so that it informs [SliverCrossAxisGroup] that it should not flex
|
||||
/// in the cross axis direction.
|
||||
///
|
||||
/// {@tool dartpad}
|
||||
/// In this sample the [SliverConstrainedCrossAxis] sizes its child so that the
|
||||
/// cross axis extent takes up less space than the actual viewport.
|
||||
///
|
||||
/// ** See code in examples/api/lib/widgets/sliver/sliver_constrained_cross_axis.0.dart **
|
||||
/// {@end-tool}
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [SliverCrossAxisGroup], the widget which makes use of 0 flex factor set by
|
||||
/// this widget.
|
||||
class SliverConstrainedCrossAxis extends StatelessWidget {
|
||||
/// Creates a sliver that constrains the cross axis extent of its sliver child.
|
||||
///
|
||||
@ -1436,7 +1445,7 @@ class _SliverZeroFlexParentDataWidget extends ParentDataWidget<SliverPhysicalPar
|
||||
}
|
||||
|
||||
@override
|
||||
Type get debugTypicalAncestorWidgetClass => Widget;
|
||||
Type get debugTypicalAncestorWidgetClass => SliverCrossAxisGroup;
|
||||
}
|
||||
|
||||
class _SliverConstrainedCrossAxis extends SingleChildRenderObjectWidget {
|
||||
@ -1461,3 +1470,107 @@ class _SliverConstrainedCrossAxis extends SingleChildRenderObjectWidget {
|
||||
renderObject.maxExtent = maxExtent;
|
||||
}
|
||||
}
|
||||
|
||||
/// Set a flex factor for allocating space in the cross axis direction.
|
||||
///
|
||||
/// This is a [ParentDataWidget] to be used in [SliverCrossAxisGroup].
|
||||
/// After all slivers with null or zero flex (e.g. [SliverConstrainedCrossAxis])
|
||||
/// are laid out (which should determine their own [SliverGeometry.crossAxisExtent]),
|
||||
/// the remaining space is laid out among the slivers with nonzero flex
|
||||
/// proportionally to their flex value.
|
||||
class SliverCrossAxisExpanded extends ParentDataWidget<SliverPhysicalContainerParentData> {
|
||||
/// Creates an object that assigns a [flex] value to the child sliver.
|
||||
///
|
||||
/// The provided [flex] value must be greater than 0.
|
||||
const SliverCrossAxisExpanded({
|
||||
super.key,
|
||||
required this.flex,
|
||||
required Widget sliver,
|
||||
}): assert(flex > 0 && flex < double.infinity),
|
||||
super(child: sliver);
|
||||
|
||||
/// Flex value for allocating cross axis extent left after laying out the children with
|
||||
/// constrained cross axis. The children with flex values will have the remaining extent
|
||||
/// allocated proportionally to their flex value. This must an integer between
|
||||
/// 0 and infinity, exclusive.
|
||||
final int flex;
|
||||
|
||||
@override
|
||||
void applyParentData(RenderObject renderObject) {
|
||||
assert(renderObject.parentData is SliverPhysicalContainerParentData);
|
||||
assert(renderObject.parent is RenderSliverCrossAxisGroup);
|
||||
final SliverPhysicalParentData parentData = renderObject.parentData! as SliverPhysicalParentData;
|
||||
bool needsLayout = false;
|
||||
|
||||
if (parentData.crossAxisFlex != flex) {
|
||||
parentData.crossAxisFlex = flex;
|
||||
needsLayout = true;
|
||||
}
|
||||
|
||||
if (needsLayout) {
|
||||
final AbstractNode? targetParent = renderObject.parent;
|
||||
if (targetParent is RenderObject) {
|
||||
targetParent.markNeedsLayout();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Type get debugTypicalAncestorWidgetClass => SliverCrossAxisGroup;
|
||||
}
|
||||
|
||||
|
||||
/// A sliver that places multiple sliver children in a linear array along
|
||||
/// the cross axis.
|
||||
///
|
||||
/// ## Layout algorithm
|
||||
///
|
||||
/// _This section describes how the framework causes [RenderSliverCrossAxisGroup]
|
||||
/// to position its children._
|
||||
///
|
||||
/// Layout for a [RenderSliverCrossAxisGroup] has four steps:
|
||||
///
|
||||
/// 1. Layout each child with a null or zero flex factor with cross axis constraint
|
||||
/// being whatever cross axis space is remaining after laying out any previous
|
||||
/// sliver. Slivers with null or zero flex factor should determine their own
|
||||
/// [SliverGeometry.crossAxisExtent]. For example, the [SliverConstrainedCrossAxis]
|
||||
/// widget uses either [SliverConstrainedCrossAxis.maxExtent] or
|
||||
/// [SliverConstraints.crossAxisExtent], deciding between whichever is smaller.
|
||||
/// 2. Divide up the remaining cross axis space among the children with non-zero flex
|
||||
/// factors according to their flex factor. For example, a child with a flex
|
||||
/// factor of 2.0 will receive twice the amount of cross axis space as a child
|
||||
/// with a flex factor 1.0.
|
||||
/// 3. Layout each of the remaining children with the cross axis constraint
|
||||
/// allocated in the previous step.
|
||||
/// 4. Set the geometry to that of whichever child has the longest
|
||||
/// [SliverGeometry.scrollExtent] with the [SliverGeometry.crossAxisExtent] adjusted
|
||||
/// to [SliverConstraints.crossAxisExtent].
|
||||
///
|
||||
/// {@tool dartpad}
|
||||
/// In this sample the [SliverCrossAxisGroup] sizes its three [children] so that
|
||||
/// the first normal [SliverList] has a flex factor of 1, the second [SliverConstrainedCrossAxis]
|
||||
/// has a flex factor of 0 and a maximum cross axis extent of 200.0, and the third
|
||||
/// [SliverCrossAxisExpanded] has a flex factor of 2.
|
||||
///
|
||||
/// ** See code in examples/api/lib/widgets/sliver/sliver_cross_axis_group.0.dart **
|
||||
/// {@end-tool}
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [SliverCrossAxisExpanded], which is the [ParentDataWidget] for setting a flex
|
||||
/// value to a widget.
|
||||
/// * [SliverConstrainedCrossAxis], which is a [RenderObjectWidget] for setting
|
||||
/// an extent to constrain the widget to.
|
||||
class SliverCrossAxisGroup extends MultiChildRenderObjectWidget {
|
||||
/// Creates a sliver that places sliver children in a linear array along
|
||||
/// the cross axis.
|
||||
const SliverCrossAxisGroup({
|
||||
super.key,
|
||||
required List<Widget> slivers,
|
||||
}): super(children: slivers);
|
||||
|
||||
@override
|
||||
RenderSliverCrossAxisGroup createRenderObject(BuildContext context) {
|
||||
return RenderSliverCrossAxisGroup();
|
||||
}
|
||||
}
|
||||
|
570
packages/flutter/test/widgets/sliver_cross_axis_group_test.dart
Normal file
570
packages/flutter/test/widgets/sliver_cross_axis_group_test.dart
Normal file
@ -0,0 +1,570 @@
|
||||
// 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 'package:flutter/material.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
const double VIEWPORT_HEIGHT = 600;
|
||||
const double VIEWPORT_WIDTH = 300;
|
||||
|
||||
void main() {
|
||||
testWidgets('SliverCrossAxisGroup is laid out properly', (WidgetTester tester) async {
|
||||
final List<int> items = List<int>.generate(20, (int i) => i);
|
||||
final ScrollController controller = ScrollController();
|
||||
|
||||
await tester.pumpWidget(_buildSliverCrossAxisGroup(
|
||||
controller: controller,
|
||||
slivers: <Widget>[
|
||||
_buildSliverList(itemMainAxisExtent: 300, items: items, label: (int item) => Text('Group 0 Tile $item')),
|
||||
_buildSliverList(itemMainAxisExtent: 200, items: items, label: (int item) => Text('Group 1 Tile $item')),
|
||||
]),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(controller.offset, 0);
|
||||
|
||||
expect(find.text('Group 0 Tile 0'), findsOneWidget);
|
||||
expect(find.text('Group 0 Tile 1'), findsOneWidget);
|
||||
expect(find.text('Group 0 Tile 2'), findsNothing);
|
||||
|
||||
expect(find.text('Group 1 Tile 0'), findsOneWidget);
|
||||
expect(find.text('Group 1 Tile 2'), findsOneWidget);
|
||||
expect(find.text('Group 1 Tile 3'), findsNothing);
|
||||
|
||||
const double scrollOffset = 18 * 300.0;
|
||||
controller.jumpTo(scrollOffset);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(controller.offset, scrollOffset);
|
||||
expect(find.text('Group 0 Tile 17'), findsNothing);
|
||||
expect(find.text('Group 0 Tile 18'), findsOneWidget);
|
||||
expect(find.text('Group 0 Tile 19'), findsOneWidget);
|
||||
expect(find.text('Group 1 Tile 19'), findsNothing);
|
||||
|
||||
final List<RenderSliverList> renderSlivers = tester.renderObjectList<RenderSliverList>(find.byType(SliverList)).toList();
|
||||
final RenderSliverList first = renderSlivers[0];
|
||||
final RenderSliverList second = renderSlivers[1];
|
||||
|
||||
expect(first.constraints.crossAxisExtent, equals(VIEWPORT_WIDTH / 2));
|
||||
expect(second.constraints.crossAxisExtent, equals(VIEWPORT_WIDTH / 2));
|
||||
|
||||
expect((first.parentData! as SliverPhysicalParentData).paintOffset.dx, equals(0));
|
||||
expect((second.parentData! as SliverPhysicalParentData).paintOffset.dx, equals(VIEWPORT_WIDTH / 2));
|
||||
|
||||
final RenderSliverCrossAxisGroup renderGroup = tester.renderObject<RenderSliverCrossAxisGroup>(find.byType(SliverCrossAxisGroup));
|
||||
expect(renderGroup.geometry!.scrollExtent, equals(300 * 20));
|
||||
});
|
||||
|
||||
testWidgets('SliverExpanded is laid out properly', (WidgetTester tester) async {
|
||||
final List<int> items = List<int>.generate(20, (int i) => i);
|
||||
await tester.pumpWidget(_buildSliverCrossAxisGroup(
|
||||
slivers: <Widget>[
|
||||
SliverCrossAxisExpanded(
|
||||
flex: 3,
|
||||
sliver: _buildSliverList(
|
||||
itemMainAxisExtent: 300,
|
||||
items: items,
|
||||
label: (int item) => Text('Group 0 Tile $item')
|
||||
),
|
||||
),
|
||||
SliverCrossAxisExpanded(
|
||||
flex: 2,
|
||||
sliver: _buildSliverList(
|
||||
itemMainAxisExtent: 200,
|
||||
items: items,
|
||||
label: (int item) => Text('Group 1 Tile $item')
|
||||
),
|
||||
),
|
||||
]),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
final List<RenderSliverList> renderSlivers = tester.renderObjectList<RenderSliverList>(find.byType(SliverList)).toList();
|
||||
final RenderSliverList first = renderSlivers[0];
|
||||
final RenderSliverList second = renderSlivers[1];
|
||||
|
||||
expect(first.constraints.crossAxisExtent, equals(3 * VIEWPORT_WIDTH / 5));
|
||||
expect(second.constraints.crossAxisExtent, equals(2 * VIEWPORT_WIDTH / 5));
|
||||
|
||||
expect((first.parentData! as SliverPhysicalParentData).paintOffset.dx, equals(0));
|
||||
expect((second.parentData! as SliverPhysicalParentData).paintOffset.dx, equals(3 * VIEWPORT_WIDTH / 5));
|
||||
|
||||
final RenderSliverCrossAxisGroup renderGroup = tester.renderObject<RenderSliverCrossAxisGroup>(find.byType(SliverCrossAxisGroup));
|
||||
expect(renderGroup.geometry!.scrollExtent, equals(300 * 20));
|
||||
});
|
||||
|
||||
testWidgets('SliverConstrainedCrossAxis is laid out properly', (WidgetTester tester) async {
|
||||
final List<int> items = List<int>.generate(20, (int i) => i);
|
||||
await tester.pumpWidget(_buildSliverCrossAxisGroup(
|
||||
slivers: <Widget>[
|
||||
SliverConstrainedCrossAxis(maxExtent: 60, sliver: _buildSliverList(itemMainAxisExtent: 300, items: items, label: (int item) => Text('Group 0 Tile $item'))),
|
||||
SliverConstrainedCrossAxis(maxExtent: 120, sliver: _buildSliverList(itemMainAxisExtent: 200, items: items, label: (int item) => Text('Group 1 Tile $item'))),
|
||||
]),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
final List<RenderSliverList> renderSlivers = tester.renderObjectList<RenderSliverList>(find.byType(SliverList)).toList();
|
||||
final RenderSliverList first = renderSlivers[0];
|
||||
final RenderSliverList second = renderSlivers[1];
|
||||
|
||||
expect(first.constraints.crossAxisExtent, equals(60));
|
||||
expect(second.constraints.crossAxisExtent, equals(120));
|
||||
|
||||
// Check that their parent SliverConstrainedCrossAxis have the correct paintOffsets.
|
||||
final List<RenderSliverConstrainedCrossAxis> renderSliversConstrained = tester.renderObjectList<RenderSliverConstrainedCrossAxis>(find.byType(SliverConstrainedCrossAxis)).toList();
|
||||
final RenderSliverConstrainedCrossAxis firstConstrained = renderSliversConstrained[0];
|
||||
final RenderSliverConstrainedCrossAxis secondConstrained = renderSliversConstrained[1];
|
||||
|
||||
expect((firstConstrained.parentData! as SliverPhysicalParentData).paintOffset.dx, equals(0));
|
||||
expect((secondConstrained.parentData! as SliverPhysicalParentData).paintOffset.dx, equals(60));
|
||||
|
||||
final RenderSliverCrossAxisGroup renderGroup = tester.renderObject<RenderSliverCrossAxisGroup>(find.byType(SliverCrossAxisGroup));
|
||||
expect(renderGroup.geometry!.scrollExtent, equals(300 * 20));
|
||||
});
|
||||
|
||||
testWidgets('Mix of slivers is laid out properly', (WidgetTester tester) async {
|
||||
final List<int> items = List<int>.generate(20, (int i) => i);
|
||||
await tester.pumpWidget(_buildSliverCrossAxisGroup(
|
||||
slivers: <Widget>[
|
||||
SliverConstrainedCrossAxis(maxExtent: 30, sliver: _buildSliverList(itemMainAxisExtent: 300, items: items, label: (int item) => Text('Group 0 Tile $item'))),
|
||||
SliverCrossAxisExpanded(flex: 2, sliver: _buildSliverList(itemMainAxisExtent: 200, items: items, label: (int item) => Text('Group 1 Tile $item'))),
|
||||
_buildSliverList(itemMainAxisExtent: 200, items: items, label: (int item) => Text('Group 2 Tile $item')),
|
||||
]),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
final List<RenderSliverList> renderSlivers = tester.renderObjectList<RenderSliverList>(find.byType(SliverList)).toList();
|
||||
final RenderSliverList first = renderSlivers[0];
|
||||
final RenderSliverList second = renderSlivers[1];
|
||||
final RenderSliverList third = renderSlivers[2];
|
||||
|
||||
expect(first.constraints.crossAxisExtent, equals(30));
|
||||
expect(second.constraints.crossAxisExtent, equals(180));
|
||||
expect(third.constraints.crossAxisExtent, equals(90));
|
||||
|
||||
// Check that paint offset for sliver children are correct as well.
|
||||
final RenderSliverCrossAxisGroup sliverCrossAxisRenderObject = tester.renderObject<RenderSliverCrossAxisGroup>(find.byType(SliverCrossAxisGroup));
|
||||
RenderSliver child = sliverCrossAxisRenderObject.firstChild!;
|
||||
expect((child.parentData! as SliverPhysicalParentData).paintOffset.dx, equals(0));
|
||||
child = sliverCrossAxisRenderObject.childAfter(child)!;
|
||||
expect((child.parentData! as SliverPhysicalParentData).paintOffset.dx, equals(30));
|
||||
child = sliverCrossAxisRenderObject.childAfter(child)!;
|
||||
expect((child.parentData! as SliverPhysicalParentData).paintOffset.dx, equals(210));
|
||||
|
||||
final RenderSliverCrossAxisGroup renderGroup = tester.renderObject<RenderSliverCrossAxisGroup>(find.byType(SliverCrossAxisGroup));
|
||||
expect(renderGroup.geometry!.scrollExtent, equals(300 * 20));
|
||||
});
|
||||
|
||||
testWidgets('Mix of slivers is laid out properly when horizontal', (WidgetTester tester) async {
|
||||
final List<int> items = List<int>.generate(20, (int i) => i);
|
||||
await tester.pumpWidget(_buildSliverCrossAxisGroup(
|
||||
scrollDirection: Axis.horizontal,
|
||||
slivers: <Widget>[
|
||||
SliverConstrainedCrossAxis(
|
||||
maxExtent: 30,
|
||||
sliver: _buildSliverList(
|
||||
scrollDirection: Axis.horizontal,
|
||||
itemMainAxisExtent: 300,
|
||||
items: items,
|
||||
label: (int item) => Text('Group 0 Tile $item')
|
||||
)
|
||||
),
|
||||
SliverCrossAxisExpanded(
|
||||
flex: 2,
|
||||
sliver: _buildSliverList(
|
||||
scrollDirection: Axis.horizontal,
|
||||
itemMainAxisExtent: 200,
|
||||
items: items,
|
||||
label: (int item) => Text('Group 1 Tile $item')
|
||||
)
|
||||
),
|
||||
_buildSliverList(
|
||||
scrollDirection: Axis.horizontal,
|
||||
itemMainAxisExtent: 200,
|
||||
items: items,
|
||||
label: (int item) => Text('Group 2 Tile $item')
|
||||
),
|
||||
]),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
final List<RenderSliverList> renderSlivers = tester.renderObjectList<RenderSliverList>(find.byType(SliverList)).toList();
|
||||
final RenderSliverList first = renderSlivers[0];
|
||||
final RenderSliverList second = renderSlivers[1];
|
||||
final RenderSliverList third = renderSlivers[2];
|
||||
|
||||
expect(first.constraints.crossAxisExtent, equals(30));
|
||||
expect(second.constraints.crossAxisExtent, equals(380));
|
||||
expect(third.constraints.crossAxisExtent, equals(190));
|
||||
|
||||
// Check that paint offset for sliver children are correct as well.
|
||||
final RenderSliverCrossAxisGroup sliverCrossAxisRenderObject = tester.renderObject<RenderSliverCrossAxisGroup>(find.byType(SliverCrossAxisGroup));
|
||||
RenderSliver child = sliverCrossAxisRenderObject.firstChild!;
|
||||
expect((child.parentData! as SliverPhysicalParentData).paintOffset.dy, equals(0));
|
||||
child = sliverCrossAxisRenderObject.childAfter(child)!;
|
||||
expect((child.parentData! as SliverPhysicalParentData).paintOffset.dy, equals(30));
|
||||
child = sliverCrossAxisRenderObject.childAfter(child)!;
|
||||
expect((child.parentData! as SliverPhysicalParentData).paintOffset.dy, equals(410));
|
||||
|
||||
final RenderSliverCrossAxisGroup renderGroup = tester.renderObject<RenderSliverCrossAxisGroup>(find.byType(SliverCrossAxisGroup));
|
||||
expect(renderGroup.geometry!.scrollExtent, equals(300 * 20));
|
||||
});
|
||||
|
||||
testWidgets('Mix of slivers is laid out properly when reversed horizontal', (WidgetTester tester) async {
|
||||
final List<int> items = List<int>.generate(20, (int i) => i);
|
||||
await tester.pumpWidget(_buildSliverCrossAxisGroup(
|
||||
scrollDirection: Axis.horizontal,
|
||||
reverse: true,
|
||||
slivers: <Widget>[
|
||||
SliverConstrainedCrossAxis(
|
||||
maxExtent: 30,
|
||||
sliver: _buildSliverList(
|
||||
scrollDirection: Axis.horizontal,
|
||||
itemMainAxisExtent: 300,
|
||||
items: items,
|
||||
label: (int item) => Text('Group 0 Tile $item')
|
||||
)
|
||||
),
|
||||
SliverCrossAxisExpanded(
|
||||
flex: 2,
|
||||
sliver: _buildSliverList(
|
||||
scrollDirection: Axis.horizontal,
|
||||
itemMainAxisExtent: 200,
|
||||
items: items,
|
||||
label: (int item) => Text('Group 1 Tile $item')
|
||||
)
|
||||
),
|
||||
_buildSliverList(
|
||||
scrollDirection: Axis.horizontal,
|
||||
itemMainAxisExtent: 200,
|
||||
items: items,
|
||||
label: (int item) => Text('Group 2 Tile $item')
|
||||
),
|
||||
]),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
final List<RenderSliverList> renderSlivers = tester.renderObjectList<RenderSliverList>(find.byType(SliverList)).toList();
|
||||
final RenderSliverList first = renderSlivers[0];
|
||||
final RenderSliverList second = renderSlivers[1];
|
||||
final RenderSliverList third = renderSlivers[2];
|
||||
|
||||
expect(first.constraints.crossAxisExtent, equals(30));
|
||||
expect(second.constraints.crossAxisExtent, equals(380));
|
||||
expect(third.constraints.crossAxisExtent, equals(190));
|
||||
|
||||
// Check that paint offset for sliver children are correct as well.
|
||||
final RenderSliverCrossAxisGroup sliverCrossAxisRenderObject = tester.renderObject<RenderSliverCrossAxisGroup>(find.byType(SliverCrossAxisGroup));
|
||||
RenderSliver child = sliverCrossAxisRenderObject.firstChild!;
|
||||
expect((child.parentData! as SliverPhysicalParentData).paintOffset.dy, equals(0));
|
||||
child = sliverCrossAxisRenderObject.childAfter(child)!;
|
||||
expect((child.parentData! as SliverPhysicalParentData).paintOffset.dy, equals(30));
|
||||
child = sliverCrossAxisRenderObject.childAfter(child)!;
|
||||
expect((child.parentData! as SliverPhysicalParentData).paintOffset.dy, equals(410));
|
||||
|
||||
final RenderSliverCrossAxisGroup renderGroup = tester.renderObject<RenderSliverCrossAxisGroup>(find.byType(SliverCrossAxisGroup));
|
||||
expect(renderGroup.geometry!.scrollExtent, equals(300 * 20));
|
||||
});
|
||||
|
||||
testWidgets('Mix of slivers is laid out properly when reversed vertical', (WidgetTester tester) async {
|
||||
final List<int> items = List<int>.generate(20, (int i) => i);
|
||||
await tester.pumpWidget(_buildSliverCrossAxisGroup(
|
||||
reverse: true,
|
||||
slivers: <Widget>[
|
||||
SliverConstrainedCrossAxis(
|
||||
maxExtent: 30,
|
||||
sliver: _buildSliverList(
|
||||
itemMainAxisExtent: 300,
|
||||
items: items,
|
||||
label: (int item) => Text('Group 0 Tile $item')
|
||||
)
|
||||
),
|
||||
SliverCrossAxisExpanded(
|
||||
flex: 2,
|
||||
sliver: _buildSliverList(
|
||||
itemMainAxisExtent: 200,
|
||||
items: items,
|
||||
label: (int item) => Text('Group 1 Tile $item')
|
||||
)
|
||||
),
|
||||
_buildSliverList(
|
||||
itemMainAxisExtent: 200,
|
||||
items: items,
|
||||
label: (int item) => Text('Group 2 Tile $item')
|
||||
),
|
||||
]),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
final List<RenderSliverList> renderSlivers = tester.renderObjectList<RenderSliverList>(find.byType(SliverList)).toList();
|
||||
final RenderSliverList first = renderSlivers[0];
|
||||
final RenderSliverList second = renderSlivers[1];
|
||||
final RenderSliverList third = renderSlivers[2];
|
||||
|
||||
expect(first.constraints.crossAxisExtent, equals(30));
|
||||
expect(second.constraints.crossAxisExtent, equals(180));
|
||||
expect(third.constraints.crossAxisExtent, equals(90));
|
||||
|
||||
// Check that paint offset for sliver children are correct as well.
|
||||
final RenderSliverCrossAxisGroup sliverCrossAxisRenderObject = tester.renderObject<RenderSliverCrossAxisGroup>(find.byType(SliverCrossAxisGroup));
|
||||
RenderSliver child = sliverCrossAxisRenderObject.firstChild!;
|
||||
expect((child.parentData! as SliverPhysicalParentData).paintOffset.dx, equals(0));
|
||||
child = sliverCrossAxisRenderObject.childAfter(child)!;
|
||||
expect((child.parentData! as SliverPhysicalParentData).paintOffset.dx, equals(30));
|
||||
child = sliverCrossAxisRenderObject.childAfter(child)!;
|
||||
expect((child.parentData! as SliverPhysicalParentData).paintOffset.dx, equals(210));
|
||||
|
||||
final RenderSliverCrossAxisGroup renderGroup = tester.renderObject<RenderSliverCrossAxisGroup>(find.byType(SliverCrossAxisGroup));
|
||||
expect(renderGroup.geometry!.scrollExtent, equals(300 * 20));
|
||||
});
|
||||
|
||||
testWidgets('Assertion error when SliverExpanded is used outside of SliverCrossAxisGroup', (WidgetTester tester) async {
|
||||
final List<FlutterErrorDetails> errors = <FlutterErrorDetails>[];
|
||||
final Function(FlutterErrorDetails)? oldHandler = FlutterError.onError;
|
||||
FlutterError.onError = (FlutterErrorDetails error) => errors.add(error);
|
||||
|
||||
await tester.pumpWidget(
|
||||
const Directionality(
|
||||
textDirection: TextDirection.ltr,
|
||||
child: Center(
|
||||
child: CustomScrollView(
|
||||
slivers: <Widget>[
|
||||
SliverCrossAxisExpanded(
|
||||
flex: 2,
|
||||
sliver: SliverToBoxAdapter(
|
||||
child: Text('Hello World'),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
FlutterError.onError = oldHandler;
|
||||
expect(errors, isNotEmpty);
|
||||
final AssertionError error = errors.first.exception as AssertionError;
|
||||
expect(
|
||||
error.toString(),
|
||||
contains('renderObject.parent is RenderSliverCrossAxisGroup'),
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('Hit test works properly on various parts of SliverCrossAxisGroup', (WidgetTester tester) async {
|
||||
final List<int> items = List<int>.generate(20, (int i) => i);
|
||||
final ScrollController controller = ScrollController();
|
||||
|
||||
String? clickedTile;
|
||||
|
||||
int group = 0;
|
||||
int tile = 0;
|
||||
|
||||
await tester.pumpWidget(_buildSliverCrossAxisGroup(
|
||||
controller: controller,
|
||||
slivers: <Widget>[
|
||||
_buildSliverList(
|
||||
itemMainAxisExtent: 300,
|
||||
items: items,
|
||||
label: (int item) => tile == item && group == 0
|
||||
? TextButton(
|
||||
onPressed: () => clickedTile = 'Group 0 Tile $item',
|
||||
child: Text('Group 0 Tile $item'),
|
||||
)
|
||||
: Text('Group 0 Tile $item'),
|
||||
),
|
||||
_buildSliverList(
|
||||
items: items,
|
||||
label: (int item) => tile == item && group == 1
|
||||
? TextButton(
|
||||
onPressed: () => clickedTile = 'Group 1 Tile $item',
|
||||
child: Text('Group 1 Tile $item'),
|
||||
)
|
||||
: Text('Group 1 Tile $item'),
|
||||
),
|
||||
]),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.byType(TextButton));
|
||||
await tester.pumpAndSettle();
|
||||
expect(clickedTile, equals('Group 0 Tile 0'));
|
||||
|
||||
clickedTile = null;
|
||||
group = 1;
|
||||
tile = 2;
|
||||
await tester.pumpWidget(_buildSliverCrossAxisGroup(
|
||||
controller: controller,
|
||||
slivers: <Widget>[
|
||||
_buildSliverList(
|
||||
itemMainAxisExtent: 300,
|
||||
items: items,
|
||||
label: (int item) => tile == item && group == 0
|
||||
? TextButton(
|
||||
onPressed: () => clickedTile = 'Group 0 Tile $item',
|
||||
child: Text('Group 0 Tile $item'),
|
||||
)
|
||||
: Text('Group 0 Tile $item'),
|
||||
),
|
||||
_buildSliverList(
|
||||
items: items,
|
||||
label: (int item) => tile == item && group == 1
|
||||
? TextButton(
|
||||
onPressed: () => clickedTile = 'Group 1 Tile $item',
|
||||
child: Text('Group 1 Tile $item'),
|
||||
)
|
||||
: Text('Group 1 Tile $item'),
|
||||
),
|
||||
]),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.byType(TextButton));
|
||||
await tester.pumpAndSettle();
|
||||
expect(clickedTile, equals('Group 1 Tile 2'));
|
||||
});
|
||||
|
||||
testWidgets('Constrained sliver takes up remaining space', (WidgetTester tester) async {
|
||||
final List<int> items = List<int>.generate(20, (int i) => i);
|
||||
await tester.pumpWidget(_buildSliverCrossAxisGroup(
|
||||
slivers: <Widget>[
|
||||
SliverConstrainedCrossAxis(maxExtent: 200, sliver: _buildSliverList(itemMainAxisExtent: 300, items: items, label: (int item) => Text('Group 0 Tile $item'))),
|
||||
SliverConstrainedCrossAxis(maxExtent: 200, sliver: _buildSliverList(itemMainAxisExtent: 200, items: items, label: (int item) => Text('Group 1 Tile $item'))),
|
||||
]),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
final List<RenderSliverList> renderSlivers = tester.renderObjectList<RenderSliverList>(find.byType(SliverList)).toList();
|
||||
final RenderSliverList first = renderSlivers[0];
|
||||
final RenderSliverList second = renderSlivers[1];
|
||||
|
||||
expect(first.constraints.crossAxisExtent, equals(200));
|
||||
expect(second.constraints.crossAxisExtent, equals(100));
|
||||
|
||||
// Check that their parent SliverConstrainedCrossAxis have the correct paintOffsets.
|
||||
final List<RenderSliverConstrainedCrossAxis> renderSliversConstrained = tester.renderObjectList<RenderSliverConstrainedCrossAxis>(find.byType(SliverConstrainedCrossAxis)).toList();
|
||||
final RenderSliverConstrainedCrossAxis firstConstrained = renderSliversConstrained[0];
|
||||
final RenderSliverConstrainedCrossAxis secondConstrained = renderSliversConstrained[1];
|
||||
|
||||
expect((firstConstrained.parentData! as SliverPhysicalParentData).paintOffset.dx, equals(0));
|
||||
expect((secondConstrained.parentData! as SliverPhysicalParentData).paintOffset.dx, equals(200));
|
||||
|
||||
final RenderSliverCrossAxisGroup renderGroup = tester.renderObject<RenderSliverCrossAxisGroup>(find.byType(SliverCrossAxisGroup));
|
||||
expect(renderGroup.geometry!.scrollExtent, equals(300 * 20));
|
||||
});
|
||||
|
||||
testWidgets('Assertion error when constrained widget runs out of cross axis extent', (WidgetTester tester) async {
|
||||
final List<FlutterErrorDetails> errors = <FlutterErrorDetails>[];
|
||||
final Function(FlutterErrorDetails)? oldHandler = FlutterError.onError;
|
||||
FlutterError.onError = (FlutterErrorDetails error) => errors.add(error);
|
||||
|
||||
final List<int> items = List<int>.generate(20, (int i) => i);
|
||||
await tester.pumpWidget(_buildSliverCrossAxisGroup(
|
||||
slivers: <Widget>[
|
||||
SliverConstrainedCrossAxis(maxExtent: 400, sliver: _buildSliverList(itemMainAxisExtent: 300, items: items, label: (int item) => Text('Group 0 Tile $item'))),
|
||||
SliverConstrainedCrossAxis(maxExtent: 200, sliver: _buildSliverList(itemMainAxisExtent: 200, items: items, label: (int item) => Text('Group 1 Tile $item'))),
|
||||
]),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
FlutterError.onError = oldHandler;
|
||||
expect(errors, isNotEmpty);
|
||||
final AssertionError error = errors.first.exception as AssertionError;
|
||||
expect(
|
||||
error.toString(),
|
||||
contains('SliverCrossAxisGroup ran out of extent before child could be laid out.'),
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('Assertion error when expanded widget runs out of cross axis extent', (WidgetTester tester) async {
|
||||
final List<FlutterErrorDetails> errors = <FlutterErrorDetails>[];
|
||||
final Function(FlutterErrorDetails)? oldHandler = FlutterError.onError;
|
||||
FlutterError.onError = (FlutterErrorDetails error) => errors.add(error);
|
||||
|
||||
final List<int> items = List<int>.generate(20, (int i) => i);
|
||||
await tester.pumpWidget(_buildSliverCrossAxisGroup(
|
||||
slivers: <Widget>[
|
||||
SliverConstrainedCrossAxis(maxExtent: 200, sliver: _buildSliverList(itemMainAxisExtent: 300, items: items, label: (int item) => Text('Group 0 Tile $item'))),
|
||||
SliverConstrainedCrossAxis(maxExtent: 100, sliver: _buildSliverList(itemMainAxisExtent: 200, items: items, label: (int item) => Text('Group 1 Tile $item'))),
|
||||
_buildSliverList(itemMainAxisExtent: 200, items: items, label: (int item) => Text('Group 2 Tile $item')),
|
||||
]),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
FlutterError.onError = oldHandler;
|
||||
expect(errors, isNotEmpty);
|
||||
final AssertionError error = errors.first.exception as AssertionError;
|
||||
expect(
|
||||
error.toString(),
|
||||
contains('SliverCrossAxisGroup ran out of extent before child could be laid out.'),
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('applyPaintTransform is implemented properly', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(_buildSliverCrossAxisGroup(
|
||||
slivers: <Widget>[
|
||||
const SliverToBoxAdapter(child: Text('first box')),
|
||||
const SliverToBoxAdapter(child: Text('second box')),
|
||||
]),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// localToGlobal calculates offset via applyPaintTransform
|
||||
final RenderBox first = tester.renderObject(find.text('first box'));
|
||||
final RenderBox second = tester.renderObject(find.text('second box'));
|
||||
expect(first.localToGlobal(Offset.zero), Offset.zero);
|
||||
expect(second.localToGlobal(Offset.zero), const Offset(VIEWPORT_WIDTH / 2, 0));
|
||||
});
|
||||
}
|
||||
|
||||
Widget _buildSliverList({
|
||||
double itemMainAxisExtent = 100,
|
||||
List<int> items = const <int>[],
|
||||
required Widget Function(int) label,
|
||||
Axis scrollDirection = Axis.vertical,
|
||||
}) {
|
||||
return SliverList(
|
||||
delegate: SliverChildBuilderDelegate(
|
||||
(BuildContext context, int i) {
|
||||
return scrollDirection == Axis.vertical
|
||||
? SizedBox(
|
||||
key: ValueKey<int>(items[i]),
|
||||
height: itemMainAxisExtent,
|
||||
child: label(items[i]),
|
||||
)
|
||||
: SizedBox(
|
||||
key: ValueKey<int>(items[i]),
|
||||
width: itemMainAxisExtent,
|
||||
child: label(items[i]));
|
||||
},
|
||||
findChildIndexCallback: (Key key) {
|
||||
final ValueKey<int> valueKey = key as ValueKey<int>;
|
||||
final int index = items.indexOf(valueKey.value);
|
||||
return index == -1 ? null : index;
|
||||
},
|
||||
childCount: items.length,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSliverCrossAxisGroup({
|
||||
required List<Widget> slivers,
|
||||
ScrollController? controller,
|
||||
double viewportHeight = VIEWPORT_HEIGHT,
|
||||
double viewportWidth = VIEWPORT_WIDTH,
|
||||
Axis scrollDirection = Axis.vertical,
|
||||
bool reverse = false,
|
||||
}) {
|
||||
return Directionality(
|
||||
textDirection: TextDirection.ltr,
|
||||
child: Align(
|
||||
alignment: Alignment.topLeft,
|
||||
child: SizedBox(
|
||||
height: viewportHeight,
|
||||
width: viewportWidth,
|
||||
child: CustomScrollView(
|
||||
scrollDirection: scrollDirection,
|
||||
reverse: reverse,
|
||||
controller: controller,
|
||||
slivers: <Widget>[SliverCrossAxisGroup(slivers: slivers)],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user