Unverified Commit d422e85f authored by jslavitz's avatar jslavitz Committed by GitHub

Large Dropdown Menu Fix (#22594)

* Adds comments clarifying the procedure used to render the menu as well as tests verifying various dropdown menu button positioning and initial scroll states.
parent e0b182e6
......@@ -329,25 +329,47 @@ class _DropdownRoute<T> extends PopupRoute<_DropdownRouteResult<T>> {
assert(debugCheckHasDirectionality(context));
final double screenHeight = MediaQuery.of(context).size.height;
final double maxMenuHeight = screenHeight - 2.0 * _kMenuItemHeight;
final double preferredMenuHeight = (items.length * _kMenuItemHeight) + kMaterialListPadding.vertical;
final double menuHeight = math.min(maxMenuHeight, preferredMenuHeight);
final double buttonTop = buttonRect.top;
final double buttonBottom = buttonRect.bottom;
// 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 selectedItemOffset = selectedIndex * _kMenuItemHeight + kMaterialListPadding.top;
double menuTop = (buttonTop - selectedItemOffset) - (_kMenuItemHeight - buttonRect.height) / 2.0;
const double topPreferredLimit = _kMenuItemHeight;
if (menuTop < topPreferredLimit)
menuTop = math.min(buttonTop, topPreferredLimit);
double bottom = menuTop + menuHeight;
final double bottomPreferredLimit = screenHeight - _kMenuItemHeight;
if (bottom > bottomPreferredLimit) {
bottom = math.max(buttonTop + _kMenuItemHeight, bottomPreferredLimit);
menuTop = bottom - menuHeight;
final double preferredMenuHeight = (items.length * _kMenuItemHeight) + kMaterialListPadding.vertical;
// If there are too many elements in the menu, we need to shrink it down
// so it is at most the maxMenuHeight.
final double menuHeight = math.min(maxMenuHeight, preferredMenuHeight);
double menuBottom = menuTop + menuHeight;
// If the computed top or bottom of the menu are outside of the range
// specified, we need to bring them into range. If the item height is larger
// than the button height and the button is at the very bottom or top of the
// screen, the menu will be aligned with the bottom or top of the button
// respectively.
if (menuTop < topLimit)
menuTop = math.min(buttonTop, topLimit);
if (menuBottom > bottomLimit) {
menuBottom = math.max(buttonBottom, bottomLimit);
menuTop = menuBottom - menuHeight;
}
if (scrollController == null) {
final double scrollOffset = (preferredMenuHeight > maxMenuHeight) ?
math.max(0.0, selectedItemOffset - (buttonTop - menuTop)) : 0.0;
// 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);
}
......
......@@ -777,4 +777,152 @@ void main() {
), ignoreId: true, ignoreRect: true, ignoreTransform: true));
semantics.dispose();
});
testWidgets('Dropdown in middle showing middle item', (WidgetTester tester) async {
final List<DropdownMenuItem<int>> items =
List<DropdownMenuItem<int>>.generate(100, (int i) =>
DropdownMenuItem<int>(value: i, child: Text('$i')));
final DropdownButton<int> button = DropdownButton<int>(
value: 50,
onChanged: (int newValue){},
items: items,
);
double getMenuScroll() {
double scrollPosition;
final ListView listView = tester.element(find.byType(ListView)).widget;
final ScrollController scrollController = listView.controller;
assert(scrollController != null);
scrollPosition = scrollController.position.pixels;
assert(scrollPosition != null);
return scrollPosition;
}
await tester.pumpWidget(
MaterialApp(
home: Material(
child: Align(
alignment: Alignment.center,
child: button,
),
),
),
);
await tester.tap(find.text('50'));
await tester.pumpAndSettle();
expect(getMenuScroll(), 2180.0);
});
testWidgets('Dropdown in top showing bottom item', (WidgetTester tester) async {
final List<DropdownMenuItem<int>> items =
List<DropdownMenuItem<int>>.generate(100, (int i) =>
DropdownMenuItem<int>(value: i, child: Text('$i')));
final DropdownButton<int> button = DropdownButton<int>(
value: 99,
onChanged: (int newValue){},
items: items,
);
double getMenuScroll() {
double scrollPosition;
final ListView listView = tester.element(find.byType(ListView)).widget;
final ScrollController scrollController = listView.controller;
assert(scrollController != null);
scrollPosition = scrollController.position.pixels;
assert(scrollPosition != null);
return scrollPosition;
}
await tester.pumpWidget(
MaterialApp(
home: Material(
child: Align(
alignment: Alignment.topCenter,
child: button,
),
),
),
);
await tester.tap(find.text('99'));
await tester.pumpAndSettle();
expect(getMenuScroll(), 4312.0);
});
testWidgets('Dropdown in bottom showing top item', (WidgetTester tester) async {
final List<DropdownMenuItem<int>> items =
List<DropdownMenuItem<int>>.generate(100, (int i) =>
DropdownMenuItem<int>(value: i, child: Text('$i')));
final DropdownButton<int> button = DropdownButton<int>(
value: 0,
onChanged: (int newValue){},
items: items,
);
double getMenuScroll() {
double scrollPosition;
final ListView listView = tester.element(find.byType(ListView)).widget;
final ScrollController scrollController = listView.controller;
assert(scrollController != null);
scrollPosition = scrollController.position.pixels;
assert(scrollPosition != null);
return scrollPosition;
}
await tester.pumpWidget(
MaterialApp(
home: Material(
child: Align(
alignment: Alignment.bottomCenter,
child: button,
),
),
),
);
await tester.tap(find.text('0'));
await tester.pumpAndSettle();
expect(getMenuScroll(), 0.0);
});
testWidgets('Dropdown in center showing bottom item', (WidgetTester tester) async {
final List<DropdownMenuItem<int>> items =
List<DropdownMenuItem<int>>.generate(100, (int i) =>
DropdownMenuItem<int>(value: i, child: Text('$i')));
final DropdownButton<int> button = DropdownButton<int>(
value: 99,
onChanged: (int newValue){},
items: items,
);
double getMenuScroll() {
double scrollPosition;
final ListView listView = tester.element(find.byType(ListView)).widget;
final ScrollController scrollController = listView.controller;
assert(scrollController != null);
scrollPosition = scrollController.position.pixels;
assert(scrollPosition != null);
return scrollPosition;
}
await tester.pumpWidget(
MaterialApp(
home: Material(
child: Align(
alignment: Alignment.center,
child: button,
),
),
),
);
await tester.tap(find.text('99'));
await tester.pumpAndSettle();
expect(getMenuScroll(), 4312.0);
});
}
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