Hibok
Nie możesz wybrać więcej, niż 25 tematów Tematy muszą się zaczynać od litery lub cyfry, mogą zawierać myślniki ('-') i mogą mieć do 35 znaków.
 
 
 
 
 
 

968 wiersze
34 KiB

  1. // Copyright 2015 The Chromium Authors. All rights reserved.
  2. // Use of this source code is governed by a BSD-style license that can be
  3. // found in the LICENSE file.
  4. import 'dart:async';
  5. import 'package:flutter/foundation.dart';
  6. import 'package:flutter/material.dart';
  7. import 'package:flutter/widgets.dart';
  8. // Examples can assume:
  9. // enum Commands { heroAndScholar, hurricaneCame }
  10. // dynamic _heroAndScholar;
  11. // dynamic _selection;
  12. // BuildContext context;
  13. // void setState(VoidCallback fn) { }
  14. const Duration _kMenuDuration = Duration(milliseconds: 300);
  15. const double _kMenuCloseIntervalEnd = 2.0 / 3.0;
  16. const double _kMenuItemHeight = 48.0;
  17. const double _kMenuDividerHeight = 16.0;
  18. const double _kMenuMaxWidth = 5.0 * _kMenuWidthStep;
  19. const double _kMenuMinWidth = 2.0 * _kMenuWidthStep;
  20. const double _kMenuVerticalPadding = 0; // 8.0;
  21. const double _kMenuWidthStep = 56.0;
  22. const double _kMenuScreenPadding = 8.0;
  23. /// A base class for entries in a material design popup menu.
  24. ///
  25. /// The popup menu widget uses this interface to interact with the menu items.
  26. /// To show a popup menu, use the [showMenu] function. To create a button that
  27. /// shows a popup menu, consider using [PopupMenuButton].
  28. ///
  29. /// The type `T` is the type of the value(s) the entry represents. All the
  30. /// entries in a given menu must represent values with consistent types.
  31. ///
  32. /// A [PopupMenuEntry] may represent multiple values, for example a row with
  33. /// several icons, or a single entry, for example a menu item with an icon (see
  34. /// [PopupMenuItem]), or no value at all (for example, [PopupMenuDivider]).
  35. ///
  36. /// See also:
  37. ///
  38. /// * [PopupMenuItem], a popup menu entry for a single value.
  39. /// * [PopupMenuDivider], a popup menu entry that is just a horizontal line.
  40. /// * [CheckedPopupMenuItem], a popup menu item with a checkmark.
  41. /// * [showMenu], a method to dynamically show a popup menu at a given location.
  42. /// * [PopupMenuButton], an [IconButton] that automatically shows a menu when
  43. /// it is tapped.
  44. abstract class PopupMenuEntry<T> extends StatefulWidget {
  45. /// Abstract const constructor. This constructor enables subclasses to provide
  46. /// const constructors so that they can be used in const expressions.
  47. const PopupMenuEntry({Key key}) : super(key: key);
  48. /// The amount of vertical space occupied by this entry.
  49. ///
  50. /// This value is used at the time the [showMenu] method is called, if the
  51. /// `initialValue` argument is provided, to determine the position of this
  52. /// entry when aligning the selected entry over the given `position`. It is
  53. /// otherwise ignored.
  54. double get height;
  55. /// Whether this entry represents a particular value.
  56. ///
  57. /// This method is used by [showMenu], when it is called, to align the entry
  58. /// representing the `initialValue`, if any, to the given `position`, and then
  59. /// later is called on each entry to determine if it should be highlighted (if
  60. /// the method returns true, the entry will have its background color set to
  61. /// the ambient [ThemeData.highlightColor]). If `initialValue` is null, then
  62. /// this method is not called.
  63. ///
  64. /// If the [PopupMenuEntry] represents a single value, this should return true
  65. /// if the argument matches that value. If it represents multiple values, it
  66. /// should return true if the argument matches any of them.
  67. bool represents(T value);
  68. }
  69. /// A horizontal divider in a material design popup menu.
  70. ///
  71. /// This widget adapts the [Divider] for use in popup menus.
  72. ///
  73. /// See also:
  74. ///
  75. /// * [PopupMenuItem], for the kinds of items that this widget divides.
  76. /// * [showMenu], a method to dynamically show a popup menu at a given location.
  77. /// * [PopupMenuButton], an [IconButton] that automatically shows a menu when
  78. /// it is tapped.
  79. // ignore: prefer_void_to_null, https://github.com/dart-lang/sdk/issues/34416
  80. class PopupMenuDivider extends PopupMenuEntry<Null> {
  81. /// Creates a horizontal divider for a popup menu.
  82. ///
  83. /// By default, the divider has a height of 16 logical pixels.
  84. const PopupMenuDivider({Key key, this.height = _kMenuDividerHeight})
  85. : super(key: key);
  86. /// The height of the divider entry.
  87. ///
  88. /// Defaults to 16 pixels.
  89. @override
  90. final double height;
  91. @override
  92. bool represents(void value) => false;
  93. @override
  94. _PopupMenuDividerState createState() => _PopupMenuDividerState();
  95. }
  96. class _PopupMenuDividerState extends State<PopupMenuDivider> {
  97. @override
  98. Widget build(BuildContext context) => Divider(height: widget.height);
  99. }
  100. /// An item in a material design popup menu.
  101. ///
  102. /// To show a popup menu, use the [showMenu] function. To create a button that
  103. /// shows a popup menu, consider using [PopupMenuButton].
  104. ///
  105. /// To show a checkmark next to a popup menu item, consider using
  106. /// [CheckedPopupMenuItem].
  107. ///
  108. /// Typically the [child] of a [PopupMenuItem] is a [Text] widget. More
  109. /// elaborate menus with icons can use a [ListTile]. By default, a
  110. /// [PopupMenuItem] is 48 pixels high. If you use a widget with a different
  111. /// height, it must be specified in the [height] property.
  112. ///
  113. /// {@tool sample}
  114. ///
  115. /// Here, a [Text] widget is used with a popup menu item. The `WhyFarther` type
  116. /// is an enum, not shown here.
  117. ///
  118. /// ```dart
  119. /// const PopupMenuItem<WhyFarther>(
  120. /// value: WhyFarther.harder,
  121. /// child: Text('Working a lot harder'),
  122. /// )
  123. /// ```
  124. /// {@end-tool}
  125. ///
  126. /// See the example at [PopupMenuButton] for how this example could be used in a
  127. /// complete menu, and see the example at [CheckedPopupMenuItem] for one way to
  128. /// keep the text of [PopupMenuItem]s that use [Text] widgets in their [child]
  129. /// slot aligned with the text of [CheckedPopupMenuItem]s or of [PopupMenuItem]
  130. /// that use a [ListTile] in their [child] slot.
  131. ///
  132. /// See also:
  133. ///
  134. /// * [PopupMenuDivider], which can be used to divide items from each other.
  135. /// * [CheckedPopupMenuItem], a variant of [PopupMenuItem] with a checkmark.
  136. /// * [showMenu], a method to dynamically show a popup menu at a given location.
  137. /// * [PopupMenuButton], an [IconButton] that automatically shows a menu when
  138. /// it is tapped.
  139. class PopupMenuItem<T> extends PopupMenuEntry<T> {
  140. /// Creates an item for a popup menu.
  141. ///
  142. /// By default, the item is [enabled].
  143. ///
  144. /// The `height` and `enabled` arguments must not be null.
  145. const PopupMenuItem({
  146. Key key,
  147. this.value,
  148. this.enabled = true,
  149. this.height = _kMenuItemHeight,
  150. @required this.child,
  151. }) : assert(enabled != null),
  152. assert(height != null),
  153. super(key: key);
  154. /// The value that will be returned by [showMenu] if this entry is selected.
  155. final T value;
  156. /// Whether the user is permitted to select this entry.
  157. ///
  158. /// Defaults to true. If this is false, then the item will not react to
  159. /// touches.
  160. final bool enabled;
  161. /// The height of the entry.
  162. ///
  163. /// Defaults to 48 pixels.
  164. @override
  165. final double height;
  166. /// The widget below this widget in the tree.
  167. ///
  168. /// Typically a single-line [ListTile] (for menus with icons) or a [Text]. An
  169. /// appropriate [DefaultTextStyle] is put in scope for the child. In either
  170. /// case, the text should be short enough that it won't wrap.
  171. final Widget child;
  172. @override
  173. bool represents(T value) => value == this.value;
  174. @override
  175. PopupMenuItemState<T, PopupMenuItem<T>> createState() =>
  176. PopupMenuItemState<T, PopupMenuItem<T>>();
  177. }
  178. /// The [State] for [PopupMenuItem] subclasses.
  179. ///
  180. /// By default this implements the basic styling and layout of Material Design
  181. /// popup menu items.
  182. ///
  183. /// The [buildChild] method can be overridden to adjust exactly what gets placed
  184. /// in the menu. By default it returns [PopupMenuItem.child].
  185. ///
  186. /// The [handleTap] method can be overridden to adjust exactly what happens when
  187. /// the item is tapped. By default, it uses [Navigator.pop] to return the
  188. /// [PopupMenuItem.value] from the menu route.
  189. ///
  190. /// This class takes two type arguments. The second, `W`, is the exact type of
  191. /// the [Widget] that is using this [State]. It must be a subclass of
  192. /// [PopupMenuItem]. The first, `T`, must match the type argument of that widget
  193. /// class, and is the type of values returned from this menu.
  194. class PopupMenuItemState<T, W extends PopupMenuItem<T>> extends State<W> {
  195. /// The menu item contents.
  196. ///
  197. /// Used by the [build] method.
  198. ///
  199. /// By default, this returns [PopupMenuItem.child]. Override this to put
  200. /// something else in the menu entry.
  201. @protected
  202. Widget buildChild() => widget.child;
  203. /// The handler for when the user selects the menu item.
  204. ///
  205. /// Used by the [InkWell] inserted by the [build] method.
  206. ///
  207. /// By default, uses [Navigator.pop] to return the [PopupMenuItem.value] from
  208. /// the menu route.
  209. @protected
  210. void handleTap() {
  211. Navigator.pop<T>(context, widget.value);
  212. }
  213. @override
  214. Widget build(BuildContext context) {
  215. final ThemeData theme = Theme.of(context);
  216. TextStyle style = theme.textTheme.subhead;
  217. if (!widget.enabled) style = style.copyWith(color: theme.disabledColor);
  218. Widget item = AnimatedDefaultTextStyle(
  219. style: style,
  220. duration: kThemeChangeDuration,
  221. child: buildChild(),
  222. );
  223. if (!widget.enabled) {
  224. final bool isDark = theme.brightness == Brightness.dark;
  225. item = IconTheme.merge(
  226. data: IconThemeData(opacity: isDark ? 0.5 : 0.38),
  227. child: item,
  228. );
  229. }
  230. return InkWell(
  231. onTap: widget.enabled ? handleTap : null,
  232. child: Container(
  233. decoration:
  234. BoxDecoration(borderRadius: BorderRadius.all(Radius.circular(20))),
  235. alignment: Alignment.center,
  236. child: item,
  237. ),
  238. );
  239. }
  240. }
  241. /// An item with a checkmark in a material design popup menu.
  242. ///
  243. /// To show a popup menu, use the [showMenu] function. To create a button that
  244. /// shows a popup menu, consider using [PopupMenuButton].
  245. ///
  246. /// A [CheckedPopupMenuItem] is 48 pixels high, which matches the default height
  247. /// of a [PopupMenuItem]. The horizontal layout uses a [ListTile]; the checkmark
  248. /// is an [Icons.done] icon, shown in the [ListTile.leading] position.
  249. ///
  250. /// {@tool sample}
  251. ///
  252. /// Suppose a `Commands` enum exists that lists the possible commands from a
  253. /// particular popup menu, including `Commands.heroAndScholar` and
  254. /// `Commands.hurricaneCame`, and further suppose that there is a
  255. /// `_heroAndScholar` member field which is a boolean. The example below shows a
  256. /// menu with one menu item with a checkmark that can toggle the boolean, and
  257. /// one menu item without a checkmark for selecting the second option. (It also
  258. /// shows a divider placed between the two menu items.)
  259. ///
  260. /// ```dart
  261. /// PopupMenuButton<Commands>(
  262. /// onSelected: (Commands result) {
  263. /// switch (result) {
  264. /// case Commands.heroAndScholar:
  265. /// setState(() { _heroAndScholar = !_heroAndScholar; });
  266. /// break;
  267. /// case Commands.hurricaneCame:
  268. /// // ...handle hurricane option
  269. /// break;
  270. /// // ...other items handled here
  271. /// }
  272. /// },
  273. /// itemBuilder: (BuildContext context) => <PopupMenuEntry<Commands>>[
  274. /// CheckedPopupMenuItem<Commands>(
  275. /// checked: _heroAndScholar,
  276. /// value: Commands.heroAndScholar,
  277. /// child: const Text('Hero and scholar'),
  278. /// ),
  279. /// const PopupMenuDivider(),
  280. /// const PopupMenuItem<Commands>(
  281. /// value: Commands.hurricaneCame,
  282. /// child: ListTile(leading: Icon(null), title: Text('Bring hurricane')),
  283. /// ),
  284. /// // ...other items listed here
  285. /// ],
  286. /// )
  287. /// ```
  288. /// {@end-tool}
  289. ///
  290. /// In particular, observe how the second menu item uses a [ListTile] with a
  291. /// blank [Icon] in the [ListTile.leading] position to get the same alignment as
  292. /// the item with the checkmark.
  293. ///
  294. /// See also:
  295. ///
  296. /// * [PopupMenuItem], a popup menu entry for picking a command (as opposed to
  297. /// toggling a value).
  298. /// * [PopupMenuDivider], a popup menu entry that is just a horizontal line.
  299. /// * [showMenu], a method to dynamically show a popup menu at a given location.
  300. /// * [PopupMenuButton], an [IconButton] that automatically shows a menu when
  301. /// it is tapped.
  302. class CheckedPopupMenuItem<T> extends PopupMenuItem<T> {
  303. /// Creates a popup menu item with a checkmark.
  304. ///
  305. /// By default, the menu item is [enabled] but unchecked. To mark the item as
  306. /// checked, set [checked] to true.
  307. ///
  308. /// The `checked` and `enabled` arguments must not be null.
  309. const CheckedPopupMenuItem({
  310. Key key,
  311. T value,
  312. this.checked = false,
  313. bool enabled = true,
  314. Widget child,
  315. }) : assert(checked != null),
  316. super(
  317. key: key,
  318. value: value,
  319. enabled: enabled,
  320. child: child,
  321. );
  322. /// Whether to display a checkmark next to the menu item.
  323. ///
  324. /// Defaults to false.
  325. ///
  326. /// When true, an [Icons.done] checkmark is displayed.
  327. ///
  328. /// When this popup menu item is selected, the checkmark will fade in or out
  329. /// as appropriate to represent the implied new state.
  330. final bool checked;
  331. /// The widget below this widget in the tree.
  332. ///
  333. /// Typically a [Text]. An appropriate [DefaultTextStyle] is put in scope for
  334. /// the child. The text should be short enough that it won't wrap.
  335. ///
  336. /// This widget is placed in the [ListTile.title] slot of a [ListTile] whose
  337. /// [ListTile.leading] slot is an [Icons.done] icon.
  338. @override
  339. Widget get child => super.child;
  340. @override
  341. _CheckedPopupMenuItemState<T> createState() =>
  342. _CheckedPopupMenuItemState<T>();
  343. }
  344. class _CheckedPopupMenuItemState<T>
  345. extends PopupMenuItemState<T, CheckedPopupMenuItem<T>>
  346. with SingleTickerProviderStateMixin {
  347. static const Duration _fadeDuration = Duration(milliseconds: 150);
  348. AnimationController _controller;
  349. Animation<double> get _opacity => _controller.view;
  350. @override
  351. void initState() {
  352. super.initState();
  353. _controller = AnimationController(duration: _fadeDuration, vsync: this)
  354. ..value = widget.checked ? 1.0 : 0.0
  355. ..addListener(() => setState(() {/* animation changed */}));
  356. }
  357. @override
  358. void handleTap() {
  359. // This fades the checkmark in or out when tapped.
  360. if (widget.checked)
  361. _controller.reverse();
  362. else
  363. _controller.forward();
  364. super.handleTap();
  365. }
  366. @override
  367. Widget buildChild() {
  368. return ListTile(
  369. enabled: widget.enabled,
  370. leading: FadeTransition(
  371. opacity: _opacity,
  372. child: Icon(_controller.isDismissed ? null : Icons.done),
  373. ),
  374. title: widget.child,
  375. );
  376. }
  377. }
  378. class _PopupMenu<T> extends StatelessWidget {
  379. const _PopupMenu({
  380. Key key,
  381. this.route,
  382. this.semanticLabel,
  383. }) : super(key: key);
  384. final _PopupMenuRoute<T> route;
  385. final String semanticLabel;
  386. @override
  387. Widget build(BuildContext context) {
  388. final double unit = 1.0 /
  389. (route.items.length +
  390. 1.5); // 1.0 for the width and 0.5 for the last item's fade.
  391. final List<Widget> children = <Widget>[];
  392. for (int i = 0; i < route.items.length; i += 1) {
  393. final double start = (i + 1) * unit;
  394. final double end = (start + 1.5 * unit).clamp(0.0, 1.0);
  395. final CurvedAnimation opacity = CurvedAnimation(
  396. parent: route.animation,
  397. curve: Interval(start, end),
  398. );
  399. Widget item = route.items[i];
  400. if (route.initialValue != null &&
  401. route.items[i].represents(route.initialValue)) {
  402. item = Container(
  403. color: Theme.of(context).highlightColor,
  404. child: item,
  405. );
  406. }
  407. children.add(FadeTransition(
  408. opacity: opacity,
  409. child: item,
  410. ));
  411. }
  412. final CurveTween opacity =
  413. CurveTween(curve: const Interval(0.0, 1.0 / 3.0));
  414. final CurveTween width = CurveTween(curve: Interval(0.0, unit));
  415. final CurveTween height =
  416. CurveTween(curve: Interval(0.0, unit * route.items.length));
  417. final Widget child = ConstrainedBox(
  418. constraints: const BoxConstraints(
  419. minWidth: _kMenuMinWidth,
  420. maxWidth: _kMenuMaxWidth,
  421. ),
  422. child: IntrinsicWidth(
  423. stepWidth: _kMenuWidthStep,
  424. child: Semantics(
  425. scopesRoute: true,
  426. namesRoute: true,
  427. explicitChildNodes: true,
  428. label: semanticLabel,
  429. child: SingleChildScrollView(
  430. padding:
  431. const EdgeInsets.symmetric(vertical: _kMenuVerticalPadding),
  432. child: ListBody(children: children),
  433. ),
  434. ),
  435. ),
  436. );
  437. return AnimatedBuilder(
  438. animation: route.animation,
  439. builder: (BuildContext context, Widget child) {
  440. return Opacity(
  441. opacity: opacity.evaluate(route.animation),
  442. child: Material(
  443. type: MaterialType.card,
  444. elevation: route.elevation,
  445. child: Align(
  446. alignment: AlignmentDirectional.topEnd,
  447. widthFactor: width.evaluate(route.animation),
  448. heightFactor: height.evaluate(route.animation),
  449. child: child,
  450. ),
  451. ),
  452. );
  453. },
  454. child: child,
  455. );
  456. }
  457. }
  458. // Positioning of the menu on the screen.
  459. class _PopupMenuRouteLayout extends SingleChildLayoutDelegate {
  460. _PopupMenuRouteLayout(
  461. this.position, this.selectedItemOffset, this.textDirection);
  462. // Rectangle of underlying button, relative to the overlay's dimensions.
  463. final RelativeRect position;
  464. // The distance from the top of the menu to the middle of selected item.
  465. //
  466. // This will be null if there's no item to position in this way.
  467. final double selectedItemOffset;
  468. // Whether to prefer going to the left or to the right.
  469. final TextDirection textDirection;
  470. // We put the child wherever position specifies, so long as it will fit within
  471. // the specified parent size padded (inset) by 8. If necessary, we adjust the
  472. // child's position so that it fits.
  473. @override
  474. BoxConstraints getConstraintsForChild(BoxConstraints constraints) {
  475. // The menu can be at most the size of the overlay minus 8.0 pixels in each
  476. // direction.
  477. return BoxConstraints.loose(constraints.biggest -
  478. const Offset(_kMenuScreenPadding * 2.0, _kMenuScreenPadding * 2.0));
  479. }
  480. @override
  481. Offset getPositionForChild(Size size, Size childSize) {
  482. // size: The size of the overlay.
  483. // childSize: The size of the menu, when fully open, as determined by
  484. // getConstraintsForChild.
  485. // Find the ideal vertical position.
  486. double y;
  487. if (selectedItemOffset == null) {
  488. y = position.top;
  489. } else {
  490. y = position.top +
  491. (size.height - position.top - position.bottom) / 2.0 -
  492. selectedItemOffset;
  493. }
  494. // Find the ideal horizontal position.
  495. double x;
  496. if (position.left > position.right) {
  497. // Menu button is closer to the right edge, so grow to the left, aligned to the right edge.
  498. x = size.width - position.right - childSize.width;
  499. } else if (position.left < position.right) {
  500. // Menu button is closer to the left edge, so grow to the right, aligned to the left edge.
  501. x = position.left;
  502. } else {
  503. // Menu button is equidistant from both edges, so grow in reading direction.
  504. assert(textDirection != null);
  505. switch (textDirection) {
  506. case TextDirection.rtl:
  507. x = size.width - position.right - childSize.width;
  508. break;
  509. case TextDirection.ltr:
  510. x = position.left;
  511. break;
  512. }
  513. }
  514. // Avoid going outside an area defined as the rectangle 8.0 pixels from the
  515. // edge of the screen in every direction.
  516. if (x < _kMenuScreenPadding)
  517. x = _kMenuScreenPadding;
  518. else if (x + childSize.width > size.width - _kMenuScreenPadding)
  519. x = size.width - childSize.width - _kMenuScreenPadding;
  520. if (y < _kMenuScreenPadding)
  521. y = _kMenuScreenPadding;
  522. else if (y + childSize.height > size.height - _kMenuScreenPadding)
  523. y = size.height - childSize.height - _kMenuScreenPadding;
  524. return Offset(x, y);
  525. }
  526. @override
  527. bool shouldRelayout(_PopupMenuRouteLayout oldDelegate) {
  528. return position != oldDelegate.position;
  529. }
  530. }
  531. class _PopupMenuRoute<T> extends PopupRoute<T> {
  532. _PopupMenuRoute({
  533. this.position,
  534. this.items,
  535. this.initialValue,
  536. this.elevation,
  537. this.theme,
  538. this.barrierLabel,
  539. this.semanticLabel,
  540. });
  541. final RelativeRect position;
  542. final List<PopupMenuEntry<T>> items;
  543. final dynamic initialValue;
  544. final double elevation;
  545. final ThemeData theme;
  546. final String semanticLabel;
  547. @override
  548. Animation<double> createAnimation() {
  549. return CurvedAnimation(
  550. parent: super.createAnimation(),
  551. curve: Curves.linear,
  552. reverseCurve: const Interval(0.0, _kMenuCloseIntervalEnd),
  553. );
  554. }
  555. @override
  556. Duration get transitionDuration => _kMenuDuration;
  557. @override
  558. bool get barrierDismissible => true;
  559. @override
  560. Color get barrierColor => null;
  561. @override
  562. final String barrierLabel;
  563. @override
  564. Widget buildPage(BuildContext context, Animation<double> animation,
  565. Animation<double> secondaryAnimation) {
  566. double selectedItemOffset;
  567. if (initialValue != null) {
  568. double y = _kMenuVerticalPadding;
  569. for (PopupMenuEntry<T> entry in items) {
  570. if (entry.represents(initialValue)) {
  571. selectedItemOffset = y + entry.height / 2.0;
  572. break;
  573. }
  574. y += entry.height;
  575. }
  576. }
  577. Widget menu = _PopupMenu<T>(route: this, semanticLabel: semanticLabel);
  578. if (theme != null) menu = Theme(data: theme, child: menu);
  579. return MediaQuery.removePadding(
  580. context: context,
  581. removeTop: true,
  582. removeBottom: true,
  583. removeLeft: true,
  584. removeRight: true,
  585. child: Builder(
  586. builder: (BuildContext context) {
  587. return CustomSingleChildLayout(
  588. delegate: _PopupMenuRouteLayout(
  589. position,
  590. selectedItemOffset,
  591. Directionality.of(context),
  592. ),
  593. child: menu,
  594. );
  595. },
  596. ),
  597. );
  598. }
  599. }
  600. /// Show a popup menu that contains the `items` at `position`.
  601. ///
  602. /// `items` should be non-null and not empty.
  603. ///
  604. /// If `initialValue` is specified then the first item with a matching value
  605. /// will be highlighted and the value of `position` gives the rectangle whose
  606. /// vertical center will be aligned with the vertical center of the highlighted
  607. /// item (when possible).
  608. ///
  609. /// If `initialValue` is not specified then the top of the menu will be aligned
  610. /// with the top of the `position` rectangle.
  611. ///
  612. /// In both cases, the menu position will be adjusted if necessary to fit on the
  613. /// screen.
  614. ///
  615. /// Horizontally, the menu is positioned so that it grows in the direction that
  616. /// has the most room. For example, if the `position` describes a rectangle on
  617. /// the left edge of the screen, then the left edge of the menu is aligned with
  618. /// the left edge of the `position`, and the menu grows to the right. If both
  619. /// edges of the `position` are equidistant from the opposite edge of the
  620. /// screen, then the ambient [Directionality] is used as a tie-breaker,
  621. /// preferring to grow in the reading direction.
  622. ///
  623. /// The positioning of the `initialValue` at the `position` is implemented by
  624. /// iterating over the `items` to find the first whose
  625. /// [PopupMenuEntry.represents] method returns true for `initialValue`, and then
  626. /// summing the values of [PopupMenuEntry.height] for all the preceding widgets
  627. /// in the list.
  628. ///
  629. /// The `elevation` argument specifies the z-coordinate at which to place the
  630. /// menu. The elevation defaults to 8, the appropriate elevation for popup
  631. /// menus.
  632. ///
  633. /// The `context` argument is used to look up the [Navigator] and [Theme] for
  634. /// the menu. It is only used when the method is called. Its corresponding
  635. /// widget can be safely removed from the tree before the popup menu is closed.
  636. ///
  637. /// The `semanticLabel` argument is used by accessibility frameworks to
  638. /// announce screen transitions when the menu is opened and closed. If this
  639. /// label is not provided, it will default to
  640. /// [MaterialLocalizations.popupMenuLabel].
  641. ///
  642. /// See also:
  643. ///
  644. /// * [PopupMenuItem], a popup menu entry for a single value.
  645. /// * [PopupMenuDivider], a popup menu entry that is just a horizontal line.
  646. /// * [CheckedPopupMenuItem], a popup menu item with a checkmark.
  647. /// * [PopupMenuButton], which provides an [IconButton] that shows a menu by
  648. /// calling this method automatically.
  649. /// * [SemanticsConfiguration.namesRoute], for a description of edge triggered
  650. /// semantics.
  651. Future<T> showMenu<T>({
  652. @required BuildContext context,
  653. @required RelativeRect position,
  654. @required List<PopupMenuEntry<T>> items,
  655. T initialValue,
  656. double elevation = 8.0,
  657. String semanticLabel,
  658. }) {
  659. assert(context != null);
  660. assert(position != null);
  661. assert(items != null && items.isNotEmpty);
  662. assert(debugCheckHasMaterialLocalizations(context));
  663. String label = semanticLabel;
  664. switch (defaultTargetPlatform) {
  665. case TargetPlatform.iOS:
  666. label = semanticLabel;
  667. break;
  668. case TargetPlatform.android:
  669. case TargetPlatform.fuchsia:
  670. label =
  671. semanticLabel ?? MaterialLocalizations.of(context)?.popupMenuLabel;
  672. }
  673. return Navigator.push(
  674. context,
  675. _PopupMenuRoute<T>(
  676. position: position,
  677. items: items,
  678. initialValue: initialValue,
  679. elevation: elevation,
  680. semanticLabel: label,
  681. theme: Theme.of(context, shadowThemeOnly: true),
  682. barrierLabel:
  683. MaterialLocalizations.of(context).modalBarrierDismissLabel,
  684. ));
  685. }
  686. /// Signature for the callback invoked when a menu item is selected. The
  687. /// argument is the value of the [PopupMenuItem] that caused its menu to be
  688. /// dismissed.
  689. ///
  690. /// Used by [PopupMenuButton.onSelected].
  691. typedef PopupMenuItemSelected<T> = void Function(T value);
  692. /// Signature for the callback invoked when a [PopupMenuButton] is dismissed
  693. /// without selecting an item.
  694. ///
  695. /// Used by [PopupMenuButton.onCanceled].
  696. typedef PopupMenuCanceled = void Function();
  697. /// Signature used by [PopupMenuButton] to lazily construct the items shown when
  698. /// the button is pressed.
  699. ///
  700. /// Used by [PopupMenuButton.itemBuilder].
  701. typedef PopupMenuItemBuilder<T> = List<PopupMenuEntry<T>> Function(
  702. BuildContext context);
  703. /// Displays a menu when pressed and calls [onSelected] when the menu is dismissed
  704. /// because an item was selected. The value passed to [onSelected] is the value of
  705. /// the selected menu item.
  706. ///
  707. /// One of [child] or [icon] may be provided, but not both. If [icon] is provided,
  708. /// then [PopupMenuButton] behaves like an [IconButton].
  709. ///
  710. /// If both are null, then a standard overflow icon is created (depending on the
  711. /// platform).
  712. ///
  713. /// {@tool sample}
  714. ///
  715. /// This example shows a menu with four items, selecting between an enum's
  716. /// values and setting a `_selection` field based on the selection.
  717. ///
  718. /// ```dart
  719. /// // This is the type used by the popup menu below.
  720. /// enum WhyFarther { harder, smarter, selfStarter, tradingCharter }
  721. ///
  722. /// // This menu button widget updates a _selection field (of type WhyFarther,
  723. /// // not shown here).
  724. /// PopupMenuButton<WhyFarther>(
  725. /// onSelected: (WhyFarther result) { setState(() { _selection = result; }); },
  726. /// itemBuilder: (BuildContext context) => <PopupMenuEntry<WhyFarther>>[
  727. /// const PopupMenuItem<WhyFarther>(
  728. /// value: WhyFarther.harder,
  729. /// child: Text('Working a lot harder'),
  730. /// ),
  731. /// const PopupMenuItem<WhyFarther>(
  732. /// value: WhyFarther.smarter,
  733. /// child: Text('Being a lot smarter'),
  734. /// ),
  735. /// const PopupMenuItem<WhyFarther>(
  736. /// value: WhyFarther.selfStarter,
  737. /// child: Text('Being a self-starter'),
  738. /// ),
  739. /// const PopupMenuItem<WhyFarther>(
  740. /// value: WhyFarther.tradingCharter,
  741. /// child: Text('Placed in charge of trading charter'),
  742. /// ),
  743. /// ],
  744. /// )
  745. /// ```
  746. /// {@end-tool}
  747. ///
  748. /// See also:
  749. ///
  750. /// * [PopupMenuItem], a popup menu entry for a single value.
  751. /// * [PopupMenuDivider], a popup menu entry that is just a horizontal line.
  752. /// * [CheckedPopupMenuItem], a popup menu item with a checkmark.
  753. /// * [showMenu], a method to dynamically show a popup menu at a given location.
  754. class PopupMenuButton<T> extends StatefulWidget {
  755. /// Creates a button that shows a popup menu.
  756. ///
  757. /// The [itemBuilder] argument must not be null.
  758. const PopupMenuButton({
  759. Key key,
  760. @required this.itemBuilder,
  761. this.initialValue,
  762. this.onSelected,
  763. this.onCanceled,
  764. this.tooltip,
  765. this.elevation = 8.0,
  766. this.padding = const EdgeInsets.all(8.0),
  767. this.child,
  768. this.icon,
  769. this.offset = Offset.zero,
  770. this.enabled = true,
  771. }) : assert(itemBuilder != null),
  772. assert(offset != null),
  773. assert(enabled != null),
  774. assert(!(child != null &&
  775. icon != null)), // fails if passed both parameters
  776. super(key: key);
  777. /// Called when the button is pressed to create the items to show in the menu.
  778. final PopupMenuItemBuilder<T> itemBuilder;
  779. /// The value of the menu item, if any, that should be highlighted when the menu opens.
  780. final T initialValue;
  781. /// Called when the user selects a value from the popup menu created by this button.
  782. ///
  783. /// If the popup menu is dismissed without selecting a value, [onCanceled] is
  784. /// called instead.
  785. final PopupMenuItemSelected<T> onSelected;
  786. /// Called when the user dismisses the popup menu without selecting an item.
  787. ///
  788. /// If the user selects a value, [onSelected] is called instead.
  789. final PopupMenuCanceled onCanceled;
  790. /// Text that describes the action that will occur when the button is pressed.
  791. ///
  792. /// This text is displayed when the user long-presses on the button and is
  793. /// used for accessibility.
  794. final String tooltip;
  795. /// The z-coordinate at which to place the menu when open. This controls the
  796. /// size of the shadow below the menu.
  797. ///
  798. /// Defaults to 8, the appropriate elevation for popup menus.
  799. final double elevation;
  800. /// Matches IconButton's 8 dps padding by default. In some cases, notably where
  801. /// this button appears as the trailing element of a list item, it's useful to be able
  802. /// to set the padding to zero.
  803. final EdgeInsetsGeometry padding;
  804. /// If provided, the widget used for this button.
  805. final Widget child;
  806. /// If provided, the icon used for this button.
  807. final Icon icon;
  808. /// The offset applied to the Popup Menu Button.
  809. ///
  810. /// When not set, the Popup Menu Button will be positioned directly next to
  811. /// the button that was used to create it.
  812. final Offset offset;
  813. /// Whether this popup menu button is interactive.
  814. ///
  815. /// Must be non-null, defaults to `true`
  816. ///
  817. /// If `true` the button will respond to presses by displaying the menu.
  818. ///
  819. /// If `false`, the button is styled with the disabled color from the
  820. /// current [Theme] and will not respond to presses or show the popup
  821. /// menu and [onSelected], [onCanceled] and [itemBuilder] will not be called.
  822. ///
  823. /// This can be useful in situations where the app needs to show the button,
  824. /// but doesn't currently have anything to show in the menu.
  825. final bool enabled;
  826. @override
  827. _PopupMenuButtonState<T> createState() => _PopupMenuButtonState<T>();
  828. }
  829. class _PopupMenuButtonState<T> extends State<PopupMenuButton<T>> {
  830. void showButtonMenu() {
  831. final RenderBox button = context.findRenderObject();
  832. final RenderBox overlay = Overlay.of(context).context.findRenderObject();
  833. final RelativeRect position = RelativeRect.fromRect(
  834. Rect.fromPoints(
  835. button.localToGlobal(widget.offset, ancestor: overlay),
  836. button.localToGlobal(button.size.bottomRight(Offset.zero),
  837. ancestor: overlay),
  838. ),
  839. Offset.zero & overlay.size,
  840. );
  841. final List<PopupMenuEntry<T>> items = widget.itemBuilder(context);
  842. // Only show the menu if there is something to show
  843. if (items.isNotEmpty) {
  844. showMenu<T>(
  845. context: context,
  846. elevation: widget.elevation,
  847. items: items,
  848. initialValue: widget.initialValue,
  849. position: position,
  850. ).then<void>((T newValue) {
  851. if (!mounted) return null;
  852. if (newValue == null) {
  853. if (widget.onCanceled != null) widget.onCanceled();
  854. return null;
  855. }
  856. if (widget.onSelected != null) widget.onSelected(newValue);
  857. });
  858. }
  859. }
  860. Icon _getIcon(TargetPlatform platform) {
  861. assert(platform != null);
  862. switch (platform) {
  863. case TargetPlatform.android:
  864. case TargetPlatform.fuchsia:
  865. return const Icon(Icons.more_vert);
  866. case TargetPlatform.iOS:
  867. return const Icon(Icons.more_horiz);
  868. }
  869. return null;
  870. }
  871. @override
  872. Widget build(BuildContext context) {
  873. assert(debugCheckHasMaterialLocalizations(context));
  874. return widget.child != null
  875. ? InkWell(
  876. onTap: widget.enabled ? showButtonMenu : null,
  877. child: widget.child,
  878. )
  879. : IconButton(
  880. icon: widget.icon ?? _getIcon(Theme.of(context).platform),
  881. padding: widget.padding,
  882. tooltip: widget.tooltip ??
  883. MaterialLocalizations.of(context).showMenuTooltip,
  884. onPressed: widget.enabled ? showButtonMenu : null,
  885. );
  886. }
  887. }