-
Notifications
You must be signed in to change notification settings - Fork 29.7k
Implement CupertinoMenu and CupertinoNestedMenu #137936
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
|
Thank you very much for putting this together! I'll block out some time to go through the design. On basing it off of the pull_down_package, that might be fine, but the most foolproof way to make the lawyers happy is if @notDmDrl was willing to sign a CLA. If not, that's perfectly fine and we can see what guidance is available. |
|
@MitchellGoodwin @davidhicks980 I signed CLA |
Thank you! |
Piinks
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Welcome @davidhicks980
|
@notDmDrl just for the complete avoidance of doubt, can you confirm whether you are ok with this code landing with the usual Flutter license and copyright rather than your MIT license and copyright? |
|
@Hixie, yes, I am ok with it :) If there is anything that needs to be done with my package for this PR to land, please let me know |
|
Thanks @Piinks and team for taking a look and giving feedback! I'm still moving things around and fixing issues so I appreciate you all directing me towards any code I may have forgotten to prune. Also, some features that were missing have since been added -- in particular, sticky submenu headers. Whenever I have a moment, I'll draft up an overview of the mechanics of the menu in case any of my comments are ambiguous. Screen.Recording.2023-11-12.at.7.34.06.AM.mov |
Piinks
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As this is still a draft I have not been through it entirely, but I wanted to ask:
- Why should this be in the framework instead of in a separate package? We typically ask this in so far as weighing the maintenance burden of adopting this into the framework.
- Can it be split up into multiple PRs so it is easier to review? I think I saw mention of different features that have been added here. Landing a basic version and adding features in follow up PRs might be easier to do, since reviewers can better focus on one aspect at a time rather than everything all at once.
|
Sounds good to me! Thank you for sharing.
Absolutely! 💯 I would recommend filing an issue for each step (or PR) of the way so it is easier to track.
Rather than committing unfinished portions, or parts that won't work until other changes land, why not land a basic version of the menu that works, then follow after it with additional features and stylings like dividers and headers? We typically do this with large features, so basic functionality is available, and then we add more bells and whistles. :) I only skimmed this a couple of times so far since it is so large, but is this using MenuAnchor? It is meant to be a primitive for building menus such as these, and may simplify a lot of the code here (as well as reduce that burden of maintenance and bug fixes since it could re-use a bunch of logic 😉 ). |
|
Sounds great.
Whoops, I meant this, but you said it a lot more clearly!
I'm asking this here but it could also be asked in the first PR. I spent some time trying to wrap this around the MenuAnchor, but there were a few problems. The implementation appears to have changed since I last took a look -- I think it's now using an OverlayPortal instead of an OverlayEntry, and the bulk of the widget has moved out of the _MenuAnchorState._open method. However, it'd still be hard to work around the following:
@override
Widget build(BuildContext context) {
Widget child = OverlayPortal(
controller: _overlayController,
overlayChildBuilder: (BuildContext context) {
return _Submenu( // Submenu
anchor: this,
menuStyle: widget.style,
alignmentOffset: widget.alignmentOffset ?? Offset.zero,
menuPosition: _menuPosition,
clipBehavior: widget.clipBehavior,
menuChildren: widget.menuChildren,
crossAxisUnconstrained: widget.crossAxisUnconstrained,
);
},
child: _buildContents(context),
);
//...
Screen.Recording.2023-11-28.at.3.15.28.PM.mov
I actually began working on this menu using overlays, but there were some more issues I came across. When scaling nested layers, the untransformed position of the nested anchor couldn't be easily be accessed via context.renderObject(), because the position would be affected by individual widget scaling and the scale of the entire menu. Scaling of underlying layers led to fragile alignment between the top nested anchor and the bottom nested anchor, which need to align perfectly after the menu has fully closed. MatrixUtils could be used to perform an inverse transform, but complexity grew to a point where the FlowDelegate appeared to be a much simpler solution. Simulator.Screen.Recording.-.iPhone.15.Pro.-.2023-11-28.at.15.50.45.mp4Simulator.Screen.Recording.-.iPhone.15.Pro.-.2023-11-28.at.16.07.32.mp4
// Runs a simulation on the given animation controller, and returns whether or
// not the simulation has stopped running.
Future<bool> _runSimulation({
required _AnimationControllerWithStatusOverride controller,
required SpringSimulation simulation,
required bool forward,
}) {
final Completer<bool> simulationCompleter = Completer<bool>();
// Each layer has an animation controller that serves as a source-of-truth
// regarding the opened/opening/closed/closing status of the menu layer.
controller
..overrideStatus(
forward
? AnimationStatus.forward
: AnimationStatus.reverse,
)
..animateWith(simulation)
.whenCompleteOrCancel(() {
if (!controller.isAnimating) {
controller.overrideStatus(
forward
? AnimationStatus.completed
: AnimationStatus.dismissed,
);
simulationCompleter.complete(true);
} else {
simulationCompleter.complete(false);
}
});
return simulationCompleter.future;
}
// Sums all current layer animations. A function is used instead of a
// CompoundAnimation to make it easier to add/remove animations.
void _updateAnimationValue() {
double value = 0;
for (
final _AnimationControllerWithStatusOverride controller
in _layerIdToAnimation.values
) {
value += controller.value;
}
_nestingAnimation?.value = value;
}Simulator.Screen.Recording.-.iPhone.15.Pro.-.2023-11-28.at.16.54.03.mp4Simulator.Screen.Recording.-.iPhone.15.Pro.-.2023-11-28.at.16.53.32.mp4Anyways, sorry for the essay! Also, I went back and fixed some of the language.. let me know if it makes sense, though. I'll start splitting things up and sending PRs to get feedback. Thank you for taking the time to look through everything -- hopefully we can start landing features! |
|
@davidhicks980 These screenshots look epic. Anxiously awaiting this! Thanks so much for your work 👍 🥇 👍 🥇 |
|
Quick update on this: I did a small rewrite to use a MenuAnchor that exposed an "overlayChildBuilder" on the MenuAnchor base class and it seems to work well at the moment. Also removed most of the hacky bits. I'll submit a PR soon. @protected
Widget overlayChildBuilder(BuildContext context) {
return _Submenu(
anchor: this,
menuStyle: widget.style,
alignmentOffset: widget.alignmentOffset ?? Offset.zero,
menuPosition: _menuPosition,
clipBehavior: widget.clipBehavior,
menuChildren: widget.menuChildren,
crossAxisUnconstrained: widget.crossAxisUnconstrained,
);
} |
…onsider preventing scroll once the menu layer begins closing.
6dd5ab5 to
6bd9029
Compare
|
@davidhicks980 Thanks for the work! Looking forward to it |
|
Any update on this? Would love for some way to try this out in the interim @davidhicks980, this is just what I need for my app. |
|
@JaidynBluesky If you need all of the features right now, you should be able to copy the menu_item.dart and the menu.dart: https://github.com/flutter/flutter/blob/6bd90293300da86e31d9f0644c5e21a8e52c1a82/packages/flutter/lib/src/cupertino/menu.dart Just place menu.dart and menu_item.dart in your project folder and replace the local imports with a single "import package:flutter/cupertino": /// In menu.dart + menu_item.dart
/// Replace this:
import 'colors.dart';
import 'constants.dart';
import 'icons.dart';
/// With this
import 'package:flutter/cupertino.dart';From there, you should just be able to import like any other file: /// Import from wherever you copied the files
import 'menu_item.dart';
import 'menu.dart';
///.... Use like any other widget
CupertinoMenuButton<int>(
itemBuilder: (BuildContext context) {
return <CupertinoMenuEntry<int>>[
const CupertinoMenuItem<int>(value: 1, child: Text('A')),
const CupertinoMenuItem<int>(value: 2, child: Text('B')),
const CupertinoMenuItem<int>(value: 3, child: Text('C')),
CupertinoNestedMenu<int>(
itemBuilder: (BuildContext context) {
return [
const CupertinoMenuItem<int>(value: 1, child: Text('D')),
const CupertinoMenuItem<int>(value: 2, child: Text('E')),
const CupertinoMenuItem<int>(value: 3, child: Text('F')),
];
},
subtitle: const Text('Subtitle'),
title: const TextSpan(text: 'Nested Menu'),
),
];
},
child: Icon(CupertinoIcons.plus, size: 24)
);
///....
(I'm not on a device I can test at the moment, so sorry in advance for any errors you may encounter. Let me know if anything is complicated though): That said, if you only need a simple menu with a single layer of items, the new version based on MenuAnchor will probably be closer to the final version of the menu, and has many more tests: |
|
Hey @davidhicks980, we just came across this in triage today. Is #143712 meant to replace this PR? If so, should we close this? |
|
@Piinks - yes to both! I'll go ahead and close. |

Resolves #60298, notDmDrl/pull_down_button#26
This PR adds a CupertinoMenu, CupertinoNestedMenu, and associated widgets that increase feature parity with iOS.
I'm submitting a draft PR to collect feedback on the overall API and layout design before writing more extensive layout and API tests. I have added a few tests based on the PopupMenu widget, and I've done some manual accessibility testing across iOS, Android, and MacOS. Comments and an example have been added, but could use review. I also added TODO annotations for any particularly pressing questions I have.
I'm still fairly new to Flutter, so I apologize for any unconventional code. Let me know if there are any pieces we need to tear out or rethink. I'm interested to hear different ideas regarding how to solve this menu, so I appreciate you all taking a look!
Also, I used @notDmDrl's repo as a base for my contributions: https://github.com/notDmDrl/pull_down_button, so I'm not sure whether I should contact him with regards to signing a CLA.
Basic example:
Screen.Recording.2023-11-06.at.7.00.25.AM.mov
@MitchellGoodwin
Pre-launch Checklist
///).If you need help, consider asking for advice on the #hackers-new channel on Discord.