mirror of
https://github.com/flutter/flutter
synced 2024-10-14 04:02:56 +00:00
Enable aligning a dropdown button's width with its menu's width (#14849)
This commit is contained in:
parent
01d8e0a143
commit
e1c38aa024
|
@ -64,16 +64,19 @@ class ButtonTheme extends InheritedWidget {
|
|||
double height: 36.0,
|
||||
EdgeInsetsGeometry padding,
|
||||
ShapeBorder shape,
|
||||
bool alignedDropdown: false,
|
||||
Widget child,
|
||||
}) : assert(textTheme != null),
|
||||
assert(minWidth != null && minWidth >= 0.0),
|
||||
assert(height != null && height >= 0.0),
|
||||
assert(alignedDropdown != null),
|
||||
data = new ButtonThemeData(
|
||||
textTheme: textTheme,
|
||||
minWidth: minWidth,
|
||||
height: height,
|
||||
padding: padding,
|
||||
shape: shape,
|
||||
alignedDropdown: alignedDropdown
|
||||
),
|
||||
super(key: key, child: child);
|
||||
|
||||
|
@ -98,16 +101,19 @@ class ButtonTheme extends InheritedWidget {
|
|||
double height: 36.0,
|
||||
EdgeInsetsGeometry padding: const EdgeInsets.symmetric(horizontal: 8.0),
|
||||
ShapeBorder shape,
|
||||
bool alignedDropdown: false,
|
||||
Widget child,
|
||||
}) : assert(textTheme != null),
|
||||
assert(minWidth != null && minWidth >= 0.0),
|
||||
assert(height != null && height >= 0.0),
|
||||
assert(alignedDropdown != null),
|
||||
data = new ButtonThemeData(
|
||||
textTheme: textTheme,
|
||||
minWidth: minWidth,
|
||||
height: height,
|
||||
padding: padding,
|
||||
shape: shape,
|
||||
alignedDropdown: alignedDropdown,
|
||||
),
|
||||
super(key: key, child: child);
|
||||
|
||||
|
@ -146,9 +152,11 @@ class ButtonThemeData extends Diagnosticable {
|
|||
this.height: 36.0,
|
||||
EdgeInsetsGeometry padding,
|
||||
ShapeBorder shape,
|
||||
this.alignedDropdown: false,
|
||||
}) : assert(textTheme != null),
|
||||
assert(minWidth != null && minWidth >= 0.0),
|
||||
assert(height != null && height >= 0.0),
|
||||
assert(alignedDropdown != null),
|
||||
_padding = padding,
|
||||
_shape = shape;
|
||||
|
||||
|
@ -229,6 +237,17 @@ class ButtonThemeData extends Diagnosticable {
|
|||
}
|
||||
final ShapeBorder _shape;
|
||||
|
||||
/// If true, then a [DropdownButton] menu's width will match the button's
|
||||
/// width.
|
||||
///
|
||||
/// If false (the default), then the dropdown's menu will be wider than
|
||||
/// its button. In either case the dropdown button will line up the leading
|
||||
/// edge of the menu's value with the leading edge of the values
|
||||
/// displayed by the menu items.
|
||||
///
|
||||
/// This property only affects [DropdownButton] and its menu.
|
||||
final bool alignedDropdown;
|
||||
|
||||
@override
|
||||
bool operator ==(dynamic other) {
|
||||
if (other.runtimeType != runtimeType)
|
||||
|
@ -238,7 +257,8 @@ class ButtonThemeData extends Diagnosticable {
|
|||
&& minWidth == typedOther.minWidth
|
||||
&& height == typedOther.height
|
||||
&& padding == typedOther.padding
|
||||
&& shape == typedOther.shape;
|
||||
&& shape == typedOther.shape
|
||||
&& alignedDropdown == typedOther.alignedDropdown;
|
||||
}
|
||||
|
||||
@override
|
||||
|
@ -249,6 +269,7 @@ class ButtonThemeData extends Diagnosticable {
|
|||
height,
|
||||
padding,
|
||||
shape,
|
||||
alignedDropdown,
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -256,13 +277,15 @@ class ButtonThemeData extends Diagnosticable {
|
|||
void debugFillProperties(DiagnosticPropertiesBuilder description) {
|
||||
super.debugFillProperties(description);
|
||||
final ButtonThemeData defaultTheme = const ButtonThemeData();
|
||||
description.add(new EnumProperty<ButtonTextTheme>('textTheme', textTheme,
|
||||
defaultValue: defaultTheme.textTheme));
|
||||
description.add(new EnumProperty<ButtonTextTheme>('textTheme', textTheme, defaultValue: defaultTheme.textTheme));
|
||||
description.add(new DoubleProperty('minWidth', minWidth, defaultValue: defaultTheme.minWidth));
|
||||
description.add(new DoubleProperty('height', height, defaultValue: defaultTheme.height));
|
||||
description.add(new DiagnosticsProperty<EdgeInsetsGeometry>('padding', padding,
|
||||
defaultValue: defaultTheme.padding));
|
||||
description.add(
|
||||
new DiagnosticsProperty<ShapeBorder>('shape', shape, defaultValue: defaultTheme.shape));
|
||||
description.add(new DiagnosticsProperty<EdgeInsetsGeometry>('padding', padding, defaultValue: defaultTheme.padding));
|
||||
description.add(new DiagnosticsProperty<ShapeBorder>('shape', shape, defaultValue: defaultTheme.shape));
|
||||
description.add(new FlagProperty('alignedDropdown',
|
||||
value: alignedDropdown,
|
||||
defaultValue: defaultTheme.alignedDropdown,
|
||||
ifTrue: 'dropdown width matches button',
|
||||
));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,6 +7,7 @@ import 'dart:math' as math;
|
|||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
import 'button_theme.dart';
|
||||
import 'colors.dart';
|
||||
import 'constants.dart';
|
||||
import 'debug.dart';
|
||||
|
@ -21,7 +22,11 @@ import 'theme.dart';
|
|||
const Duration _kDropdownMenuDuration = const Duration(milliseconds: 300);
|
||||
const double _kMenuItemHeight = 48.0;
|
||||
const double _kDenseButtonHeight = 24.0;
|
||||
const EdgeInsets _kMenuHorizontalPadding = const EdgeInsets.symmetric(horizontal: 16.0);
|
||||
const EdgeInsets _kMenuItemPadding = const EdgeInsets.symmetric(horizontal: 16.0);
|
||||
const EdgeInsetsGeometry _kAlignedButtonPadding = const EdgeInsetsDirectional.only(start: 16.0, end: 4.0);
|
||||
const EdgeInsets _kUnalignedButtonPadding = EdgeInsets.zero;
|
||||
const EdgeInsets _kAlignedMenuMargin = EdgeInsets.zero;
|
||||
const EdgeInsetsGeometry _kUnalignedMenuMargin = const EdgeInsetsDirectional.only(start: 16.0, end: 24.0);
|
||||
|
||||
class _DropdownMenuPainter extends CustomPainter {
|
||||
_DropdownMenuPainter({
|
||||
|
@ -91,10 +96,12 @@ class _DropdownScrollBehavior extends ScrollBehavior {
|
|||
class _DropdownMenu<T> extends StatefulWidget {
|
||||
const _DropdownMenu({
|
||||
Key key,
|
||||
this.padding,
|
||||
this.route,
|
||||
}) : super(key: key);
|
||||
|
||||
final _DropdownRoute<T> route;
|
||||
final EdgeInsets padding;
|
||||
|
||||
@override
|
||||
_DropdownMenuState<T> createState() => new _DropdownMenuState<T>();
|
||||
|
@ -149,7 +156,7 @@ class _DropdownMenuState<T> extends State<_DropdownMenu<T>> {
|
|||
opacity: opacity,
|
||||
child: new InkWell(
|
||||
child: new Container(
|
||||
padding: _kMenuHorizontalPadding,
|
||||
padding: widget.padding,
|
||||
child: route.items[itemIndex],
|
||||
),
|
||||
onTap: () => Navigator.pop(
|
||||
|
@ -212,7 +219,7 @@ class _DropdownMenuRouteLayout<T> extends SingleChildLayoutDelegate {
|
|||
final double maxHeight = math.max(0.0, constraints.maxHeight - 2 * _kMenuItemHeight);
|
||||
// The width of a menu should be at most the view width. This ensures that
|
||||
// the menu does not extend past the left and right edges of the screen.
|
||||
final double width = math.min(constraints.maxWidth, buttonRect.width + 8.0);
|
||||
final double width = math.min(constraints.maxWidth, buttonRect.width);
|
||||
return new BoxConstraints(
|
||||
minWidth: width,
|
||||
maxWidth: width,
|
||||
|
@ -238,7 +245,7 @@ class _DropdownMenuRouteLayout<T> extends SingleChildLayoutDelegate {
|
|||
double left;
|
||||
switch (textDirection) {
|
||||
case TextDirection.rtl:
|
||||
left = buttonRect.right.clamp(0.0, size.width - childSize.width) - childSize.width;
|
||||
left = buttonRect.right.clamp(0.0, size.width) - childSize.width;
|
||||
break;
|
||||
case TextDirection.ltr:
|
||||
left = buttonRect.left.clamp(0.0, size.width - childSize.width);
|
||||
|
@ -279,6 +286,7 @@ class _DropdownRouteResult<T> {
|
|||
class _DropdownRoute<T> extends PopupRoute<_DropdownRouteResult<T>> {
|
||||
_DropdownRoute({
|
||||
this.items,
|
||||
this.padding,
|
||||
this.buttonRect,
|
||||
this.selectedIndex,
|
||||
this.elevation: 8,
|
||||
|
@ -288,6 +296,7 @@ class _DropdownRoute<T> extends PopupRoute<_DropdownRouteResult<T>> {
|
|||
}) : assert(style != null);
|
||||
|
||||
final List<DropdownMenuItem<T>> items;
|
||||
final EdgeInsetsGeometry padding;
|
||||
final Rect buttonRect;
|
||||
final int selectedIndex;
|
||||
final int elevation;
|
||||
|
@ -336,7 +345,12 @@ class _DropdownRoute<T> extends PopupRoute<_DropdownRouteResult<T>> {
|
|||
scrollController = new ScrollController(initialScrollOffset: scrollOffset);
|
||||
}
|
||||
|
||||
Widget menu = new _DropdownMenu<T>(route: this);
|
||||
final TextDirection textDirection = Directionality.of(context);
|
||||
Widget menu = new _DropdownMenu<T>(
|
||||
route: this,
|
||||
padding: padding.resolve(textDirection),
|
||||
);
|
||||
|
||||
if (theme != null)
|
||||
menu = new Theme(data: theme, child: menu);
|
||||
|
||||
|
@ -353,7 +367,7 @@ class _DropdownRoute<T> extends PopupRoute<_DropdownRouteResult<T>> {
|
|||
buttonRect: buttonRect,
|
||||
menuTop: menuTop,
|
||||
menuHeight: menuHeight,
|
||||
textDirection: Directionality.of(context),
|
||||
textDirection: textDirection,
|
||||
),
|
||||
child: menu,
|
||||
);
|
||||
|
@ -566,11 +580,16 @@ class _DropdownButtonState<T> extends State<DropdownButton<T>> with WidgetsBindi
|
|||
void _handleTap() {
|
||||
final RenderBox itemBox = context.findRenderObject();
|
||||
final Rect itemRect = itemBox.localToGlobal(Offset.zero) & itemBox.size;
|
||||
final TextDirection textDirection = Directionality.of(context);
|
||||
final EdgeInsetsGeometry menuMargin = ButtonTheme.of(context).alignedDropdown
|
||||
?_kAlignedMenuMargin
|
||||
: _kUnalignedMenuMargin;
|
||||
|
||||
assert(_dropdownRoute == null);
|
||||
_dropdownRoute = new _DropdownRoute<T>(
|
||||
items: widget.items,
|
||||
buttonRect: _kMenuHorizontalPadding.inflateRect(itemRect),
|
||||
buttonRect: menuMargin.resolve(textDirection).inflateRect(itemRect),
|
||||
padding: _kMenuItemPadding.resolve(textDirection),
|
||||
selectedIndex: _selectedIndex ?? 0,
|
||||
elevation: widget.elevation,
|
||||
theme: Theme.of(context, shadowThemeOnly: true),
|
||||
|
@ -613,9 +632,14 @@ class _DropdownButtonState<T> extends State<DropdownButton<T>> with WidgetsBindi
|
|||
));
|
||||
}
|
||||
|
||||
final EdgeInsetsGeometry padding = ButtonTheme.of(context).alignedDropdown
|
||||
? _kAlignedButtonPadding
|
||||
: _kUnalignedButtonPadding;
|
||||
|
||||
Widget result = new DefaultTextStyle(
|
||||
style: _textStyle,
|
||||
child: new SizedBox(
|
||||
child: new Container(
|
||||
padding: padding.resolve(Directionality.of(context)),
|
||||
height: widget.isDense ? _denseButtonHeight : null,
|
||||
child: new Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
|
|
|
@ -491,7 +491,7 @@ class ThemeData extends Diagnosticable {
|
|||
Color unselectedWidgetColor,
|
||||
Color disabledColor,
|
||||
Color buttonColor,
|
||||
Color buttonTheme,
|
||||
ButtonThemeData buttonTheme,
|
||||
Color secondaryHeaderColor,
|
||||
Color textSelectionColor,
|
||||
Color textSelectionHandleColor,
|
||||
|
|
|
@ -14,6 +14,7 @@ void main() {
|
|||
expect(theme.shape, const RoundedRectangleBorder(
|
||||
borderRadius: const BorderRadius.all(const Radius.circular(2.0)),
|
||||
));
|
||||
expect(theme.alignedDropdown, false);
|
||||
});
|
||||
|
||||
test('ButtonThemeData default overrides', () {
|
||||
|
@ -23,11 +24,13 @@ void main() {
|
|||
height: 200.0,
|
||||
padding: EdgeInsets.zero,
|
||||
shape: const RoundedRectangleBorder(),
|
||||
alignedDropdown: true,
|
||||
);
|
||||
expect(theme.textTheme, ButtonTextTheme.primary);
|
||||
expect(theme.constraints, const BoxConstraints(minWidth: 100.0, minHeight: 200.0));
|
||||
expect(theme.padding, EdgeInsets.zero);
|
||||
expect(theme.shape, const RoundedRectangleBorder());
|
||||
expect(theme.alignedDropdown, true);
|
||||
});
|
||||
|
||||
testWidgets('ButtonTheme defaults', (WidgetTester tester) async {
|
||||
|
@ -173,4 +176,109 @@ void main() {
|
|||
expect(tester.widget<Material>(find.byType(Material)).color, const Color(0xFF00FF00));
|
||||
expect(tester.getSize(find.byType(Material)), const Size(100.0, 200.0));
|
||||
});
|
||||
|
||||
testWidgets('ButtonTheme alignedDropdown', (WidgetTester tester) async {
|
||||
final Key dropdownKey = new UniqueKey();
|
||||
|
||||
Widget buildFrame({ bool alignedDropdown, TextDirection textDirection }) {
|
||||
return new MaterialApp(
|
||||
builder: (BuildContext context, Widget child) {
|
||||
return new Directionality(
|
||||
textDirection: textDirection,
|
||||
child: child,
|
||||
);
|
||||
},
|
||||
home: new ButtonTheme(
|
||||
alignedDropdown: alignedDropdown,
|
||||
child: new Material(
|
||||
child: new Builder(
|
||||
builder: (BuildContext context) {
|
||||
return new Container(
|
||||
alignment: Alignment.center,
|
||||
child: new DropdownButtonHideUnderline(
|
||||
child: new Container(
|
||||
width: 200.0,
|
||||
child: new DropdownButton<String>(
|
||||
key: dropdownKey,
|
||||
onChanged: (String value) { },
|
||||
value: 'foo',
|
||||
items: const <DropdownMenuItem<String>>[
|
||||
const DropdownMenuItem<String>(
|
||||
value: 'foo',
|
||||
child: const Text('foo'),
|
||||
),
|
||||
const DropdownMenuItem<String>(
|
||||
value: 'bar',
|
||||
child: const Text('bar'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
final Finder button = find.byKey(dropdownKey);
|
||||
final Finder menu = find.byWidgetPredicate((Widget w) => '${w.runtimeType}' == '_DropdownMenu<String>');
|
||||
|
||||
await tester.pumpWidget(
|
||||
buildFrame(
|
||||
alignedDropdown: false,
|
||||
textDirection: TextDirection.ltr,
|
||||
),
|
||||
);
|
||||
await tester.tap(button);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// 240 = 200.0 (button width) + _kUnalignedMenuMargin (20.0 left and right)
|
||||
expect(tester.getSize(button).width, 200.0);
|
||||
expect(tester.getSize(menu).width, 240.0);
|
||||
|
||||
// Dismiss the menu.
|
||||
await tester.tapAt(Offset.zero);
|
||||
await tester.pumpAndSettle();
|
||||
expect(menu, findsNothing);
|
||||
|
||||
await tester.pumpWidget(
|
||||
buildFrame(
|
||||
alignedDropdown: true,
|
||||
textDirection: TextDirection.ltr,
|
||||
),
|
||||
);
|
||||
await tester.tap(button);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Aligneddropdown: true means the button and menu widths match
|
||||
expect(tester.getSize(button).width, 200.0);
|
||||
expect(tester.getSize(menu).width, 200.0);
|
||||
|
||||
// There are two 'foo' widgets: the selected menu item's label and the drop
|
||||
// down button's label. The should both appear at the same location.
|
||||
final Finder fooText = find.text('foo');
|
||||
expect(fooText, findsNWidgets(2));
|
||||
expect(tester.getRect(fooText.at(0)), tester.getRect(fooText.at(1)));
|
||||
|
||||
// Dismiss the menu.
|
||||
await tester.tapAt(Offset.zero);
|
||||
await tester.pumpAndSettle();
|
||||
expect(menu, findsNothing);
|
||||
|
||||
// Same test as above execpt RTL
|
||||
await tester.pumpWidget(
|
||||
buildFrame(
|
||||
alignedDropdown: true,
|
||||
textDirection: TextDirection.rtl,
|
||||
),
|
||||
);
|
||||
await tester.tap(button);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(fooText, findsNWidgets(2));
|
||||
expect(tester.getRect(fooText.at(0)), tester.getRect(fooText.at(1)));
|
||||
});
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue