Unverified Commit c6e9d411 authored by xubaolin's avatar xubaolin Committed by GitHub

Update PopupMenuButton widget (#80420)

parent 6bdc380b
...@@ -732,6 +732,8 @@ class _PopupMenuRoute<T> extends PopupRoute<T> { ...@@ -732,6 +732,8 @@ class _PopupMenuRoute<T> extends PopupRoute<T> {
this.shape, this.shape,
this.color, this.color,
required this.capturedThemes, required this.capturedThemes,
this.menuKey,
this.positionCallback,
}) : itemSizes = List<Size?>.filled(items.length, null); }) : itemSizes = List<Size?>.filled(items.length, null);
final RelativeRect position; final RelativeRect position;
...@@ -743,6 +745,8 @@ class _PopupMenuRoute<T> extends PopupRoute<T> { ...@@ -743,6 +745,8 @@ class _PopupMenuRoute<T> extends PopupRoute<T> {
final ShapeBorder? shape; final ShapeBorder? shape;
final Color? color; final Color? color;
final CapturedThemes capturedThemes; final CapturedThemes capturedThemes;
final Key? menuKey;
final PopupMenuButtonPositionCallback? positionCallback;
@override @override
Animation<double> createAnimation() { Animation<double> createAnimation() {
...@@ -778,12 +782,13 @@ class _PopupMenuRoute<T> extends PopupRoute<T> { ...@@ -778,12 +782,13 @@ class _PopupMenuRoute<T> extends PopupRoute<T> {
final Widget menu = _PopupMenu<T>(route: this, semanticLabel: semanticLabel); final Widget menu = _PopupMenu<T>(route: this, semanticLabel: semanticLabel);
return Builder( return StatefulBuilder(
builder: (BuildContext context) { key: menuKey,
builder: (BuildContext context, StateSetter setState) {
final MediaQueryData mediaQuery = MediaQuery.of(context); final MediaQueryData mediaQuery = MediaQuery.of(context);
return CustomSingleChildLayout( return CustomSingleChildLayout(
delegate: _PopupMenuRouteLayout( delegate: _PopupMenuRouteLayout(
position, positionCallback == null ? position : positionCallback!(),
itemSizes, itemSizes,
selectedItemIndex, selectedItemIndex,
Directionality.of(context), Directionality.of(context),
...@@ -801,6 +806,10 @@ class _PopupMenuRoute<T> extends PopupRoute<T> { ...@@ -801,6 +806,10 @@ class _PopupMenuRoute<T> extends PopupRoute<T> {
/// ///
/// `items` should be non-null and not empty. /// `items` should be non-null and not empty.
/// ///
/// Prefer to use `positionCallback` to obtain position instead of 'position'
/// when `positionCallback` is non-null. In this way, the position of the menu
/// can be recalculated through this callback during the rebuild phase of the menu.
///
/// If `initialValue` is specified then the first item with a matching value /// If `initialValue` is specified then the first item with a matching value
/// will be highlighted and the value of `position` gives the rectangle whose /// will be highlighted and the value of `position` gives the rectangle whose
/// vertical center will be aligned with the vertical center of the highlighted /// vertical center will be aligned with the vertical center of the highlighted
...@@ -862,6 +871,8 @@ Future<T?> showMenu<T>({ ...@@ -862,6 +871,8 @@ Future<T?> showMenu<T>({
ShapeBorder? shape, ShapeBorder? shape,
Color? color, Color? color,
bool useRootNavigator = false, bool useRootNavigator = false,
Key? menuKey,
PopupMenuButtonPositionCallback? positionCallback,
}) { }) {
assert(context != null); assert(context != null);
assert(position != null); assert(position != null);
...@@ -891,6 +902,8 @@ Future<T?> showMenu<T>({ ...@@ -891,6 +902,8 @@ Future<T?> showMenu<T>({
shape: shape, shape: shape,
color: color, color: color,
capturedThemes: InheritedTheme.capture(from: context, to: navigator.context), capturedThemes: InheritedTheme.capture(from: context, to: navigator.context),
menuKey: menuKey,
positionCallback: positionCallback,
)); ));
} }
...@@ -1090,11 +1103,44 @@ class PopupMenuButton<T> extends StatefulWidget { ...@@ -1090,11 +1103,44 @@ class PopupMenuButton<T> extends StatefulWidget {
PopupMenuButtonState<T> createState() => PopupMenuButtonState<T>(); PopupMenuButtonState<T> createState() => PopupMenuButtonState<T>();
} }
/// Signature for the callback used by [showMenu] to obtain the position of the
/// [PopupMenuButton].
///
/// Used by [showMenu].
typedef PopupMenuButtonPositionCallback = RelativeRect Function();
/// The [State] for a [PopupMenuButton]. /// The [State] for a [PopupMenuButton].
/// ///
/// See [showButtonMenu] for a way to programmatically open the popup menu /// See [showButtonMenu] for a way to programmatically open the popup menu
/// of your button state. /// of your button state.
class PopupMenuButtonState<T> extends State<PopupMenuButton<T>> { class PopupMenuButtonState<T> extends State<PopupMenuButton<T>> {
GlobalKey _menuGlobalKey = GlobalKey();
RelativeRect? _buttonPosition;
RelativeRect _getButtonPosition() => _buttonPosition!;
RelativeRect _calculateButtonPosition() {
final RenderBox button = context.findRenderObject()! as RenderBox;
final RenderBox overlay = Navigator.of(context).overlay!.context.findRenderObject()! as RenderBox;
return RelativeRect.fromRect(
Rect.fromPoints(
button.localToGlobal(widget.offset, ancestor: overlay),
button.localToGlobal(button.size.bottomRight(Offset.zero) + widget.offset, ancestor: overlay),
),
Offset.zero & overlay.size,
);
}
void _maybeUpdateMenuPosition() {
WidgetsBinding.instance!.addPostFrameCallback((Duration duration) {
final RelativeRect newPosition = _calculateButtonPosition();
if (newPosition != _buttonPosition) {
_menuGlobalKey.currentState?.setState(() {});
_buttonPosition = newPosition;
}
});
}
/// A method to show a popup menu with the items supplied to /// A method to show a popup menu with the items supplied to
/// [PopupMenuButton.itemBuilder] at the position of your [PopupMenuButton]. /// [PopupMenuButton.itemBuilder] at the position of your [PopupMenuButton].
/// ///
...@@ -1105,16 +1151,12 @@ class PopupMenuButtonState<T> extends State<PopupMenuButton<T>> { ...@@ -1105,16 +1151,12 @@ class PopupMenuButtonState<T> extends State<PopupMenuButton<T>> {
/// show the menu of the button with `globalKey.currentState.showButtonMenu`. /// show the menu of the button with `globalKey.currentState.showButtonMenu`.
void showButtonMenu() { void showButtonMenu() {
final PopupMenuThemeData popupMenuTheme = PopupMenuTheme.of(context); final PopupMenuThemeData popupMenuTheme = PopupMenuTheme.of(context);
final RenderBox button = context.findRenderObject()! as RenderBox;
final RenderBox overlay = Navigator.of(context).overlay!.context.findRenderObject()! as RenderBox;
final RelativeRect position = RelativeRect.fromRect(
Rect.fromPoints(
button.localToGlobal(widget.offset, ancestor: overlay),
button.localToGlobal(button.size.bottomRight(Offset.zero) + widget.offset, ancestor: overlay),
),
Offset.zero & overlay.size,
);
final List<PopupMenuEntry<T>> items = widget.itemBuilder(context); final List<PopupMenuEntry<T>> items = widget.itemBuilder(context);
// It is possible that the fade-out animation of the menu has not finished
// yet, and the key needs to be regenerated at this time, otherwise there will
// be an exception of duplicate GlobalKey.
if (_menuGlobalKey.currentState != null)
_menuGlobalKey = GlobalKey();
// Only show the menu if there is something to show // Only show the menu if there is something to show
if (items.isNotEmpty) { if (items.isNotEmpty) {
showMenu<T?>( showMenu<T?>(
...@@ -1122,9 +1164,11 @@ class PopupMenuButtonState<T> extends State<PopupMenuButton<T>> { ...@@ -1122,9 +1164,11 @@ class PopupMenuButtonState<T> extends State<PopupMenuButton<T>> {
elevation: widget.elevation ?? popupMenuTheme.elevation, elevation: widget.elevation ?? popupMenuTheme.elevation,
items: items, items: items,
initialValue: widget.initialValue, initialValue: widget.initialValue,
position: position, position: _buttonPosition!,
shape: widget.shape ?? popupMenuTheme.shape, shape: widget.shape ?? popupMenuTheme.shape,
color: widget.color ?? popupMenuTheme.color, color: widget.color ?? popupMenuTheme.color,
menuKey: _menuGlobalKey,
positionCallback: _getButtonPosition,
) )
.then<void>((T? newValue) { .then<void>((T? newValue) {
if (!mounted) if (!mounted)
...@@ -1148,6 +1192,18 @@ class PopupMenuButtonState<T> extends State<PopupMenuButton<T>> { ...@@ -1148,6 +1192,18 @@ class PopupMenuButtonState<T> extends State<PopupMenuButton<T>> {
} }
} }
@override
void didUpdateWidget(PopupMenuButton<T> oldWidget) {
_maybeUpdateMenuPosition();
super.didUpdateWidget(oldWidget);
}
@override
void didChangeDependencies() {
_maybeUpdateMenuPosition();
super.didChangeDependencies();
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final bool enableFeedback = widget.enableFeedback final bool enableFeedback = widget.enableFeedback
......
...@@ -2209,6 +2209,87 @@ void main() { ...@@ -2209,6 +2209,87 @@ void main() {
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expect(find.text('foo'), findsOneWidget); expect(find.text('foo'), findsOneWidget);
}); });
testWidgets('The opened menu should follow if the button\'s position changed', (WidgetTester tester) async {
final GlobalKey buttonKey = GlobalKey();
Widget buildFrame(double width, double height) {
return MaterialApp(
home: Scaffold(
body: SizedBox(
height: height,
width: width,
child: Center(
child: PopupMenuButton<int>(
child: SizedBox(
key: buttonKey,
height: 10.0,
width: 10.0,
child: const ColoredBox(
color: Colors.pink,
),
),
itemBuilder: (BuildContext context) => <PopupMenuEntry<int>>[
const PopupMenuItem<int>(child: Text('-1-'), value: 1),
const PopupMenuItem<int>(child: Text('-2-'), value: 2),
],
),
),
),
),
);
}
await tester.pumpWidget(buildFrame(100.0, 100.0));
// Open the menu.
await tester.tap(find.byKey(buttonKey));
await tester.pumpAndSettle();
// +--------+--------+ 100
// | | |
// | | (50,50)|
// +--------+--------+
// | | |
// | | |
// 100 +--------+--------+
//
// The button is a rectangle of 10 * 10 size and is centered,
// so its top-left offset should be (45.0, 45.0).
Offset buttonOffset = tester.getTopLeft(find.byKey(buttonKey));
expect(buttonOffset, const Offset(45.0, 45.0));
// The top-left corner of the menu and button should be aligned.
Offset popupMenuOffset = tester.getTopLeft(find.byType(SingleChildScrollView));
expect(popupMenuOffset, buttonOffset);
// Keep the menu opened and re-layout the screen.
await tester.pumpWidget(buildFrame(200.0, 300.0));
// +-----------+-----------+ 200
// | | |
// | | |
// | | |
// | | |
// | | (100,150) |
// +-----------+-----------+
// | | |
// | | |
// | | |
// | | |
// | | |
// 300 +-----------+-----------+
//
// The button is a rectangle of 10 * 10 size and is centered,
// so its top-left offset should be (95.0, 145.0).
await tester.pump(); // Need a frame to update the menu.
buttonOffset = tester.getTopLeft(find.byKey(buttonKey));
expect(buttonOffset, const Offset(95.0, 145.0));
// The popup menu should follow the button.
popupMenuOffset = tester.getTopLeft(find.byType(SingleChildScrollView));
expect(popupMenuOffset, buttonOffset);
});
} }
class TestApp extends StatefulWidget { class TestApp 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