Unverified Commit 41622725 authored by Michael Goderbauer's avatar Michael Goderbauer Committed by GitHub

Overlay supports unconstrained environments (#139513)

Fixes https://github.com/flutter/flutter/issues/137875.

Unfortunately, we cannot auto-detect which OverlayEntry should be sizing the Overlay in unconstrained environment. So, this PR adds a special flag to annotate the Overlay Entry that should be used.
parent cea2726b
......@@ -82,6 +82,7 @@ class OverlayEntry implements Listenable {
required this.builder,
bool opaque = false,
bool maintainState = false,
this.canSizeOverlay = false,
}) : _opaque = opaque,
_maintainState = maintainState {
if (kFlutterMemoryAllocationsEnabled) {
......@@ -138,6 +139,25 @@ class OverlayEntry implements Listenable {
_overlay!._didChangeEntryOpacity();
}
/// Whether the content of this [OverlayEntry] can be used to size the
/// [Overlay].
///
/// In most situations the overlay sizes itself based on its incoming
/// constraints to be as large as possible. However, if that would result in
/// an infinite size, it has to rely on one of its children to size itself. In
/// this situation, the overlay will consult the topmost non-[Positioned]
/// overlay entry that has this property set to true, lay it out with the
/// incoming [BoxConstraints] of the overlay, and force all other
/// non-[Positioned] overlay entries to have the same size. The [Positioned]
/// entries are laid out as usual based on the calculated size of the overlay.
///
/// Overlay entries that set this to true must be able to handle unconstrained
/// [BoxConstraints].
///
/// Setting this to true has no effect if the overlay entry uses a [Positioned]
/// widget to position itself in the overlay.
final bool canSizeOverlay;
/// Whether the [OverlayEntry] is currently mounted in the widget tree.
///
/// The [OverlayEntry] notifies its listeners when this value changes.
......@@ -830,6 +850,7 @@ class _WrappingOverlay extends StatefulWidget {
class _WrappingOverlayState extends State<_WrappingOverlay> {
late final OverlayEntry _entry = OverlayEntry(
canSizeOverlay: true,
opaque: true,
builder: (BuildContext context) {
return widget.child;
......@@ -940,28 +961,17 @@ mixin _RenderTheaterMixin on RenderBox {
}
}
@override
bool get sizedByParent => true;
@override
void performLayout() {
final Iterator<RenderBox> iterator = _childrenInPaintOrder().iterator;
// Same BoxConstraints as used by RenderStack for StackFit.expand.
final BoxConstraints nonPositionedChildConstraints = BoxConstraints.tight(constraints.biggest);
void layoutChild(RenderBox child, BoxConstraints nonPositionedChildConstraints) {
final StackParentData childParentData = child.parentData! as StackParentData;
final Alignment alignment = theater._resolvedAlignment;
while (iterator.moveNext()) {
final RenderBox child = iterator.current;
final StackParentData childParentData = child.parentData! as StackParentData;
if (!childParentData.isPositioned) {
child.layout(nonPositionedChildConstraints, parentUsesSize: true);
childParentData.offset = alignment.alongOffset(size - child.size as Offset);
} else {
assert(child is! _RenderDeferredLayoutBox, 'all _RenderDeferredLayoutBoxes must be non-positioned children.');
RenderStack.layoutPositionedChild(child, childParentData, size, alignment);
}
assert(child.parentData == childParentData);
if (!childParentData.isPositioned) {
child.layout(nonPositionedChildConstraints, parentUsesSize: true);
childParentData.offset = Offset.zero;
} else {
assert(child is! _RenderDeferredLayoutBox, 'all _RenderDeferredLayoutBoxes must be non-positioned children.');
RenderStack.layoutPositionedChild(child, childParentData, size, alignment);
}
assert(child.parentData == childParentData);
}
@override
......@@ -1208,8 +1218,10 @@ class _RenderTheater extends RenderBox with ContainerRenderObjectMixin<RenderBox
@override
Size computeDryLayout(BoxConstraints constraints) {
assert(constraints.biggest.isFinite);
return constraints.biggest;
if (constraints.biggest.isFinite) {
return constraints.biggest;
}
return _findSizeDeterminingChild().getDryLayout(constraints);
}
@override
......@@ -1249,6 +1261,53 @@ class _RenderTheater extends RenderBox with ContainerRenderObjectMixin<RenderBox
}
}
@override
bool get sizedByParent => false;
@override
void performLayout() {
RenderBox? sizeDeterminingChild;
if (constraints.biggest.isFinite) {
size = constraints.biggest;
} else {
sizeDeterminingChild = _findSizeDeterminingChild();
layoutChild(sizeDeterminingChild, constraints);
size = sizeDeterminingChild.size;
}
// Equivalent to BoxConstraints used by RenderStack for StackFit.expand.
final BoxConstraints nonPositionedChildConstraints = BoxConstraints.tight(size);
for (final RenderBox child in _childrenInPaintOrder()) {
if (child != sizeDeterminingChild) {
layoutChild(child, nonPositionedChildConstraints);
}
}
}
RenderBox _findSizeDeterminingChild() {
RenderBox? child = _lastOnstageChild;
while (child != null) {
final _TheaterParentData childParentData = child.parentData! as _TheaterParentData;
if ((childParentData.overlayEntry?.canSizeOverlay ?? false) && !childParentData.isPositioned) {
return child;
}
child = childParentData.previousSibling;
}
throw FlutterError.fromParts(<DiagnosticsNode>[
ErrorSummary('Overlay was given infinite constraints and cannot be sized by a suitable child.'),
ErrorDescription(
'The constraints given to the overlay ($constraints) would result in an illegal '
'infinite size (${constraints.biggest}). To avoid that, the Overlay tried to size '
'itself to one of its children, but no suitable non-positioned child that belongs to an '
'OverlayEntry with canSizeOverlay set to true could be found.',
),
ErrorHint(
'Try wrapping the Overlay in a SizedBox to give it a finite size or '
'use an OverlayEntry with canSizeOverlay set to true.',
),
]);
}
final LayerHandle<ClipRectLayer> _clipRectLayer = LayerHandle<ClipRectLayer>();
@override
......@@ -2185,6 +2244,9 @@ final class _RenderDeferredLayoutBox extends RenderProxyBox with _RenderTheaterM
_callingMarkParentNeedsLayout = false;
}
@override
bool get sizedByParent => true;
bool _needsLayout = true;
@override
void markNeedsLayout() {
......@@ -2254,7 +2316,8 @@ final class _RenderDeferredLayoutBox extends RenderProxyBox with _RenderTheaterM
_needsLayout = false;
return;
}
super.performLayout();
assert(constraints.isTight);
layoutChild(child, constraints);
assert(() {
_debugMutationsLocked = false;
return true;
......
......@@ -1847,7 +1847,7 @@ abstract class ModalRoute<T> extends TransitionRoute<T> with LocalHistoryRoute<T
Iterable<OverlayEntry> createOverlayEntries() {
return <OverlayEntry>[
_modalBarrier = OverlayEntry(builder: _buildModalBarrier),
_modalScope = OverlayEntry(builder: _buildModalScope, maintainState: maintainState),
_modalScope = OverlayEntry(builder: _buildModalScope, maintainState: maintainState, canSizeOverlay: opaque),
];
}
......
......@@ -1602,6 +1602,19 @@ void main() {
expect(find.byType(AnimatedTheme), findsNothing);
expect(find.byType(Theme), findsOneWidget);
});
// Regression test for https://github.com/flutter/flutter/issues/137875.
testWidgets('MaterialApp works in an unconstrained environment', (WidgetTester tester) async {
await tester.pumpWidget(
const UnconstrainedBox(
child: MaterialApp(
home: SizedBox(width: 123, height: 456),
),
),
);
expect(tester.getSize(find.byType(MaterialApp)), const Size(123, 456));
});
}
class MockScrollBehavior extends ScrollBehavior {
......
......@@ -1593,6 +1593,177 @@ void main() {
expect(find.text('Bye, bye'), findsOneWidget);
expect(tester.state(find.byType(Overlay)), same(overlayState));
});
testWidgets('Overlay.wrap is sized by child in an unconstrained environment', (WidgetTester tester) async {
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: UnconstrainedBox(
child: Overlay.wrap(
child: const Center(
child: SizedBox(
width: 123,
height: 456,
)
),
),
),
),
);
expect(tester.getSize(find.byType(Overlay)), const Size(123, 456));
});
testWidgets('Overlay is sized by child in an unconstrained environment', (WidgetTester tester) async {
final OverlayEntry initialEntry = OverlayEntry(
opaque: true,
canSizeOverlay: true,
builder: (BuildContext context) {
return const SizedBox(width: 123, height: 456);
}
);
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: UnconstrainedBox(
child: Overlay(
initialEntries: <OverlayEntry>[initialEntry]
),
),
),
);
expect(tester.getSize(find.byType(Overlay)), const Size(123, 456));
final OverlayState overlay = tester.state<OverlayState>(find.byType(Overlay));
final OverlayEntry nonSizingEntry = OverlayEntry(
builder: (BuildContext context) {
return const SizedBox(
width: 600,
height: 600,
child: Center(child: Text('Hello')),
);
},
);
overlay.insert(nonSizingEntry);
await tester.pump();
expect(tester.getSize(find.byType(Overlay)), const Size(123, 456));
expect(find.text('Hello'), findsOneWidget);
final OverlayEntry sizingEntry = OverlayEntry(
canSizeOverlay: true,
builder: (BuildContext context) {
return const SizedBox(
width: 222,
height: 111,
child: Center(child: Text('World')),
);
},
);
overlay.insert(sizingEntry);
await tester.pump();
expect(tester.getSize(find.byType(Overlay)), const Size(222, 111));
expect(find.text('Hello'), findsOneWidget);
expect(find.text('World'), findsOneWidget);
nonSizingEntry.remove();
await tester.pump();
expect(tester.getSize(find.byType(Overlay)), const Size(222, 111));
expect(find.text('Hello'), findsNothing);
expect(find.text('World'), findsOneWidget);
sizingEntry.remove();
await tester.pump();
expect(tester.getSize(find.byType(Overlay)), const Size(123, 456));
expect(find.text('Hello'), findsNothing);
expect(find.text('World'), findsNothing);
});
testWidgets('Overlay throws if unconstrained and has no child', (WidgetTester tester) async {
final List<FlutterErrorDetails> errors = <FlutterErrorDetails>[];
final FlutterExceptionHandler? oldHandler = FlutterError.onError;
FlutterError.onError = errors.add;
await tester.pumpWidget(
const Directionality(
textDirection: TextDirection.ltr,
child: UnconstrainedBox(
child: Overlay(),
),
),
);
FlutterError.onError = oldHandler;
expect(
errors.first.toString().replaceAll('\n', ' '),
contains('Overlay was given infinite constraints and cannot be sized by a suitable child.'),
);
});
testWidgets('Overlay throws if unconstrained and only positioned child', (WidgetTester tester) async {
final List<FlutterErrorDetails> errors = <FlutterErrorDetails>[];
final FlutterExceptionHandler? oldHandler = FlutterError.onError;
FlutterError.onError = errors.add;
final OverlayEntry entry = OverlayEntry(
canSizeOverlay: true,
builder: (BuildContext context) {
return const Positioned(
top: 100,
child: SizedBox(width: 600, height: 600),
);
},
);
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: UnconstrainedBox(
child: Overlay(
initialEntries: <OverlayEntry>[entry],
),
),
),
);
FlutterError.onError = oldHandler;
expect(
errors.first.toString().replaceAll('\n', ' '),
contains('Overlay was given infinite constraints and cannot be sized by a suitable child.'),
);
});
testWidgets('Overlay throws if unconstrained and no canSizeOverlay child', (WidgetTester tester) async {
final List<FlutterErrorDetails> errors = <FlutterErrorDetails>[];
final FlutterExceptionHandler? oldHandler = FlutterError.onError;
FlutterError.onError = errors.add;
final OverlayEntry entry = OverlayEntry(
builder: (BuildContext context) {
return const SizedBox(width: 600, height: 600);
},
);
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: UnconstrainedBox(
child: Overlay(
initialEntries: <OverlayEntry>[entry],
),
),
),
);
FlutterError.onError = oldHandler;
expect(
errors.first.toString().replaceAll('\n', ' '),
contains('Overlay was given infinite constraints and cannot be sized by a suitable child.'),
);
});
}
class StatefulTestWidget extends StatefulWidget {
......
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