Skip to content

Conversation

@davidhicks980
Copy link
Contributor

@davidhicks980 davidhicks980 commented Oct 3, 2025

  • Adds MD3 animations to MenuAnchor.
  • Adds a "hoverOpenDelay" parameter on SubmenuButton
  • Moves the layout algorithm from SingleChildLayoutDelegate to a custom render object. This 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 animated parameter on MenuAnchor and SubmenuButton. 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 or DropdownMenu.

The only other API addition is a hoverOpenDelay parameter on SubmenuButton. This parameter delays the start of a submenu opening by the specified duration. It is independent of the animated parameter.

When animated == false, the menu runs its forward and reverse animations with a duration of 0. I originally disposed of all animation assets when animated was false, but found that the code/testing complexity to be unwieldy.

Blocked by #176503

Examples

  • Basic example adapted from material-web
Screen.Recording.2025-11-20.at.5.00.48.AM.mov
  • At 1/10 speed
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-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.

@github-actions github-actions bot added framework flutter/packages/flutter repository. See also f: labels. f: material design flutter/packages/flutter/material repository. labels Oct 3, 2025
@guidezpl
Copy link
Member

guidezpl commented Oct 7, 2025

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.

@davidhicks980
Copy link
Contributor Author

@guidezpl I was thinking the same thing. Fix is pushed.

@github-actions github-actions bot added d: api docs Issues with https://api.flutter.dev/ d: examples Sample code and demos labels Oct 10, 2025
@davidhicks980 davidhicks980 marked this pull request as ready for review October 10, 2025 05:31
Copy link
Contributor

@dkwingsmt dkwingsmt left a 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 {
Copy link
Contributor

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?)

Copy link
Contributor Author

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() {
Copy link
Contributor

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.

Copy link
Contributor Author

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()

Copy link
Contributor

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 _handleTogglingInteraction into _openSubmenu and _closeSubmenu, and handlePointerHover calls the opening one, and _handleFocusChange calls either based on status, which is clearer as well.
  • Rename _handleTogglingInteraction to something like _openCloseMenuBasedOnFocus.

Copy link
Contributor Author

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

Copy link
Contributor

@QuncCccccc QuncCccccc left a 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);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I checked the specs, the enter animation for menu should use md.sys.motion.easing.emphasized which should be this curve (reference)

Copy link
Contributor Author

@davidhicks980 davidhicks980 Nov 20, 2025

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;
Copy link
Contributor

@QuncCccccc QuncCccccc Nov 19, 2025

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.

Copy link
Contributor

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)!

Copy link
Contributor Author

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,
Copy link
Contributor

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.

Copy link
Contributor Author

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.

@QuncCccccc
Copy link
Contributor

Can we also update the demo in the PR description to show the correct Scrollbar behavior:)?

@davidhicks980
Copy link
Contributor Author

@QuncCccccc Video should be updated!

reidbaker pushed a commit to AbdeMohlbi/flutter that referenced this pull request Dec 10, 2025
…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
@dkwingsmt dkwingsmt requested a review from QuncCccccc December 17, 2025 19:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

d: api docs Issues with https://api.flutter.dev/ d: examples Sample code and demos f: material design flutter/packages/flutter/material repository. framework flutter/packages/flutter repository. See also f: labels.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add open and close animations for MenuAnchor

5 participants