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.
This commit is contained in:
parent
e0b182e61a
commit
d422e85f5b
@ -329,25 +329,47 @@ class _DropdownRoute<T> extends PopupRoute<_DropdownRouteResult<T>> {
|
|||||||
assert(debugCheckHasDirectionality(context));
|
assert(debugCheckHasDirectionality(context));
|
||||||
final double screenHeight = MediaQuery.of(context).size.height;
|
final double screenHeight = MediaQuery.of(context).size.height;
|
||||||
final double maxMenuHeight = screenHeight - 2.0 * _kMenuItemHeight;
|
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 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;
|
final double selectedItemOffset = selectedIndex * _kMenuItemHeight + kMaterialListPadding.top;
|
||||||
|
|
||||||
double menuTop = (buttonTop - selectedItemOffset) - (_kMenuItemHeight - buttonRect.height) / 2.0;
|
double menuTop = (buttonTop - selectedItemOffset) - (_kMenuItemHeight - buttonRect.height) / 2.0;
|
||||||
const double topPreferredLimit = _kMenuItemHeight;
|
final double preferredMenuHeight = (items.length * _kMenuItemHeight) + kMaterialListPadding.vertical;
|
||||||
if (menuTop < topPreferredLimit)
|
|
||||||
menuTop = math.min(buttonTop, topPreferredLimit);
|
// If there are too many elements in the menu, we need to shrink it down
|
||||||
double bottom = menuTop + menuHeight;
|
// so it is at most the maxMenuHeight.
|
||||||
final double bottomPreferredLimit = screenHeight - _kMenuItemHeight;
|
final double menuHeight = math.min(maxMenuHeight, preferredMenuHeight);
|
||||||
if (bottom > bottomPreferredLimit) {
|
|
||||||
bottom = math.max(buttonTop + _kMenuItemHeight, bottomPreferredLimit);
|
double menuBottom = menuTop + menuHeight;
|
||||||
menuTop = bottom - 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) {
|
if (scrollController == null) {
|
||||||
final double scrollOffset = (preferredMenuHeight > maxMenuHeight) ?
|
// The limit is asymmetrical because we do not care how far positive the
|
||||||
math.max(0.0, selectedItemOffset - (buttonTop - menuTop)) : 0.0;
|
// 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);
|
scrollController = ScrollController(initialScrollOffset: scrollOffset);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -777,4 +777,152 @@ void main() {
|
|||||||
), ignoreId: true, ignoreRect: true, ignoreTransform: true));
|
), ignoreId: true, ignoreRect: true, ignoreTransform: true));
|
||||||
semantics.dispose();
|
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);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user