Skip to content

Add RawMenuAnchor animation callbacks #167806

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

Open
wants to merge 52 commits into
base: master
Choose a base branch
from

Conversation

davidhicks980
Copy link
Contributor

@davidhicks980 davidhicks980 commented Apr 25, 2025

Alternative to #163481, #167537, #163481 that uses callbacks.

@dkwingsmt - you inspired me to simplify the menu behavior. I didn't end up using Actions, mainly because nested behavior was unwieldy and capturing BuildContext has drawbacks. This uses a basic callback mechanism to animate the menu open and closed. Check out the examples.


The problem

RawMenuAnchor synchronously shows or hides an overlay menu in response to MenuController.open() and MenuController.close, respectively. Because animations cannot be run on a hidden overlay, there currently is no way for developers to add animations to RawMenuAnchor and its subclasses (MenuAnchor, DropdownMenuButton, etc).

The solution

This PR:

  • Adds two callbacks -- onOpenRequested and onCloseRequested -- to RawMenuAnchor.
  • onOpenRequested is called with a position and a showOverlay callback, which opens the menu when called.
  • onCloseRequested is called with a hideOverlay callback, which hides the menu when called.

When MenuController.open() and MenuController.close() are called, onOpenRequested and onCloseRequested are invoked, respectively.

Precursor for #143416, #135025, #143712

Demo

Screen.Recording.2025-02-17.at.8.58.43.AM.mov
// 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.

// ignore_for_file: public_member_api_docs

import 'dart:ui' as ui;

import 'package:flutter/material.dart' hide MenuController, RawMenuAnchor, RawMenuOverlayInfo;

import 'raw_menu_anchor.dart';

/// Flutter code sample for a [RawMenuAnchor] that animates a simple menu using
/// [RawMenuAnchor.onOpenRequested] and [RawMenuAnchor.onCloseRequested].
void main() {
  runApp(const App());
}

class Menu extends StatefulWidget {
  const Menu({super.key});

  @override
  State<Menu> createState() => _MenuState();
}

class _MenuState extends State<Menu> with SingleTickerProviderStateMixin {
  late final AnimationController animationController;
  final MenuController menuController = MenuController();

  @override
  void initState() {
    super.initState();
    animationController = AnimationController(
      vsync: this,
      duration: const Duration(milliseconds: 300),
    );
  }

  @override
  void dispose() {
    animationController.dispose();
    super.dispose();
  }

  void _handleMenuOpenRequest(Offset? position, void Function({Offset? position}) showOverlay) {
    // Mount or reposition the menu before animating the menu open.
    showOverlay(position: position);

    if (animationController.isForwardOrCompleted) {
      // If the menu is already open or opening, the animation is already
      // running forward.
      return;
    }

    // Animate the menu into view. This will cancel the closing animation.
    animationController.forward();
  }

  void _handleMenuCloseRequest(VoidCallback hideOverlay) {
    if (!animationController.isForwardOrCompleted) {
      // If the menu is already closed or closing, do nothing.
      return;
    }

    // Animate the menu out of view.
    //
    // Be sure to use `whenComplete` so that the closing animation
    // can be interrupted by an opening animation.
    animationController.reverse().whenComplete(() {
      if (mounted) {
        // Hide the menu after the menu has closed
        hideOverlay();
      }
    });
  }

  @override
  Widget build(BuildContext context) {
    return RawMenuAnchor(
      controller: menuController,
      onOpenRequested: _handleMenuOpenRequest,
      onCloseRequested: _handleMenuCloseRequest,
      overlayBuilder: (BuildContext context, RawMenuOverlayInfo info) {
        final ui.Offset position = info.anchorRect.bottomLeft;
        return Positioned(
          top: position.dy + 5,
          left: position.dx,
          child: TapRegion(
            groupId: info.tapRegionGroupId,
            child: Material(
              color: ColorScheme.of(context).primaryContainer,
              shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
              elevation: 3,
              child: SizeTransition(
                sizeFactor: animationController,
                child: const SizedBox(
                  height: 200,
                  width: 150,
                  child: Center(child: Text('Howdy', textAlign: TextAlign.center)),
                ),
              ),
            ),
          ),
        );
      },
      builder: (BuildContext context, MenuController menuController, Widget? child) {
        return FilledButton(
          onPressed: () {
            if (animationController.isForwardOrCompleted) {
              menuController.close();
            } else {
              menuController.open();
            }
          },
          child: const Text('Toggle Menu'),
        );
      },
    );
  }
}

class App extends StatelessWidget {
  const App({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      theme: ThemeData(colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue)),
      home: const Scaffold(body: Center(child: Menu())),
    );
  }
}

Pre-launch Checklist

If you need help, consider asking for advice on the #hackers-new channel on Discord.

@github-actions github-actions bot added framework flutter/packages/flutter repository. See also f: labels. d: api docs Issues with https://api.flutter.dev/ d: examples Sample code and demos labels Apr 25, 2025
@davidhicks980 davidhicks980 marked this pull request as draft April 25, 2025 11:44
@davidhicks980 davidhicks980 changed the title Implement callbacks Add RawMenuAnchor animation callbacks Apr 25, 2025
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.

Nice one! I like how this solution targets the core obstacle in a direct and simple way, i.e. how to pass the message from the menu controller to the themed menu.

This entire batch of suggestions focus on the documentation and naming, mostly because I myself was a bit lost while reading everything, especially everything related to "request". I'd like to propose a different set of terms to make the structure a bit clearer, and to also verify my understanding. Feel free to take any or no parts of my suggestion or change it whatever way you think fits.

@dkwingsmt
Copy link
Contributor

Also cc @chunhtai

@davidhicks980 davidhicks980 marked this pull request as ready for review May 5, 2025 02:09
@davidhicks980
Copy link
Contributor Author

@dkwingsmt

No, I mean keeping the handleOpenRequested as pure method and move the implementation to the two state classes. Is it possible?

I missed this comment. I think I pushed changes that do this -- let me know if it looks right. By pure method, are you referring to an abstract method in _RawMenuAnchorBaseMixin?

@chunhtai chunhtai self-requested a review May 5, 2025 17:21
Copy link
Contributor

@chunhtai chunhtai left a comment

Choose a reason for hiding this comment

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

Overall LGTM, I think this pr is cleaner now. Thanks for working on this!

@override
void handleCloseRequest() {
if (widget.onCloseRequested != null) {
if (SchedulerBinding.instance.schedulerPhase != SchedulerPhase.persistentCallbacks) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Why do we need a postframe callback? would be good to have some comments on this

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Changes in MediaQuery.sizeOf(context) cause RawMenuAnchor to close during didChangeDependencies. When this happens, calling setState during the closing sequence (i.e. handleCloseRequest -> onCloseRequested -> hideOverlay) will throw an error, since we're scheduling a build during a build. I think this could be fixed if OverlayPortal is given a declarative API (see #136769). I'll add comments explaining this.

While I was investigating how to phrase the comments, I realized that we could remove the post-frame callbacks if we used didChangeMetrics instead of MediaQuery, since didChangeMetrics is called during the idle phase. The drawback of this approach is that we would be rebuilding when the screen size changes rather than when the MediaQuery size changes. As a result, if a user modified the size of an ancestor MediaQuery widget, RawMenuAnchor wouldn't close. I submitted an issue about this here: #168539

image

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 filed a PR regarding this issue here: #168623. For now, I'll comment in the explanation.

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

/// is still open. In this case, opening delays or animations should be
/// skipped, and `showOverlay` should be called with the new `position` value.
///
/// Defaults to null, which means that the menu will be opened immediately.
Copy link
Contributor

Choose a reason for hiding this comment

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

Somewhere in here or other place should document the invoking order of onOpenRequested, showOverlay and onOpen same for onCloseRequest and onClose

Copy link
Contributor Author

Choose a reason for hiding this comment

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

So, I initially wrote out a detailed list, but I found it easier to digest when reading:

onOpenRequested:

  /// When [MenuController.open] is called, the full opening sequence is:
  /// 1. [onOpenRequested]
  /// 2. (optional delay)
  /// 3. `showOverlay`
  /// 4. [onOpen]
  /// 5. (optional animation)

onCloseRequested:

  /// When [MenuController.close] is called, the full closing sequence is:
  /// 1. [onCloseRequested]
  /// 2. (optional animation)
  /// 3. `hideOverlay`
  /// 4. [onClose]

I also tried combining both steps and placing them in the widget description, but I found it more intuitive to read parameter definitions.

Let me know if I should add more detail or change anything.

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.

Great so far! More discussion on comments and minor API design.

Also: Can you remove the comment here? Overriding methods should not typically have comments (unless they're doing very differently from what the base method instructs).

@davidhicks980
Copy link
Contributor Author

davidhicks980 commented Jun 4, 2025

But still, how do you think of my analysis above about the logic that an opening/closing should invalidate earlier callbacks in order to make fast opening/closing work properly

Clarify this a bit more. What do you mean by fast opening/closing?

What happens if a show/hideOverlay from an earlier open/close is called?

The menu would open or close, as expected. For a given menu, showOverlay and hideOverlay are the equivalent of calling _RawMenuAnchorState.open() and _RawMenuAnchorState.close(), which stay the same throughout the lifecycle of a widget.

What happens if a show/hideOverlay is called for multiple times?

Calling showOverlay on an already-open menu effectively just calls onOpen and setState, which rebuilds the overlayBuilder.

Calling hideOverlay on an already-closed menu just returns. This means that onClose isn't called. We could change that behavior.

@dkwingsmt
Copy link
Contributor

dkwingsmt commented Jun 4, 2025

For example, the themed menu is controlled by two buttons, one opens the menu and the other closes the menu.

The onOpenRequested is implemented with a 100ms delay followed by a 100ms-long opening animation. The onCloseRequested is implemented with a 100ms delay with no closing animation.

Consider the following event sequence, where the user quickly taps open then close.

  0ms    The user opens the menu, scheduling a `showOverlay` at 100ms
 60ms    The user closes the menu, scheduling a `hideOverlay` at 160ms
100ms    The themed menu calls `showOverlay` and starts an opening animation that ends at 200ms.
160ms    The themed menu calls `hideOverlay`, which terminates the opening animation and hides the overlay.

With the current way, the overlay would appear between 100ms and 160ms. I think this is not desired. Because when the user pressed the closing button, the user intends that the menu should now be closed, and any scheduled opening sequence should be terminated.

I propose that upon MenuController.close, all previous showOverlays should be invalidated. Calling them becomes a noop. Same for the other way around.

@davidhicks980
Copy link
Contributor Author

@dkwingsmt OHH I see what you are suggesting: we should make previous showOverlay/hideOverlay callbacks do nothing if onOpenRequested/onCloseRequested is called again.

It's not a bad idea, but I would advise against it. For lower-level apis, imo predictability is more important than convenience. Changing the behavior of a callback depending on external factors (in this case, whether the parent function has been called again) makes the behavior of a system harder to reason about.

Also, it's easy to mitigate redundant calls. TickerFuture.whenComplete (shown in the PR examples) and Timer.cancel (shown in the example I gave above) address most use cases, including the example you provided.

@dkwingsmt
Copy link
Contributor

dkwingsmt commented Jun 5, 2025

IMO it's not that hard to understand: a showOverlay closure belongs to the open that provides it. Subsequent open/close calls invalidates earlier open, thus invalidating their showOverlay closure. Meanwhile, this comes at the cost of boilerplate for almost every widget that uses it. Handling asynchronous events can be hard and error prone, and it will save implementors a lot of effort by solving them in the base widget, instead of having them figure out TickerFuture and/or Timer rather than Future.

If we really want to go this direction, it's ok as well, but now that the show/hideOverlay between different onOpen/CloseRequested calls are indifferent, it makes no sense to only provide them in the callback: the implementers could call them any time, as many times as needed, or even save a callback for calling way later. We should probably make show/hideOverlay methods of MenuController.

@davidhicks980
Copy link
Contributor Author

davidhicks980 commented Jun 5, 2025

It's not the difficulty of the concept. It's the fact that the function behaves differently depending on when it is called. We could add an assert function to check whether a stale callback is used, but I don't think the functions should be made noop.

showOverlay and hideOverlay don't affect the position of a menu's animation or delay, so I think expanding the MenuController api would lead to confusion. For example, a menu could stay invisible after calling showOverlay, which is confusing. I also don't think we should try to add additional methods to an API unless it's absolutely necessary for what we are trying to accomplish with this PR.

@dkwingsmt
Copy link
Contributor

dkwingsmt commented Jun 5, 2025

You're probably right. Keeping MenuController's interface friendly to app developers, instead of also for package developers, is good for clarity. As for show/hideOverlay, although they're usable outside of onXxxRequested calls, our documentation should have been clear enough that package developers are supposed to use them correctly.

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.

LGTM. What a journey! Thank you for sticking with the PR all along.

@dkwingsmt
Copy link
Contributor

@chunhtai Can you take another look?

@davidhicks980
Copy link
Contributor Author

@dkwingsmt Sounds good, although we can always expand the MenuController api with future PRs. I'm still curious whether anyone will ask for a way to observe whether a menu anchor is opening/closing... we shall see.

Anyways, thanks for the feedback! I think we landed on a solid design, so I'm excited to see what the community does with it.

@dkwingsmt dkwingsmt requested a review from chunhtai June 5, 2025 20:49
super.dispose();
}

void _handleMenuOpenRequest(Offset? position, void Function({Offset? position}) showOverlay) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
void _handleMenuOpenRequest(Offset? position, void Function({Offset? position}) showOverlay) {
void _handleMenuOpenRequest(Offset? position, RawMenuAnchorShowOverlayCallback showOverlay) {

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Replaced with VoidCallback


class Menu extends StatefulWidget {
const Menu({super.key, required this.panelBuilder, required this.builder});
final Widget Function(BuildContext, AnimationStatus) panelBuilder;
Copy link
Contributor

Choose a reason for hiding this comment

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

typedef this

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

/// animation, the themed menu should show the overlay before starting the
/// opening animation, since the animation plays on the overlay itself.
///
/// The `position` argument is the `position` that [MenuController.open] was
Copy link
Contributor

Choose a reason for hiding this comment

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

curious why we expose the position parameter if it is just going to be fed into the showOverlay callback. is there a use case for position parameter?

Copy link
Contributor Author

@davidhicks980 davidhicks980 Jun 7, 2025

Choose a reason for hiding this comment

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

Actually, I meant to remove the position parameter from the showOverlay callback. My mistake.

We kept the position in RawMenuAnchorOpenRequestedCallback for animations. It's the only way to access the position outside of overlayBuilder. I don't mind removing it -- we can always provide a different method of accessing the position if needed. Let me know and I'll push an update.

  • I forgot: the other reason was to allow users to determine if the position has changed. Tong suggested that, when a user calls showOverlay, we stop checking if the position has changed before calling onOpen and continuing on with the function execution. Previously, we would only run the function body if the menu was previously closed/closing or when the position changed. Now, we call onOpen any time showOverlay is called. If users want to change this behavior, the position is an escape hatch.

Separate question: we could choose to only call onOpen and onClose when the overlay is first shown or hidden (e.g. not during opening <-> closing or position changes). I'm not sure what is most useful for users.

Copy link
Contributor

@dkwingsmt dkwingsmt Jun 7, 2025

Choose a reason for hiding this comment

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

The position is provided by the caller of MenuController.open. For example, it might be from the pointer event outside.

MenuController.open passed this position to RawMenuController.handleOpenRequest, which then send this position to the themed menu via onOpenRequested. Later it will also send this position to overlayBuilder. (The showOverlay no longer receives a position.)

Although in most typical cases the position is only used for the overlayBuilder to build an overlay at the correct position, we decided to also send it to onOpenRequested to maximize flexibility of the themed menu. This is because onOpenRequested is the earliest occasion when the themed menu can know the position. The themed menu might use for different reasons. One big reason could be to build something that also relies on the position but is not part of the overlay. There might be other usages, which I can't name, but I like this design for being simple and ensuring maximal flexibility.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Animating reopening when the position changes is another good use case

Copy link
Contributor

Choose a reason for hiding this comment

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

sounds good to me to keep this position and also removing the position from showOverlay

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

/// even when the menu overlay is already showing. As a result, this callback
/// can be used to observe when a menu is repositioned, or when a menu is
/// reopened during a closing animation.
///
Copy link
Contributor

Choose a reason for hiding this comment

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

maybe mention this callback can be used for triggering animation for the opened menu

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

/// This callback is used by themed menu widgets to intercept close requests,
/// typically with the intent of adding a delay or an animation before the
/// menu is hidden.
///
Copy link
Contributor

Choose a reason for hiding this comment

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

handleCloseRequest is actually called regardless of whether the overlay is showing.

Is it only possible when someone called MenuController.closeChildren? still curious why we trigger this callback if menu is already closed

// Mount or reposition the menu before animating the menu open.
showOverlay(position: position);

if (_animationStatus.isForwardOrCompleted) {
Copy link
Contributor

Choose a reason for hiding this comment

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

if the possition is open in a different place, is it typical to retrigger the animation in the new place?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

OneDrive and Google Drive run the opening animation every time, MacOS, Google Docs, and Microsoft Word don't animate at all. If we wanted to animate on repositions, we'd need access to the position.

/// Typically, [onCloseRequested] consists of the following steps:
///
/// 1. Optionally start the closing animation and wait for it to complete.
/// 2. Call `hideOverlay` (whose call chain eventually invokes [onClose]).
Copy link
Contributor

@chunhtai chunhtai Jun 6, 2025

Choose a reason for hiding this comment

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

Calling a widget onClose after a delay which the state may or may not have been destroyed is a footgun API. the parent widget which pass in the onClose callback may not expect it to be called after it is disposed.

not entirely sure what may be the best way to fix this issue with this type of api. Should the onClose be called synchronously when onCloseRequested is called? same of onOpen

I recently came across this in #162062 (comment)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Right now, onClose can't be called after a disposal. If you check the dispose implementation in _RawMenuAnchorBaseMixin:

image

And if you check the close method on _RawMenuAnchorState:

image

When overlayController.hide() is called, isOpen synchronously flips to false. When isOpen is false, onClose won't get called. So, since dispose() calls close(), which hides the overlay and flips isOpen to false, onClose will not be called after disposal.

However, the open() method doesn't perform the same checks, so calling showOverlay after dispose() could call onOpen.

That said, even if showOverlay is called, isOpen will remain false since OverlayPortal checks to see whether a target is attached when returning a value for isShowing:

image

and the target (_OverlayPortalState) is detached during disposal:

image

This doesn't solve onOpen being called after disposal, but it does mean that onClose will never be called after disposal, since isOpen will always be false after disposal.

If you do try to call showOverlay after disposal, you should get an assertion, because the menu controller is no longer attached to RawMenuAnchor. This assertion prevents onOpen from being called, but if we wanted to block calls to showOverlay instead of asserting, we could just check for whether the component is mounted.

Copy link
Contributor

@dkwingsmt dkwingsmt Jun 7, 2025

Choose a reason for hiding this comment

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

Do you think you can make it a noop instead of an assertion? (Or make it throw an exception or a FlutterError if you deem proper) Assertions should only be used for internal errors (bugs with the framework's implementation) instead of user errors.

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 I didn't realize that... I switched it to checking if the component is mounted. Lmk if you all have a preference.

Copy link
Contributor

Choose a reason for hiding this comment

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

In this case, I vote for noop. This is likely only show up in a corner case, which may not be easy to catch during development cycle. and it will make it slightly verbose to need to be aware of lifecycle during the async callback, and all customers will probably use the same code to prevent calls to showOverlay, might as well handle in the RawMenuAnchorState for the developer.

We should document this behavior in the api doc though, that calling showOverlay will be noop and onOpen will not be called if the widget is disposed

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. It was previously documented for onClose and onOpen, but I added docs for onCloseRequested and onOpenRequested. I also changed tests to reflect this behavior (before, I was testing for assertion)

widget.onCloseRequested(close);
} else {
SchedulerBinding.instance.addPostFrameCallback((_) {
widget.onCloseRequested(close);
Copy link
Contributor

Choose a reason for hiding this comment

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

Same since this may potentially become a footgun

should this check if the widget has been disposed? i.e context.mounted

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 couldn't create a situation where this threw an error, possibly because of the phase checking. The finalization phase comes before the post frame callbacks, so I would expect an error. That said, since it's minimal overhead to add the mounted check for safety, I'll add a mounted check. Should I include a comment that clarifies that this may be an unnecessary check? I ask because there were many ambigious phase checks in the original codebase.

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 left out the comment, but did perform the mounted check.

Copy link
Contributor

@chunhtai chunhtai left a comment

Choose a reason for hiding this comment

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

Left another comment in an old thread https://github.com/flutter/flutter/pull/167806/files/632b74dd01378e623e376780331b57b5c23006e0#r2138681431

besides the new comment i added, LGTM

/// This signature is used by the `showOverlay` argument in
/// [RawMenuAnchor.onOpenRequested]. See [RawMenuAnchor.onOpenRequested] for
/// more information.
typedef RawMenuAnchorShowOverlayCallback = void Function();
Copy link
Contributor

Choose a reason for hiding this comment

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

voidCallback

Copy link
Contributor

Choose a reason for hiding this comment

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

or just use void callback in RawMenuAnchorOpenRequestedCallback, RawMenuAnchorCloseRequestedCallback

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

/// callback can be used to add a delay or a closing animation before the menu
/// is hidden.
///
/// After a close request is intercepted and closing behaviors have completed,
Copy link
Contributor

Choose a reason for hiding this comment

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

somewhere in this should mention this will be noop if disposed

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Added:

/// Calling `hideOverlay` after disposal is a no-op, meaning it will not
/// trigger [onClose] or hide the menu overlay.

/// A typical [onOpenRequested] consists of the following steps:
///
/// 1. Optional delay.
/// 2. Call `showOverlay` (whose call chain eventually invokes [onOpen]).
Copy link
Contributor

Choose a reason for hiding this comment

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

should also mention noop when disposed

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Added:

/// Calling `showOverlay` after disposal is a no-op, meaning it will not trigger
/// [onOpen] or show the menu overlay.

@davidhicks980
Copy link
Contributor Author

Note: I just added a change to reduce the number of scrolling/resizing subscriptions. No tests broke, and it should be effectively the same code.

image

final Size newSize = MediaQuery.sizeOf(context);
if (_viewSize != null && newSize != _viewSize && isOpen) {
// Close the menus if the view changes size.
handleCloseRequest();
Copy link
Contributor

Choose a reason for hiding this comment

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

check for isOpen? or maybe add the shortcircuit logic in handleCloseRequest

Copy link
Contributor

Choose a reason for hiding this comment

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

also continue on the discussion on whether to call handleCloseRequest when isClosed or closing.

When it isClosed, I think we have agreement that handleCloseRequest shouldn't be called. Regardless how it is triggered. (let me know otherwise)

When it is closing, do we allow people to not call hideOverlay when handleCloseRequest is called? if so I think we can't really tell whether it is still pending closing or the previous handleCloseRequest is aborted right?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

When it isClosed, I think we have agreement that handleCloseRequest shouldn't be called. Regardless how it is triggered. (let me know otherwise)

  • I agree that we shouldn't call handleCloseRequested on resize/gesture if the menu is closed (this should already be implemented)
  • I prefer calling handleCloseRequested when users call MenuController.close() while the menu is closed. Rationale:
    • It seems confusing that handleOpenRequested is called when a menu is open, but handleCloseRequested is not called if a menu is closed.
    • For developers of themed menus, adding animations is already trivial and blocking handleCloseRequested calls wouldn't make the code simpler
      • image
    • It's slightly less opinionated
    • To your other point, we have no way of blocking handleCloseRequested while the menu is closing. As a result, it's unexpected for handleCloseRequested to be called while the menu is closing, but not while the menu is closed.

That said, I also understand your point and am fine with preventing handleCloseRequested calls after the menu is closed, but I wanted to share the current reasoning and get your confirmation before blocking handleCloseRequested calls while the menu is closed.

When it is closing, do we allow people to not call hideOverlay when handleCloseRequest is called? if so I think we can't really tell whether it is still pending closing or the previous handleCloseRequest is aborted right?

Just to clarify: you're asking whether we should block handleCloseRequested calls if the menu is closing, but not closed?

To your point, we can't reliably tell if a menu is closing. While we could try to infer that a menu is closing if handleCloseRequested is called without being interrupted by handleOpenRequested, this would block new handleCloseRequested calls if a previous handleCloseRequested call is aborted (which you mentioned). As a result, themed menus could no longer choose to conditionally prevent menu closures.

So, it's more flexible to just allow redundant handleCloseRequested while the menu is closing. While this does lead to onCloseRequested being repeatedly called during resize, it's easy enough for themed menus to block redundant calls.

Anyways, let me know what your thoughts are and I'll update the codebase!

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 framework flutter/packages/flutter repository. See also f: labels. will affect goldens Changes to golden files
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants