diff --git a/packages/flutter/lib/src/material/popup_menu.dart b/packages/flutter/lib/src/material/popup_menu.dart index 07569ae711..493592f5a1 100644 --- a/packages/flutter/lib/src/material/popup_menu.dart +++ b/packages/flutter/lib/src/material/popup_menu.dart @@ -613,7 +613,14 @@ class _PopupMenu extends StatelessWidget { // Positioning of the menu on the screen. class _PopupMenuRouteLayout extends SingleChildLayoutDelegate { - _PopupMenuRouteLayout(this.position, this.itemSizes, this.selectedItemIndex, this.textDirection); + _PopupMenuRouteLayout( + this.position, + this.itemSizes, + this.selectedItemIndex, + this.textDirection, + this.topPadding, + this.bottomPadding, + ); // Rectangle of underlying button, relative to the overlay's dimensions. final RelativeRect position; @@ -629,6 +636,12 @@ class _PopupMenuRouteLayout extends SingleChildLayoutDelegate { // Whether to prefer going to the left or to the right. final TextDirection textDirection; + // Top padding of unsafe area. + final double topPadding; + + // Bottom padding of unsafe area. + final double bottomPadding; + // We put the child wherever position specifies, so long as it will fit within // the specified parent size padded (inset) by 8. If necessary, we adjust the // child's position so that it fits. @@ -637,7 +650,8 @@ class _PopupMenuRouteLayout extends SingleChildLayoutDelegate { BoxConstraints getConstraintsForChild(BoxConstraints constraints) { // The menu can be at most the size of the overlay minus 8.0 pixels in each // direction. - return BoxConstraints.loose(constraints.biggest).deflate(const EdgeInsets.all(_kMenuScreenPadding)); + return BoxConstraints.loose(constraints.biggest).deflate( + const EdgeInsets.all(_kMenuScreenPadding) + EdgeInsets.only(top: topPadding, bottom: bottomPadding)); } @override @@ -646,6 +660,7 @@ class _PopupMenuRouteLayout extends SingleChildLayoutDelegate { // childSize: The size of the menu, when fully open, as determined by // getConstraintsForChild. + final double buttonHeight = size.height - position.top - position.bottom; // Find the ideal vertical position. double y = position.top; if (selectedItemIndex != null && itemSizes != null) { @@ -653,7 +668,7 @@ class _PopupMenuRouteLayout extends SingleChildLayoutDelegate { for (int index = 0; index < selectedItemIndex!; index += 1) selectedItemOffset += itemSizes[index]!.height; selectedItemOffset += itemSizes[selectedItemIndex!]!.height / 2; - y = position.top + (size.height - position.top - position.bottom) / 2.0 - selectedItemOffset; + y = y + buttonHeight / 2.0 - selectedItemOffset; } // Find the ideal horizontal position. @@ -683,10 +698,10 @@ class _PopupMenuRouteLayout extends SingleChildLayoutDelegate { x = _kMenuScreenPadding; else if (x + childSize.width > size.width - _kMenuScreenPadding) x = size.width - childSize.width - _kMenuScreenPadding; - if (y < _kMenuScreenPadding) - y = _kMenuScreenPadding; - else if (y + childSize.height > size.height - _kMenuScreenPadding) - y = size.height - childSize.height - _kMenuScreenPadding; + if (y < _kMenuScreenPadding + topPadding) + y = _kMenuScreenPadding + topPadding; + else if (y + childSize.height > size.height - _kMenuScreenPadding - bottomPadding) + y = size.height - bottomPadding - _kMenuScreenPadding - childSize.height ; return Offset(x, y); } @@ -698,9 +713,11 @@ class _PopupMenuRouteLayout extends SingleChildLayoutDelegate { assert(itemSizes.length == oldDelegate.itemSizes.length); return position != oldDelegate.position - || selectedItemIndex != oldDelegate.selectedItemIndex - || textDirection != oldDelegate.textDirection - || !listEquals(itemSizes, oldDelegate.itemSizes); + || selectedItemIndex != oldDelegate.selectedItemIndex + || textDirection != oldDelegate.textDirection + || !listEquals(itemSizes, oldDelegate.itemSizes) + || topPadding != oldDelegate.topPadding + || bottomPadding != oldDelegate.bottomPadding; } } @@ -761,20 +778,21 @@ class _PopupMenuRoute extends PopupRoute { final Widget menu = _PopupMenu(route: this, semanticLabel: semanticLabel); - return SafeArea( - child: Builder( - builder: (BuildContext context) { - return CustomSingleChildLayout( - delegate: _PopupMenuRouteLayout( - position, - itemSizes, - selectedItemIndex, - Directionality.of(context), - ), - child: capturedThemes.wrap(menu), - ); - }, - ), + return Builder( + builder: (BuildContext context) { + final MediaQueryData mediaQuery = MediaQuery.of(context); + return CustomSingleChildLayout( + delegate: _PopupMenuRouteLayout( + position, + itemSizes, + selectedItemIndex, + Directionality.of(context), + mediaQuery.padding.top, + mediaQuery.padding.bottom, + ), + child: capturedThemes.wrap(menu), + ); + }, ); } } diff --git a/packages/flutter/test/material/popup_menu_test.dart b/packages/flutter/test/material/popup_menu_test.dart index 0117211c35..19349d7dc6 100644 --- a/packages/flutter/test/material/popup_menu_test.dart +++ b/packages/flutter/test/material/popup_menu_test.dart @@ -1963,8 +1963,6 @@ void main() { testWidgets('Vertically long PopupMenu does not overlap with the status bar and bottom notch', (WidgetTester tester) async { const double windowPaddingTop = 44; const double windowPaddingBottom = 34; - final GlobalKey _firstKey = GlobalKey(); - final GlobalKey _lastKey = GlobalKey(); await tester.pumpWidget( MaterialApp( @@ -1987,8 +1985,6 @@ void main() { child: const Text('Show Menu'), itemBuilder: (BuildContext context) => Iterable>.generate( 20, (int i) => PopupMenuItem( - // Set globalKey to the first and last item. - key: i == 0 ? _firstKey : i == 19 ? _lastKey : null, value: i, child: Text('Item $i'), ), @@ -2001,17 +1997,65 @@ void main() { await tester.tap(find.text('Show Menu')); await tester.pumpAndSettle(); - // Check whether the first item is not overlapping with status bar. - expect(tester.getTopLeft(find.byKey(_firstKey)).dy, greaterThan(windowPaddingTop)); + final Offset topRightOfMenu = tester.getTopRight(find.byType(SingleChildScrollView)); + final Offset bottomRightOfMenu = tester.getBottomRight(find.byType(SingleChildScrollView)); - await tester.ensureVisible(find.byKey(_lastKey, skipOffstage: false)); + expect(topRightOfMenu.dy, windowPaddingTop + 8.0); + expect(bottomRightOfMenu.dy, 600.0 - windowPaddingBottom - 8.0); // Screen height is 600. + }); + + testWidgets('PopupMenu position test when have unsafe area', (WidgetTester tester) async { + final GlobalKey buttonKey = GlobalKey(); + + Widget buildFrame(double width, double height) { + return MaterialApp( + builder: (BuildContext context, Widget? child) { + return MediaQuery( + data: const MediaQueryData( + padding: EdgeInsets.only( + top: 32.0, + bottom: 32.0, + ), + ), + child: child!, + ); + }, + home: Scaffold( + appBar: AppBar( + title: const Text('PopupMenu Test'), + actions: [PopupMenuButton( + child: SizedBox( + key: buttonKey, + height: height, + width: width, + child: const ColoredBox( + color: Colors.pink, + ), + ), + itemBuilder: (BuildContext context) => >[ + const PopupMenuItem(child: Text('-1-'), value: 1,), + const PopupMenuItem(child: Text('-2-'), value: 2,), + ], + )], + ), + body: Container(), + ), + ); + } + + await tester.pumpWidget(buildFrame(20.0, 20.0)); + + await tester.tap(find.byKey(buttonKey)); await tester.pumpAndSettle(); - // Check whether the last item is not overlapping with bottom notch. - expect( - tester.getBottomLeft(find.byKey(_lastKey)).dy, - lessThan(600 - windowPaddingBottom), // Device height is 600. - ); + final Offset button = tester.getTopRight(find.byKey(buttonKey)); + expect(button, const Offset(800.0, 32.0)); // The topPadding is 32.0. + + final Offset popupMenu = tester.getTopRight(find.byType(SingleChildScrollView)); + + // The menu should be positioned directly next to the top of the button. + // The 8.0 pixels is [_kMenuScreenPadding]. + expect(popupMenu, Offset(button.dx - 8.0, button.dy + 8.0)); }); group('feedback', () {