Unverified Commit 1a0b03cb authored by Tae Hyung Kim's avatar Tae Hyung Kim Committed by GitHub

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.
parent 2282eb36
// 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,
),
),
],
),
],
);
}
}
// 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'; ...@@ -61,6 +61,7 @@ export 'src/rendering/sliver.dart';
export 'src/rendering/sliver_fill.dart'; export 'src/rendering/sliver_fill.dart';
export 'src/rendering/sliver_fixed_extent_list.dart'; export 'src/rendering/sliver_fixed_extent_list.dart';
export 'src/rendering/sliver_grid.dart'; export 'src/rendering/sliver_grid.dart';
export 'src/rendering/sliver_group.dart';
export 'src/rendering/sliver_list.dart'; export 'src/rendering/sliver_list.dart';
export 'src/rendering/sliver_multi_box_adaptor.dart'; export 'src/rendering/sliver_multi_box_adaptor.dart';
export 'src/rendering/sliver_padding.dart'; export 'src/rendering/sliver_padding.dart';
......
...@@ -760,8 +760,10 @@ class SliverGeometry with Diagnosticable { ...@@ -760,8 +760,10 @@ class SliverGeometry with Diagnosticable {
/// ///
/// See also: /// See also:
/// ///
/// * [SliverConstrainedCrossAxis] for an example of a sliver which takes up /// * [SliverConstrainedCrossAxis] for an example of a sliver which takes up
/// a smaller cross axis extent than the provided constraint. /// 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; final double? crossAxisExtent;
/// Asserts that this geometry is internally consistent. /// Asserts that this geometry is internally consistent.
...@@ -1004,7 +1006,20 @@ class SliverPhysicalParentData extends ParentData { ...@@ -1004,7 +1006,20 @@ class SliverPhysicalParentData extends ParentData {
/// The [crossAxisFlex] factor to use for this sliver child. /// 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 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; int? crossAxisFlex;
/// Apply the [paintOffset] to the given [transform]. /// Apply the [paintOffset] to the given [transform].
......
// 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> { ...@@ -1375,12 +1375,21 @@ class KeepAlive extends ParentDataWidget<KeepAliveParentDataMixin> {
/// This is useful when you want to apply a custom cross-axis extent constraint /// 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. /// 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} /// {@tool dartpad}
/// In this sample the [SliverConstrainedCrossAxis] sizes its child so that the /// In this sample the [SliverConstrainedCrossAxis] sizes its child so that the
/// cross axis extent takes up less space than the actual viewport. /// 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 ** /// ** See code in examples/api/lib/widgets/sliver/sliver_constrained_cross_axis.0.dart **
/// {@end-tool} /// {@end-tool}
///
/// See also:
///
/// * [SliverCrossAxisGroup], the widget which makes use of 0 flex factor set by
/// this widget.
class SliverConstrainedCrossAxis extends StatelessWidget { class SliverConstrainedCrossAxis extends StatelessWidget {
/// Creates a sliver that constrains the cross axis extent of its sliver child. /// Creates a sliver that constrains the cross axis extent of its sliver child.
/// ///
...@@ -1436,7 +1445,7 @@ class _SliverZeroFlexParentDataWidget extends ParentDataWidget<SliverPhysicalPar ...@@ -1436,7 +1445,7 @@ class _SliverZeroFlexParentDataWidget extends ParentDataWidget<SliverPhysicalPar
} }
@override @override
Type get debugTypicalAncestorWidgetClass => Widget; Type get debugTypicalAncestorWidgetClass => SliverCrossAxisGroup;
} }
class _SliverConstrainedCrossAxis extends SingleChildRenderObjectWidget { class _SliverConstrainedCrossAxis extends SingleChildRenderObjectWidget {
...@@ -1461,3 +1470,107 @@ class _SliverConstrainedCrossAxis extends SingleChildRenderObjectWidget { ...@@ -1461,3 +1470,107 @@ class _SliverConstrainedCrossAxis extends SingleChildRenderObjectWidget {
renderObject.maxExtent = maxExtent; 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();
}
}
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