From 751f119c93e5fa3ce727ac93b6f38b56a1bee94b Mon Sep 17 00:00:00 2001 From: Hans Muller Date: Mon, 25 Feb 2019 10:58:17 -0800 Subject: [PATCH] Ensure that the DropdownButton menu respects its parents bounds (#28371) --- .../flutter/lib/src/material/dropdown.dart | 67 ++++++++++++++++--- .../flutter/test/material/dropdown_test.dart | 45 +++++++++++++ 2 files changed, 101 insertions(+), 11 deletions(-) diff --git a/packages/flutter/lib/src/material/dropdown.dart b/packages/flutter/lib/src/material/dropdown.dart index d3caf96810..f1c4f15562 100644 --- a/packages/flutter/lib/src/material/dropdown.dart +++ b/packages/flutter/lib/src/material/dropdown.dart @@ -327,19 +327,67 @@ class _DropdownRoute extends PopupRoute<_DropdownRouteResult> { @override Widget buildPage(BuildContext context, Animation animation, Animation secondaryAnimation) { + return LayoutBuilder( + builder: (BuildContext context, BoxConstraints constraints) { + return _DropdownRoutePage( + route: this, + constraints: constraints, + items: items, + padding: padding, + buttonRect: buttonRect, + selectedIndex: selectedIndex, + elevation: elevation, + theme: theme, + style: style, + ); + } + ); + } + + void _dismiss() { + navigator?.removeRoute(this); + } +} + +class _DropdownRoutePage extends StatelessWidget { + const _DropdownRoutePage({ + Key key, + this.route, + this.constraints, + this.items, + this.padding, + this.buttonRect, + this.selectedIndex, + this.elevation = 8, + this.theme, + this.style, + }) : super(key: key); + + final _DropdownRoute route; + final BoxConstraints constraints; + final List> items; + final EdgeInsetsGeometry padding; + final Rect buttonRect; + final int selectedIndex; + final int elevation; + final ThemeData theme; + final TextStyle style; + + @override + Widget build(BuildContext context) { assert(debugCheckHasDirectionality(context)); - final double screenHeight = MediaQuery.of(context).size.height; - final double maxMenuHeight = screenHeight - 2.0 * _kMenuItemHeight; + final double availableHeight = constraints.maxHeight; + final double maxMenuHeight = availableHeight - 2.0 * _kMenuItemHeight; final double buttonTop = buttonRect.top; - final double buttonBottom = buttonRect.bottom; + final double buttonBottom = math.min(buttonRect.bottom, availableHeight); // If the button is placed on the bottom or top of the screen, its top or // bottom may be less than [_kMenuItemHeight] from the edge of the screen. // In this case, we want to change the menu limits to align with the top // or bottom edge of the button. final double topLimit = math.min(_kMenuItemHeight, buttonTop); - final double bottomLimit = math.max(screenHeight - _kMenuItemHeight, buttonBottom); + final double bottomLimit = math.max(availableHeight - _kMenuItemHeight, buttonBottom); final double selectedItemOffset = selectedIndex * _kMenuItemHeight + kMaterialListPadding.top; @@ -359,24 +407,25 @@ class _DropdownRoute extends PopupRoute<_DropdownRouteResult> { // respectively. if (menuTop < topLimit) menuTop = math.min(buttonTop, topLimit); + if (menuBottom > bottomLimit) { menuBottom = math.max(buttonBottom, bottomLimit); menuTop = menuBottom - menuHeight; } - if (scrollController == null) { + if (route.scrollController == null) { // The limit is asymmetrical because we do not care how far positive the // limit goes. We are only concerned about the case where the value of // [buttonTop - menuTop] is larger than selectedItemOffset, ie. when // the button is close to the bottom of the screen and the selected item // is close to 0. final double scrollOffset = preferredMenuHeight > maxMenuHeight ? math.max(0.0, selectedItemOffset - (buttonTop - menuTop)) : 0.0; - scrollController = ScrollController(initialScrollOffset: scrollOffset); + route.scrollController = ScrollController(initialScrollOffset: scrollOffset); } final TextDirection textDirection = Directionality.of(context); Widget menu = _DropdownMenu( - route: this, + route: route, padding: padding.resolve(textDirection), ); @@ -404,10 +453,6 @@ class _DropdownRoute extends PopupRoute<_DropdownRouteResult> { ), ); } - - void _dismiss() { - navigator?.removeRoute(this); - } } /// An item in a menu created by a [DropdownButton]. diff --git a/packages/flutter/test/material/dropdown_test.dart b/packages/flutter/test/material/dropdown_test.dart index bdd074faf8..da063969ec 100644 --- a/packages/flutter/test/material/dropdown_test.dart +++ b/packages/flutter/test/material/dropdown_test.dart @@ -1035,4 +1035,49 @@ void main() { await tester.pumpAndSettle(); expect(getMenuScroll(), 4312.0); }); + + testWidgets('Dropdown menu respects parent size limits', (WidgetTester tester) async { + // Regression test for https://github.com/flutter/flutter/issues/24417 + + int selectedIndex; + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + bottomNavigationBar: const SizedBox(height: 200), + body: Navigator( + onGenerateRoute: (RouteSettings settings) { + return MaterialPageRoute( + builder: (BuildContext context) { + return SafeArea( + child: Container( + alignment: Alignment.topLeft, + // From material/dropdown.dart (menus are unaligned by default): + // _kUnalignedMenuMargin = EdgeInsetsDirectional.only(start: 16.0, end: 24.0) + // This padding ensures that the entire menu will be visible + padding: const EdgeInsetsDirectional.only(start: 16.0, end: 24.0), + child: DropdownButton( + value: 12, + onChanged: (int i) { selectedIndex = i; }, + items: List>.generate(100, (int i) { + return DropdownMenuItem(value: i, child: Text('$i')); + }), + ), + ), + ); + }, + ); + }, + ), + ), + ), + ); + + await tester.tap(find.text('12')); + await tester.pumpAndSettle(); + expect(selectedIndex, null); + + await tester.tap(find.text('13').last); + await tester.pumpAndSettle(); + expect(selectedIndex, 13); + }); }