Update PopupMenuButton widget (#80420)
This commit is contained in:
parent
6bdc380b3d
commit
c6e9d4115c
@ -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> {
|
|||||||
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> {
|
|||||||
|
|
||||||
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> {
|
|||||||
///
|
///
|
||||||
/// `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>({
|
|||||||
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>({
|
|||||||
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 {
|
|||||||
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>> {
|
|||||||
/// 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>> {
|
|||||||
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>> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@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() {
|
|||||||
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 {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user