mirror of
https://github.com/flutter/flutter.git
synced 2026-02-20 02:29:02 +08:00
Allow OverlayPortal.overlayChildLayoutBuilder to choose root Overlay (#174239)
<!-- Thanks for filing a pull request! Reviewers are typically assigned within a week of filing a request. To learn more about code review, see our documentation on Tree Hygiene: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md --> fixes https://github.com/flutter/flutter/issues/168785 ## Pre-launch Checklist - [ ] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [ ] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [ ] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [ ] I signed the [CLA]. - [ ] I listed at least one issue that this PR fixes in the description above. - [ ] I updated/added relevant documentation (doc comments with `///`). - [ ] I added new tests to check the change I am making, or this PR is [test-exempt]. - [ ] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [ ] All existing and new tests are passing. If you need help, consider asking for advice on the #hackers-new channel on [Discord]. **Note**: The Flutter team is currently trialing the use of [Gemini Code Assist for GitHub](https://developers.google.com/gemini-code-assist/docs/review-github-code). Comments from the `gemini-code-assist` bot should not be taken as authoritative feedback from the Flutter team. If you find its comments useful you can update your code accordingly, but if you are unsure or disagree with the feedback, please feel free to wait for a Flutter team member's review for guidance on which automated comments should be addressed. <!-- Links --> [Contributor Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview [Tree Hygiene]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md [test-exempt]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests [Flutter Style Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md [Features we expect every widget to implement]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement [CLA]: https://cla.developers.google.com/ [flutter/tests]: https://github.com/flutter/tests [breaking change policy]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes [Discord]: https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md [Data Driven Fixes]: https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md
This commit is contained in:
parent
ad41861345
commit
ae3eb26237
@ -1703,6 +1703,23 @@ class OverlayPortalController {
|
||||
}
|
||||
}
|
||||
|
||||
/// The location of the [Overlay] that an [OverlayPortal] renders its overlay
|
||||
/// child on.
|
||||
///
|
||||
/// This is typically used in [OverlayPortal].
|
||||
enum OverlayChildLocation {
|
||||
/// The [OverlayPortal] renders its overlay child on the closest ancestor
|
||||
/// [Overlay] above the widget tree.
|
||||
nearestOverlay,
|
||||
|
||||
/// The [OverlayPortal] renders its overlay child on the root [Overlay] above
|
||||
/// the widget tree.
|
||||
///
|
||||
/// In case of multi-view apps, the root [Overlay] refers to the first Overlay
|
||||
/// below the View.
|
||||
rootOverlay,
|
||||
}
|
||||
|
||||
/// A widget that renders its overlay child on an [Overlay].
|
||||
///
|
||||
/// The overlay child is initially hidden until [OverlayPortalController.show]
|
||||
@ -1784,22 +1801,31 @@ class OverlayPortal extends StatefulWidget {
|
||||
/// Creates an [OverlayPortal] that renders the widget [overlayChildBuilder]
|
||||
/// builds on the closest [Overlay] when [OverlayPortalController.show] is
|
||||
/// called.
|
||||
///
|
||||
/// The [overlayLocation] sets which [Overlay] this widget attaches the widget
|
||||
/// returned by [overlayChildBuilder] to. Defaults to
|
||||
/// [OverlayChildLocation.nearestOverlay].
|
||||
const OverlayPortal({
|
||||
super.key,
|
||||
required this.controller,
|
||||
required this.overlayChildBuilder,
|
||||
this.overlayLocation = OverlayChildLocation.nearestOverlay,
|
||||
this.child,
|
||||
}) : _targetRootOverlay = false;
|
||||
});
|
||||
|
||||
/// Creates an [OverlayPortal] that renders the widget [overlayChildBuilder]
|
||||
/// builds on the root [Overlay] when [OverlayPortalController.show] is
|
||||
/// called.
|
||||
@Deprecated(
|
||||
'Use OverlayPortal with root overlay instead. '
|
||||
'This feature was deprecated after v3.33.0-0.0.pre.',
|
||||
)
|
||||
const OverlayPortal.targetsRootOverlay({
|
||||
super.key,
|
||||
required this.controller,
|
||||
required this.overlayChildBuilder,
|
||||
this.child,
|
||||
}) : _targetRootOverlay = true;
|
||||
}) : overlayLocation = OverlayChildLocation.rootOverlay;
|
||||
|
||||
/// Creates an [OverlayPortal] that renders the widget `overlayChildBuilder`
|
||||
/// builds on the closest [Overlay] when [OverlayPortalController.show] is
|
||||
@ -1822,16 +1848,22 @@ class OverlayPortal extends StatefulWidget {
|
||||
/// [CompositedTransformFollower] between the [OverlayPortal] and the [Overlay]
|
||||
/// may resulting in an incorrect child paint transform being provided to the
|
||||
/// `overlayChildBuilder` and will cause an assertion in debug mode.
|
||||
///
|
||||
/// The [overlayLocation] sets which [Overlay] this widget attaches the widget
|
||||
/// returned by `overlayChildBuilder` to. Defaults to
|
||||
/// [OverlayChildLocation.nearestOverlay].
|
||||
OverlayPortal.overlayChildLayoutBuilder({
|
||||
Key? key,
|
||||
required OverlayPortalController controller,
|
||||
required OverlayChildLayoutBuilder overlayChildBuilder,
|
||||
OverlayChildLocation overlayLocation = OverlayChildLocation.nearestOverlay,
|
||||
required Widget? child,
|
||||
}) : this(
|
||||
key: key,
|
||||
controller: controller,
|
||||
overlayChildBuilder: (_) => _OverlayChildLayoutBuilder(builder: overlayChildBuilder),
|
||||
child: child,
|
||||
overlayLocation: overlayLocation,
|
||||
);
|
||||
|
||||
/// The controller to show, hide and bring to top the overlay child.
|
||||
@ -1858,7 +1890,9 @@ class OverlayPortal extends StatefulWidget {
|
||||
/// A widget below this widget in the tree.
|
||||
final Widget? child;
|
||||
|
||||
final bool _targetRootOverlay;
|
||||
/// The [Overlay] that the widget returns from [overlayChildBuilder] is
|
||||
/// attached to.
|
||||
final OverlayChildLocation overlayLocation;
|
||||
|
||||
@override
|
||||
State<OverlayPortal> createState() => _OverlayPortalState();
|
||||
@ -1883,11 +1917,11 @@ class _OverlayPortalState extends State<OverlayPortal> {
|
||||
locationCache._theater == marker.theater;
|
||||
}
|
||||
|
||||
_OverlayEntryLocation _getLocation(int zOrderIndex, bool targetRootOverlay) {
|
||||
_OverlayEntryLocation _getLocation(int zOrderIndex, OverlayChildLocation overlayLocation) {
|
||||
final _OverlayEntryLocation? cachedLocation = _locationCache;
|
||||
late final _RenderTheaterMarker marker = _RenderTheaterMarker.of(
|
||||
context,
|
||||
targetRootOverlay: targetRootOverlay,
|
||||
targetRootOverlay: overlayLocation == OverlayChildLocation.rootOverlay,
|
||||
);
|
||||
final bool isCacheValid =
|
||||
cachedLocation != null &&
|
||||
@ -1941,7 +1975,7 @@ class _OverlayPortalState extends State<OverlayPortal> {
|
||||
void didUpdateWidget(OverlayPortal oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
_childModelMayHaveChanged =
|
||||
_childModelMayHaveChanged || oldWidget._targetRootOverlay != widget._targetRootOverlay;
|
||||
_childModelMayHaveChanged || oldWidget.overlayLocation != widget.overlayLocation;
|
||||
if (oldWidget.controller != widget.controller) {
|
||||
oldWidget.controller._attachTarget = null;
|
||||
_setupController(widget.controller);
|
||||
@ -1990,7 +2024,7 @@ class _OverlayPortalState extends State<OverlayPortal> {
|
||||
return _OverlayPortal(overlayLocation: null, overlayChild: null, child: widget.child);
|
||||
}
|
||||
return _OverlayPortal(
|
||||
overlayLocation: _getLocation(zOrderIndex, widget._targetRootOverlay),
|
||||
overlayLocation: _getLocation(zOrderIndex, widget.overlayLocation),
|
||||
overlayChild: _DeferredLayout(child: Builder(builder: widget.overlayChildBuilder)),
|
||||
child: widget.child,
|
||||
);
|
||||
|
||||
@ -823,19 +823,14 @@ class _RawMenuAnchorState extends State<RawMenuAnchor> with _RawMenuAnchorBaseMi
|
||||
),
|
||||
);
|
||||
|
||||
if (useRootOverlay) {
|
||||
return OverlayPortal.targetsRootOverlay(
|
||||
controller: _overlayController,
|
||||
overlayChildBuilder: _buildOverlay,
|
||||
child: child,
|
||||
);
|
||||
} else {
|
||||
return OverlayPortal(
|
||||
controller: _overlayController,
|
||||
overlayChildBuilder: _buildOverlay,
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
return OverlayPortal(
|
||||
controller: _overlayController,
|
||||
overlayChildBuilder: _buildOverlay,
|
||||
overlayLocation: useRootOverlay
|
||||
? OverlayChildLocation.rootOverlay
|
||||
: OverlayChildLocation.nearestOverlay,
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
@ -8,6 +8,7 @@ import 'package:flutter/widgets.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart';
|
||||
|
||||
import 'multi_view_testing.dart';
|
||||
import 'semantics_tester.dart';
|
||||
|
||||
class _ManyRelayoutBoundaries extends StatelessWidget {
|
||||
@ -1242,13 +1243,7 @@ void main() {
|
||||
);
|
||||
final _RenderLayoutCounter overlayLayoutCounter = _RenderLayoutCounter();
|
||||
int layoutCount = 0;
|
||||
OverlayPortal Function({
|
||||
Widget? child,
|
||||
required OverlayPortalController controller,
|
||||
Key? key,
|
||||
required WidgetBuilder overlayChildBuilder,
|
||||
})
|
||||
constructorToUse = OverlayPortal.new;
|
||||
OverlayChildLocation location = OverlayChildLocation.nearestOverlay;
|
||||
late StateSetter setState;
|
||||
|
||||
// This tree has 3 nested Overlays.
|
||||
@ -1278,9 +1273,10 @@ void main() {
|
||||
return Center(
|
||||
child: Builder(
|
||||
builder: (BuildContext context) {
|
||||
return constructorToUse(
|
||||
return OverlayPortal(
|
||||
key: widgetKey,
|
||||
controller: controller1,
|
||||
overlayLocation: location,
|
||||
overlayChildBuilder: (BuildContext context) {
|
||||
return LayoutBuilder(
|
||||
builder:
|
||||
@ -1331,7 +1327,7 @@ void main() {
|
||||
|
||||
// Now targets the root overlay.
|
||||
setState(() {
|
||||
constructorToUse = OverlayPortal.targetsRootOverlay;
|
||||
location = OverlayChildLocation.rootOverlay;
|
||||
});
|
||||
await tester.pump();
|
||||
|
||||
@ -1344,6 +1340,198 @@ void main() {
|
||||
verifyTreeIsClean();
|
||||
});
|
||||
|
||||
testWidgets('Listens to overlay changes', (WidgetTester tester) async {
|
||||
// Use global key to ensure `OverlayCatcher` will be reparented instead
|
||||
// of destroyed when Overlay gets swapped.
|
||||
const GlobalObjectKey container = GlobalObjectKey('container');
|
||||
final OverlayPortalController controller1 = OverlayPortalController();
|
||||
final UniqueKey overlayPortal = UniqueKey();
|
||||
final Widget overlayBody = SizedBox(
|
||||
width: 100,
|
||||
height: 100,
|
||||
child: OverlayPortal(
|
||||
controller: controller1,
|
||||
overlayChildBuilder: (BuildContext context) => Placeholder(key: overlayPortal),
|
||||
),
|
||||
);
|
||||
|
||||
final OverlayEntry overlayEntry1 = OverlayEntry(
|
||||
builder: (BuildContext context) {
|
||||
return Container(key: container, child: overlayBody);
|
||||
},
|
||||
);
|
||||
addTearDown(
|
||||
() => overlayEntry1
|
||||
..remove()
|
||||
..dispose(),
|
||||
);
|
||||
final OverlayEntry overlayEntry2 = OverlayEntry(
|
||||
builder: (BuildContext context) {
|
||||
return Container(key: container, child: overlayBody);
|
||||
},
|
||||
);
|
||||
addTearDown(
|
||||
() => overlayEntry2
|
||||
..remove()
|
||||
..dispose(),
|
||||
);
|
||||
|
||||
await tester.pumpWidget(
|
||||
Directionality(
|
||||
textDirection: TextDirection.ltr,
|
||||
child: OverlaySwaps(overlayEntry1, overlayEntry2),
|
||||
),
|
||||
);
|
||||
|
||||
controller1.show();
|
||||
await tester.pump();
|
||||
|
||||
final RenderObject parentTheater = _ancestorRenderTheaters(
|
||||
tester.renderObject(find.byKey(overlayPortal)),
|
||||
).single;
|
||||
|
||||
final OverlaySwapsState swaps = tester.state<OverlaySwapsState>(find.byType(OverlaySwaps));
|
||||
swaps.swaps();
|
||||
await tester.pump();
|
||||
|
||||
final RenderObject newParentTheater = _ancestorRenderTheaters(
|
||||
tester.renderObject(find.byKey(overlayPortal)),
|
||||
).single;
|
||||
expect(parentTheater, isNot(newParentTheater));
|
||||
});
|
||||
|
||||
testWidgets('Listens to root overlay changes', (WidgetTester tester) async {
|
||||
final GlobalKey<OverlayState> oldRoot = GlobalKey<OverlayState>();
|
||||
final GlobalKey<OverlayState> newRoot = GlobalKey<OverlayState>();
|
||||
final UniqueKey overlayPortal = UniqueKey();
|
||||
final Widget overlayBody = SizedBox(
|
||||
width: 100,
|
||||
height: 100,
|
||||
child: OverlayPortal(
|
||||
controller: controller1,
|
||||
overlayLocation: OverlayChildLocation.rootOverlay,
|
||||
overlayChildBuilder: (BuildContext context) => Placeholder(key: overlayPortal),
|
||||
),
|
||||
);
|
||||
|
||||
final OverlayEntry innerEntry = OverlayEntry(
|
||||
builder: (BuildContext context) {
|
||||
return Container(child: overlayBody);
|
||||
},
|
||||
);
|
||||
addTearDown(
|
||||
() => innerEntry
|
||||
..remove()
|
||||
..dispose(),
|
||||
);
|
||||
|
||||
final OverlayEntry midEntry = OverlayEntry(
|
||||
builder: (BuildContext context) {
|
||||
return Overlay(initialEntries: <OverlayEntry>[innerEntry]);
|
||||
},
|
||||
);
|
||||
addTearDown(
|
||||
() => midEntry
|
||||
..remove()
|
||||
..dispose(),
|
||||
);
|
||||
|
||||
await tester.pumpWidget(
|
||||
Directionality(
|
||||
textDirection: TextDirection.ltr,
|
||||
child: Overlay(key: oldRoot, initialEntries: <OverlayEntry>[midEntry]),
|
||||
),
|
||||
);
|
||||
|
||||
RenderObject parentTheater = _ancestorRenderTheaters(
|
||||
tester.renderObject(find.byKey(overlayPortal)),
|
||||
).single;
|
||||
expect(parentTheater, oldRoot.currentContext?.findRenderObject());
|
||||
|
||||
final OverlayEntry outerEntry = OverlayEntry(
|
||||
builder: (BuildContext context) {
|
||||
return Overlay(key: oldRoot, initialEntries: <OverlayEntry>[midEntry]);
|
||||
},
|
||||
);
|
||||
addTearDown(
|
||||
() => outerEntry
|
||||
..remove()
|
||||
..dispose(),
|
||||
);
|
||||
|
||||
await tester.pumpWidget(
|
||||
Directionality(
|
||||
textDirection: TextDirection.ltr,
|
||||
child: Overlay(
|
||||
// Add a new root.
|
||||
key: newRoot,
|
||||
initialEntries: <OverlayEntry>[outerEntry],
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
parentTheater = _ancestorRenderTheaters(tester.renderObject(find.byKey(overlayPortal))).single;
|
||||
expect(parentTheater, newRoot.currentContext?.findRenderObject());
|
||||
});
|
||||
|
||||
testWidgets('Root location uses view boundary', (WidgetTester tester) async {
|
||||
final UniqueKey overlayPortal = UniqueKey();
|
||||
final GlobalKey<OverlayState> outer = GlobalKey<OverlayState>();
|
||||
final GlobalKey<OverlayState> inner = GlobalKey<OverlayState>();
|
||||
final Widget overlayBody = SizedBox(
|
||||
width: 100,
|
||||
height: 100,
|
||||
child: OverlayPortal(
|
||||
controller: controller1,
|
||||
overlayLocation: OverlayChildLocation.rootOverlay,
|
||||
overlayChildBuilder: (BuildContext context) => Placeholder(key: overlayPortal),
|
||||
),
|
||||
);
|
||||
|
||||
final OverlayEntry innerEntry = OverlayEntry(
|
||||
builder: (BuildContext context) {
|
||||
return overlayBody;
|
||||
},
|
||||
);
|
||||
addTearDown(
|
||||
() => innerEntry
|
||||
..remove()
|
||||
..dispose(),
|
||||
);
|
||||
|
||||
final OverlayEntry outerEntry = OverlayEntry(
|
||||
builder: (BuildContext context) {
|
||||
return ViewAnchor(
|
||||
view: View(
|
||||
view: FakeView(tester.view),
|
||||
child: Overlay(key: inner, initialEntries: <OverlayEntry>[innerEntry]),
|
||||
),
|
||||
child: const Placeholder(),
|
||||
);
|
||||
},
|
||||
);
|
||||
addTearDown(
|
||||
() => outerEntry
|
||||
..remove()
|
||||
..dispose(),
|
||||
);
|
||||
|
||||
await tester.pumpWidget(
|
||||
Directionality(
|
||||
textDirection: TextDirection.ltr,
|
||||
child: Overlay(key: outer, initialEntries: <OverlayEntry>[outerEntry]),
|
||||
),
|
||||
);
|
||||
|
||||
controller1.show();
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(
|
||||
_ancestorRenderTheaters(tester.renderObject(find.byKey(overlayPortal))).single,
|
||||
inner.currentContext?.findRenderObject(),
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('PortalController can be assigned to another after deactivate', (
|
||||
WidgetTester tester,
|
||||
) async {
|
||||
@ -3037,3 +3225,36 @@ class _RenderPaintRecorder extends RenderProxyBox {
|
||||
super.paint(context, offset);
|
||||
}
|
||||
}
|
||||
|
||||
class OverlaySwaps extends StatefulWidget {
|
||||
const OverlaySwaps(this.entry, this.entryAfterSwap, {super.key});
|
||||
|
||||
final OverlayEntry entry;
|
||||
final OverlayEntry entryAfterSwap;
|
||||
@override
|
||||
State<StatefulWidget> createState() => OverlaySwapsState();
|
||||
}
|
||||
|
||||
class OverlaySwapsState extends State<OverlaySwaps> {
|
||||
late UniqueKey overlayKey;
|
||||
late OverlayEntry entry;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
overlayKey = UniqueKey();
|
||||
entry = widget.entry;
|
||||
}
|
||||
|
||||
void swaps() {
|
||||
setState(() {
|
||||
overlayKey = UniqueKey();
|
||||
entry = widget.entryAfterSwap;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Overlay(key: overlayKey, initialEntries: <OverlayEntry>[entry]);
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user