Instantly share code, notes, and snippets.
Created
June 25, 2019 05:11
-
Star
0
(0)
You must be signed in to star a gist -
Fork
0
(0)
You must be signed in to fork a gist
-
Save zmtzawqlp/2c70dfe209fcd25cf1d38d0decfdcca7 to your computer and use it in GitHub Desktop.
MyPopupMenuButton
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import 'package:flutter/material.dart'; | |
import 'package:flutter/foundation.dart'; | |
///弹出下拉框 | |
class MyPopupMenuItem<T> extends MyPopupMenuEntry<T> { | |
/// Creates an item for a popup menu. | |
/// | |
/// By default, the item is [enabled]. | |
/// | |
/// The `height` and `enabled` arguments must not be null. | |
const MyPopupMenuItem({ | |
Key key, | |
this.value, | |
this.enabled = true, | |
this.height = 48.0, | |
@required this.child, | |
}) : assert(enabled != null), | |
assert(height != null), | |
super(key: key); | |
/// The value that will be returned by [showMenu] if this entry is selected. | |
final T value; | |
/// 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 | |
bool represents(T value) => value == this.value; | |
@override | |
PopupMenuItemState<T, MyPopupMenuItem<T>> createState() => | |
PopupMenuItemState<T, MyPopupMenuItem<T>>(); | |
} | |
/// The [State] for [MyPopupMenuItem] subclasses. | |
/// | |
/// By default this implements the basic styling and layout of Material Design | |
/// popup menu items. | |
/// | |
/// The [buildChild] method can be overridden to adjust exactly what gets placed | |
/// in the menu. By default it returns [MyPopupMenuItem.child]. | |
/// | |
/// The [handleTap] method can be overridden to adjust exactly what happens when | |
/// the item is tapped. By default, it uses [Navigator.pop] to return the | |
/// [MyPopupMenuItem.value] from the menu route. | |
/// | |
/// This class takes two type arguments. The second, `W`, is the exact type of | |
/// the [Widget] that is using this [State]. It must be a subclass of | |
/// [MyPopupMenuItem]. The first, `T`, must match the type argument of that widget | |
/// class, and is the type of values returned from this menu. | |
class PopupMenuItemState<T, W extends MyPopupMenuItem<T>> extends State<W> { | |
/// The menu item contents. | |
/// | |
/// Used by the [build] method. | |
/// | |
/// By default, this returns [MyPopupMenuItem.child]. Override this to put | |
/// something else in the menu entry. | |
@protected | |
Widget buildChild() => widget.child; | |
/// The handler for when the user selects the menu item. | |
/// | |
/// Used by the [InkWell] inserted by the [build] method. | |
/// | |
/// By default, uses [Navigator.pop] to return the [MyPopupMenuItem.value] from | |
/// the menu route. | |
@protected | |
void handleTap() { | |
Navigator.pop<T>(context, widget.value); | |
} | |
@override | |
Widget build(BuildContext context) { | |
final ThemeData theme = Theme.of(context); | |
TextStyle style = theme.textTheme.subhead; | |
if (!widget.enabled) style = style.copyWith(color: theme.disabledColor); | |
// | |
Widget item = AnimatedDefaultTextStyle( | |
style: style, | |
duration: kThemeChangeDuration, | |
child: Baseline( | |
baseline: widget.height, | |
baselineType: style.textBaseline, | |
child: buildChild(), | |
)); | |
if (!widget.enabled) { | |
final bool isDark = theme.brightness == Brightness.dark; | |
item = IconTheme.merge( | |
data: IconThemeData(opacity: isDark ? 0.5 : 0.38), | |
child: item, | |
); | |
} | |
return GestureDetector( | |
behavior: HitTestBehavior.translucent, | |
onTap: widget.enabled ? handleTap : null, | |
child: Container( | |
height: widget.height, | |
//padding: const EdgeInsets.symmetric(horizontal: _kMenuHorizontalPadding), | |
child: buildChild(), | |
), | |
); | |
} | |
} | |
// Examples can assume: | |
// enum Commands { heroAndScholar, hurricaneCame } | |
// dynamic _heroAndScholar; | |
const Duration _kMenuDuration = Duration(milliseconds: 300); | |
//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; | |
//const double _kMenuWidthStep = 56.0; | |
//const double _kMenuWidthStep = 56.0; | |
const double _kMenuScreenPadding = 8.0; | |
/// A base class for entries in a material design popup menu. | |
/// | |
/// The popup menu widget uses this interface to interact with the menu items. | |
/// To show a popup menu, use the [showMenu] function. To create a button that | |
/// shows a popup menu, consider using [MyPopupMenuButton]. | |
/// | |
/// 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 [MyPopupMenuEntry] may represent multiple values, for example a row with | |
/// several icons, or a single entry, for example a menu item with an icon (see | |
/// [MyPopupMenuItem]), or no value at all (for example, [MyPopupMenuDivider]). | |
/// | |
/// See also: | |
/// | |
/// * [MyPopupMenuItem], a popup menu entry for a single value. | |
/// * [MyPopupMenuDivider], 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. | |
/// * [MyPopupMenuButton], an [IconButton] that automatically shows a menu when | |
/// it is tapped. | |
abstract class MyPopupMenuEntry<T> extends StatefulWidget { | |
/// Abstract const constructor. This constructor enables subclasses to provide | |
/// const constructors so that they can be used in const expressions. | |
const MyPopupMenuEntry({Key key}) : super(key: key); | |
/// The amount of vertical space occupied by this entry. | |
/// | |
/// 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; | |
/// 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 [MyPopupMenuEntry] 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 adapts the [Divider] for use in popup menus. | |
/// | |
/// See also: | |
/// | |
/// * [MyPopupMenuItem], for the kinds of items that this widget divides. | |
/// * [showMenu], a method to dynamically show a popup menu at a given location. | |
/// * [MyPopupMenuButton], an [IconButton] that automatically shows a menu when | |
/// it is tapped. | |
// ignore: prefer_void_to_null, https://github.com/dart-lang/sdk/issues/34416 | |
class MyPopupMenuDivider extends MyPopupMenuEntry<Null> { | |
/// Creates a horizontal divider for a popup menu. | |
/// | |
/// By default, the divider has a height of 16 logical pixels. | |
const MyPopupMenuDivider( | |
{Key key, this.height = _kMenuDividerHeight, this.color}) | |
: super(key: key); | |
/// The height of the divider entry. | |
/// | |
/// Defaults to 16 pixels. | |
@override | |
final double height; | |
final Color color; | |
@override | |
// ignore: prefer_void_to_null, https://github.com/dart-lang/sdk/issues/34416 | |
bool represents(Null value) => false; | |
@override | |
_PopupMenuDividerState createState() => _PopupMenuDividerState(); | |
} | |
class _PopupMenuDividerState extends State<MyPopupMenuDivider> { | |
@override | |
Widget build(BuildContext context) => Divider( | |
height: widget.height, | |
color: widget.color, | |
); | |
} | |
/// An item in a material design popup menu. | |
/// | |
/// To show a popup menu, use the [showMenu] function. To create a button that | |
/// shows a popup menu, consider using [MyPopupMenuButton]. | |
/// | |
/// To show a checkmark next to a popup menu item, consider using | |
/// [CheckedPopupMenuItem]. | |
/// | |
/// Typically the [child] of a [MyPopupMenuItem] is a [Text] widget. More | |
/// elaborate menus with icons can use a [ListTile]. By default, a | |
/// [MyPopupMenuItem] 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: Text('Working a lot harder'), | |
/// ) | |
/// ``` | |
/// | |
/// See the example at [MyPopupMenuButton] 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 [MyPopupMenuItem]s that use [Text] widgets in their [child] | |
/// slot aligned with the text of [CheckedPopupMenuItem]s or of [MyPopupMenuItem] | |
/// that use a [ListTile] in their [child] slot. | |
/// | |
/// See also: | |
/// | |
/// * [MyPopupMenuDivider], which can be used to divide items from each other. | |
/// * [CheckedPopupMenuItem], a variant of [MyPopupMenuItem] with a checkmark. | |
/// * [showMenu], a method to dynamically show a popup menu at a given location. | |
/// * [MyPopupMenuButton], an [IconButton] that automatically shows a menu when | |
/// it is tapped. | |
/// The [State] for [MyPopupMenuItem] subclasses. | |
/// | |
/// By default this implements the basic styling and layout of Material Design | |
/// popup menu items. | |
/// | |
/// The [buildChild] method can be overridden to adjust exactly what gets placed | |
/// in the menu. By default it returns [MyPopupMenuItem.child]. | |
/// | |
/// The [handleTap] method can be overridden to adjust exactly what happens when | |
/// the item is tapped. By default, it uses [Navigator.pop] to return the | |
/// [MyPopupMenuItem.value] from the menu route. | |
/// | |
/// This class takes two type arguments. The second, `W`, is the exact type of | |
/// the [Widget] that is using this [State]. It must be a subclass of | |
/// [MyPopupMenuItem]. The first, `T`, must match the type argument of that widget | |
/// class, and is the type of values returned from this menu. | |
/// An item with a checkmark in a material design popup menu. | |
/// | |
/// To show a popup menu, use the [showMenu] function. To create a button that | |
/// shows a popup menu, consider using [MyPopupMenuButton]. | |
/// | |
/// A [CheckedPopupMenuItem] is 48 pixels high, which matches the default height | |
/// of a [MyPopupMenuItem]. 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 | |
/// 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>>[ | |
/// CheckedPopupMenuItem<Commands>( | |
/// checked: _heroAndScholar, | |
/// value: Commands.heroAndScholar, | |
/// child: const Text('Hero and scholar'), | |
/// ), | |
/// const PopupMenuDivider(), | |
/// const PopupMenuItem<Commands>( | |
/// value: Commands.hurricaneCame, | |
/// child: ListTile(leading: Icon(null), title: 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: | |
/// | |
/// * [MyPopupMenuItem], a popup menu entry for picking a command (as opposed to | |
/// toggling a value). | |
/// * [MyPopupMenuDivider], a popup menu entry that is just a horizontal line. | |
/// * [showMenu], a method to dynamically show a popup menu at a given location. | |
/// * [MyPopupMenuButton], an [IconButton] that automatically shows a menu when | |
/// it is tapped. | |
class CheckedPopupMenuItem<T> extends MyPopupMenuItem<T> { | |
/// Creates a popup menu item with a checkmark. | |
/// | |
/// 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, | |
}) : assert(checked != null), | |
super( | |
key: key, | |
value: value, | |
enabled: enabled, | |
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() => | |
_CheckedPopupMenuItemState<T>(); | |
} | |
class _CheckedPopupMenuItemState<T> | |
extends PopupMenuItemState<T, CheckedPopupMenuItem<T>> | |
with SingleTickerProviderStateMixin { | |
static const Duration _fadeDuration = Duration(milliseconds: 150); | |
AnimationController _controller; | |
Animation<double> get _opacity => _controller.view; | |
@override | |
void initState() { | |
super.initState(); | |
_controller = AnimationController(duration: _fadeDuration, vsync: this) | |
..value = widget.checked ? 1.0 : 0.0 | |
..addListener(() => setState(() {/* animation changed */})); | |
} | |
@override | |
void handleTap() { | |
// This fades the checkmark in or out when tapped. | |
if (widget.checked) | |
_controller.reverse(); | |
else | |
_controller.forward(); | |
super.handleTap(); | |
} | |
@override | |
Widget buildChild() { | |
return ListTile( | |
enabled: widget.enabled, | |
leading: FadeTransition( | |
opacity: _opacity, | |
child: Icon(_controller.isDismissed ? null : Icons.done)), | |
title: widget.child, | |
); | |
} | |
} | |
class _PopupMenu<T> extends StatelessWidget { | |
const _PopupMenu( | |
{Key key, | |
this.route, | |
this.semanticLabel, | |
this.itemWidth, | |
this.popupBackground}) | |
: super(key: key); | |
final _PopupMenuRoute<T> route; | |
final String semanticLabel; | |
final double itemWidth; | |
///弹出框背景色 | |
final Color popupBackground; | |
@override | |
Widget build(BuildContext context) { | |
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 += 1) { | |
final double start = (i + 1) * unit; | |
final double end = (start + 1.5 * unit).clamp(0.0, 1.0); | |
final CurvedAnimation opacity = | |
CurvedAnimation(parent: route.animation, curve: Interval(start, end)); | |
Widget item = route.items[i]; | |
if (route.initialValue != null && | |
route.items[i].represents(route.initialValue)) { | |
item = Container( | |
color: Theme.of(context).highlightColor, | |
child: item, | |
); | |
} | |
children.add(FadeTransition( | |
opacity: opacity, | |
child: item, | |
)); | |
} | |
final CurveTween opacity = | |
CurveTween(curve: const Interval(0.0, 1.0 / 3.0)); | |
final CurveTween width = CurveTween(curve: Interval(0.0, unit)); | |
final CurveTween height = | |
CurveTween(curve: Interval(0.0, unit * route.items.length)); | |
// final Widget child = ConstrainedBox( | |
// constraints: BoxConstraints( | |
//// minWidth: _kMenuMinWidth, | |
//// maxWidth: _kMenuMaxWidth, | |
// maxWidth: itemWidth ?? double.infinity, | |
// ), | |
// child: IntrinsicWidth( | |
// stepWidth: _kMenuWidthStep, | |
// child: Semantics( | |
// scopesRoute: true, | |
// namesRoute: true, | |
// explicitChildNodes: true, | |
// label: semanticLabel, | |
// child: SingleChildScrollView( | |
//// padding: const EdgeInsets.symmetric( | |
//// vertical: _kMenuVerticalPadding | |
//// ), | |
// child: ListBody(children: children), | |
// ), | |
// ), | |
// ), | |
// ); | |
Widget child = IntrinsicWidth( | |
stepWidth: 1.0, | |
child: Semantics( | |
scopesRoute: true, | |
namesRoute: true, | |
explicitChildNodes: true, | |
label: semanticLabel, | |
child: SingleChildScrollView( | |
// padding: const EdgeInsets.symmetric( | |
// vertical: _kMenuVerticalPadding | |
// ), | |
// padding: EdgeInsets.all(0.0), | |
child: ListBody(children: children), | |
), | |
), | |
); | |
if (itemWidth != null) | |
child = Container( | |
child: child, | |
width: itemWidth, | |
); | |
return AnimatedBuilder( | |
animation: route.animation, | |
builder: (BuildContext context, Widget child) { | |
return Opacity( | |
opacity: opacity.evaluate(route.animation), | |
child: Container( | |
padding: EdgeInsets.all(0.0), | |
decoration: BoxDecoration( | |
border: Border.all(color: Colors.grey, width: 1.0), | |
boxShadow: [ | |
BoxShadow( | |
color: Color(0xff999999).withOpacity(0.4), | |
blurRadius: 5.0, // has the effect of softening the shadow | |
spreadRadius: 1.0, // has the effect of extending the shadow | |
// offset: Offset( | |
// 10.0, // horizontal, move right 10 | |
// 10.0, // vertical, move down 10 | |
// ), | |
) | |
]), | |
child: Material( | |
type: MaterialType.card, | |
elevation: 0.0 ?? route.elevation, | |
color: | |
popupBackground ?? Theme.of(context).scaffoldBackgroundColor, | |
// shadowColor: Color(0xff999999).withOpacity(0.4), | |
//borderRadius: BorderRadius.all(Radius.circular(3.0)), | |
child: Align( | |
alignment: AlignmentDirectional.topEnd, | |
widthFactor: width.evaluate(route.animation), | |
heightFactor: height.evaluate(route.animation), | |
child: child, | |
), | |
), | |
), | |
); | |
}, | |
child: child, | |
); | |
} | |
} | |
// Positioning of the menu on the screen. | |
class _PopupMenuRouteLayout extends SingleChildLayoutDelegate { | |
_PopupMenuRouteLayout( | |
this.position, this.selectedItemOffset, this.textDirection); | |
// Rectangle of underlying button, relative to the overlay's dimensions. | |
final RelativeRect position; | |
// The distance from the top of the menu to the middle of selected item. | |
// | |
// This will be null if there's no item to position in this way. | |
final double selectedItemOffset; | |
// Whether to prefer going to the left or to the right. | |
final TextDirection textDirection; | |
// We put the child wherever position specifies, so long as it will fit within | |
// the specified parent size padded (inset) by 8. If necessary, we adjust the | |
// child's position so that it fits. | |
@override | |
BoxConstraints getConstraintsForChild(BoxConstraints constraints) { | |
// The menu can be at most the size of the overlay minus 8.0 pixels in each | |
// direction. | |
return BoxConstraints.loose(constraints.biggest - | |
const Offset(_kMenuScreenPadding * 2.0, _kMenuScreenPadding * 2.0)); | |
} | |
@override | |
Offset getPositionForChild(Size size, Size childSize) { | |
// size: The size of the overlay. | |
// childSize: The size of the menu, when fully open, as determined by | |
// getConstraintsForChild. | |
// Find the ideal vertical position. | |
double y; | |
if (selectedItemOffset == null) { | |
y = position.top; | |
} else { | |
y = position.top + | |
(size.height - position.top - position.bottom) / 2.0 - | |
selectedItemOffset; | |
} | |
// Find the ideal horizontal position. | |
double x; | |
if (position.left > position.right) { | |
// Menu button is closer to the right edge, so grow to the left, aligned to the right edge. | |
x = size.width - position.right - childSize.width; | |
} else if (position.left < position.right) { | |
// Menu button is closer to the left edge, so grow to the right, aligned to the left edge. | |
x = position.left; | |
} else { | |
// Menu button is equidistant from both edges, so grow in reading direction. | |
assert(textDirection != null); | |
switch (textDirection) { | |
case TextDirection.rtl: | |
x = size.width - position.right - childSize.width; | |
break; | |
case TextDirection.ltr: | |
x = position.left; | |
break; | |
} | |
} | |
// Avoid going outside an area defined as the rectangle 8.0 pixels from the | |
// edge of the screen in every direction. | |
if (x < _kMenuScreenPadding) | |
x = _kMenuScreenPadding; | |
else if (x + childSize.width > size.width - _kMenuScreenPadding) | |
x = size.width - childSize.width - _kMenuScreenPadding; | |
if (y < _kMenuScreenPadding) | |
y = _kMenuScreenPadding; | |
else if (y + childSize.height > size.height - _kMenuScreenPadding) | |
y = size.height - childSize.height - _kMenuScreenPadding; | |
return Offset(x, y); | |
} | |
@override | |
bool shouldRelayout(_PopupMenuRouteLayout oldDelegate) { | |
return position != oldDelegate.position; | |
} | |
} | |
class _PopupMenuRoute<T> extends PopupRoute<T> { | |
_PopupMenuRoute( | |
{this.position, | |
this.items, | |
this.initialValue, | |
this.elevation, | |
this.theme, | |
this.barrierLabel, | |
this.semanticLabel, | |
this.itemWidth, | |
this.popupBackground}); | |
final RelativeRect position; | |
final List<MyPopupMenuEntry<T>> items; | |
final dynamic initialValue; | |
final double elevation; | |
final ThemeData theme; | |
final String semanticLabel; | |
final double itemWidth; | |
///弹出框背景色 | |
final Color popupBackground; | |
@override | |
Animation<double> createAnimation() { | |
return CurvedAnimation( | |
parent: super.createAnimation(), | |
curve: Curves.linear, | |
reverseCurve: const Interval(0.0, _kMenuCloseIntervalEnd)); | |
} | |
@override | |
Duration get transitionDuration => _kMenuDuration; | |
@override | |
bool get barrierDismissible => true; | |
@override | |
Color get barrierColor => null; | |
@override | |
final String barrierLabel; | |
@override | |
Widget buildPage(BuildContext context, Animation<double> animation, | |
Animation<double> secondaryAnimation) { | |
double selectedItemOffset; | |
if (initialValue != null) { | |
double y = _kMenuVerticalPadding; | |
for (MyPopupMenuEntry<T> entry in items) { | |
if (entry.represents(initialValue)) { | |
selectedItemOffset = y + entry.height / 2.0; | |
break; | |
} | |
y += entry.height; | |
} | |
} | |
Widget menu = _PopupMenu<T>( | |
route: this, | |
semanticLabel: semanticLabel, | |
itemWidth: itemWidth, | |
popupBackground: popupBackground, | |
); | |
if (theme != null) menu = Theme(data: theme, child: menu); | |
return MediaQuery.removePadding( | |
context: context, | |
removeTop: true, | |
removeBottom: true, | |
removeLeft: true, | |
removeRight: true, | |
child: Builder( | |
builder: (BuildContext context) { | |
return CustomSingleChildLayout( | |
delegate: _PopupMenuRouteLayout( | |
position, | |
selectedItemOffset, | |
Directionality.of(context), | |
), | |
child: menu, | |
); | |
}, | |
), | |
); | |
} | |
} | |
/// Show a popup menu that contains the `items` at `position`. | |
/// | |
/// If `initialValue` is specified then the first item with a matching value | |
/// will be highlighted and the value of `position` gives the rectangle whose | |
/// vertical center will be aligned with the vertical center of the highlighted | |
/// item (when possible). | |
/// | |
/// If `initialValue` is not specified then the top of the menu will be aligned | |
/// with the top of the `position` rectangle. | |
/// | |
/// In both cases, the menu position will be adjusted if necessary to fit on the | |
/// screen. | |
/// | |
/// Horizontally, the menu is positioned so that it grows in the direction that | |
/// has the most room. For example, if the `position` describes a rectangle on | |
/// the left edge of the screen, then the left edge of the menu is aligned with | |
/// the left edge of the `position`, and the menu grows to the right. If both | |
/// edges of the `position` are equidistant from the opposite edge of the | |
/// screen, then the ambient [Directionality] is used as a tie-breaker, | |
/// preferring to grow in the reading direction. | |
/// | |
/// The positioning of the `initialValue` at the `position` is implemented by | |
/// iterating over the `items` to find the first whose | |
/// [MyPopupMenuEntry.represents] method returns true for `initialValue`, and then | |
/// summing the values of [MyPopupMenuEntry.height] for all the preceding widgets | |
/// in the list. | |
/// | |
/// 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 `context` argument is used to look up the [Navigator] and [Theme] for | |
/// the menu. It is only used when the method is called. Its corresponding | |
/// widget can be safely removed from the tree before the popup menu is closed. | |
/// | |
/// The `semanticLabel` argument is used by accessibility frameworks to | |
/// announce screen transitions when the menu is opened and closed. If this | |
/// label is not provided, it will default to | |
/// [MaterialLocalizations.popupMenuLabel]. | |
/// | |
/// See also: | |
/// | |
/// * [MyPopupMenuItem], a popup menu entry for a single value. | |
/// * [MyPopupMenuDivider], a popup menu entry that is just a horizontal line. | |
/// * [CheckedPopupMenuItem], a popup menu item with a checkmark. | |
/// * [MyPopupMenuButton], which provides an [IconButton] that shows a menu by | |
/// calling this method automatically. | |
/// * [SemanticsConfiguration.namesRoute], for a description of edge triggered | |
/// semantics. | |
Future<T> showMenu<T>({ | |
@required BuildContext context, | |
RelativeRect position, | |
@required List<MyPopupMenuEntry<T>> items, | |
T initialValue, | |
double elevation = 8.0, | |
String semanticLabel, | |
double itemWidth, | |
///弹出框背景色 | |
Color popupBackground, | |
}) { | |
assert(context != null); | |
assert(items != null && items.isNotEmpty); | |
assert(debugCheckHasMaterialLocalizations(context)); | |
String label = semanticLabel; | |
switch (defaultTargetPlatform) { | |
case TargetPlatform.iOS: | |
label = semanticLabel; | |
break; | |
case TargetPlatform.android: | |
case TargetPlatform.fuchsia: | |
label = | |
semanticLabel ?? MaterialLocalizations.of(context)?.popupMenuLabel; | |
} | |
return Navigator.push( | |
context, | |
_PopupMenuRoute<T>( | |
position: position, | |
items: items, | |
initialValue: initialValue, | |
elevation: elevation, | |
semanticLabel: label, | |
theme: Theme.of(context, shadowThemeOnly: true), | |
barrierLabel: | |
MaterialLocalizations.of(context).modalBarrierDismissLabel, | |
itemWidth: itemWidth, | |
popupBackground: popupBackground)); | |
} | |
/// Signature for the callback invoked when a menu item is selected. The | |
/// argument is the value of the [MyPopupMenuItem] that caused its menu to be | |
/// dismissed. | |
/// | |
/// Used by [MyPopupMenuButton.onSelected]. | |
typedef PopupMenuItemSelected<T> = void Function(T value); | |
/// Signature for the callback invoked when a [MyPopupMenuButton] is dismissed | |
/// without selecting an item. | |
/// | |
/// Used by [MyPopupMenuButton.onCanceled]. | |
typedef PopupMenuCanceled = void Function(); | |
/// Signature used by [MyPopupMenuButton] to lazily construct the items shown when | |
/// the button is pressed. | |
/// | |
/// Used by [MyPopupMenuButton.itemBuilder]. | |
typedef PopupMenuItemBuilder<T> = List<MyPopupMenuEntry<T>> Function( | |
BuildContext context); | |
/// Displays a menu when pressed and calls [onSelected] when the menu is dismissed | |
/// because an item was selected. The value passed to [onSelected] is the value of | |
/// the selected menu item. | |
/// | |
/// One of [child] or [icon] may be provided, but not both. If [icon] is provided, | |
/// then [MyPopupMenuButton] behaves like an [IconButton]. | |
/// | |
/// If both are null, then a standard overflow icon is created (depending on the | |
/// platform). | |
/// | |
/// ## Sample code | |
/// | |
/// 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). | |
/// PopupMenuButton<WhyFarther>( | |
/// onSelected: (WhyFarther result) { setState(() { _selection = result; }); }, | |
/// itemBuilder: (BuildContext context) => <PopupMenuEntry<WhyFarther>>[ | |
/// const PopupMenuItem<WhyFarther>( | |
/// value: WhyFarther.harder, | |
/// child: Text('Working a lot harder'), | |
/// ), | |
/// const PopupMenuItem<WhyFarther>( | |
/// value: WhyFarther.smarter, | |
/// child: Text('Being a lot smarter'), | |
/// ), | |
/// const PopupMenuItem<WhyFarther>( | |
/// value: WhyFarther.selfStarter, | |
/// child: Text('Being a self-starter'), | |
/// ), | |
/// const PopupMenuItem<WhyFarther>( | |
/// value: WhyFarther.tradingCharter, | |
/// child: Text('Placed in charge of trading charter'), | |
/// ), | |
/// ], | |
/// ) | |
/// ``` | |
/// | |
/// See also: | |
/// | |
/// * [MyPopupMenuItem], a popup menu entry for a single value. | |
/// * [MyPopupMenuDivider], 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 MyPopupMenuButton<T> extends StatefulWidget { | |
/// Creates a button that shows a popup menu. | |
/// | |
/// The [itemBuilder] argument must not be null. | |
const MyPopupMenuButton( | |
{Key key, | |
@required this.itemBuilder, | |
this.initialValue, | |
this.onSelected, | |
this.onCanceled, | |
this.tooltip, | |
this.elevation = 8.0, | |
this.padding = const EdgeInsets.all(8.0), | |
this.child, | |
this.icon, | |
this.offset = Offset.zero, | |
this.onOpened, | |
this.onClosed, | |
this.itemWidth, | |
this.popupBackground}) | |
: assert(itemBuilder != null), | |
assert(offset != null), | |
assert(!(child != null && | |
icon != null)), // fails if passed both parameters | |
super(key: key); | |
/// Called when the button is pressed to create the items to show in the menu. | |
final PopupMenuItemBuilder<T> itemBuilder; | |
/// The value of the menu item, if any, that should be highlighted when the menu opens. | |
final T initialValue; | |
/// Called when the user selects a value from the popup menu created by this button. | |
/// | |
/// If the popup menu is dismissed without selecting a value, [onCanceled] is | |
/// called instead. | |
final PopupMenuItemSelected<T> onSelected; | |
/// Called when the user dismisses the popup menu without selecting an item. | |
/// | |
/// If the user selects a value, [onSelected] is called instead. | |
final PopupMenuCanceled onCanceled; | |
final void Function() onOpened; | |
final void Function() onClosed; | |
/// Text that describes the action that will occur when the button is pressed. | |
/// | |
/// This text is displayed when the user long-presses on the button and is | |
/// used for accessibility. | |
final String tooltip; | |
/// The z-coordinate at which to place the menu when open. This controls the | |
/// size of the shadow below the menu. | |
/// | |
/// Defaults to 8, the appropriate elevation for popup menus. | |
final double elevation; | |
/// Matches IconButton's 8 dps padding by default. In some cases, notably where | |
/// this button appears as the trailing element of a list item, it's useful to be able | |
/// to set the padding to zero. | |
final EdgeInsetsGeometry padding; | |
/// If provided, the widget used for this button. | |
final Widget child; | |
/// If provided, the icon used for this button. | |
final Icon icon; | |
/// The offset applied to the Popup Menu Button. | |
/// | |
/// When not set, the Popup Menu Button will be positioned directly next to | |
/// the button that was used to create it. | |
final Offset offset; | |
///Item的大小 | |
final double itemWidth; | |
///弹出框背景色 | |
final Color popupBackground; | |
@override | |
_MyPopupMenuButtonState<T> createState() => _MyPopupMenuButtonState<T>(); | |
} | |
class _MyPopupMenuButtonState<T> extends State<MyPopupMenuButton<T>> { | |
void showButtonMenu() { | |
final RenderBox button = context.findRenderObject(); | |
final RenderBox overlay = Overlay.of(context).context.findRenderObject(); | |
final RelativeRect position = RelativeRect.fromRect( | |
Rect.fromPoints( | |
button.localToGlobal(widget.offset, ancestor: overlay), | |
button.localToGlobal(button.size.bottomRight(widget.offset), | |
ancestor: overlay), | |
), | |
Offset.zero & overlay.size, | |
); | |
showMenu<T>( | |
context: context, | |
elevation: widget.elevation, | |
items: widget.itemBuilder(context), | |
initialValue: widget.initialValue, | |
position: position, | |
itemWidth: widget.itemWidth, | |
popupBackground: widget.popupBackground) | |
.then<void>((T newValue) { | |
if (!mounted) return null; | |
if (widget.onClosed != null) widget.onClosed(); | |
if (newValue == null) { | |
if (widget.onCanceled != null) widget.onCanceled(); | |
return null; | |
} | |
if (widget.onSelected != null) widget.onSelected(newValue); | |
}); | |
if (widget.onOpened != null) widget.onOpened(); | |
} | |
Icon _getIcon(TargetPlatform platform) { | |
assert(platform != null); | |
switch (platform) { | |
case TargetPlatform.android: | |
case TargetPlatform.fuchsia: | |
return const Icon(Icons.more_vert); | |
case TargetPlatform.iOS: | |
return const Icon(Icons.more_horiz); | |
} | |
return null; | |
} | |
@override | |
Widget build(BuildContext context) { | |
assert(debugCheckHasMaterialLocalizations(context)); | |
return widget.child != null | |
? InkWell( | |
onTap: showButtonMenu, | |
child: widget.child, | |
) | |
: IconButton( | |
icon: widget.icon ?? _getIcon(Theme.of(context).platform), | |
padding: widget.padding, | |
tooltip: widget.tooltip ?? | |
MaterialLocalizations.of(context).showMenuTooltip, | |
onPressed: showButtonMenu, | |
); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment