-
Notifications
You must be signed in to change notification settings - Fork 29.7k
[material/menu_anchor.dart] Add animations to MenuAnchor. #176494
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
base: master
Are you sure you want to change the base?
Conversation
|
Looks good and seems consistent with https://m3.material.io/components/menus/guidelines#691d98ba-0375-4681-8c47-a5c3e7b58798. Only suggestion I would have is to not have a scrollbar appear temporarily during the animation, if there's not going to be one in the completed animated state. |
|
@guidezpl I was thinking the same thing. Fix is pushed. |
dkwingsmt
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.
Generally looking good.
| } | ||
| } | ||
|
|
||
| class _TweenCurve extends Curve { |
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.
Add a brief doc. (What's its difference from Interval?)
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.
Done!
| } | ||
|
|
||
| // Handles interactions that could open or close the menu. | ||
| void _handleTogglingInteraction() { |
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.
I think it's easier to understand as _handleFocusChange instead of _handleTogglingInteraction, because it made me wonder why any event would trigger either opening or closing depending on some kind of condition, while in fact it's a function that has to deal with "onGainFocus" and "onLoseFocus" together due to API design and split at the first line of the body.
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.
Yeah, I agree. I added _handleTogglingInteraction because hovering can now trigger _handleFocusChange, even if the text button is already focused.
if (_buttonFocusNode.hasPrimaryFocus) {
_handleFocusChange();
} else {
_buttonFocusNode.requestFocus();
}But, I can just call handleFocusChange()
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.
I suggest either of the following way:
- Split
_handleTogglingInteractioninto_openSubmenuand_closeSubmenu, andhandlePointerHovercalls the opening one, and_handleFocusChangecalls either based on status, which is clearer as well. - Rename
_handleTogglingInteractionto something like_openCloseMenuBasedOnFocus.
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.
I kept the closing behavior in the _handleFocusChange function and split off the opening behavior into a function called _maybeOpenMenuOnHoverOrFocus
QuncCccccc
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.
Overall looks good to me! Thanks for adding this feature:)
| const Duration _kMenuClosingDuration = Duration(milliseconds: 150); | ||
|
|
||
| // The default curve used to animate the height of the menu panel when opening. | ||
| const Curve _kMenuPanelHeightForwardCurve = Cubic(.3, 0, 0, 1); |
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.
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.
Hmm, so I pushed the change, but the animation now looks noticeably worse:
new.mov
old.mov
It's more noticeable on screen with a higher refresh rate, but the opacity animation of the items doesn't line up with the motion of the menu:
new_slow.mov
old_slow.mov
I imagine this may be because the Material spec was made with the Android implementation in mind, which doesn't use a cubic bezier for md.sys.motion.easing.emphasized.
All of that to say, how would the material team feel if we went with the curve from the web implementation? Or, I could try implementing the path-curve Android is using (FYI - I have no idea how much of a rabbit hole this could be).
That said, feel free to override my opinion here!
| // fraction of the opening duration. | ||
| // | ||
| // 50 ms of the 150 ms animation | ||
| const double _kMenuItemRelativeFadeOutDelay = 1 / 3; |
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.
Seems there is some inconsistency between the variable name and the doc. The doc mentioned this is for fade-in delay.
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.
I'm not sure whether we can share the internal specs here but I checked the specs, everything here is pretty accurate (except this one and _kMenuPanelHeightForwardCurve)!
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.
Oh good catch! Docs should be fixed. I also removed some extraneous docs (e.g. spec being obtained from typescript version).
| this.crossAxisUnconstrained = true, | ||
| this.useRootOverlay = false, | ||
| this.animated = false, | ||
| this.onAnimationStatusChanged, |
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.
Also curious the use case for this callback.
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.
Good question. We actually had this discussion during the RawMenuAnchor animation implementation. If you don't have access to the animation status, you can't open a menu while that menu is in the process of closing (i.e. duration AnimationStatus.reverse)
Since MenuController.isOpen represents whether the overlay is visible, MenuController.isOpen is true while the menu is opening, open, and closing (AnimationStatus.forward, .completed, and .reverse, respectively). That means that, if you press the anchor button while the menu is closing, MenuController.isOpen will be true, so the button will call MenuController.close() despite the menu already closing. If you want to make a toggleable menu, you need to have access to the AnimationStatus so that you can test if AnimationStatus.reverse is true.
I didn't use an onAnimationStatusChanged listener on the example because I didn't want users to think that migration to an animated menu requires extra steps. But, do you think it'd be helpful to add it to the example? I did so on the CupertinoMenuAnchor implementation to allow toggleability: https://github.com/flutter/flutter/pull/174695/files#diff-330ffad0cc9875f50a0466e01e8a25e1612e30d5331ff40a6834c30ae7da7b3eR98-R104.
|
Can we also update the demo in the PR description to show the correct Scrollbar behavior:)? |
|
@QuncCccccc Video should be updated! |
…eDryLayout (flutter#176503) _RenderDropdownMenuBody.computeDryLayout accesses this.constraints, but it should only access the constraints parameter passed into computeDryLayout. This PR removes this.constraints access. <img width="621" height="73" alt="image" src="https://github.com/user-attachments/assets/495573e3-3b91-4091-8a7c-76594c98e22f" /> Also, I removed a line in which the child's offset is being set in computeDryLayout. This appears to be an error: <img width="402" height="56" alt="image" src="https://github.com/user-attachments/assets/0045761d-c958-451b-a6ec-cbdf0fe7bd09" /> Finally, I'm curious whether there is a reason why only the first child is being used to set the height? Resolves flutter#176494 Blocking flutter#176494 ## Pre-launch Checklist - [x] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [x] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [x] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [x] I signed the [CLA]. - [x] I listed at least one issue that this PR fixes in the description above. - [x] I updated/added relevant documentation (doc comments with `///`). - [x] I added new tests to check the change I am making, or this PR is [test-exempt]. - [x] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [x] All existing and new tests are passing. If you need help, consider asking for advice on the #hackers-new channel on [Discord]. **Note**: The Flutter team is currently trialing the use of [Gemini Code Assist for GitHub](https://developers.google.com/gemini-code-assist/docs/review-github-code). Comments from the `gemini-code-assist` bot should not be taken as authoritative feedback from the Flutter team. If you find its comments useful you can update your code accordingly, but if you are unsure or disagree with the feedback, please feel free to wait for a Flutter team member's review for guidance on which automated comments should be addressed. <!-- Links --> [Contributor Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview [Tree Hygiene]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md [test-exempt]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests [Flutter Style Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md [Features we expect every widget to implement]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement [CLA]: https://cla.developers.google.com/ [flutter/tests]: https://github.com/flutter/tests [breaking change policy]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes [Discord]: https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md [Data Driven Fixes]: https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md
SubmenuButtonThis was done because I needed to access the dry layout of the menu panel for the best effect. I'll demonstrate below.Turns out using dry layout significantly limits what children you can use with a widget. As a result, I'm calculating the final height using the inverse height factor.Fixes #135025
I don't have access to the internal Google documentation, so I did my best to deduce the spec from https://github.com/material-components/material-web/blob/main/menu/internal/menu.ts.
This change is currently opt-in via an
animatedparameter onMenuAnchorandSubmenuButton. As a result, this is not a breaking change. Because the PR is already quite large, I chose to not add animation customization to this PR. I also didn't change any theming files orDropdownMenu.The only other API addition is a
hoverOpenDelayparameter onSubmenuButton. This parameter delays the start of a submenu opening by the specified duration. It is independent of theanimatedparameter.When
animated == false, the menu runs its forward and reverse animations with a duration of 0. I originally disposed of all animation assets whenanimatedwas false, but found that the code/testing complexity to be unwieldy.Blocked by #176503
Examples
Screen.Recording.2025-11-20.at.5.00.48.AM.mov
Screen.Recording.2025-11-20.at.5.00.08.AM.mov
Props to @QuncCccccc and @dkwingsmt for their work on this issue. Sorry for throwing a wrench into the original PR by introducing
RawMenuAnchor...Pre-launch Checklist
///).If you need help, consider asking for advice on the #hackers-new channel on Discord.
Note: The Flutter team is currently trialing the use of Gemini Code Assist for GitHub. Comments from the
gemini-code-assistbot should not be taken as authoritative feedback from the Flutter team. If you find its comments useful you can update your code accordingly, but if you are unsure or disagree with the feedback, please feel free to wait for a Flutter team member's review for guidance on which automated comments should be addressed.