mirror of
https://github.com/flutter/flutter.git
synced 2025-06-03 00:51:18 +00:00
Sliver Constrained Cross Axis (#125239)
Reimplements what we reverted here: #125233.
This commit is contained in:
parent
7d9f2082f1
commit
482d1aaf13
@ -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));
|
||||||
|
});
|
||||||
|
}
|
@ -2,6 +2,7 @@
|
|||||||
// Use of this source code is governed by a BSD-style license that can be
|
// Use of this source code is governed by a BSD-style license that can be
|
||||||
// found in the LICENSE file.
|
// found in the LICENSE file.
|
||||||
|
|
||||||
|
import 'dart:math';
|
||||||
import 'dart:ui' as ui show Color;
|
import 'dart:ui' as ui show Color;
|
||||||
|
|
||||||
import 'package:flutter/animation.dart';
|
import 'package:flutter/animation.dart';
|
||||||
@ -414,3 +415,43 @@ class RenderSliverAnimatedOpacity extends RenderProxySliver with RenderAnimatedO
|
|||||||
child = sliver;
|
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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -560,6 +560,7 @@ class SliverGeometry with Diagnosticable {
|
|||||||
double? layoutExtent,
|
double? layoutExtent,
|
||||||
this.maxPaintExtent = 0.0,
|
this.maxPaintExtent = 0.0,
|
||||||
this.maxScrollObstructionExtent = 0.0,
|
this.maxScrollObstructionExtent = 0.0,
|
||||||
|
this.crossAxisExtent,
|
||||||
double? hitTestExtent,
|
double? hitTestExtent,
|
||||||
bool? visible,
|
bool? visible,
|
||||||
this.hasVisualOverflow = false,
|
this.hasVisualOverflow = false,
|
||||||
@ -571,6 +572,36 @@ class SliverGeometry with Diagnosticable {
|
|||||||
cacheExtent = cacheExtent ?? layoutExtent ?? paintExtent,
|
cacheExtent = cacheExtent ?? layoutExtent ?? paintExtent,
|
||||||
visible = visible ?? paintExtent > 0.0;
|
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.
|
/// A sliver that occupies no space at all.
|
||||||
static const SliverGeometry zero = SliverGeometry();
|
static const SliverGeometry zero = SliverGeometry();
|
||||||
|
|
||||||
@ -719,6 +750,20 @@ class SliverGeometry with Diagnosticable {
|
|||||||
/// * [RenderViewport.cacheExtent] for a description of a viewport's cache area.
|
/// * [RenderViewport.cacheExtent] for a description of a viewport's cache area.
|
||||||
final double cacheExtent;
|
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.
|
/// Asserts that this geometry is internally consistent.
|
||||||
///
|
///
|
||||||
/// Does nothing if asserts are disabled. Always returns true.
|
/// Does nothing if asserts are disabled. Always returns true.
|
||||||
@ -957,6 +1002,11 @@ class SliverPhysicalParentData extends ParentData {
|
|||||||
/// top left visible corner of the sliver.
|
/// top left visible corner of the sliver.
|
||||||
Offset paintOffset = Offset.zero;
|
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].
|
/// Apply the [paintOffset] to the given [transform].
|
||||||
///
|
///
|
||||||
/// Used to implement [RenderObject.applyPaintTransform] by slivers that use
|
/// Used to implement [RenderObject.applyPaintTransform] by slivers that use
|
||||||
|
@ -1365,3 +1365,99 @@ class KeepAlive extends ParentDataWidget<KeepAliveParentDataMixin> {
|
|||||||
properties.add(DiagnosticsProperty<bool>('keepAlive', keepAlive));
|
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),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user