Let `OverflowBox` be shrink-wrappable (#129095)
Close https://github.com/flutter/flutter/issues/129094
I have demonstrated how this PR fixes the problem using tests in #129094. I will further add tests in this PR if the PR looks roughly acceptable :)
diff --git a/packages/flutter/lib/src/rendering/shifted_box.dart b/packages/flutter/lib/src/rendering/shifted_box.dart
index dc6de8a..21a5152 100644
--- a/packages/flutter/lib/src/rendering/shifted_box.dart
+++ b/packages/flutter/lib/src/rendering/shifted_box.dart
@@ -533,6 +533,20 @@
}
}
+/// How much space should be occupied by the [OverflowBox] if there is no
+/// overflow.
+enum OverflowBoxFit {
+ /// The widget will size itself to be as large as the parent allows.
+ max,
+
+ /// The widget will follow the child's size.
+ ///
+ /// More specifically, the render object will size itself to match the size of
+ /// its child within the constraints of its parent, or as small as the
+ /// parent allows if no child is set.
+ deferToChild,
+}
+
/// A render object that imposes different constraints on its child than it gets
/// from its parent, possibly allowing the child to overflow the parent.
///
@@ -571,12 +585,14 @@
double? maxWidth,
double? minHeight,
double? maxHeight,
+ OverflowBoxFit fit = OverflowBoxFit.max,
super.alignment,
super.textDirection,
}) : _minWidth = minWidth,
_maxWidth = maxWidth,
_minHeight = minHeight,
- _maxHeight = maxHeight;
+ _maxHeight = maxHeight,
+ _fit = fit;
/// The minimum width constraint to give the child. Set this to null (the
/// default) to use the constraint from the parent instead.
@@ -626,6 +642,24 @@
markNeedsLayout();
}
+ /// The way to size the render object.
+ ///
+ /// This only affects scenario when the child does not indeed overflow.
+ /// If set to [OverflowBoxFit.deferToChild], the render object will size
+ /// itself to match the size of its child within the constraints of its
+ /// parent, or as small as the parent allows if no child is set.
+ /// If set to [OverflowBoxFit.max] (the default), the
+ /// render object will size itself to be as large as the parent allows.
+ OverflowBoxFit get fit => _fit;
+ OverflowBoxFit _fit;
+ set fit(OverflowBoxFit value) {
+ if (_fit == value) {
+ return;
+ }
+ _fit = value;
+ markNeedsLayoutForSizedByParentChange();
+ }
+
BoxConstraints _getInnerConstraints(BoxConstraints constraints) {
return BoxConstraints(
minWidth: _minWidth ?? constraints.minWidth,
@@ -636,19 +670,46 @@
}
@override
- bool get sizedByParent => true;
+ bool get sizedByParent {
+ switch (fit) {
+ case OverflowBoxFit.max:
+ return true;
+ case OverflowBoxFit.deferToChild:
+ // If deferToChild, the size will be as small as its child when non-overflowing,
+ // thus it cannot be sizedByParent.
+ return false;
+ }
+ }
@override
@protected
Size computeDryLayout(covariant BoxConstraints constraints) {
- return constraints.biggest;
+ switch (fit) {
+ case OverflowBoxFit.max:
+ return constraints.biggest;
+ case OverflowBoxFit.deferToChild:
+ return child?.getDryLayout(constraints) ?? constraints.smallest;
+ }
}
@override
void performLayout() {
if (child != null) {
- child?.layout(_getInnerConstraints(constraints), parentUsesSize: true);
+ child!.layout(_getInnerConstraints(constraints), parentUsesSize: true);
+ switch (fit) {
+ case OverflowBoxFit.max:
+ assert(sizedByParent);
+ case OverflowBoxFit.deferToChild:
+ size = constraints.constrain(child!.size);
+ }
alignChild();
+ } else {
+ switch (fit) {
+ case OverflowBoxFit.max:
+ assert(sizedByParent);
+ case OverflowBoxFit.deferToChild:
+ size = constraints.smallest;
+ }
}
}
@@ -659,6 +720,7 @@
properties.add(DoubleProperty('maxWidth', maxWidth, ifNull: 'use parent maxWidth constraint'));
properties.add(DoubleProperty('minHeight', minHeight, ifNull: 'use parent minHeight constraint'));
properties.add(DoubleProperty('maxHeight', maxHeight, ifNull: 'use parent maxHeight constraint'));
+ properties.add(EnumProperty<OverflowBoxFit>('fit', fit));
}
}
diff --git a/packages/flutter/lib/src/widgets/basic.dart b/packages/flutter/lib/src/widgets/basic.dart
index 3e0a894..f565298 100644
--- a/packages/flutter/lib/src/widgets/basic.dart
+++ b/packages/flutter/lib/src/widgets/basic.dart
@@ -3051,6 +3051,7 @@
this.maxWidth,
this.minHeight,
this.maxHeight,
+ this.fit = OverflowBoxFit.max,
super.child,
});
@@ -3090,6 +3091,16 @@
/// default) to use the constraint from the parent instead.
final double? maxHeight;
+ /// The way to size the render object.
+ ///
+ /// This only affects scenario when the child does not indeed overflow.
+ /// If set to [OverflowBoxFit.deferToChild], the render object will size itself to
+ /// match the size of its child within the constraints of its parent or be
+ /// as small as the parent allows if no child is set. If set to
+ /// [OverflowBoxFit.max] (the default), the render object will size itself
+ /// to be as large as the parent allows.
+ final OverflowBoxFit fit;
+
@override
RenderConstrainedOverflowBox createRenderObject(BuildContext context) {
return RenderConstrainedOverflowBox(
@@ -3098,6 +3109,7 @@
maxWidth: maxWidth,
minHeight: minHeight,
maxHeight: maxHeight,
+ fit: fit,
textDirection: Directionality.maybeOf(context),
);
}
@@ -3110,6 +3122,7 @@
..maxWidth = maxWidth
..minHeight = minHeight
..maxHeight = maxHeight
+ ..fit = fit
..textDirection = Directionality.maybeOf(context);
}
@@ -3121,6 +3134,7 @@
properties.add(DoubleProperty('maxWidth', maxWidth, defaultValue: null));
properties.add(DoubleProperty('minHeight', minHeight, defaultValue: null));
properties.add(DoubleProperty('maxHeight', maxHeight, defaultValue: null));
+ properties.add(EnumProperty<OverflowBoxFit>('fit', fit));
}
}
diff --git a/packages/flutter/test/rendering/limited_box_test.dart b/packages/flutter/test/rendering/limited_box_test.dart
index 8d5cea2..cf6b200 100644
--- a/packages/flutter/test/rendering/limited_box_test.dart
+++ b/packages/flutter/test/rendering/limited_box_test.dart
@@ -42,6 +42,7 @@
' │ maxWidth: Infinity\n'
' │ minHeight: 0.0\n'
' │ maxHeight: Infinity\n'
+ ' │ fit: max\n'
' │\n'
' └─child: RenderLimitedBox#00000 relayoutBoundary=up1 NEEDS-PAINT NEEDS-COMPOSITING-BITS-UPDATE\n'
' │ parentData: offset=Offset(350.0, 200.0) (can use size)\n'
@@ -128,6 +129,7 @@
' │ maxWidth: 500.0\n'
' │ minHeight: 0.0\n'
' │ maxHeight: Infinity\n'
+ ' │ fit: max\n'
' │\n'
' └─child: RenderLimitedBox#00000 relayoutBoundary=up1 NEEDS-PAINT\n'
' parentData: offset=Offset(395.0, 300.0) (can use size)\n'
@@ -164,6 +166,7 @@
' │ maxWidth: use parent maxWidth constraint\n'
' │ minHeight: use parent minHeight constraint\n'
' │ maxHeight: use parent maxHeight constraint\n'
+ ' │ fit: max\n'
' │\n'
' └─child: RenderLimitedBox#00000 relayoutBoundary=up1 NEEDS-PAINT\n'
' parentData: offset=Offset(395.0, 0.0) (can use size)\n'
diff --git a/packages/flutter/test/widgets/overflow_box_test.dart b/packages/flutter/test/widgets/overflow_box_test.dart
index bc97f50..1d01316 100644
--- a/packages/flutter/test/widgets/overflow_box_test.dart
+++ b/packages/flutter/test/widgets/overflow_box_test.dart
@@ -31,6 +31,64 @@
expect(box.size, equals(const Size(100.0, 50.0)));
});
+ // Adapted from https://github.com/flutter/flutter/issues/129094
+ group('when fit is OverflowBoxFit.deferToChild', () {
+ group('OverflowBox behavior with long and short content', () {
+ for (final bool contentSuperLong in <bool>[false, true]) {
+ testWidgetsWithLeakTracking('contentSuperLong=$contentSuperLong', (WidgetTester tester) async {
+ final GlobalKey<State<StatefulWidget>> key = GlobalKey();
+
+ final Column child = Column(
+ mainAxisSize: MainAxisSize.min,
+ children: <Widget>[
+ SizedBox(width: 100, height: contentSuperLong ? 10000 : 100),
+ ],
+ );
+
+ await tester.pumpWidget(Directionality(
+ textDirection: TextDirection.ltr,
+ child: Stack(
+ children: <Widget>[
+ Container(
+ key: key,
+ child: OverflowBox(
+ maxHeight: 1000000,
+ fit: OverflowBoxFit.deferToChild,
+ child: child,
+ ),
+ ),
+ ],
+ ),
+ ));
+
+ expect(tester.getBottomLeft(find.byKey(key)).dy, contentSuperLong ? 600 : 100);
+ });
+ }
+ });
+
+ testWidgetsWithLeakTracking('no child', (WidgetTester tester) async {
+ final GlobalKey<State<StatefulWidget>> key = GlobalKey();
+
+ await tester.pumpWidget(Directionality(
+ textDirection: TextDirection.ltr,
+ child: Stack(
+ children: <Widget>[
+ Container(
+ key: key,
+ child: const OverflowBox(
+ maxHeight: 1000000,
+ fit: OverflowBoxFit.deferToChild,
+ // no child
+ ),
+ ),
+ ],
+ ),
+ ));
+
+ expect(tester.getBottomLeft(find.byKey(key)).dy, 0);
+ });
+ });
+
testWidgetsWithLeakTracking('OverflowBox implements debugFillProperties', (WidgetTester tester) async {
final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder();
const OverflowBox(
@@ -48,6 +106,7 @@
'maxWidth: 2.0',
'minHeight: 3.0',
'maxHeight: 4.0',
+ 'fit: max',
]);
});