Commit 08bf1b6b authored by Hans Muller's avatar Hans Muller Committed by GitHub

Scrollable dropdown, dropdown underline cosmetics (#4766)

parent 5fc04dab
...@@ -135,26 +135,61 @@ class _ButtonsDemoState extends State<ButtonsDemo> { ...@@ -135,26 +135,61 @@ class _ButtonsDemoState extends State<ButtonsDemo> {
); );
} }
String dropdownValue = 'Free'; // https://en.wikipedia.org/wiki/Free_Four
String dropdown1Value = 'Free';
String dropdown2Value = 'Four';
Widget buildDropdownButton() { Widget buildDropdownButton() {
return new Align( return new Padding(
alignment: new FractionalOffset(0.5, 0.4), padding: const EdgeInsets.all(24.0),
child: new DropDownButton<String>( child: new Column(
value: dropdownValue, mainAxisAlignment: MainAxisAlignment.start,
onChanged: (String newValue) { children: <Widget>[
setState(() { new ListItem(
if (newValue != null) title: new Text('Scrollable dropdown:'),
dropdownValue = newValue; trailing: new DropDownButton<String>(
}); value: dropdown1Value,
}, onChanged: (String newValue) {
items: <String>['One', 'Two', 'Free', 'Four'] setState(() {
.map((String value) { if (newValue != null)
return new DropDownMenuItem<String>( dropdown1Value = newValue;
value: value, });
child: new Text(value)); },
}) items: <String>[
.toList() 'One', 'Two', 'Free', 'Four', 'Can', 'I', 'Have', 'A', 'Little',
'Bit', 'More', 'Five', 'Six', 'Seven', 'Eight', 'Nine', 'Ten'
]
.map((String value) {
return new DropDownMenuItem<String>(
value: value,
child: new Text(value));
})
.toList()
)
),
new SizedBox(
height: 24.0
),
new ListItem(
title: new Text('Simple dropdown:'),
trailing: new DropDownButton<String>(
value: dropdown2Value,
onChanged: (String newValue) {
setState(() {
if (newValue != null)
dropdown2Value = newValue;
});
},
items: <String>['One', 'Two', 'Free', 'Four']
.map((String value) {
return new DropDownMenuItem<String>(
value: value,
child: new Text(value));
})
.toList()
)
)
]
) )
); );
} }
......
...@@ -8,10 +8,12 @@ import 'dart:math' as math; ...@@ -8,10 +8,12 @@ import 'dart:math' as math;
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'package:meta/meta.dart'; import 'package:meta/meta.dart';
import 'colors.dart';
import 'debug.dart'; import 'debug.dart';
import 'icon.dart'; import 'icon.dart';
import 'icons.dart'; import 'icons.dart';
import 'ink_well.dart'; import 'ink_well.dart';
import 'scrollbar.dart';
import 'shadows.dart'; import 'shadows.dart';
import 'theme.dart'; import 'theme.dart';
import 'material.dart'; import 'material.dart';
...@@ -19,10 +21,7 @@ import 'material.dart'; ...@@ -19,10 +21,7 @@ import 'material.dart';
const Duration _kDropDownMenuDuration = const Duration(milliseconds: 300); const Duration _kDropDownMenuDuration = const Duration(milliseconds: 300);
const double _kMenuItemHeight = 48.0; const double _kMenuItemHeight = 48.0;
const EdgeInsets _kMenuVerticalPadding = const EdgeInsets.symmetric(vertical: 8.0); const EdgeInsets _kMenuVerticalPadding = const EdgeInsets.symmetric(vertical: 8.0);
const EdgeInsets _kMenuHorizontalPadding = const EdgeInsets.symmetric(horizontal: 4.0); const EdgeInsets _kMenuHorizontalPadding = const EdgeInsets.symmetric(horizontal: 16.0);
const double _kBaselineOffsetFromBottom = 20.0;
const double _kBottomBorderHeight = 2.0;
const Border _kDropDownUnderline = const Border(bottom: const BorderSide(color: const Color(0xFFBDBDBD), width: _kBottomBorderHeight));
class _DropDownMenuPainter extends CustomPainter { class _DropDownMenuPainter extends CustomPainter {
_DropDownMenuPainter({ _DropDownMenuPainter({
...@@ -121,7 +120,6 @@ class _DropDownMenuState<T> extends State<_DropDownMenu<T>> { ...@@ -121,7 +120,6 @@ class _DropDownMenuState<T> extends State<_DropDownMenu<T>> {
// //
// When the menu is dismissed we just fade the entire thing out // When the menu is dismissed we just fade the entire thing out
// in the first 0.25s. // in the first 0.25s.
final _DropDownRoute<T> route = config.route; final _DropDownRoute<T> route = config.route;
final double unit = 0.5 / (route.items.length + 1.5); final double unit = 0.5 / (route.items.length + 1.5);
final List<Widget> children = <Widget>[]; final List<Widget> children = <Widget>[];
...@@ -161,10 +159,13 @@ class _DropDownMenuState<T> extends State<_DropDownMenu<T>> { ...@@ -161,10 +159,13 @@ class _DropDownMenuState<T> extends State<_DropDownMenu<T>> {
child: new Material( child: new Material(
type: MaterialType.transparency, type: MaterialType.transparency,
textStyle: route.style, textStyle: route.style,
child: new ScrollableList( child: new Scrollbar(
padding: _kMenuVerticalPadding, child: new ScrollableList(
itemExtent: _kMenuItemHeight, scrollableKey: config.route.scrollableKey,
children: children padding: _kMenuVerticalPadding,
itemExtent: _kMenuItemHeight,
children: children
)
) )
) )
) )
...@@ -172,11 +173,14 @@ class _DropDownMenuState<T> extends State<_DropDownMenu<T>> { ...@@ -172,11 +173,14 @@ class _DropDownMenuState<T> extends State<_DropDownMenu<T>> {
} }
} }
class _DropDownMenuRouteLayout extends SingleChildLayoutDelegate { class _DropDownMenuRouteLayout<T> extends SingleChildLayoutDelegate {
_DropDownMenuRouteLayout(this.buttonRect, this.selectedIndex); _DropDownMenuRouteLayout({ this.route });
final Rect buttonRect; final _DropDownRoute<T> route;
final int selectedIndex;
Rect get buttonRect => route.buttonRect;
int get selectedIndex => route.selectedIndex;
GlobalKey<ScrollableState> get scrollableKey => route.scrollableKey;
@override @override
BoxConstraints getConstraintsForChild(BoxConstraints constraints) { BoxConstraints getConstraintsForChild(BoxConstraints constraints) {
...@@ -185,7 +189,7 @@ class _DropDownMenuRouteLayout extends SingleChildLayoutDelegate { ...@@ -185,7 +189,7 @@ class _DropDownMenuRouteLayout extends SingleChildLayoutDelegate {
// with which to dismiss the menu. // with which to dismiss the menu.
// -- https://www.google.com/design/spec/components/menus.html#menus-simple-menus // -- https://www.google.com/design/spec/components/menus.html#menus-simple-menus
final double maxHeight = math.max(0.0, constraints.maxHeight - 2 * _kMenuItemHeight); final double maxHeight = math.max(0.0, constraints.maxHeight - 2 * _kMenuItemHeight);
final double width = buttonRect.width; final double width = buttonRect.width + 8.0;
return new BoxConstraints( return new BoxConstraints(
minWidth: width, minWidth: width,
maxWidth: width, maxWidth: width,
...@@ -197,12 +201,13 @@ class _DropDownMenuRouteLayout extends SingleChildLayoutDelegate { ...@@ -197,12 +201,13 @@ class _DropDownMenuRouteLayout extends SingleChildLayoutDelegate {
@override @override
Offset getPositionForChild(Size size, Size childSize) { Offset getPositionForChild(Size size, Size childSize) {
final double buttonTop = buttonRect.top; final double buttonTop = buttonRect.top;
double top = buttonTop - selectedIndex * _kMenuItemHeight - _kMenuVerticalPadding.top; final double selectedItemOffset = selectedIndex * _kMenuItemHeight + _kMenuVerticalPadding.top;
double topPreferredLimit = _kMenuItemHeight; double top = buttonTop - selectedItemOffset;
final double topPreferredLimit = _kMenuItemHeight;
if (top < topPreferredLimit) if (top < topPreferredLimit)
top = math.min(buttonTop, topPreferredLimit); top = math.min(buttonTop, topPreferredLimit);
double bottom = top + childSize.height; double bottom = top + childSize.height;
double bottomPreferredLimit = size.height - _kMenuItemHeight; final double bottomPreferredLimit = size.height - _kMenuItemHeight;
if (bottom > bottomPreferredLimit) { if (bottom > bottomPreferredLimit) {
bottom = math.max(buttonTop + _kMenuItemHeight, bottomPreferredLimit); bottom = math.max(buttonTop + _kMenuItemHeight, bottomPreferredLimit);
top = bottom - childSize.height; top = bottom - childSize.height;
...@@ -218,14 +223,18 @@ class _DropDownMenuRouteLayout extends SingleChildLayoutDelegate { ...@@ -218,14 +223,18 @@ class _DropDownMenuRouteLayout extends SingleChildLayoutDelegate {
} }
return true; return true;
}); });
if (route.initialLayout) {
route.initialLayout = false;
final double scrollOffset = selectedItemOffset - (buttonTop - top);
scrollableKey.currentState.scrollTo(scrollOffset);
}
return new Offset(buttonRect.left, top); return new Offset(buttonRect.left, top);
} }
@override @override
bool shouldRelayout(_DropDownMenuRouteLayout oldDelegate) { bool shouldRelayout(_DropDownMenuRouteLayout<T> oldDelegate) => oldDelegate.route != route;
return oldDelegate.buttonRect != buttonRect
|| oldDelegate.selectedIndex != selectedIndex;
}
} }
// We box the return value so that the return value can be null. Otherwise, // We box the return value so that the return value can be null. Otherwise,
...@@ -260,10 +269,14 @@ class _DropDownRoute<T> extends PopupRoute<_DropDownRouteResult<T>> { ...@@ -260,10 +269,14 @@ class _DropDownRoute<T> extends PopupRoute<_DropDownRouteResult<T>> {
assert(style != null); assert(style != null);
} }
final GlobalKey<ScrollableState> scrollableKey = new GlobalKey<ScrollableState>(debugLabel: '_DropDownMenu');
final List<DropDownMenuItem<T>> items; final List<DropDownMenuItem<T>> items;
final Rect buttonRect; final Rect buttonRect;
final int selectedIndex; final int selectedIndex;
final int elevation; final int elevation;
// The layout gets this route's scrollableKey so that it can scroll the
/// selected item into position, but only on the initial layout.
bool initialLayout = true;
TextStyle get style => _style; TextStyle get style => _style;
TextStyle _style; TextStyle _style;
...@@ -288,7 +301,7 @@ class _DropDownRoute<T> extends PopupRoute<_DropDownRouteResult<T>> { ...@@ -288,7 +301,7 @@ class _DropDownRoute<T> extends PopupRoute<_DropDownRouteResult<T>> {
@override @override
Widget buildPage(BuildContext context, Animation<double> animation, Animation<double> forwardAnimation) { Widget buildPage(BuildContext context, Animation<double> animation, Animation<double> forwardAnimation) {
return new CustomSingleChildLayout( return new CustomSingleChildLayout(
delegate: new _DropDownMenuRouteLayout(buttonRect, selectedIndex), delegate: new _DropDownMenuRouteLayout<T>(route: this),
child: new _DropDownMenu<T>(route: this) child: new _DropDownMenu<T>(route: this)
); );
} }
...@@ -324,10 +337,8 @@ class DropDownMenuItem<T> extends StatelessWidget { ...@@ -324,10 +337,8 @@ class DropDownMenuItem<T> extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return new Container( return new Container(
height: _kMenuItemHeight, height: _kMenuItemHeight,
padding: const EdgeInsets.symmetric(horizontal: 8.0), child: new Align(
child: new Baseline( alignment: FractionalOffset.centerLeft,
baselineType: TextBaseline.alphabetic,
baseline: _kMenuItemHeight - _kBaselineOffsetFromBottom,
child: child child: child
) )
); );
...@@ -387,7 +398,7 @@ class DropDownButton<T> extends StatefulWidget { ...@@ -387,7 +398,7 @@ class DropDownButton<T> extends StatefulWidget {
@required this.onChanged, @required this.onChanged,
this.elevation: 8, this.elevation: 8,
this.style, this.style,
this.iconSize: 36.0 this.iconSize: 24.0
}) : super(key: key) { }) : super(key: key) {
assert(items != null); assert(items != null);
assert(items.where((DropDownMenuItem<T> item) => item.value == value).length == 1); assert(items.where((DropDownMenuItem<T> item) => item.value == value).length == 1);
...@@ -416,7 +427,7 @@ class DropDownButton<T> extends StatefulWidget { ...@@ -416,7 +427,7 @@ class DropDownButton<T> extends StatefulWidget {
/// The size to use for the drop-down button's down arrow icon button. /// The size to use for the drop-down button's down arrow icon button.
/// ///
/// Defaults to 36.0. /// Defaults to 24.0.
final double iconSize; final double iconSize;
@override @override
...@@ -493,18 +504,37 @@ class _DropDownButtonState<T> extends State<DropDownButton<T>> { ...@@ -493,18 +504,37 @@ class _DropDownButtonState<T> extends State<DropDownButton<T>> {
alignment: FractionalOffset.centerLeft, alignment: FractionalOffset.centerLeft,
children: config.items children: config.items
), ),
new Icon(Icons.arrow_drop_down, size: config.iconSize) new Icon(Icons.arrow_drop_down,
size: config.iconSize,
// These colors are not defined in the Material Design spec.
color: Theme.of(context).brightness == Brightness.light ? Colors.grey[700] : Colors.white70
)
] ]
) )
); );
if (!DropDownButtonHideUnderline.at(context)) { if (!DropDownButtonHideUnderline.at(context)) {
result = new Container( result = new Stack(
decoration: const BoxDecoration(border: _kDropDownUnderline), children: <Widget>[
child: result result,
new Positioned(
left: 0.0,
right: 0.0,
bottom: 8.0,
child: new Container(
height: 1.0,
decoration: const BoxDecoration(
border: const Border(bottom: const BorderSide(color: const Color(0xFFBDBDBD), width: 0.0))
)
)
)
]
); );
} }
return new GestureDetector( return new GestureDetector(
onTap: _handleTap, onTap: _handleTap,
behavior: HitTestBehavior.opaque,
child: result child: result
); );
} }
......
...@@ -49,11 +49,7 @@ void main() { ...@@ -49,11 +49,7 @@ void main() {
await tester.tap(find.byConfig(button)); await tester.tap(find.byConfig(button));
expect(value, 4); expect(value, 4);
await tester.idle(); // this waits for the route's completer to complete, which calls handleChanged await tester.idle(); // this waits for the route's completer to complete, which calls handleChanged
expect(value, 4);
// Ideally this would be 4 because the menu would be overscrolled to the
// correct position, but currently we just reposition the menu so that it
// is visible on screen.
expect(value, 0);
// TODO(abarth): Remove these calls to pump once navigator cleans up its // TODO(abarth): Remove these calls to pump once navigator cleans up its
// pop transitions. // pop transitions.
......
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