popupButton_my.dart 32 KB

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