Reland #179643, only scroll hit-testable primary scroll views on status bar tap#182391
Conversation
e705324 to
4ec4722
Compare
7f45eb7 to
3f7d345
Compare
There was a problem hiding this comment.
Code Review
This pull request refactors the handling of status bar taps on iOS to fix a crash. Previously, fake touch events were sent, which could cause issues if the scaffold was not laid out. The new implementation introduces a dedicated flutter/status_bar method channel to communicate the tap event from the native side to the framework. In the framework, Scaffold and CupertinoPageScaffold now use a hit-test at the origin to ensure only the foreground, hit-testable scroll view responds to the tap, preventing crashes and incorrect behavior. The changes also include updated tests and new testing infrastructure to support this new mechanism. There is one area of code duplication that should be addressed.
| final class _HitTestableAtOrigin extends StatelessWidget { | ||
| const _HitTestableAtOrigin(this.globalKey); | ||
|
|
||
| final GlobalKey globalKey; | ||
|
|
||
| /// Whether the render box of the [_HitTestableAtOrigin] widget associated | ||
| /// with the given global `key` is hit-testable at [Offset.zero]. | ||
| /// | ||
| /// This is used by the `handleStatusBarTap` implementation to avoid sending | ||
| /// status bar tap events to scroll views in offscreen subtrees. | ||
| static bool hitTestableAtOrigin(GlobalKey key) { | ||
| final context = key.currentContext as Element?; | ||
| if (context == null) { | ||
| assert( | ||
| false, | ||
| 'BuildContext associated with $key is not mounted. ' | ||
| 'If you see this in a test, this is likely because the test was trying ' | ||
| 'to simulate status bar tap on a non-iOS platform', | ||
| ); | ||
| return false; | ||
| } | ||
| final renderObject = context.renderObject! as RenderMetaData; | ||
| final int viewId = View.of(context).viewId; | ||
| final result = HitTestResult(); | ||
| WidgetsBinding.instance.hitTestInView(result, Offset.zero, viewId); | ||
| return result.path.any((HitTestEntry entry) => entry.target == renderObject); | ||
| } | ||
|
|
||
| @override | ||
| Widget build(BuildContext context) { | ||
| return MetaData( | ||
| key: globalKey, | ||
| behavior: HitTestBehavior.translucent, | ||
| child: const SizedBox.expand(), | ||
| ); | ||
| } | ||
| } |
There was a problem hiding this comment.
This _HitTestableAtOrigin class is a duplicate of the one defined in packages/flutter/lib/src/cupertino/page_scaffold.dart. To avoid code duplication and improve maintainability, consider moving this class to a shared location.
A possible approach would be to create a new file, for example lib/src/widgets/hit_testable_at_origin.dart, and define a public HitTestableAtOrigin widget there. This file would not be exported by widgets.dart, keeping it as an internal utility. Both material/scaffold.dart and cupertino/page_scaffold.dart could then import and use this shared widget.
When refactoring, please consider keeping the more descriptive assert message from this material/scaffold.dart version.
References
- This class is duplicated in another file. To adhere to the principle of avoiding duplication and keeping a single source of truth, it should be refactored into a shared utility. (link)
|
I've tested the patch that this works on iPad / iPhone simulators. |
| } | ||
| final Widget? statusBar = switch (themeData.platform) { | ||
| TargetPlatform.iOS || | ||
| TargetPlatform.macOS => widget.primary ? _HitTestableAtOrigin(_statusBarKey) : null, |
There was a problem hiding this comment.
Ideally this should be for iOS only, as on macOS the _HitTestableAtOrigin widget will never be used.
| case TargetPlatform.windows: | ||
| break; | ||
| } | ||
| final Widget? statusBar = switch (themeData.platform) { |
There was a problem hiding this comment.
The change here (and in cupertino page scaffold) only replaces the tap gesture recognizer with _HitTestableAtOrigin.
| /// | ||
| /// * [Scaffold] and [CupertinoPageScaffold] which use this callback to implement | ||
| /// iOS scroll-to-top. | ||
| void handleStatusBarTap() {} |
There was a problem hiding this comment.
If this need to have the prevent bubble up , this should return bool similar to handleStartBackGesture
There was a problem hiding this comment.
You mean we stop delivering a status bar tap event to the remaining listeners, as soon as one of the callbacks returned true? Unfortunately we deliver status bar tap events to listeners in registration order, and since the registration will most likely happen in initState / activate, I don't think we can guarantee that States with higher precedence for handling status bar taps will be mounted before low precedence ones (especially when there're LayoutBuilders in the app). Also there could be use cases that two scrollables should handle a single status bar tap simultaneously.
There was a problem hiding this comment.
looking at the code, the _HitTestableAtOrigin.hitTestableAtOrigin decided whether it should scroll to the top for a scaffold? can that be used to determine whether to return true or false?
Also there could be use cases that two scrollables should handle a single status bar tap simultaneously.
Can you talk more about this? does that mean there are two scaffold that has overlapping hittest origin?
There was a problem hiding this comment.
Also there could be use cases that two scrollables should handle a single status bar tap simultaneously.
The case I have in mind is that there are two scrollables placed next to each other and the app developer decided that both of them should respond to the same status bar tap (maybe for something like text diff UI). This is the WidgetsBindingObserver API so it's possible for app developers to add their own observers that unconditionally handle status bar taps, not just Scaffolds.
There was a problem hiding this comment.
In that case, why just do this when its top is align with view's top? is this the existing behavior before the change?
other than that I think using void is fine.
There was a problem hiding this comment.
Yeah the Offset.zero hit-test is from the old implementation (fake pointer down + up events at the origin).
| // the status bar tap is handled by the same Scaffolds as before. | ||
| // Switch to a better solution when available: | ||
| // https://github.com/flutter/flutter/issues/182403 | ||
| _HitTestableAtOrigin.hitTestableAtOrigin(_statusBarKey)) { |
There was a problem hiding this comment.
can you talk more about this extra check? what is a counter example if we don't have this check?
There was a problem hiding this comment.
I put the relevant code comment at the declaration site of the hitTestableAtOrigin (because there's already a TODO here). Do you prefer moving that comment up to the call site?
| /// | ||
| /// * [Scaffold] and [CupertinoPageScaffold] which use this callback to implement | ||
| /// iOS scroll-to-top. | ||
| void handleStatusBarTap() {} |
There was a problem hiding this comment.
looking at the code, the _HitTestableAtOrigin.hitTestableAtOrigin decided whether it should scroll to the top for a scaffold? can that be used to determine whether to return true or false?
Also there could be use cases that two scrollables should handle a single status bar tap simultaneously.
Can you talk more about this? does that mean there are two scaffold that has overlapping hittest origin?
| primaryScrollController.hasClients && | ||
| // TODO(LongCatIsLooong): the iOS embedder used to send status bar tap | ||
| // evets as fake touches at Offset.zero. The hit-test here makes sure | ||
| // the status bar tap is handled by the same Scaffolds as before. |
There was a problem hiding this comment.
What does the as before mean?
There was a problem hiding this comment.
rephrased the todo to make it clear that hit test is to keep the legacy behavior.
There was a problem hiding this comment.
this is not help either, reader won't know what legacy behavior is, we should just state what the behavior is
There was a problem hiding this comment.
The comment goes like this:
// TODO(LongCatIsLooong): the iOS embedder used to send status bar tap
// evets as fake touches at Offset.zero, such that at most one Scaffold
// (usually the foreground primary Scaffold) can handle the status bar
// tap event, thanks to hit-testing and gesture disambiguation.
// To keep that behavior, this widget performs an additional hit-test here
// to make sure the status bar tap is only handled if the scaffold is
// hit-testable (thus in the foreground)
// Switch to a better solution when available:
// https://github.com/flutter/flutter/issues/182403
…or iOS status bar tap
3f7d345 to
a5a36c5
Compare
|
The iOS parts of this are a direct reland and the changes are all part of the framework, so I'll leave the approval up @chunhtai |
|
Merging this after verifying that the error handler is catching the user callback error in case the callback throws again. Update: it is not, because it's an async call. #182233 (comment) |
…ws on status bar tap (flutter/flutter#182391)
…ws on status bar tap (flutter/flutter#182391)
…ws on status bar tap (flutter/flutter#182391)
…ws on status bar tap (flutter/flutter#182391)
|
Failed to create CP due to merge conflicts. |
1 similar comment
|
Failed to create CP due to merge conflicts. |
…11088) Manual roll Flutter from c023e5b2474f to 91b2d41a66d1 (31 revisions) Manual roll requested by [email protected] flutter/flutter@c023e5b...91b2d41 2026-02-19 [email protected] Reland #179643, only scroll hit-testable primary scroll views on status bar tap (flutter/flutter#182391) 2026-02-19 [email protected] Replace References to `flutter/engine` with `flutter/flutter` (flutter/flutter#182600) 2026-02-19 [email protected] Remove specific iOS extended attributes to fix code signing (flutter/flutter#180710) 2026-02-19 [email protected] Manual roll Skia from 7bbdc51ab0aa to ce5854495a3a (flutter/flutter#182637) 2026-02-19 [email protected] [pv]add integration test for original untappable web view link behind context menu bug (flutter/flutter#182111) 2026-02-19 [email protected] Roll pub packages (flutter/flutter#182579) 2026-02-19 [email protected] Remove Material import from scroll_view_test.dart (flutter/flutter#181281) 2026-02-19 [email protected] Add RawTooltip.ignorePointer (flutter/flutter#182527) 2026-02-19 [email protected] [web] Stop double loading fonts for WebParagraph (flutter/flutter#182026) 2026-02-19 [email protected] Migrate abi build paths to use new abi filtering api #AGP9 (flutter/flutter#181828) 2026-02-19 [email protected] [web] Flutter errors should be reported with console.error() (flutter/flutter#178886) 2026-02-19 [email protected] Manual roll Skia from dfe78d132e24 to 7bbdc51ab0aa (8 revisions) (flutter/flutter#182612) 2026-02-19 [email protected] Refactor autofill_group_test.dart to remove Material dependencies (flutter/flutter#181903) 2026-02-19 [email protected] Roll Packages from 59f905c to 9da22bf (8 revisions) (flutter/flutter#182611) 2026-02-19 [email protected] Roll Fuchsia Linux SDK from Ihau0pUz3u5ajw42u... to KfPgw04T0OEADLJA5... (flutter/flutter#182607) 2026-02-19 [email protected] Marks Mac_arm64_mokey entrypoint_dart_registrant unflaky (flutter/flutter#181648) 2026-02-19 [email protected] Remove material from Modal barrier tests (flutter/flutter#181708) 2026-02-19 [email protected] Remove material from ticker mode test (flutter/flutter#181696) 2026-02-19 [email protected] Remove material imports from Inherited Model, Magnifier, SafeArea, UndoHistory, Navigator and Layers test (flutter/flutter#181709) 2026-02-19 [email protected] docs: fix grammar in animation library documentation (flutter/flutter#182461) 2026-02-19 [email protected] Handle#6537 first grouped tests (flutter/flutter#182077) 2026-02-19 [email protected] Move SelectionArea web test from widgets to material folder (flutter/flutter#181951) 2026-02-19 [email protected] Roll Dart SDK from 44895e617182 to 2642761fca94 (6 revisions) (flutter/flutter#182572) 2026-02-19 [email protected] Update create template to always generate both SwiftPM and CocoaPods support for iOS/macOS plugins (flutter/flutter#181251) 2026-02-18 [email protected] Fix(Material): DateRangePicker ignores DatePickerTheme.dayShape (flutter/flutter#181658) 2026-02-18 [email protected] Fixing ExpansionTile expandedAlignment not Accepts AlignmentGeometry … (flutter/flutter#180814) 2026-02-18 [email protected] Give guided error message when CocoaPod and SwiftPM dependency conflicts (flutter/flutter#182392) 2026-02-18 [email protected] Remove material from interactive_viewer_test.dart (flutter/flutter#181465) 2026-02-18 [email protected] Bring Windows misc coverage out of bringup (flutter/flutter#182332) 2026-02-18 [email protected] Update android symbolication instructions (flutter/flutter#181267) 2026-02-18 [email protected] Unmark stable vulkan platform view tests as bringup (flutter/flutter#182554) 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. To file a bug in Packages: https://github.com/flutter/flutter/issues/new/choose To report a problem with the AutoRoller itself, please file a bug: https://issues.skia.org/issues/new?component=1389291&template=1850622 Documentation for the AutoRoller is here: ...
…on status bar tap (flutter#182391) Reland flutter#179643, with [changes](flutter@3f7d345) to fix flutter#182233. The reland closes flutter#177992, closes flutter#175606. The crash happened because `Scaffold.handleStatusBar` (and subsequently `ScrollController.animateTo`) is called on a scaffold that [has never been laid out](flutter#182233 (comment)). Before flutter#179643, the iOS embedder sends a fake touch down pointer event at `(0, 0)` to the framework, followed by a touch up pointer event at the same location. That ensured only the foreground `Scaffold` could claim the tap gesture and handle the status bar tap. The fix tries to restore the previous behavior (where the iOS behavior sends fake touches instead of platform messages) by performing a hit-test at `(0, 0)` before trying to scroll the primary controller of a `Scaffold` to the top, this should make sure that only the foreground scaffold (in theory it's still possible for more than one Scaffold to receive the hittest and handle the status bar tap event, but the foreground scaffold should block the hit-test on background ones instead of relying on gesture disambiguation). - [ ] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [ ] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [ ] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [ ] I signed the [CLA]. - [ ] I listed at least one issue that this PR fixes in the description above. - [ ] I updated/added relevant documentation (doc comments with `///`). - [ ] I added new tests to check the change I am making, or this PR is [test-exempt]. - [ ] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [ ] All existing and new tests are passing. If you need help, consider asking for advice on the #hackers-new channel on [Discord]. **Note**: The Flutter team is currently trialing the use of [Gemini Code Assist for GitHub](https://developers.google.com/gemini-code-assist/docs/review-github-code). Comments from the `gemini-code-assist` bot should not be taken as authoritative feedback from the Flutter team. If you find its comments useful you can update your code accordingly, but if you are unsure or disagree with the feedback, please feel free to wait for a Flutter team member's review for guidance on which automated comments should be addressed. <!-- Links --> [Contributor Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview [Tree Hygiene]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md [test-exempt]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests [Flutter Style Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md [Features we expect every widget to implement]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement [CLA]: https://cla.developers.google.com/ [flutter/tests]: https://github.com/flutter/tests [breaking change policy]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes [Discord]: https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md [Data Driven Fixes]: https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md
…on status bar tap (flutter#182391) Reland flutter#179643, with [changes](flutter@3f7d345) to fix flutter#182233. The reland closes flutter#177992, closes flutter#175606. The crash happened because `Scaffold.handleStatusBar` (and subsequently `ScrollController.animateTo`) is called on a scaffold that [has never been laid out](flutter#182233 (comment)). Before flutter#179643, the iOS embedder sends a fake touch down pointer event at `(0, 0)` to the framework, followed by a touch up pointer event at the same location. That ensured only the foreground `Scaffold` could claim the tap gesture and handle the status bar tap. The fix tries to restore the previous behavior (where the iOS behavior sends fake touches instead of platform messages) by performing a hit-test at `(0, 0)` before trying to scroll the primary controller of a `Scaffold` to the top, this should make sure that only the foreground scaffold (in theory it's still possible for more than one Scaffold to receive the hittest and handle the status bar tap event, but the foreground scaffold should block the hit-test on background ones instead of relying on gesture disambiguation). - [ ] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [ ] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [ ] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [ ] I signed the [CLA]. - [ ] I listed at least one issue that this PR fixes in the description above. - [ ] I updated/added relevant documentation (doc comments with `///`). - [ ] I added new tests to check the change I am making, or this PR is [test-exempt]. - [ ] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [ ] All existing and new tests are passing. If you need help, consider asking for advice on the #hackers-new channel on [Discord]. **Note**: The Flutter team is currently trialing the use of [Gemini Code Assist for GitHub](https://developers.google.com/gemini-code-assist/docs/review-github-code). Comments from the `gemini-code-assist` bot should not be taken as authoritative feedback from the Flutter team. If you find its comments useful you can update your code accordingly, but if you are unsure or disagree with the feedback, please feel free to wait for a Flutter team member's review for guidance on which automated comments should be addressed. <!-- Links --> [Contributor Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview [Tree Hygiene]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md [test-exempt]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests [Flutter Style Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md [Features we expect every widget to implement]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement [CLA]: https://cla.developers.google.com/ [flutter/tests]: https://github.com/flutter/tests [breaking change policy]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes [Discord]: https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md [Data Driven Fixes]: https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md
…l views on status bar tap (flutter#182391)" This reverts commit d2eeab6.
…on status bar tap (flutter#182391) Reland flutter#179643, with [changes](flutter@3f7d345) to fix flutter#182233. The reland closes flutter#177992, closes flutter#175606. The crash happened because `Scaffold.handleStatusBar` (and subsequently `ScrollController.animateTo`) is called on a scaffold that [has never been laid out](flutter#182233 (comment)). Before flutter#179643, the iOS embedder sends a fake touch down pointer event at `(0, 0)` to the framework, followed by a touch up pointer event at the same location. That ensured only the foreground `Scaffold` could claim the tap gesture and handle the status bar tap. The fix tries to restore the previous behavior (where the iOS behavior sends fake touches instead of platform messages) by performing a hit-test at `(0, 0)` before trying to scroll the primary controller of a `Scaffold` to the top, this should make sure that only the foreground scaffold (in theory it's still possible for more than one Scaffold to receive the hittest and handle the status bar tap event, but the foreground scaffold should block the hit-test on background ones instead of relying on gesture disambiguation). - [ ] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [ ] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [ ] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [ ] I signed the [CLA]. - [ ] I listed at least one issue that this PR fixes in the description above. - [ ] I updated/added relevant documentation (doc comments with `///`). - [ ] I added new tests to check the change I am making, or this PR is [test-exempt]. - [ ] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [ ] All existing and new tests are passing. If you need help, consider asking for advice on the #hackers-new channel on [Discord]. **Note**: The Flutter team is currently trialing the use of [Gemini Code Assist for GitHub](https://developers.google.com/gemini-code-assist/docs/review-github-code). Comments from the `gemini-code-assist` bot should not be taken as authoritative feedback from the Flutter team. If you find its comments useful you can update your code accordingly, but if you are unsure or disagree with the feedback, please feel free to wait for a Flutter team member's review for guidance on which automated comments should be addressed. <!-- Links --> [Contributor Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview [Tree Hygiene]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md [test-exempt]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests [Flutter Style Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md [Features we expect every widget to implement]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement [CLA]: https://cla.developers.google.com/ [flutter/tests]: https://github.com/flutter/tests [breaking change policy]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes [Discord]: https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md [Data Driven Fixes]: https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md
This pull request is manually created because there were merge conflicts. <details> <summary><b>Click here for the conflicts</b></summary> ```diff diff --cc packages/flutter/test/widgets/interactive_viewer_test.dart index e4ef349,63b1c353d72..00000000000 --- a/packages/flutter/test/widgets/interactive_viewer_test.dart +++ b/packages/flutter/test/widgets/interactive_viewer_test.dart @@@ -1044,25 -956,22 +1044,43 @@@ void main() var calledStart = false; var calledUpdate = false; var calledEnd = false; + const sizedBox = SizedBox(width: 200.0, height: 200.0); await tester.pumpWidget( ++<<<<<<< HEAD + MaterialApp( + home: Scaffold( + body: Center( + child: InteractiveViewer( + transformationController: transformationController, + scaleEnabled: false, + onInteractionStart: (ScaleStartDetails details) { + calledStart = true; + }, + onInteractionUpdate: (ScaleUpdateDetails details) { + calledUpdate = true; + }, + onInteractionEnd: (ScaleEndDetails details) { + calledEnd = true; + }, + child: const SizedBox(width: 200.0, height: 200.0), + ), + ), ++======= + Center( + child: InteractiveViewer( + transformationController: transformationController, + scaleEnabled: false, + onInteractionStart: (ScaleStartDetails details) { + calledStart = true; + }, + onInteractionUpdate: (ScaleUpdateDetails details) { + calledUpdate = true; + }, + onInteractionEnd: (ScaleEndDetails details) { + calledEnd = true; + }, + child: sizedBox, ++>>>>>>> 91b2d41 (Reland #179643, only scroll hit-testable primary scroll views on status bar tap (#182391)) ), ), ); @@@ -1119,25 -1028,22 +1137,43 @@@ var calledStart = false; var calledUpdate = false; var calledEnd = false; + const sizedBox = SizedBox(width: 200.0, height: 200.0); await tester.pumpWidget( ++<<<<<<< HEAD + MaterialApp( + home: Scaffold( + body: Center( + child: InteractiveViewer( + transformationController: transformationController, + scaleEnabled: false, + onInteractionStart: (ScaleStartDetails details) { + calledStart = true; + }, + onInteractionUpdate: (ScaleUpdateDetails details) { + calledUpdate = true; + }, + onInteractionEnd: (ScaleEndDetails details) { + calledEnd = true; + }, + child: const SizedBox(width: 200.0, height: 200.0), + ), + ), ++======= + Center( + child: InteractiveViewer( + transformationController: transformationController, + scaleEnabled: false, + onInteractionStart: (ScaleStartDetails details) { + calledStart = true; + }, + onInteractionUpdate: (ScaleUpdateDetails details) { + calledUpdate = true; + }, + onInteractionEnd: (ScaleEndDetails details) { + calledEnd = true; + }, + child: sizedBox, ++>>>>>>> 91b2d41 (Reland #179643, only scroll hit-testable primary scroll views on status bar tap (#182391)) ), ), ); ``` </details> ### Issue Link: What is the link to the issue this cherry-pick is addressing? #182233 The bug was introduced in #179643 which was cherry picked to 3.41 beta (so the crash should exist on both current beta and stable). ### Impact Description: On iOS, the app may crash when the user taps on the status bar to scroll to top, when the app has a primary scroll view that has never been laid out (for example, in a route that has been obstructed by other routes since its creation). ### Changelog Description: - [flutter/182233](#182233) - Tapping on the status bar may crash the app on iOS when there's a primary scroll view that has never been laid out. Additionally this patch only dispatches the "scroll to top" command to the topmost Scaffold which is generally better UX for most apps (this doesn't have to be in the changlog) ### Workaround: No known easy workarounds. ### Risk: What is the risk level of this cherry-pick? ### Test Coverage: Are you confident that your fix is well-tested by automated tests? ### Validation Steps: What are the steps to validate that this fix works? I've manually tested the fix on iOS/iPadOS simulators. There are also unit tests.
This pull request is manually created because there were merge conflicts. <details> <summary><b>Click here for the conflicts</b></summary> ```diff diff --cc packages/flutter/test/widgets/interactive_viewer_test.dart index e4ef349,63b1c353d72..00000000000 --- a/packages/flutter/test/widgets/interactive_viewer_test.dart +++ b/packages/flutter/test/widgets/interactive_viewer_test.dart @@@ -1044,25 -956,22 +1044,43 @@@ void main() var calledStart = false; var calledUpdate = false; var calledEnd = false; + const sizedBox = SizedBox(width: 200.0, height: 200.0); await tester.pumpWidget( ++<<<<<<< HEAD + MaterialApp( + home: Scaffold( + body: Center( + child: InteractiveViewer( + transformationController: transformationController, + scaleEnabled: false, + onInteractionStart: (ScaleStartDetails details) { + calledStart = true; + }, + onInteractionUpdate: (ScaleUpdateDetails details) { + calledUpdate = true; + }, + onInteractionEnd: (ScaleEndDetails details) { + calledEnd = true; + }, + child: const SizedBox(width: 200.0, height: 200.0), + ), + ), ++======= + Center( + child: InteractiveViewer( + transformationController: transformationController, + scaleEnabled: false, + onInteractionStart: (ScaleStartDetails details) { + calledStart = true; + }, + onInteractionUpdate: (ScaleUpdateDetails details) { + calledUpdate = true; + }, + onInteractionEnd: (ScaleEndDetails details) { + calledEnd = true; + }, + child: sizedBox, ++>>>>>>> 91b2d41 (Reland #179643, only scroll hit-testable primary scroll views on status bar tap (#182391)) ), ), ); @@@ -1119,25 -1028,22 +1137,43 @@@ var calledStart = false; var calledUpdate = false; var calledEnd = false; + const sizedBox = SizedBox(width: 200.0, height: 200.0); await tester.pumpWidget( ++<<<<<<< HEAD + MaterialApp( + home: Scaffold( + body: Center( + child: InteractiveViewer( + transformationController: transformationController, + scaleEnabled: false, + onInteractionStart: (ScaleStartDetails details) { + calledStart = true; + }, + onInteractionUpdate: (ScaleUpdateDetails details) { + calledUpdate = true; + }, + onInteractionEnd: (ScaleEndDetails details) { + calledEnd = true; + }, + child: const SizedBox(width: 200.0, height: 200.0), + ), + ), ++======= + Center( + child: InteractiveViewer( + transformationController: transformationController, + scaleEnabled: false, + onInteractionStart: (ScaleStartDetails details) { + calledStart = true; + }, + onInteractionUpdate: (ScaleUpdateDetails details) { + calledUpdate = true; + }, + onInteractionEnd: (ScaleEndDetails details) { + calledEnd = true; + }, + child: sizedBox, ++>>>>>>> 91b2d41 (Reland #179643, only scroll hit-testable primary scroll views on status bar tap (#182391)) ), ), ); ``` </details> ### Issue Link: What is the link to the issue this cherry-pick is addressing? #182233 The bug was introduced in #179643 which was cherry picked to 3.41 beta (so the crash should exist on both current beta and stable). ### Impact Description: On iOS, the app may crash when the user taps on the status bar to scroll to top, when the app has a primary scroll view that has never been laid out (for example, in a route that has been obstructed by other routes). ### Changelog Description: - [flutter/182233](#182233) - Tapping on the status bar may crash the app on iOS when there's a primary scroll view that has never been laid out. Additionally this patch only dispatches the "scroll to top" command to the topmost Scaffold which is generally better UX for most apps (this doesn't have to be in the changlog) ### Workaround: No known easy workarounds. ### Risk: What is the risk level of this cherry-pick? ### Test Coverage: Are you confident that your fix is well-tested by automated tests? ### Validation Steps: What are the steps to validate that this fix works? I've manually tested the fix on iOS/iPadOS simulators. There are also unit tests.
…on status bar tap (flutter#182391) Reland flutter#179643, with [changes](flutter@3f7d345) to fix flutter#182233. The reland closes flutter#177992, closes flutter#175606. The crash happened because `Scaffold.handleStatusBar` (and subsequently `ScrollController.animateTo`) is called on a scaffold that [has never been laid out](flutter#182233 (comment)). Before flutter#179643, the iOS embedder sends a fake touch down pointer event at `(0, 0)` to the framework, followed by a touch up pointer event at the same location. That ensured only the foreground `Scaffold` could claim the tap gesture and handle the status bar tap. The fix tries to restore the previous behavior (where the iOS behavior sends fake touches instead of platform messages) by performing a hit-test at `(0, 0)` before trying to scroll the primary controller of a `Scaffold` to the top, this should make sure that only the foreground scaffold (in theory it's still possible for more than one Scaffold to receive the hittest and handle the status bar tap event, but the foreground scaffold should block the hit-test on background ones instead of relying on gesture disambiguation). ## Pre-launch Checklist - [ ] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [ ] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [ ] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [ ] I signed the [CLA]. - [ ] I listed at least one issue that this PR fixes in the description above. - [ ] I updated/added relevant documentation (doc comments with `///`). - [ ] I added new tests to check the change I am making, or this PR is [test-exempt]. - [ ] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [ ] All existing and new tests are passing. If you need help, consider asking for advice on the #hackers-new channel on [Discord]. **Note**: The Flutter team is currently trialing the use of [Gemini Code Assist for GitHub](https://developers.google.com/gemini-code-assist/docs/review-github-code). Comments from the `gemini-code-assist` bot should not be taken as authoritative feedback from the Flutter team. If you find its comments useful you can update your code accordingly, but if you are unsure or disagree with the feedback, please feel free to wait for a Flutter team member's review for guidance on which automated comments should be addressed. <!-- Links --> [Contributor Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview [Tree Hygiene]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md [test-exempt]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests [Flutter Style Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md [Features we expect every widget to implement]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement [CLA]: https://cla.developers.google.com/ [flutter/tests]: https://github.com/flutter/tests [breaking change policy]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes [Discord]: https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md [Data Driven Fixes]: https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md
Reland #179643, with changes to fix #182233. The reland closes #177992, closes #175606.
The crash happened because
Scaffold.handleStatusBar(and subsequentlyScrollController.animateTo) is called on a scaffold that has never been laid out. Before #179643, the iOS embedder sends a fake touch down pointer event at(0, 0)to the framework, followed by a touch up pointer event at the same location. That ensured only the foregroundScaffoldcould claim the tap gesture and handle the status bar tap.The fix tries to restore the previous behavior (where the iOS behavior sends fake touches instead of platform messages) by performing a hit-test at
(0, 0)before trying to scroll the primary controller of aScaffoldto the top, this should make sure that only the foreground scaffold (in theory it's still possible for more than one Scaffold to receive the hittest and handle the status bar tap event, but the foreground scaffold should block the hit-test on background ones instead of relying on gesture disambiguation).Pre-launch Checklist
///).If you need help, consider asking for advice on the #hackers-new channel on Discord.
Note: The Flutter team is currently trialing the use of Gemini Code Assist for GitHub. Comments from the
gemini-code-assistbot should not be taken as authoritative feedback from the Flutter team. If you find its comments useful you can update your code accordingly, but if you are unsure or disagree with the feedback, please feel free to wait for a Flutter team member's review for guidance on which automated comments should be addressed.