Alternative to https://github.com/flutter/flutter/pull/163481,
https://github.com/flutter/flutter/pull/167537,
https://github.com/flutter/flutter/pull/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.
<hr />
### 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 https://github.com/flutter/flutter/pull/143416,
https://github.com/flutter/flutter/issues/135025,
https://github.com/flutter/flutter/pull/143712
## Demo
https://github.com/user-attachments/assets/bb14abca-af26-45fe-8d45-289b5d07dab2
```dart
// 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
- [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].
<!-- 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
---------
Co-authored-by: Tong Mu <dkwingsmt@users.noreply.github.com>