-
Notifications
You must be signed in to change notification settings - Fork 29.7k
Implement RawMenuAnchorAnimationDelegate for animating menus. #165556
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 for making this prototype! I think this design makes me feel much more comfortable than the decorator one. I don't think the /// Flutter code sample for a [RawMenuAnchorAnimationDelegate] that animates a
/// [RawMenuAnchor] with a [SpringSimulation].
void main() {
runApp(const RawMenuAnchorAnimationDelegateApp());
}
class RawMenuAnchorAnimationDelegateExample extends StatefulWidget {
const RawMenuAnchorAnimationDelegateExample({super.key});
@override
State<RawMenuAnchorAnimationDelegateExample> createState() => _RawMenuAnchorAnimationDelegateExampleState();
}
class _RawMenuAnchorAnimationDelegateExampleState extends State<RawMenuAnchorAnimationDelegateExample>
with SingleTickerProviderStateMixin, RawMenuAnchorAnimationDelegate {
late final AnimationController animationController;
final MenuController menuController = MenuController();
@override
void initState() {
super.initState();
// Use an unbounded animation controller so that simulations are not clamped.
animationController = AnimationController.unbounded(vsync: this);
}
@override
void dispose() {
animationController.dispose();
super.dispose();
}
SpringSimulation get forwardSpring => SpringSimulation(
SpringDescription.withDampingRatio(mass: 1.0, stiffness: 150, ratio: 0.7),
animationController.value,
1.0,
0.0,
);
SpringSimulation get reverseSpring => SpringSimulation(
SpringDescription.withDampingRatio(mass: 1.0, stiffness: 200, ratio: 0.7),
animationController.value,
0.0,
0.0,
);
@override
void handleMenuOpenRequest({ui.Offset? position}) {
// Call whenComplete() rather than whenCompleteOrCancel() to avoid marking
// the menu as opened when the [AnimationStatus] moves from forward to
// reverse.
animationController.animateWith(forwardSpring).whenComplete(markMenuOpened);
}
@override
void handleMenuCloseRequest() {
// Call whenComplete() rather than whenCompleteOrCancel() to avoid marking
// the menu as closed when the [AnimationStatus] moves from reverse to
// forward.
animationController.animateBackWith(reverseSpring).whenComplete(markMenuClosed);
}
@override
Widget build(BuildContext context) {
return RawMenuAnchor(
controller: menuController,
delegate: this,
overlayBuilder: (BuildContext context, RawMenuOverlayInfo info) {
// omitted, unchanged
},
builder: (BuildContext context, MenuController menuController, Widget? child) {
// omitted, unchanged
},
);
}
}
class RawMenuAnchorAnimationDelegateApp extends StatelessWidget {
// omitted, unchanged
}So in general I would say let's move forward with this direction. Let me see if I can find anyone else's opinion! |
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.
If I understand the the use case correctly, we want a way to add animation to the MenuAnchor. by default, there won't be animation when open or close. Is my understanding correct?
If so, the current code seems to be a weird way to wire up the animation. We let the parent owns the animation controller, and use hook from MenuAnchor to trigger the animation and getting animation status from it.
Can we do things this way?
RawMenuAnchor takes a animationController and a curve or simulation as input (either required parameter or creating a zero duration animation controller if not provided) and the RawMenuAnchor calls forward and or backward when open or close.
Which hooks are you referencing?
So, a few issues. Considering RawMenuAnchor is only supposed to be used when implementing themed menus (and therefore should be as customizable as possible), a single animation controller is constraining. The examples I gave are trivial to make demos easy to understand, but the api is meant to be flexible. As an example: what if you want the menu to animate closed with a different animation depending on whether the user scrolled or the user pressed escape? Since RawMenuAnchor would be responding to these events internally, there would be no way to decide which curve should be used. Because simulations and animations are triggered differently, we'd have to call forward or animateWith, and reverse or animateBackWith, depending on whether a simulation or a curve is added. We'd also need to either add an assert for only specifying a forward tween or a forward simulation and a reverse tween or reverse simulation, or add two constructors: one for simulations and one for tweens Last, RawMenuAnchor doesn't animate anything and doesn't need to animate anything, so I'm hesitant to constrain component library developers. It is true that passing Duration.zero would work for immediate closure. Also, does this mean passing in both MenuController and AnimationController? If we wanted to continue allowing users to pass a MenuController into MenuAnchor, we would need to pass that MenuController to RawMenuAnchor, since MenuAnchor has no way of observing when MenuController.open is called. Alternatively
Nevermind - I remembered this doesn't work because descendents calling MenuController.close immediately close the menu. |
handleMenuOpenRequest/handleMenuCloseRequest to trigger the animation IMO, this API it is a bit confusing unless developer know how they are used internally in RawMenuAnchor.
In some of the past example, we let the base class owns animation controller, and expose the animation<double> in the builder so that the toplevel widget can build Transition in the builder method. We assume all the consumer of the base widget will want the animation. Is this not the case for MenuAnchor?
Can the current approach tell about how animation is triggered? I am slightly concern we may be over-designing this, do we have use case that we want different curve/simulation? Either way, even if we pass in animation controller or let RawMenuAnchor create one as mentioned above, we can add this flexibility by adding a callback For example, enum Cause {
escape,
tap,
scroll,
...
}
RawMenuAnchro(
onGenerateSimulation: (Cause cause, bool isClose) => createSimulation(cause, isClose),
) |
|
So, I'm a bit paranoid of writing an API that imposes any constraints. That's why the The only reason why RawMenuAnchor exists at all is so that menus use a common, inherited MenuController, so that's why I'm a bit hesitant to manage animation from within RawMenuAnchor.
I was thinking that the In the demo, Dartpad: Code for DartPad// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
/// @docImport 'package:flutter/material.dart';
library;
import 'dart:ui' as ui;
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/scheduler.dart';
import 'package:flutter/services.dart';
import 'package:flutter/physics.dart';
/// Flutter code sample for a [RawMenuAnchorAnimationDelegate] that animates a
/// [RawMenuAnchor] with a [SpringSimulation].
void main() {
runApp(const App());
}
class App extends StatelessWidget {
const App({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData.from(
useMaterial3: true,
colorScheme: ColorScheme.fromSeed(
seedColor: Colors.blue,
dynamicSchemeVariant: DynamicSchemeVariant.vibrant,
),
),
home: const Scaffold(body: Center(child: Example())),
);
}
}
class Example extends StatelessWidget {
const Example({super.key});
@override
Widget build(BuildContext context) {
return MenuAnchor(
panelBuilder: (BuildContext context, animation) {
final MenuController rootMenuController =
MenuController.maybeOf(context)!;
return FadeTransition(
opacity: animation,
child: SizeTransition(
sizeFactor: animation,
child: Align(
alignment: Alignment.topRight,
child: Column(
children: <Widget>[
for (int i = 0; i < 4; i++)
MenuAnchor(
panelBuilder: (BuildContext context, animation) {
final String animationStatus =
MenuController.maybeAnimationStatusOf(
context,
)!.name;
return FadeTransition(
opacity: animation,
child: SizeTransition(
sizeFactor: animation,
child: SizedBox(
height: 120,
width: 120,
child: Center(
child: Text(
'Panel $i:\n$animationStatus',
textAlign: TextAlign.center,
),
),
),
),
);
},
builder: (
BuildContext context,
MenuController controller,
) {
return MenuItemButton(
onFocusChange: (bool focused) {
if (focused) {
rootMenuController.closeChildren();
controller.open();
}
},
onPressed: () {
if (!controller
.animationStatus
.isForwardOrCompleted) {
rootMenuController.closeChildren();
controller.open();
} else {
controller.close();
}
},
trailingIcon: const Text('▶'),
child: Text('Submenu $i'),
);
},
),
],
),
),
),
);
},
builder: (BuildContext context, MenuController controller) {
return FilledButton(
onPressed: () {
if (controller.animationStatus.isForwardOrCompleted) {
controller.close();
} else {
controller.open();
}
},
child: const Text('Menu'),
);
},
);
}
}
class MenuAnchor extends StatefulWidget {
const MenuAnchor({
super.key,
required this.panelBuilder,
required this.builder,
});
final Widget Function(BuildContext, Animation<double>) panelBuilder;
final Widget Function(BuildContext, MenuController) builder;
@override
State<MenuAnchor> createState() => MenuAnchorState();
}
class MenuAnchorState extends State<MenuAnchor>
with SingleTickerProviderStateMixin, RawMenuAnchorAnimationDelegate {
final MenuController menuController = MenuController();
late final AnimationController animationController;
late final CurvedAnimation animation;
bool get isSubmenu => MenuController.maybeOf(context) != null;
@override
void initState() {
super.initState();
animationController = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 200),
);
animation = CurvedAnimation(
parent: animationController,
curve: Curves.easeOutQuart,
);
}
@override
void dispose() {
animationController.dispose();
animation.dispose();
super.dispose();
}
@override
void handleMenuOpenRequest({ui.Offset? position}) {
// Call whenComplete() rather than whenCompleteOrCancel() to avoid marking
// the menu as opened when the AnimationStatus moves from forward to
// reverse.
animationController.forward().whenComplete(markMenuOpened);
}
@override
void handleMenuCloseRequest() {
// Animate the children of this menu closed.
menuController.closeChildren();
animationController.reverse().whenComplete(markMenuClosed);
}
@override
Widget build(BuildContext context) {
return RawMenuAnchor(
controller: menuController,
delegate: this,
overlayBuilder: (BuildContext context, RawMenuOverlayInfo info) {
final ui.Offset position =
isSubmenu ? info.anchorRect.topRight : info.anchorRect.bottomLeft;
final ColorScheme colorScheme = ColorScheme.of(context);
return Positioned(
top: position.dy,
left: position.dx,
child: Semantics(
explicitChildNodes: true,
scopesRoute: true,
child: ExcludeFocus(
// Remove focus while the menu is closing.
excluding: animation.status == AnimationStatus.reverse,
child: TapRegion(
groupId: info.tapRegionGroupId,
onTapOutside: (PointerDownEvent event) {
menuController.close();
},
child: FadeTransition(
opacity: animation,
child: Material(
elevation: 8,
clipBehavior: Clip.antiAlias,
borderRadius: BorderRadius.circular(8),
shadowColor: colorScheme.shadow,
child: widget.panelBuilder(context, animation),
),
),
),
),
),
);
},
builder: (
BuildContext context,
MenuController controller,
Widget? child,
) {
return widget.builder(context, controller);
},
);
}
}IMO, for menus that have an animation with Duration.zero, it'd be unusual to pass an Animation to their panels.
OH my mistake. I misunderstood. So, you're suggesting That makes more sense to me since To your point, there are widgets that pass animations to their children (e.g. PageRouteBuilder). But, the purpose is to let the component developer decide whether to pass an Animation, a progress value (e.g. 0.5), or nothing at all, rather than RawMenuAnchor. An issue that may come up with only passing an Animation from RawMenuAnchor is that a parent, such as For example, when closing a CupertinoMenuAnchor, tapping an underlying submenu will immediately close the submenu which is currently closing. I'm not sure it there would be a simple way to trigger this behavior without access to the AnimationController. Simulator.Screen.Recording.-.iPhone.16.Pro.-.2025-03-29.at.19.15.09.mp4
The delegate approach doesn't tell you how the menu close was requested, but it does tell you when the close is requested, and you can listen for whether "Escape" is pressed or whether a scroll controller passed to the menu panel is registering scrolls to determine if an escape or a scroll occurs. I agree that adding any additional info about how the menu was closed is overkill. This is an example of an edge case which doesn't have a good solution. The point of the api is to make it so that no menu is impossible to implement without internal access to RawMenuAnchor, because forking RawMenuAnchor eliminates the benefits of a common menu interface. When I first implemented CupertinoMenuAnchor, I also had multiple issues adapting MenuAnchor to be compatible with CupertinoMenuAnchor due to various quirks with iOS, and I imagine there are behaviors I can't anticipate that will arise with other developers. The example in the video is an example that I wouldn't be able to replicate without access to the AnimationController. Another problem that I encountered in the past was that the material menu anchor spec runs multiple animations with different durations. This can be done with a single Animation run through several tweens, but it's much easier to coordinate to trigger all animations from an onOpenRequest/onCloseRequest callback. FYI the spec is internal so I'm not sure what the GooglePlex link says -- I'm basing this off of the implementation. So, I'm a bit paranoid of writing an API that imposes any constraints, considering RawMenuAnchor is supposed to be for component library developers rather than for end users. That's also why the That said, the main inspiration for this api is that it used to be impossible to run a simulation in reverse, but since Tong added animateBackWith, that is one less constraint.
You're definitely right about the current implementation being confusing. We could return a future from We could also rename the callbacks to something simpler e.g. willMenuOpen/menuOpenRequested/didRequestMenuOpening. I was also wondering whether we should use "MenuStatus" rather than AnimationStatus so that developers wouldn't conflate animations with menu behavior. Anyways, sorry for the long response. Let me know your thoughts. |
|
Thanks for the detailed reply. I am actually neutral on whether we should bake animation controller in the RawMenuAnchor or not. so I am ok if we pull animation from RawMenuAnchor completely. With that in mind, I think the current pr is still in a weird place where it is still in the middle that RawMenuAnchor still some how aware of that an animation may be going on outside itsself, i.e. the markMenuOpen/Close and the _RawMenuAnchorBaseMixin.animationstatus. Can we do it this way?
The default implementation of MenuController will just change its isOpen state and notify listener. I think this will make it cleaner in terms of APIs and internal implementation. WDYT? |
|
If I understand the proposal correctly (please correct me if I'm wrong), there are a few problems that may occur with a ChangeNotifier/subclass design
So, here's an alternative proposal that removes the AnimationStatus parts and some of the complexity of the delegate. MenuControllerDecorator is used instead of the delegate. The implementation will be a simplified version of the original decorator proposal here. If we don't need to notify children of the animation status, we could remove AnimationStatus from _RawMenuAnchorBaseMixin and the markMenuOpened method. The only method that we need for animation is a way to hide the menu overlay once the menu closing animation is finished. Right now, markMenuClosed hides the overlay, but we could rename it to hideMenu or something simpler. Users of themed menus like MenuAnchor wouldn't have to change anything and could continue using MenuController, while developers using RawMenuAnchor would have the option to add animations or make no changes. Usage would look like: class _MenuAnimator extends MenuControllerDecorator {
const _MenuAnimator({required super.menuController, required this.animationController});
final AnimationController animationController;
// Called when MenuController.open() is called
@override
void handleMenuOpenRequest({ui.Offset? position}) {
animationController.forward()
}
// Called when MenuController.close() is called
@override
void handleMenuCloseRequest() {
animationController.reverse().whenComplete(hideMenu)
}
}
// An animated menu, like MenuAnchor or CupertinoMenuAnchor, meant to be consumed by end users
class ThemedMenu extends StatefulWidget {
const Menu({super.key, required this.menuController});
final MenuController menuController;
@override
State<Menu> createState() => _MenuState();
}
class _ThemedMenuState extends State<Menu>
with SingleTickerProviderStateMixin {
late final AnimationController animationController;
late final _MenuAnimator menuAnimator;
@override
void initState() {
super.initState();
animationController = AnimationController.unbounded(vsync: this);
menuAnimator = _MenuAnimator(
menuController: widget.menuController,
animationController: animationController,
);
}
@override
void didUpdateWidget(oldWidget) {
if (oldWidget.menuController != widget.menuController) {
menuAnimator = _MenuAnimator(
menuController: widget.menuController,
animationController: animationController
);
}
}
@override
void dispose() {
animationController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return RawMenuAnchor(
controller: menuAnimator, // Pass in the decorated controller
overlayBuilder: (context, info) {
// Use your animation controller to animate the menu
},
child: widget.child
);
}This is almost the same as subclassing MenuController, except we are calling hideMenu instead of super.close(), and we can use the same MenuController interface across all menus (the decorator is only used by the implementer of the themed menu, but the user just uses MenuController). Let me know if that makes sense. |
Yes, i dislike subclassing the controllers. but since we are moving the animation out of RawMenuAnchor, some object has to own the trigger the animationcontroller and maintain it. it is either the MenuAnchorState or MenuController. the current pr uses
I wonder why we need
We already does this, except now it is a getter. but people can subclass it and do anything they want. This is not a strong argument though, we could declare we don't support this use case or like use said add a postframecallback
yes this is unfortunate. The current pr is fine because the controller can't be listen even though it has a isOpen getter, which is not ideal as well.
I think we may have the same issue in this pr if the requestOpen calls markMenuOpen synchronously right?
yes this will be a problem. Do you know what use case is for animationstatus in RawMenuAnchor? looking at the code looks like it was used to shortcircuit the open and close request and as a inherited widget. I think we can infer the short-circuit logic if we let the open()/close() to update the isOpen property synchronously before trigger the animation if we want to, this will also solve the isOpen not syncing with overlayController.isShowing. If so, the menuController don't need to know the animation at all if the RawMenAnchor don't need to know when the animation finishes
I think this is fine, as long as we agree on what the right way to approach this. I won't see this as a blocker. |
|
Sorry I hadn't been participating lately, partly because I was engaged in other issues and also partly because I knew that letting chunhtai handle it would bring in new ideas. Unfortunately I'm also going OOO from tomorrow to the end of the week but I really wanted to check how it goes and contribute to the discussion before I leave. I tried my best to read the discussion so far but I don't think I've fully grasped every details, so forgive me if I missed something that have been discussed (especially since I'm much less experienced on widgets with animations than either of you). First a small clarification:
This shouldn't be a big concern. I wrote about the relationship between curve-based animation and simulation-based ones in this comment; in short, theoretically we can (and should) use simulations for everything, because curves can be converted to simulations: Curves are just a special case of simulations that have pre-determined curve and duration. Now onto the hard part, the API design for animations. I think I now understand better how the decorator design helps. When we don't add animations, the themed menu could simply pass the menu controller received from the app straight to the
I completely agree. Even though during the opening and closing animations the raw menu anchor will be told to display contents that are scaled and faded, it doesn't care why they're like this. The raw menu anchor will simply consider itself opened as long as any pixels are displaying. But I think we can make it one step further. It seems to me that the ultimate motivation for the decorator design is so that the More specifically, my proposal is:
Do you think this will work? |
|
Howdy Tong,
I was just about to hop off for the day, but I wanted to respond quickly
since I actually used this exact design in CupertinoMenuAnchor v1. I really
like the design, but I ran into one issue: how does the external menu
controller notify the themed menu when changes occur? In other words, when
close is called on the external controller, how will the themed menu know
that the menu should begin animating closed?
I think I ended up subclassing Menu controller into
CupertinoMenuController, but there may be a simpler method that I
overlooked. Let me know!
…On Thu, Apr 10, 2025, 6:20 AM Tong Mu ***@***.***> wrote:
Sorry I hadn't been participating lately, partly because I was engaged in
other issues and also partly because I knew that letting Chunheng handle it
would bring in new ideas. Unfortunately I'm also going OOO from tomorrow to
the end of the week but I really want to see how it goes and see what I can
contribute to the discussion before I leave.
------------------------------
First a small clarification:
Because simulations and animations are triggered differently, we'd have to
call forward or animateWith, and reverse or animateBackWith, depending on
whether a simulation or a curve is added.
This shouldn't be a big concern. I wrote about the relationship between
curve-based animation and simulation-based ones in this comment
<#152587 (comment)>;
in short, theoretically we can (and should) use simulations for everything,
because curves can be converted to simulations (curves are just a special
case of simulations that have pre-determined curve and duration).
------------------------------
Now onto the hard part, the API design for animations. Frankly speaking,
I'm much, much experienced on writing widgets with animations than either
of you, so even though I read through a few times, I don't think I've yet
fully comprehended the impact of some design details such as where to put
the animation controller. But I think I've grasped some ideas.
I think I understand more how the decorator design helps. Without
animations, the themed menu could simply pass the menu controller received
from the app straight to the RawMenuAnchor, which attaches with the
controller and interacts with it. However, in order to play animations, the
themed menu must be notified when the user opens or closes on the menu
anchor, and even delaying RawMenuAnchor's closing status. This requires
the themed menu to "intercept" MenuController's calls. Indeed, the
decorator design allows maximum flexibility of control.
RawMenuAnchor needs to know whether the overlay is showing or is not
showing. It isn't running animations, so it has no use for the
AnimationController.
I also came to this conclusion. During the opening and closing animations
the raw menu anchor will be told to display contents that are scaled and
faded, but it doesn't care why they're like this. The raw menu anchor will
simply consider itself opened as long as any pixels are displaying.
But I think we can make it one step further. It seems to me that the
ultimate motivation for the decorator design is that the RawMenuAnchor
must consider itself closed much later than MenuController.close() is
called. What if the RawMenuAnchor doesn't share the same opening/closing
status as the themed menu anchor? What if they don't even use the same
controller?
More specifically, my proposal is:
- No delegates and no decorators.
- The RawMenuAnchor simply does not animate at all.
- While the themed menu anchor receives an external MenuController, it
doesn't pass it to its raw menu anchor child. Instead it creates and
manages an internal menu controller for the raw menu anchor.
- The internal menu controller becomes opened as soon as the external
menu controller is opened.
- The internal menu controller becomes closed only when the closing
animation is completed.
- The animation controller may be managed by the themed menu anchor,
or may be passed in by the user.
Do you think this will work?
(My apology if I missed some details that have been discussed. I'll
re-read them when I return!)
—
Reply to this email directly, view it on GitHub
<#165556 (comment)>,
or unsubscribe
<https://github.com/notifications/unsubscribe-auth/AODY6MPTOWFP5XTFZUHMGTT2YZAVTAVCNFSM6AAAAABZM5JCMCVHI2DSMVQWIX3LMV43OSLTON2WKQ3PNVWWK3TUHMZDOOJSGI3DQNZZGY>
.
You are receiving this because you authored the thread.Message ID:
***@***.***>
*dkwingsmt* left a comment (flutter/flutter#165556)
<#165556 (comment)>
Sorry I hadn't been participating lately, partly because I was engaged in
other issues and also partly because I knew that letting Chunheng handle it
would bring in new ideas. Unfortunately I'm also going OOO from tomorrow to
the end of the week but I really want to see how it goes and see what I can
contribute to the discussion before I leave.
------------------------------
First a small clarification:
Because simulations and animations are triggered differently, we'd have to
call forward or animateWith, and reverse or animateBackWith, depending on
whether a simulation or a curve is added.
This shouldn't be a big concern. I wrote about the relationship between
curve-based animation and simulation-based ones in this comment
<#152587 (comment)>;
in short, theoretically we can (and should) use simulations for everything,
because curves can be converted to simulations (curves are just a special
case of simulations that have pre-determined curve and duration).
------------------------------
Now onto the hard part, the API design for animations. Frankly speaking,
I'm much, much experienced on writing widgets with animations than either
of you, so even though I read through a few times, I don't think I've yet
fully comprehended the impact of some design details such as where to put
the animation controller. But I think I've grasped some ideas.
I think I understand more how the decorator design helps. Without
animations, the themed menu could simply pass the menu controller received
from the app straight to the RawMenuAnchor, which attaches with the
controller and interacts with it. However, in order to play animations, the
themed menu must be notified when the user opens or closes on the menu
anchor, and even delaying RawMenuAnchor's closing status. This requires
the themed menu to "intercept" MenuController's calls. Indeed, the
decorator design allows maximum flexibility of control.
RawMenuAnchor needs to know whether the overlay is showing or is not
showing. It isn't running animations, so it has no use for the
AnimationController.
I also came to this conclusion. During the opening and closing animations
the raw menu anchor will be told to display contents that are scaled and
faded, but it doesn't care why they're like this. The raw menu anchor will
simply consider itself opened as long as any pixels are displaying.
But I think we can make it one step further. It seems to me that the
ultimate motivation for the decorator design is that the RawMenuAnchor
must consider itself closed much later than MenuController.close() is
called. What if the RawMenuAnchor doesn't share the same opening/closing
status as the themed menu anchor? What if they don't even use the same
controller?
More specifically, my proposal is:
- No delegates and no decorators.
- The RawMenuAnchor simply does not animate at all.
- While the themed menu anchor receives an external MenuController, it
doesn't pass it to its raw menu anchor child. Instead it creates and
manages an internal menu controller for the raw menu anchor.
- The internal menu controller becomes opened as soon as the external
menu controller is opened.
- The internal menu controller becomes closed only when the closing
animation is completed.
- The animation controller may be managed by the themed menu anchor,
or may be passed in by the user.
Do you think this will work?
(My apology if I missed some details that have been discussed. I'll
re-read them when I return!)
—
Reply to this email directly, view it on GitHub
<#165556 (comment)>,
or unsubscribe
<https://github.com/notifications/unsubscribe-auth/AODY6MPTOWFP5XTFZUHMGTT2YZAVTAVCNFSM6AAAAABZM5JCMCVHI2DSMVQWIX3LMV43OSLTON2WKQ3PNVWWK3TUHMZDOOJSGI3DQNZZGY>
.
You are receiving this because you authored the thread.Message ID:
***@***.***>
|
|
Oh, I forgot that there was one more issue I ran into using the dual menu controller method. When RawMenuAnchor is closed internally by pressing escape or resizing the screen, there was no way to animate the menu closed. This is because the themed menu isn't able to intercept the call to close. I wasn't able to solve this problem with the original implementation -- I just didn't animate closure caused by these two methods. I'm revisiting the solution now and I'm not sure if there is a good way around those two problems, but I do still really like the simplicity of the solution you proposed. I'll try to think if there is a way around these problems. |
|
(Sorry it took me a few days to get my errands cleared and my thoughts straight.) Well that makes it more and more complicated. Let me think about it one by one.
I think this one is the easiest to handle.
I'm not 100% sure where the obstacle is here. My understanding is that the
|
|
Whoops, I didn't get notified of your message for some reason.
The problem is that externalMenuController.close() would do nothing since it's not being attached to RawMenuAnchor. Calling internalMenuController.close() again would just emit another dismiss intent, so I'm not sure how we would actually close the menu.
Clarify what you mean by record... Also, how would the menu actually be closed from the DismissAction? Calling internalMenuController.close() or externalMenuController.close() would just emit another DismissIntent(), but we need a way to actually close the menu. |
|
I've drafted a prototype, #167537, that refactors RawMenuAnchor based on
Essentially it turns out a bit similar to decorator design. Both designs have something to actuate the changes ( I'm not heavily leaning towards this design though. It wins at leveraging Flutter's existing mechanism instead of introducing a new paradigm that has never been seen in Flutter ("decorator"). But it is equal, if not worse, in terms of verbosity, due to the duplicate code in themed menus to define |
|
I'll post the same comment on this PR and the delegate PR so that viewers of the other PR have some context. I spent some time playing with the actions PR last night. It's fundamentally similar to the decorator, but I liked your idea of themed menus overriding the actions of their RawMenuAnchor, because it feels like it fits with Flutter paradigms. I ended up with something completely different, but in the spirit of your PR. Here's my thought process: What I like about the actions proposal:
A few issues that I came across:
So, I tried a couple different changes to simplify things. For example, we could pass a "inner" MenuController inside of the OpenMenuIntent/CloseMenuIntent that could be used by actions to perform a different opening or closing action. This would remove the need for attachment/detachment and BuildContext references. However, after thinking about it more, I realized our solutions are basically trying to accomplish the following:
I'm curious of your thoughts on this but what if skipped actions and decorators, and just added a flag to MenuController.open() and MenuController.close() that chose whether to trigger animation callbacks on RawMenuAnchor. For example, if final MenuController menuController = MenuController;
late final AnimationController animationController = AnimationController(vsync: this, duration: Duration(milliseconds: 200));
void _handleCloseRequest() {
animationController.reverse().whenComplete(() {
menuController.close(transition: false); // Hides the overlay without calling onCloseRequest
});
}
void _handleOpenRequest() {
menuController.open(transition: false); // Shows the overlay without calling onOpenRequested
animationController.forward();
}
// Example function called by the anchor
void _handleAnchorTapped() {
if (animationController.isForwardOrCompleted) {
menuController.close();
} else {
menuController.open();
}
}
@override
Widget build(BuildContext context) {
return RawMenuAnchor(
controller: menuController,
onCloseRequested: _handleCloseRequest, // Called when MenuController.close() is called
onOpenRequested: _handleOpenRequest. // Called when MenuController.open() is called
...
)
)I initially avoided this approach because I thought that would break the function signature, but it appears optional parameters can be ignored by the type checker (e.g. MenuController.close can be used as a VoidCallback). Let me know what you think! |

Delegate alternative to #163481.
@dkwingsmt
I'm still leaning a bit towards the decorator because it doesn't need to be attached and detached to the anchor, and, as a result, it is const.
This PR adds a
RawMenuAnchorAnimationDelegatemixin class that intercepts calls to MenuController.open() and MenuController.close() to allow users to run an animation or add a delay prior to a menu opening or closing.Additionally, this PR adds a
MenuController.animationStatusgetter and aMenuController.maybeAnimationStatusOfstatic method to interrogate the current animation status of the nearest menu. I was torn between creating a new enum, such asand sticking with
AnimationStatus. I stuck with AnimationStatus, but let me know if you have a preference.Precursor for #143416, #135025, #143712
Demo
Screen.Recording.2025-02-17.at.8.58.43.AM.mov
Pre-launch Checklist
///).If you need help, consider asking for advice on the #hackers-new channel on Discord.