Skip to content

Commit

Permalink
Sliver Constrained Cross Axis (#125239)
Browse files Browse the repository at this point in the history
Reimplements what we reverted here: #125233.
  • Loading branch information
thkim1011 committed Apr 24, 2023
1 parent 7d9f208 commit 482d1aa
Show file tree
Hide file tree
Showing 6 changed files with 350 additions and 0 deletions.
@@ -0,0 +1,51 @@
// 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 SliverConstrainedCrossAxisExampleApp());

class SliverConstrainedCrossAxisExampleApp extends StatelessWidget {
const SliverConstrainedCrossAxisExampleApp({super.key});

@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(title: const Text('SliverConstrainedCrossAxis Sample')),
body: const SliverConstrainedCrossAxisExample(),
),
);
}
}

class SliverConstrainedCrossAxisExample extends StatelessWidget {
const SliverConstrainedCrossAxisExample({super.key});

@override
Widget build(BuildContext context) {
return CustomScrollView(
slivers: <Widget>[
SliverConstrainedCrossAxis(
maxExtent: 200,
sliver: 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: 10,
),
),
],
);
}
}
@@ -0,0 +1,20 @@
// 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_constrained_cross_axis.0.dart'
as example;
import 'package:flutter_test/flutter_test.dart';

void main() {
testWidgets('SliverConstrainedCrossAxis example', (WidgetTester tester) async {
await tester.pumpWidget(
const example.SliverConstrainedCrossAxisExampleApp(),
);

final RenderSliverList renderSliverList = tester.renderObject(find.byType(SliverList));
expect(renderSliverList.constraints.crossAxisExtent, equals(200));
});
}
41 changes: 41 additions & 0 deletions packages/flutter/lib/src/rendering/proxy_sliver.dart
Expand Up @@ -2,6 +2,7 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'dart:math';
import 'dart:ui' as ui show Color;

import 'package:flutter/animation.dart';
Expand Down Expand Up @@ -414,3 +415,43 @@ class RenderSliverAnimatedOpacity extends RenderProxySliver with RenderAnimatedO
child = sliver;
}
}

/// Applies a cross-axis constraint to its sliver child.
///
/// This render object takes a [maxExtent] parameter and uses the smaller of
/// [maxExtent] and the parent's [SliverConstraints.crossAxisExtent] as the
/// cross axis extent of the [SliverConstraints] passed to the sliver child.
class RenderSliverConstrainedCrossAxis extends RenderProxySliver {
/// Creates a render object that constrains the cross axis extent of its sliver child.
///
/// The [maxExtent] parameter must not be null and must be nonnegative.
RenderSliverConstrainedCrossAxis({
required double maxExtent
}) : _maxExtent = maxExtent,
assert(maxExtent >= 0.0);

/// The cross axis extent to apply to the sliver child.
///
/// This value must be nonnegative.
double get maxExtent => _maxExtent;
double _maxExtent;
set maxExtent(double value) {
if (_maxExtent == value) {
return;
}
_maxExtent = value;
markNeedsLayout();
}

@override
void performLayout() {
assert(child != null);
assert(maxExtent >= 0.0);
child!.layout(
constraints.copyWith(crossAxisExtent: min(_maxExtent, constraints.crossAxisExtent)),
parentUsesSize: true,
);
final SliverGeometry childLayoutGeometry = child!.geometry!;
geometry = childLayoutGeometry.copyWith(crossAxisExtent: min(_maxExtent, constraints.crossAxisExtent));
}
}
50 changes: 50 additions & 0 deletions packages/flutter/lib/src/rendering/sliver.dart
Expand Up @@ -560,6 +560,7 @@ class SliverGeometry with Diagnosticable {
double? layoutExtent,
this.maxPaintExtent = 0.0,
this.maxScrollObstructionExtent = 0.0,
this.crossAxisExtent,
double? hitTestExtent,
bool? visible,
this.hasVisualOverflow = false,
Expand All @@ -571,6 +572,36 @@ class SliverGeometry with Diagnosticable {
cacheExtent = cacheExtent ?? layoutExtent ?? paintExtent,
visible = visible ?? paintExtent > 0.0;

/// Creates a copy of this object but with the given fields replaced with the
/// new values.
SliverGeometry copyWith({
double? scrollExtent,
double? paintExtent,
double? paintOrigin,
double? layoutExtent,
double? maxPaintExtent,
double? maxScrollObstructionExtent,
double? crossAxisExtent,
double? hitTestExtent,
bool? visible,
bool? hasVisualOverflow,
double? cacheExtent,
}) {
return SliverGeometry(
scrollExtent: scrollExtent ?? this.scrollExtent,
paintExtent: paintExtent ?? this.paintExtent,
paintOrigin: paintOrigin ?? this.paintOrigin,
layoutExtent: layoutExtent ?? this.layoutExtent,
maxPaintExtent: maxPaintExtent ?? this.maxPaintExtent,
maxScrollObstructionExtent: maxScrollObstructionExtent ?? this.maxScrollObstructionExtent,
crossAxisExtent: crossAxisExtent ?? this.crossAxisExtent,
hitTestExtent: hitTestExtent ?? this.hitTestExtent,
visible: visible ?? this.visible,
hasVisualOverflow: hasVisualOverflow ?? this.hasVisualOverflow,
cacheExtent: cacheExtent ?? this.cacheExtent,
);
}

/// A sliver that occupies no space at all.
static const SliverGeometry zero = SliverGeometry();

Expand Down Expand Up @@ -719,6 +750,20 @@ class SliverGeometry with Diagnosticable {
/// * [RenderViewport.cacheExtent] for a description of a viewport's cache area.
final double cacheExtent;

/// The amount of space allocated to the cross axis.
///
/// This value will be typically null unless it is different from
/// [SliverConstraints.crossAxisExtent]. If null, then the cross axis extent of
/// the sliver is assumed to be the same as the [SliverConstraints.crossAxisExtent].
/// This is because slivers typically consume all of the extent that is available
/// in the cross axis.
///
/// See also:
///
/// * [SliverConstrainedCrossAxis] for an example of a sliver which takes up
/// a smaller cross axis extent than the provided constraint.
final double? crossAxisExtent;

/// Asserts that this geometry is internally consistent.
///
/// Does nothing if asserts are disabled. Always returns true.
Expand Down Expand Up @@ -957,6 +1002,11 @@ class SliverPhysicalParentData extends ParentData {
/// top left visible corner of the sliver.
Offset paintOffset = Offset.zero;

/// The [crossAxisFlex] factor to use for this sliver child.
///
/// If null or zero, the child is inflexible and determines its own size in the cross axis.
int? crossAxisFlex;

/// Apply the [paintOffset] to the given [transform].
///
/// Used to implement [RenderObject.applyPaintTransform] by slivers that use
Expand Down
96 changes: 96 additions & 0 deletions packages/flutter/lib/src/widgets/sliver.dart
Expand Up @@ -1365,3 +1365,99 @@ class KeepAlive extends ParentDataWidget<KeepAliveParentDataMixin> {
properties.add(DiagnosticsProperty<bool>('keepAlive', keepAlive));
}
}

/// A sliver that constrains the cross axis extent of its sliver child.
///
/// The [SliverConstrainedCrossAxis] takes a [maxExtent] parameter and uses it as
/// the cross axis extent of the [SliverConstraints] passed to the sliver child.
/// The widget ensures that the [maxExtent] is a nonnegative value.
///
/// 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.
///
/// {@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}
class SliverConstrainedCrossAxis extends StatelessWidget {
/// Creates a sliver that constrains the cross axis extent of its sliver child.
///
/// The [maxExtent] parameter is required and must be nonnegative.
const SliverConstrainedCrossAxis({
super.key,
required this.maxExtent,
required this.sliver,
});

/// The cross axis extent to apply to the sliver child.
///
/// This value must be nonnegative.
final double maxExtent;

/// The widget below this widget in the tree.
///
/// Must be a sliver.
final Widget sliver;

@override
Widget build(BuildContext context) {
return _SliverZeroFlexParentDataWidget(
sliver: _SliverConstrainedCrossAxis(
maxExtent: maxExtent,
sliver: sliver,
)
);
}
}
class _SliverZeroFlexParentDataWidget extends ParentDataWidget<SliverPhysicalParentData> {
const _SliverZeroFlexParentDataWidget({
required Widget sliver,
}) : super(child: sliver);

@override
void applyParentData(RenderObject renderObject) {
assert(renderObject.parentData is SliverPhysicalParentData);
final SliverPhysicalParentData parentData = renderObject.parentData! as SliverPhysicalParentData;
bool needsLayout = false;
if (parentData.crossAxisFlex != 0) {
parentData.crossAxisFlex = 0;
needsLayout = true;
}

if (needsLayout) {
final AbstractNode? targetParent = renderObject.parent;
if (targetParent is RenderObject) {
targetParent.markNeedsLayout();
}

}
}

@override
Type get debugTypicalAncestorWidgetClass => Widget;
}

class _SliverConstrainedCrossAxis extends SingleChildRenderObjectWidget {
const _SliverConstrainedCrossAxis({
required this.maxExtent,
required Widget sliver,
}) : assert(maxExtent >= 0.0),
super(child: sliver);

/// The cross axis extent to apply to the sliver child.
///
/// This value must be nonnegative.
final double maxExtent;

@override
RenderSliverConstrainedCrossAxis createRenderObject(BuildContext context) {
return RenderSliverConstrainedCrossAxis(maxExtent: maxExtent);
}

@override
void updateRenderObject(BuildContext context, RenderSliverConstrainedCrossAxis renderObject) {
renderObject.maxExtent = maxExtent;
}
}
@@ -0,0 +1,92 @@
// 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 = 500;
const double VIEWPORT_WIDTH = 300;

void main() {
testWidgets('SliverConstrainedCrossAxis basic test', (WidgetTester tester) async {
await tester.pumpWidget(_buildSliverConstrainedCrossAxis(maxExtent: 50));

final RenderBox box = tester.renderObject(find.byType(Container));
expect(box.size.height, 100);
expect(box.size.width, 50);

final RenderSliver sliver = tester.renderObject(find.byType(SliverToBoxAdapter));
expect(sliver.geometry!.paintExtent, equals(100));
});

testWidgets('SliverConstrainedCrossAxis updates correctly', (WidgetTester tester) async {
await tester.pumpWidget(_buildSliverConstrainedCrossAxis(maxExtent: 50));

final RenderBox box1 = tester.renderObject(find.byType(Container));
expect(box1.size.height, 100);
expect(box1.size.width, 50);

await tester.pumpWidget(_buildSliverConstrainedCrossAxis(maxExtent: 80));

final RenderBox box2 = tester.renderObject(find.byType(Container));
expect(box2.size.height, 100);
expect(box2.size.width, 80);
});

testWidgets('SliverConstrainedCrossAxis uses parent extent if maxExtent is greater', (WidgetTester tester) async {
await tester.pumpWidget(_buildSliverConstrainedCrossAxis(maxExtent: 400));

final RenderBox box = tester.renderObject(find.byType(Container));
expect(box.size.height, 100);
expect(box.size.width, VIEWPORT_WIDTH);
});

testWidgets('SliverConstrainedCrossAxis constrains the height when direction is horizontal', (WidgetTester tester) async {
await tester.pumpWidget(_buildSliverConstrainedCrossAxis(
maxExtent: 50,
scrollDirection: Axis.horizontal,
));

final RenderBox box = tester.renderObject(find.byType(Container));
expect(box.size.height, 50);
});

testWidgets('SliverConstrainedCrossAxis sets its own flex to 0', (WidgetTester tester) async {
await tester.pumpWidget(_buildSliverConstrainedCrossAxis(
maxExtent: 50,
));

final RenderSliver sliver = tester.renderObject(find.byType(SliverConstrainedCrossAxis));
expect((sliver.parentData! as SliverPhysicalParentData).crossAxisFlex, equals(0));
});
}

Widget _buildSliverConstrainedCrossAxis({
required double maxExtent,
Axis scrollDirection = Axis.vertical,
}) {
return Directionality(
textDirection: TextDirection.ltr,
child: Center(
child: SizedBox(
width: VIEWPORT_WIDTH,
height: VIEWPORT_HEIGHT,
child: CustomScrollView(
scrollDirection: scrollDirection,
slivers: <Widget>[
SliverConstrainedCrossAxis(
maxExtent: maxExtent,
sliver: SliverToBoxAdapter(
child: scrollDirection == Axis.vertical
? Container(height: 100)
: Container(width: 100),
),
),
],
),
),
),
);
}

0 comments on commit 482d1aa

Please sign in to comment.