-
Notifications
You must be signed in to change notification settings - Fork 29.7k
Fix: Update anchorRect for overlayBuilder when anchor moves #169814
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
SchedulerBinding.instance.addPostFrameCallback((Duration timestamp) {
if (!mounted || !_overlayController.isShowing) {
return;
}
if (info != _getRawMenuOverlayInfo(context)) {
// Rebuild overlayBuilder
_overlayController.hide();
_overlayController.show();
}
});Somehow being callback inside build method, I am not feeling good + the way I am rebuilding overlay. I would love to hear feedback, how we can achieve this otherwise. |
3155855 to
a681e98
Compare
|
|
||
| Widget _buildOverlay(BuildContext context) { | ||
| final RawMenuOverlayInfo info = _getRawMenuOverlayInfo(context); | ||
| SchedulerBinding.instance.addPostFrameCallback((Duration timestamp) { |
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.
Does this always need to be in a postframe 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.
Yes because otherwise we always use older anchorRect.
Also if overlay is opened in first frame, it will open at (0,0).
Because logically first anchor should settle down its position then overlayBuilder should lay itself.
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 you want to pin an overlay entry based on other render object position, we usually do CompositedTransformTarget and CompositedTransformFollower
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.
not always needed though, the difference is that you will see the bouncing effect when the target moves if uses current way to calculate position, because the reposition of the overlay entry will always delayed by 1 frame.
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.
looks like in this case, using target/follower probably the way to go
|
@davidhicks980 Would you like to take a look? (I wonder why you proposed the |
940cb51 to
2ce1be6
Compare
|
Addressing @Piinks' point, the post-frame callback causes a single-frame stutter. To @chunhtai's point, you can use leader/follower for single-layer menus, but:
AFAIK, the whole reason why the menu was programmed to close on scroll/resize is because the position doesn't update. That said, @LongCatIsLooong just landed flutter/packages/flutter/lib/src/widgets/overlay.dart Lines 1828 to 1839 in 8d89d1e
To add support for the root overlay, I proposed making the _OverlayChildLayoutBuilder Hope that makes sense! I wrote this in the main thread so that it wouldn't get buried. |
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 looks like the API is already based on providing screen location in relative to overlay. so converting to using layer leader will have to redesign the API.
edit: after talked to @LongCatIsLooong , there isn't a reason to use layerlink in overlayportal, my information was outdated
| if (!mounted || !_overlayController.isShowing) { | ||
| return; | ||
| } | ||
| if (info != _getRawMenuOverlayInfo(context)) { |
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.
Curious why do we only check for one framew after the build? is it possible that the position move without calling the build?
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.
Its after one frame because when current frame is being build, it doesn't have information of position of anchor.
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.
is this only needed for first build? or is this meant to track the anchor movement over the life time of this overlay entry?
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 it is the first, we should do this in initState or didChangeDepencies if it require context
it is the latter, why do we check ONLY one frame? what if the anchor move after two frames?
|
We expect we'll be able to come back to this next week due to some of us being OOO, thanks @rkishan516 for the patience. |
chunhtai
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.
Sorry for the delay, just back from vacation
To add support for the root overlay, I proposed making the _OverlayChildLayoutBuilder callback widget (returned by OverlayPortal.overlayChildLayoutBuilder)
or just take a targetRootOverlay boolean in the method? not sure if there will be layut complication if use root overlay. I still prefer we go with overlayChildLayoutBuilder if possible without relying on frame delay
| if (!mounted || !_overlayController.isShowing) { | ||
| return; | ||
| } | ||
| if (info != _getRawMenuOverlayInfo(context)) { |
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.
is this only needed for first build? or is this meant to track the anchor movement over the life time of this overlay entry?
| if (!mounted || !_overlayController.isShowing) { | ||
| return; | ||
| } | ||
| if (info != _getRawMenuOverlayInfo(context)) { |
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 it is the first, we should do this in initState or didChangeDepencies if it require context
it is the latter, why do we check ONLY one frame? what if the anchor move after two frames?
No, its required for every position change for anchor.
Because just after position of anchor is decided, immediate post frame callback we can utilize it to render overlay entry at correct position. if anchor moves after n frame, then postFramecallback will be called after that frame is rendered. |
|
@chunhtai I'm also not sure if there are layout complications with the root overlay... A drawback to the boolean approach is that OverlayPortal derivatives, such as RawMenuAnchor, MenuAnchor, and CupertinoMenuAnchor, would need two booleans ( |
the postframecallback will only be called once after the build is called. Do we have code path that triggers the build to be called again after the anchor is moved?
Why do we need |
Yes, we are calling overlayController.show in postFrameCallback which ends up building overlay. |
What I meant was that the anchor box change after the first frame will not notify the overlay to re-calculate the rect. For example class CustomPadding extends StatefulWidget {
@override
State<StatefulWidget> createState() => CustomPaddingState();
}
class CustomPaddingState extends State<CustomPadding> {
double left = 0;
void setPadding(double newLeft) {
setState(() {
left = newLeft;
});
}
@override
Widget build(BuildContext context) {
return Padding(
padding: EdgeInsetsGeometry.only(left: left),
child: const AnchorButton(Tag.anchor),
);
}
}
testWidgets("test", (WidgetTester tester) async {
Rect? anchorRect;
await tester.pumpWidget(
App(
RawMenuAnchor(
controller: controller,
overlayBuilder: (BuildContext context, RawMenuOverlayInfo position) {
anchorRect = position.anchorRect;
return Panel(children: <Widget>[Text(Tag.a.text)]);
},
child: CustomPadding(),
),
),
);
await tester.tap(find.text(Tag.anchor.text));
await tester.pump();
expect(controller.isOpen, isTrue);
expect(find.text(Tag.a.text), findsOneWidget);
final CustomPaddingState state = tester.state<CustomPaddingState>(find.byType(CustomPadding));
final RenderBox anchorBox = state.context.findRenderObject()! as RenderBox;
ui.Offset upperLeft = anchorBox.localToGlobal(Offset.zero);
ui.Offset bottomRight = anchorBox.localToGlobal(anchorBox.size.bottomRight(Offset.zero));
expect(anchorRect, Rect.fromPoints(upperLeft, bottomRight));
/// Shift the box to the right by 10
state.setPadding(10);
await tester.pumpAndSettle();
await tester.pumpAndSettle();
await tester.pumpAndSettle();
await tester.pumpAndSettle();
upperLeft = anchorBox.localToGlobal(Offset.zero);
bottomRight = anchorBox.localToGlobal(anchorBox.size.bottomRight(Offset.zero));
expect(anchorRect, Rect.fromPoints(upperLeft, bottomRight)); // This failed
}); |
|
@chunhtai This example doesn't exactly match, how
Step3 schedule of callback happens every time |
|
@chunhtai That's a good catch. There will be situations where the anchor may move following the first frame. An example for visually confirming: Screen.Recording.2025-08-21.at.12.08.52.AM.movimport 'package:flutter/widgets.dart' hide MenuController, RawMenuAnchor, RawMenuOverlayInfo;
import 'new_raw_menu_anchor.dart';
void main() {
runApp(const GoodCatch());
}
class GoodCatch extends StatefulWidget {
const GoodCatch({super.key});
@override
State<StatefulWidget> createState() => GoodCatchState();
}
class GoodCatchState extends State<GoodCatch> with TickerProviderStateMixin {
double left = 10;
MenuController controller = MenuController();
AnimationController? animationController;
@override
void initState() {
super.initState();
animationController = AnimationController(
duration: const Duration(milliseconds: 300),
vsync: this,
);
Future<void>.delayed(const Duration(milliseconds: 100), controller.open);
Future<void>.delayed(const Duration(seconds: 1), () {
animationController?.repeat(reverse: true);
});
}
@override
Widget build(BuildContext context) {
return Directionality(
textDirection: TextDirection.ltr,
child: Overlay(
initialEntries: <OverlayEntry>[
OverlayEntry(
builder: (BuildContext context) {
return Align(
child: RawMenuAnchor(
controller: controller,
overlayBuilder: (BuildContext context, RawMenuOverlayInfo info) {
return Positioned.fromRect(
rect: info.anchorRect.shift(const Offset(0, 50)),
child: const ColoredBox(
color: Color(0xFFFF00FF),
child: Text('Not jiggy with it :('),
),
);
},
builder: (BuildContext context, MenuController controller, Widget? child) {
return AnimatedBuilder(
animation: animationController!,
builder: (BuildContext context, Widget? child) {
return Padding(
padding: EdgeInsets.only(left: left * animationController!.value),
child: child,
);
},
child: const ColoredBox(
color: Color(0xFF0F00F0),
child: Text("Gettin' jiggy with it"),
),
);
},
),
);
},
),
],
),
);
}
}
|
|
The current solution won't catch all the corner cases, and using postframe callback in build method has potential to cause unending rebuilds, which is quite dangerous For example, I am currently working on #168785 hopefully this can give a way to solve this problem |
Yes, that's what my concern was with this. Okay, I will wait for #168785. |
2ce1be6 to
0a857f8
Compare
|
@rkishan516 FYI it looks like the issue this was waiting on (#168785) has been closed as completed. |
0a857f8 to
f871ca0
Compare
Thanks for reminder @justinmc, I have updated implementation. |
victorsanni
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.
This is part of #173440.
|
@LongCatIsLooong Just to verify, does it matter that MenuAnchor allows for the use of CompositedTransformFollower? flutter/packages/flutter/lib/src/material/menu_anchor.dart Lines 3427 to 3431 in c18530c
Curious because of this line |
You'll get an assertion in debug mode if an |
|
@LongCatIsLooong That makes sense and your doc makes sense. My main concern is existing apps which implement something like the following will now assert in debug: void main() {
runApp(const OverlayTest());
}
class OverlayTest extends StatelessWidget {
const OverlayTest({super.key});
@override
Widget build(BuildContext context) {
return Directionality(
textDirection: TextDirection.ltr,
child: MaterialApp(
home: Column(
children: <Widget>[
MenuAnchor(
layerLink: LayerLink(),
builder: (BuildContext context, MenuController controller, Widget? child) {
return ElevatedButton(
onPressed: () {
if (controller.isOpen) {
controller.close();
} else {
controller.open();
}
},
child: const Text('Show Menu'),
);
},
menuChildren: const <Widget>[
SubmenuButton(
menuChildren: <Widget>[
SizedBox()
],
child: Text('Submenu'),
),
],
),
],
),
),
);
}
}Not technically breaking since no tests are broken, but we may want to deprecate the LayerLink at some point in the future. |
…169814) Fix: Update anchorRect for overlayBuilder when anchor moves fixes: flutter#169457 part of flutter#173440 ## 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.
…169814) Fix: Update anchorRect for overlayBuilder when anchor moves fixes: flutter#169457 part of flutter#173440 ## 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.
Roll Flutter from 908012d58baa to e11e2c11288b (39 revisions) flutter/flutter@908012d...e11e2c1 2025-10-08 [email protected] Configure FfiNative resolver on dart:io (flutter/flutter#176621) 2025-10-08 [email protected] Marks Linux_pixel_7pro service_extensions_test to be unflaky (flutter/flutter#176700) 2025-10-08 [email protected] Keyboard Animation Fix (flutter/flutter#176418) 2025-10-08 [email protected] Feat: Add carousel view builder (flutter/flutter#172837) 2025-10-08 [email protected] Roll Skia from d10a0d877ff4 to ea7cdbc6b986 (15 revisions) (flutter/flutter#176686) 2025-10-08 [email protected] Roll Fuchsia Linux SDK from jJr3my9C6TwYWPygi... to xrIAL91ngrd-wNr9S... (flutter/flutter#176682) 2025-10-08 [email protected] Fix InputDecoration helper/error padding is not compliant (flutter/flutter#176353) 2025-10-08 [email protected] Fix PopupMenu does not update when PopupMenuTheme in Theme changes. (flutter/flutter#175513) 2025-10-07 [email protected] Roll Dart SDK to 3.10.0-290.1.beta (flutter/flutter#176629) 2025-10-07 [email protected] [ Tool ] Output `app.dtd` and `app.devTools` in machine mode (flutter/flutter#176655) 2025-10-07 [email protected] Rename UIScene integration test projects and fix Xcode compatibility (flutter/flutter#176635) 2025-10-07 [email protected] Selecting an implementation widget with the on-device inspector opens the code location for the nearest project widget (flutter/flutter#176530) 2025-10-07 [email protected] Migrate to `WidgetStateInputBorder` (flutter/flutter#176386) 2025-10-07 [email protected] Make it clear that you need to install clangd in VSCode intellisense c++ config (flutter/flutter#176609) 2025-10-07 [email protected] Bump the customer tests to pick up an update to Zulip's tests. (flutter/flutter#176463) 2025-10-07 [email protected] Roll Packages from d3ef88b to 8ca6416 (2 revisions) (flutter/flutter#176633) 2025-10-07 [email protected] Add fallback for 'scene:willConnectToSession:options' (flutter/flutter#176580) 2025-10-07 [email protected] Roll Skia from d09786dfb854 to d10a0d877ff4 (11 revisions) (flutter/flutter#176616) 2025-10-07 [email protected] [ Widget Preview ] Rework UI and theming (flutter/flutter#176581) 2025-10-07 [email protected] Handle FlutterEngine registration when embedded in Multi-Scene apps (flutter/flutter#176490) 2025-10-07 [email protected] Fix code style in Linux embedder template (flutter/flutter#176256) 2025-10-07 [email protected] Add tooling to migrate to UIScene (flutter/flutter#176427) 2025-10-06 [email protected] Bump customer tests.version to 986c4326b4e4bb4e37bc963c2cc2aaa10b943859 (flutter/flutter#176594) 2025-10-06 [email protected] Fix typo in pages.dart (flutter/flutter#176438) 2025-10-06 [email protected] Fix: Update anchorRect for overlayBuilder when anchor moves (flutter/flutter#169814) 2025-10-06 [email protected] Roll Fuchsia Linux SDK from Zm6K_3gP3VCaMy9rH... to jJr3my9C6TwYWPygi... (flutter/flutter#176591) 2025-10-06 [email protected] Fix deprecated configureStatusBarForFullscreenFlutterExperience for Android 15+ (flutter/flutter#175501) 2025-10-06 [email protected] updates docs for flutter engine footprint (flutter/flutter#176217) 2025-10-06 [email protected] [ Widget Preview ] Fix `WidgetInspectorService` override (flutter/flutter#176550) 2025-10-06 [email protected] Fix NavigatorBar lacks visual feedback (flutter/flutter#175182) 2025-10-06 [email protected] Roll Packages from e401aeb to d3ef88b (4 revisions) (flutter/flutter#176582) 2025-10-06 [email protected] Roll Dart SDK from 898380a41c90 to 6b0193498f09 (2 revisions) (flutter/flutter#176576) 2025-10-06 [email protected] Roll Skia from bc7cf194f4ee to d09786dfb854 (1 revision) (flutter/flutter#176577) 2025-10-06 [email protected] Roll vulkan-deps to a9e2ca3b (flutter/flutter#176322) 2025-10-06 [email protected] Add an AppDelegate callback for implicit FlutterEngines (flutter/flutter#176240) 2025-10-06 [email protected] Roll Skia from 45191c22b15c to bc7cf194f4ee (2 revisions) (flutter/flutter#176572) 2025-10-06 [email protected] [ Widget Preview ] Fix type error when retrieving flags from persistent preferences (flutter/flutter#176546) 2025-10-06 [email protected] Roll Skia from 1fd0ca1f2120 to 45191c22b15c (3 revisions) (flutter/flutter#176556) 2025-10-05 [email protected] Roll Dart SDK from 016a8c0045fd to 898380a41c90 (1 revision) (flutter/flutter#176549) If this roll has caused a breakage, revert this CL and stop the roller using the controls here: https://autoroll.skia.org/r/flutter-packages Please CC [email protected],[email protected] on the revert to ensure that a human is aware of the problem. ...
…169814) Fix: Update anchorRect for overlayBuilder when anchor moves fixes: flutter#169457 part of flutter#173440 ## 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.
Fix: Update anchorRect for overlayBuilder when anchor moves
fixes: #169457
part of #173440
Pre-launch Checklist
///).