Commit ccad2849 authored by Ian Hickson's avatar Ian Hickson Committed by GitHub

Docs for menus (#10396)

Also, clean up the menus code a bit.

Also, make it easier to write a PopupMenuEntry that has itself many
items (for example, the way Chrome's menu has icons in a row).
parent c713aa00
......@@ -79,9 +79,9 @@ class MenuDemoState extends State<MenuDemo> {
value: 'Hooray!',
child: const Text('Hooray!')
),
]
)
]
],
),
],
),
body: new ListView(
padding: kMaterialListPadding,
......
......@@ -21,6 +21,7 @@ const double _kBaselineOffsetFromBottom = 20.0;
const double _kMenuCloseIntervalEnd = 2.0 / 3.0;
const double _kMenuHorizontalPadding = 16.0;
const double _kMenuItemHeight = 48.0;
const double _kMenuDividerHeight = 16.0;
const double _kMenuMaxWidth = 5.0 * _kMenuWidthStep;
const double _kMenuMinWidth = 2.0 * _kMenuWidthStep;
const double _kMenuVerticalPadding = 8.0;
......@@ -33,16 +34,21 @@ const double _kMenuScreenPadding = 8.0;
/// To show a popup menu, use the [showMenu] function. To create a button that
/// shows a popup menu, consider using [PopupMenuButton].
///
/// The type `T` is the type of the value the entry represents. All the entries
/// in a given menu must represent values with consistent types.
/// The type `T` is the type of the value(s) the entry represents. All the
/// entries in a given menu must represent values with consistent types.
///
/// A [PopupMenuEntry] may represent multiple values, for example a row with
/// several icons, or a single entry, for example a menu item with an icon (see
/// [PopupMenuItem]), or no value at all (for example, [PopupMenuDivider]).
///
/// See also:
///
/// * [PopupMenuItem]
/// * [PopupMenuDivider]
/// * [CheckedPopupMenuItem]
/// * [showMenu]
/// * [PopupMenuButton]
/// * [PopupMenuItem], a popup menu entry for a single value.
/// * [PopupMenuDivider], a popup menu entry that is just a horizontal line.
/// * [CheckedPopupMenuItem], a popup menu item with a checkmark.
/// * [showMenu], a method to dynamically show a popup menu at a given location.
/// * [PopupMenuButton], an [IconButton] that automatically shows a menu when
/// it is tapped.
abstract class PopupMenuEntry<T> extends StatefulWidget {
/// Abstract const constructor. This constructor enables subclasses to provide
/// const constructors so that they can be used in const expressions.
......@@ -50,36 +56,51 @@ abstract class PopupMenuEntry<T> extends StatefulWidget {
/// The amount of vertical space occupied by this entry.
///
/// This value must remain constant for a given instance.
/// This value is used at the time the [showMenu] method is called, if the
/// `initialValue` argument is provided, to determine the position of this
/// entry when aligning the selected entry over the given `position`. It is
/// otherwise ignored.
double get height;
/// The value that should be returned by [showMenu] when the user selects this entry.
T get value => null;
/// Whether the user is permitted to select this entry.
bool get enabled;
/// Whether this entry represents a particular value.
///
/// This method is used by [showMenu], when it is called, to align the entry
/// representing the `initialValue`, if any, to the given `position`, and then
/// later is called on each entry to determine if it should be highlighted (if
/// the method returns true, the entry will have its background color set to
/// the ambient [ThemeData.highlightColor]). If `initialValue` is null, then
/// this method is not called.
///
/// If the [PopupMenuEntry] represents a single value, this should return true
/// if the argument matches that value. If it represents multiple values, it
/// should return true if the argument matches any of them.
bool represents(T value);
}
/// A horizontal divider in a material design popup menu.
///
/// This widget adatps the [Divider] for use in popup menus.
/// This widget adapts the [Divider] for use in popup menus.
///
/// See also:
///
/// * [PopupMenuItem]
/// * [showMenu]
/// * [PopupMenuButton]
class PopupMenuDivider extends PopupMenuEntry<dynamic> {
/// * [PopupMenuItem], for the kinds of items that this widget divides.
/// * [showMenu], a method to dynamically show a popup menu at a given location.
/// * [PopupMenuButton], an [IconButton] that automatically shows a menu when
/// it is tapped.
class PopupMenuDivider extends PopupMenuEntry<Null> {
/// Creates a horizontal divider for a popup menu.
///
/// By default, the divider has a height of 16.0 logical pixels.
const PopupMenuDivider({ Key key, this.height: 16.0 }) : super(key: key);
/// By default, the divider has a height of 16 logical pixels.
const PopupMenuDivider({ Key key, this.height: _kMenuDividerHeight }) : super(key: key);
/// The height of the divider entry.
///
/// Defaults to 16 pixels.
@override
final double height;
@override
bool get enabled => false;
bool represents(dynamic value) => false;
@override
_PopupMenuDividerState createState() => new _PopupMenuDividerState();
......@@ -98,34 +119,76 @@ class _PopupMenuDividerState extends State<PopupMenuDivider> {
/// To show a checkmark next to a popup menu item, consider using
/// [CheckedPopupMenuItem].
///
/// Typically the [child] of a [PopupMenuItem] is a [Text] widget. More
/// elaborate menus with icons can use a [ListTile]. By default, a
/// [PopupMenuItem] is 48 pixels high. If you use a widget with a different
/// height, it must be specified in the [height] property.
///
/// ## Sample code
///
/// Here, a [Text] widget is used with a popup menu item. The `WhyFarther` type
/// is an enum, not shown here.
///
/// ```dart
/// const PopupMenuItem<WhyFarther>(
/// value: WhyFarther.harder,
/// child: const Text('Working a lot harder'),
/// ),
/// ```
///
/// See the example at [PopupMenuButton] for how this example could be used in a
/// complete menu, and see the example at [CheckedPopupMenuItem] for one way to
/// keep the text of [PopupMenuItem]s that use [Text] widgets in their [child]
/// slot aligned with the text of [CheckedPopupMenuItem]s or of [PopupMenuItem]
/// that use a [ListTile] in their [child] slot.
///
/// See also:
///
/// * [PopupMenuDivider]
/// * [CheckedPopupMenuItem]
/// * [showMenu]
/// * [PopupMenuButton]
/// * [PopupMenuDivider], which can be used to divide items from each other.
/// * [CheckedPopupMenuItem], a variant of [PopupMenuItem] with a checkmark.
/// * [showMenu], a method to dynamically show a popup menu at a given location.
/// * [PopupMenuButton], an [IconButton] that automatically shows a menu when
/// it is tapped.
class PopupMenuItem<T> extends PopupMenuEntry<T> {
/// Creates an item for a popup menu.
///
/// By default, the item is enabled.
/// By default, the item is [enabled].
///
/// The `height` and `enabled` arguments must not be null.
const PopupMenuItem({
Key key,
this.value,
this.enabled: true,
this.height: _kMenuItemHeight,
@required this.child,
}) : super(key: key);
}) : assert(enabled != null),
assert(height != null),
super(key: key);
@override
/// The value that will be returned by [showMenu] if this entry is selected.
final T value;
@override
/// Whether the user is permitted to select this entry.
///
/// Defaults to true. If this is false, then the item will not react to
/// touches.
final bool enabled;
/// The height of the entry.
///
/// Defaults to 48 pixels.
@override
final double height;
/// The widget below this widget in the tree.
///
/// Typically a single-line [ListTile] (for menus with icons) or a [Text]. An
/// appropriate [DefaultTextStyle] is put in scope for the child. In either
/// case, the text should be short enough that it won't wrap.
final Widget child;
@override
double get height => _kMenuItemHeight;
bool represents(T value) => value == this.value;
@override
_PopupMenuItemState<PopupMenuItem<T>> createState() => new _PopupMenuItemState<PopupMenuItem<T>>();
......@@ -135,7 +198,7 @@ class _PopupMenuItemState<T extends PopupMenuItem<dynamic>> extends State<T> {
// Override this to put something else in the menu entry.
Widget buildChild() => widget.child;
void onTap() {
void handleTap() {
Navigator.pop(context, widget.value);
}
......@@ -152,24 +215,24 @@ class _PopupMenuItemState<T extends PopupMenuItem<dynamic>> extends State<T> {
child: new Baseline(
baseline: widget.height - _kBaselineOffsetFromBottom,
baselineType: TextBaseline.alphabetic,
child: buildChild()
child: buildChild(),
)
);
if (!widget.enabled) {
final bool isDark = theme.brightness == Brightness.dark;
item = IconTheme.merge(
data: new IconThemeData(opacity: isDark ? 0.5 : 0.38),
child: item
child: item,
);
}
return new InkWell(
onTap: widget.enabled ? onTap : null,
onTap: widget.enabled ? handleTap : null,
child: new MergeSemantics(
child: new Container(
height: widget.height,
padding: const EdgeInsets.symmetric(horizontal: _kMenuHorizontalPadding),
child: item
child: item,
)
)
);
......@@ -181,33 +244,102 @@ class _PopupMenuItemState<T extends PopupMenuItem<dynamic>> extends State<T> {
/// To show a popup menu, use the [showMenu] function. To create a button that
/// shows a popup menu, consider using [PopupMenuButton].
///
/// A [CheckedPopupMenuItem] is 48 pixels high, which matches the default height
/// of a [PopupMenuItem]. The horizontal layout uses a [ListTile]; the checkmark
/// is an [Icons.done] icon, shown in the [ListTile.leading] position.
///
/// ## Sample code
///
/// Suppose a `Commands` enum exists that lists the possible commands from a
/// particular popup menu, including `Commands.heroAndScholar` and
/// `Commands.hurricaneCame`, and further suppose that there is a
/// `_heroAndScholar` member field which is a boolean. The example below shows a
/// menu with one menu item with a checkmark that can toggle the boolean, and
/// one menu item without a checkmark for selecting the second option. (It also
/// shows a divider placed between the two menu items.)
///
/// ```dart
/// new PopupMenuButton<Commands>(
/// onSelected: (Commands result) {
/// switch (result) {
/// case Commands.heroAndScholar:
/// setState(() { _heroAndScholar = !_heroAndScholar; });
/// break;
/// case Commands.hurricaneCame:
/// // ...handle hurricane option
/// break;
/// // ...other items handled here
/// }
/// },
/// itemBuilder: (BuildContext context) => <PopupMenuEntry<Commands>>[
/// new CheckedPopupMenuItem<Commands>(
/// checked: _heroAndScholar,
/// value: Commands.heroAndScholar,
/// child: const Text('Hero and scholar'),
/// ),
/// const PopupMenuDivider(),
/// const PopupMenuItem<Commands>(
/// value: Commands.hurricaneCame,
/// child: const ListTile(leading: const Icon(null), title: const Text('Bring hurricane')),
/// ),
/// // ...other items listed here
/// ],
/// )
/// ```
///
/// In particular, observe how the second menu item uses a [ListTile] with a
/// blank [Icon] in the [ListTile.leading] position to get the same alignment as
/// the item with the checkmark.
///
/// See also:
///
/// * [PopupMenuItem]
/// * [PopupMenuDivider]
/// * [CheckedPopupMenuItem]
/// * [showMenu]
/// * [PopupMenuButton]
/// * [PopupMenuItem], a popup menu entry for picking a command (as opposed to
/// toggling a value).
/// * [PopupMenuDivider], a popup menu entry that is just a horizontal line.
/// * [showMenu], a method to dynamically show a popup menu at a given location.
/// * [PopupMenuButton], an [IconButton] that automatically shows a menu when
/// it is tapped.
class CheckedPopupMenuItem<T> extends PopupMenuItem<T> {
/// Creates a popup menu item with a checkmark.
///
/// By default, the menu item is enabled but unchecked.
/// By default, the menu item is [enabled] but unchecked. To mark the item as
/// checked, set [checked] to true.
///
/// The `checked` and `enabled` arguments must not be null.
const CheckedPopupMenuItem({
Key key,
T value,
this.checked: false,
bool enabled: true,
Widget child
}) : super(
Widget child,
}) : assert(checked != null),
super(
key: key,
value: value,
enabled: enabled,
child: child
child: child,
);
/// Whether to display a checkmark next to the menu item.
///
/// Defaults to false.
///
/// When true, an [Icons.done] checkmark is displayed.
///
/// When this popup menu item is selected, the checkmark will fade in or out
/// as appropriate to represent the implied new state.
final bool checked;
/// The widget below this widget in the tree.
///
/// Typically a [Text]. An appropriate [DefaultTextStyle] is put in scope for
/// the child. The text should be short enough that it won't wrap.
///
/// This widget is placed in the [ListTile.title] slot of a [ListTile] whose
/// [ListTile.leading] slot is an [Icons.done] icon.
@override
Widget get child => super.child;
@override
_CheckedPopupMenuItemState<T> createState() => new _CheckedPopupMenuItemState<T>();
}
......@@ -226,13 +358,13 @@ class _CheckedPopupMenuItemState<T> extends _PopupMenuItemState<CheckedPopupMenu
}
@override
void onTap() {
void handleTap() {
// This fades the checkmark in or out when tapped.
if (widget.checked)
_controller.reverse();
else
_controller.forward();
super.onTap();
super.handleTap();
}
@override
......@@ -243,7 +375,7 @@ class _CheckedPopupMenuItemState<T> extends _PopupMenuItemState<CheckedPopupMenu
opacity: _opacity,
child: new Icon(_controller.isDismissed ? null : Icons.done)
),
title: widget.child
title: widget.child,
);
}
}
......@@ -261,7 +393,7 @@ class _PopupMenu<T> extends StatelessWidget {
final double unit = 1.0 / (route.items.length + 1.5); // 1.0 for the width and 0.5 for the last item's fade.
final List<Widget> children = <Widget>[];
for (int i = 0; i < route.items.length; ++i) {
for (int i = 0; i < route.items.length; i += 1) {
final double start = (i + 1) * unit;
final double end = (start + 1.5 * unit).clamp(0.0, 1.0);
final CurvedAnimation opacity = new CurvedAnimation(
......@@ -269,15 +401,15 @@ class _PopupMenu<T> extends StatelessWidget {
curve: new Interval(start, end)
);
Widget item = route.items[i];
if (route.initialValue != null && route.initialValue == route.items[i].value) {
if (route.initialValue != null && route.items[i].represents(route.initialValue)) {
item = new Container(
color: Theme.of(context).highlightColor,
child: item
child: item,
);
}
children.add(new FadeTransition(
opacity: opacity,
child: item
child: item,
));
}
......@@ -318,7 +450,7 @@ class _PopupMenu<T> extends StatelessWidget {
),
);
},
child: child
child: child,
);
}
}
......@@ -345,7 +477,7 @@ class _PopupMenuRouteLayout extends SingleChildLayoutDelegate {
?? (position?.bottom != null ? size.height - (position.bottom - childSize.height) : _kMenuScreenPadding);
if (selectedItemOffset != null)
y -= selectedItemOffset + _kMenuVerticalPadding + _kMenuItemHeight / 2.0;
y -= selectedItemOffset + _kMenuVerticalPadding;
if (x < _kMenuScreenPadding)
x = _kMenuScreenPadding;
......@@ -402,10 +534,12 @@ class _PopupMenuRoute<T> extends PopupRoute<T> {
double selectedItemOffset;
if (initialValue != null) {
selectedItemOffset = 0.0;
for (int i = 0; i < items.length; i++) {
if (initialValue == items[i].value)
for (PopupMenuEntry<T> entry in items) {
if (entry.represents(initialValue)) {
selectedItemOffset += entry.height / 2.0;
break;
selectedItemOffset += items[i].height;
}
selectedItemOffset += entry.height;
}
}
......@@ -415,7 +549,7 @@ class _PopupMenuRoute<T> extends PopupRoute<T> {
return new CustomSingleChildLayout(
delegate: new _PopupMenuRouteLayout(position, selectedItemOffset),
child: menu
child: menu,
);
}
}
......@@ -432,6 +566,20 @@ class _PopupMenuRoute<T> extends PopupRoute<T> {
/// The `elevation` argument specifies the z-coordinate at which to place the
/// menu. The elevation defaults to 8, the appropriate elevation for popup
/// menus.
///
/// The positioning of the `initialValue` at the `position` is implemented by
/// iterating over the `items` to find the first whose
/// [PopupMenuEntry.represents] method returns true for `initialValue`, and then
/// summing the values of [PopupMenuEntry.height] for all the preceding widgets
/// in the list.
///
/// See also:
///
/// * [PopupMenuItem], a popup menu entry for a single value.
/// * [PopupMenuDivider], a popup menu entry that is just a horizontal line.
/// * [CheckedPopupMenuItem], a popup menu item with a checkmark.
/// * [PopupMenuButton], which provides an [IconButton] that shows a menu by
/// calling this method automatically.
Future<T> showMenu<T>({
@required BuildContext context,
RelativeRect position,
......@@ -467,6 +615,45 @@ typedef List<PopupMenuEntry<T>> PopupMenuItemBuilder<T>(BuildContext context);
/// because an item was selected. The value passed to [onSelected] is the value of
/// the selected menu item. If child is null then a standard 'navigation/more_vert'
/// icon is created.
///
/// This example shows a menu with four items, selecting between an enum's
/// values and setting a `_selection` field based on the selection.
///
/// ```dart
/// // This is the type used by the popup menu below.
/// enum WhyFarther { harder, smarter, selfStarter, tradingCharter }
///
/// // This menu button widget updates a _selection field (of type WhyFarther,
/// // not shown here).
/// new PopupMenuButton<WhyFarther>(
/// onSelected: (WhyFarther result) { setState(() { _selection = result; }); },
/// itemBuilder: (BuildContext context) => <PopupMenuEntry<WhyFarther>>[
/// const PopupMenuItem<WhyFarther>(
/// value: WhyFarther.harder,
/// child: const Text('Working a lot harder'),
/// ),
/// const PopupMenuItem<WhyFarther>(
/// value: WhyFarther.smarter,
/// child: const Text('Being a lot smarter'),
/// ),
/// const PopupMenuItem<WhyFarther>(
/// value: WhyFarther.selfStarter,
/// child: const Text('Being a self-starter'),
/// ),
/// const PopupMenuItem<WhyFarther>(
/// value: WhyFarther.tradingCharter,
/// child: const Text('Placed in charge of trading charter'),
/// ),
/// ],
/// )
/// ```
///
/// See also:
///
/// * [PopupMenuItem], a popup menu entry for a single value.
/// * [PopupMenuDivider], a popup menu entry that is just a horizontal line.
/// * [CheckedPopupMenuItem], a popup menu item with a checkmark.
/// * [showMenu], a method to dynamically show a popup menu at a given location.
class PopupMenuButton<T> extends StatefulWidget {
/// Creates a button that shows a popup menu.
///
......
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