Revert "Send statusBarTouch events via dedicated messages (#179643)" (#182223)
This reverts commit f4c83de1bf17e4fb4706122ff9d0a5f9f6ed4308.
See b/482565401.
## 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].
**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
diff --git a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterEngine.mm b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterEngine.mm
index 41c5a66..90035fe 100644
--- a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterEngine.mm
+++ b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterEngine.mm
@@ -147,9 +147,6 @@
@property(nonatomic, strong) FlutterMethodChannel* navigationChannel;
@property(nonatomic, strong) FlutterMethodChannel* restorationChannel;
@property(nonatomic, strong) FlutterMethodChannel* platformChannel;
-// This channel only sends status bar related events to the framework thus has
-// no handlers.
-@property(nonatomic, strong) FlutterMethodChannel* statusBarChannel;
@property(nonatomic, strong) FlutterMethodChannel* platformViewsChannel;
@property(nonatomic, strong) FlutterMethodChannel* textInputChannel;
@property(nonatomic, strong) FlutterMethodChannel* undoManagerChannel;
@@ -580,7 +577,6 @@
self.navigationChannel = nil;
self.restorationChannel = nil;
self.platformChannel = nil;
- self.statusBarChannel = nil;
self.platformViewsChannel = nil;
self.textInputChannel = nil;
self.undoManagerChannel = nil;
@@ -645,12 +641,6 @@
binaryMessenger:self.binaryMessenger
codec:[FlutterJSONMethodCodec sharedInstance]];
- self.statusBarChannel =
- [[FlutterMethodChannel alloc] initWithName:@"flutter/status_bar"
- binaryMessenger:self.binaryMessenger
- codec:[FlutterJSONMethodCodec sharedInstance]];
- [self.statusBarChannel resizeChannelBuffer:0]; // No buffering.
-
self.platformViewsChannel =
[[FlutterMethodChannel alloc] initWithName:@"flutter/platform_views"
binaryMessenger:self.binaryMessenger
@@ -1493,13 +1483,6 @@
[self.localizationChannel invokeMethod:@"setLocale" arguments:localeData];
}
-- (void)onStatusBarTap {
- // Called by FlutterViewController to notify the framework that a tap landed
- // on the status bar, and the most relevant vertical scroll view visible in the
- // app, if applicable, should scroll to top.
- [self.statusBarChannel invokeMethod:@"handleScrollToTop" arguments:nil];
-}
-
- (void)waitForFirstFrameSync:(NSTimeInterval)timeout
callback:(NS_NOESCAPE void (^_Nonnull)(BOOL didTimeout))callback {
fml::TimeDelta waitTime = fml::TimeDelta::FromMilliseconds(timeout * 1000);
diff --git a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterEngine_Internal.h b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterEngine_Internal.h
index a1818b2..c7e061d 100644
--- a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterEngine_Internal.h
+++ b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterEngine_Internal.h
@@ -124,7 +124,6 @@
- (void)sendDeepLinkToFramework:(NSURL*)url completionHandler:(void (^)(BOOL success))completion;
-- (void)onStatusBarTap;
@end
@interface FlutterImplicitEngineBridgeImpl : NSObject <FlutterImplicitEngineBridge>
diff --git a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterViewController.mm b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterViewController.mm
index 831568d..a5b726b 100644
--- a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterViewController.mm
+++ b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterViewController.mm
@@ -554,13 +554,29 @@
return pointer_data;
}
+static void SendFakeTouchEvent(UIScreen* screen,
+ FlutterEngine* engine,
+ CGPoint location,
+ flutter::PointerData::Change change) {
+ const CGFloat scale = screen.scale;
+ flutter::PointerData pointer_data = [[engine viewController] generatePointerDataForFake];
+ pointer_data.physical_x = location.x * scale;
+ pointer_data.physical_y = location.y * scale;
+ auto packet = std::make_unique<flutter::PointerDataPacket>(/*count=*/1);
+ pointer_data.change = change;
+ packet->SetPointerData(0, pointer_data);
+ [engine dispatchPointerDataPacket:std::move(packet)];
+}
+
- (BOOL)scrollViewShouldScrollToTop:(UIScrollView*)scrollView {
if (!self.engine) {
return NO;
}
- if (self.isViewLoaded) {
- // Status bar taps before the UI is visible should be ignored.
- [self.engine onStatusBarTap];
+ CGPoint statusBarPoint = CGPointZero;
+ UIScreen* screen = self.flutterScreenIfViewLoaded;
+ if (screen) {
+ SendFakeTouchEvent(screen, self.engine, statusBarPoint, flutter::PointerData::Change::kDown);
+ SendFakeTouchEvent(screen, self.engine, statusBarPoint, flutter::PointerData::Change::kUp);
}
return NO;
}
diff --git a/engine/src/flutter/testing/ios_scenario_app/README.md b/engine/src/flutter/testing/ios_scenario_app/README.md
index f12654e..4efafa0 100644
--- a/engine/src/flutter/testing/ios_scenario_app/README.md
+++ b/engine/src/flutter/testing/ios_scenario_app/README.md
@@ -18,26 +18,11 @@
## Adding a New Scenario
-Like a regular Flutter iOS app, the Scenario app consists of the [iOS embedding
-code](ios/Scenarios/Scenarios/AppDelegate.m) and the dart logic that are
-`Scenario`s.
-
-To introduce a new subclass of [Scenario](lib/src/scenario.dart), add it to the map
-in [scenarios.dart](lib/src/scenarios.dart). For an example,
+Create a new subclass of [Scenario](lib/src/scenario.dart) and add it to the map
+in [scenarios.dart](lib/src/scenarios.dart). For an example, see
[animated_color_square.dart](lib/src/animated_color_square.dart), which draws a
continuously animating colored square that bounces off the sides of the
viewport.
-The Scenarios app loads a `Scenario` when it receives a `set_scenario` method call
-on the `driver` platform channel from the objective-c code. However if you're
-adding a UI test this is typically not needed as you typically should add a new
-launch argument. See
-[ScenariosUITests](ios/Scenarios/ScenariosUITests/README.md) for more details.
-
-## Running a specific test
-
-The `run_ios_tests.sh` script runs all tests in the `Scenarios` project. If you're
-debugging a specific test, rebuild the `ios_debug_sim_unopt_arm64` engine variant
-(assuming testing on a simulator on Apple Silicon chips), and open
-`src/out/ios_debug_sim_unopt_arm64/ios_scenario_app/Scenarios.xcworkspace` in xcode.
-Use the xcode UI to run the test.
+Then set the scenario from the iOS app by calling `set_scenario` on platform
+channel `driver`.
diff --git a/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/Scenarios.xcodeproj/xcshareddata/xcschemes/Scenarios.xcscheme b/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/Scenarios.xcodeproj/xcshareddata/xcschemes/Scenarios.xcscheme
index fa70fe5..dcd0cd0 100644
--- a/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/Scenarios.xcodeproj/xcshareddata/xcschemes/Scenarios.xcscheme
+++ b/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/Scenarios.xcodeproj/xcshareddata/xcschemes/Scenarios.xcscheme
@@ -78,10 +78,6 @@
isEnabled = "NO">
</CommandLineArgument>
<CommandLineArgument
- argument = "--tap-status-bar"
- isEnabled = "NO">
- </CommandLineArgument>
- <CommandLineArgument
argument = "--bogus-font-text"
isEnabled = "NO">
</CommandLineArgument>
diff --git a/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/Scenarios/AppDelegate.m b/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/Scenarios/AppDelegate.m
index 41d032c..60d7d35 100644
--- a/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/Scenarios/AppDelegate.m
+++ b/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/Scenarios/AppDelegate.m
@@ -16,10 +16,6 @@
@end
-@interface FlutterEngine ()
-@property(nonatomic, strong) FlutterMethodChannel* statusBarChannel;
-@end
-
@implementation NoStatusBarViewController
- (BOOL)prefersStatusBarHidden {
return YES;
@@ -214,28 +210,11 @@
FlutterPlatformViewGestureRecognizersBlockingPolicyWaitUntilTouchesEnded];
UIViewController* rootViewController = flutterViewController;
+ // Make Flutter View's origin x/y not 0.
if ([scenarioIdentifier isEqualToString:@"non_full_screen_flutter_view_platform_view"]) {
- // Make Flutter View's origin x/y not 0.
rootViewController = [[NoStatusBarViewController alloc] init];
[rootViewController.view addSubview:flutterViewController.view];
flutterViewController.view.frame = CGRectMake(150, 150, 500, 500);
- } else if ([scenarioIdentifier isEqualToString:@"tap_status_bar"]) {
- [engine.binaryMessenger
- setMessageHandlerOnChannel:@"flutter/status_bar"
- binaryMessageHandler:^(NSData* _Nullable message, FlutterBinaryReply _Nonnull reply) {
- NSDictionary* dict = [NSJSONSerialization JSONObjectWithData:message
- options:0
- error:nil];
- FlutterBasicMessageChannel* channel = [[FlutterBasicMessageChannel alloc]
- initWithName:@"display_data"
- binaryMessenger:engine.binaryMessenger
- codec:[FlutterJSONMessageCodec sharedInstance]];
- [channel sendMessage:@{@"data" : dict}];
- UITextField* text =
- [[UITextField alloc] initWithFrame:CGRectMake(0, 400, 300, 100)];
- text.text = dict[@"method"];
- [flutterViewController.view addSubview:text];
- }];
}
self.window.rootViewController = rootViewController;
diff --git a/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/README.md b/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/README.md
index 9e6d9a2..652fbed 100644
--- a/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/README.md
+++ b/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/README.md
@@ -1,19 +1,3 @@
-# Adding a New Scenario for a XCUITest
-
-An XCUITest is different from a regular XCTest in that the test subject app runs
-in a different process than the test suite, making it trickier for the test code
-to communicate with the app. For instance, you won't have access to
-the view controller or engine instances from within the test code.
-
-For this reason, the test code typically uses **launch arguments** to configure
-the app (for example, use [launchArgsMap](../Scenarios/AppDelegate.m) to inform
-the app which `Scenario` to load), and use UIKit UI components to collect test
-results (for example, every messsage received on the `display_data` channel adds
-a new `UITextField` to the app, which will be visible to the test code. See [touches_scenario.dart](../../../lib/src/touches_scenario.dart) for an example).
-
-Refer to the [Adding a New Scenario](./../../../README.md) section for how to
-register a new dart `Scenario`.
-
# Golden UI Tests
This folder contains golden image tests. It renders UI (for instance, a platform
@@ -33,8 +17,3 @@
but will contain an attachment with the expected screen shot. If the screen
shot looks good, add it with the correct name to the project and run the test
again - it should pass this time.
-
-## Running a specific Scenario
-
-Add and enable the new launch argument to the `Arguments Passed On Launch`
-section of the `Debug - Run` scheme, and build and run the app.
diff --git a/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/StatusBarTest.m b/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/StatusBarTest.m
index dd1b326..aee1a63 100644
--- a/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/StatusBarTest.m
+++ b/engine/src/flutter/testing/ios_scenario_app/ios/Scenarios/ScenariosUITests/StatusBarTest.m
@@ -16,10 +16,6 @@
}
- (void)testTapStatusBar {
- XCUIElement* textField = self.application.textFields[@"handleScrollToTop"];
- BOOL exists = [textField waitForExistenceWithTimeout:1];
- XCTAssertFalse(exists, @"");
-
XCUIApplication* systemApp =
[[XCUIApplication alloc] initWithBundleIdentifier:@"com.apple.springboard"];
XCUIElement* statusBar = [systemApp.statusBars firstMatch];
@@ -29,7 +25,21 @@
XCUICoordinate* coordinates = [statusBar coordinateWithNormalizedOffset:CGVectorMake(0, 0)];
[coordinates tap];
}
- exists = [textField waitForExistenceWithTimeout:1];
+
+ XCUIElement* addTextField =
+ self.application
+ .textFields[@"0,PointerChange.add,device=0,buttons=0,signalKind=PointerSignalKind.none"];
+ BOOL exists = [addTextField waitForExistenceWithTimeout:1];
+ XCTAssertTrue(exists, @"");
+ XCUIElement* downTextField =
+ self.application
+ .textFields[@"1,PointerChange.down,device=0,buttons=0,signalKind=PointerSignalKind.none"];
+ exists = [downTextField waitForExistenceWithTimeout:1];
+ XCTAssertTrue(exists, @"");
+ XCUIElement* upTextField =
+ self.application
+ .textFields[@"2,PointerChange.up,device=0,buttons=0,signalKind=PointerSignalKind.none"];
+ exists = [upTextField waitForExistenceWithTimeout:1];
XCTAssertTrue(exists, @"");
}
diff --git a/engine/src/flutter/testing/ios_scenario_app/lib/src/platform_channel_echo.dart b/engine/src/flutter/testing/ios_scenario_app/lib/src/platform_channel_echo.dart
deleted file mode 100644
index 67e8063..0000000
--- a/engine/src/flutter/testing/ios_scenario_app/lib/src/platform_channel_echo.dart
+++ /dev/null
@@ -1,30 +0,0 @@
-// Copyright 2013 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.
-
-import 'dart:typed_data' show ByteData;
-import 'dart:ui';
-
-import 'scenario.dart';
-
-/// A scenario which intercepts all messages on the given channel, and sends back
-/// the same message to the engine on a channel with the same name.
-class EchoPlatformChannelScenario extends Scenario {
- /// Constructor for `EchoPlatformChannelScenario`.
- EchoPlatformChannelScenario(super.view, {required this.channel}) {
- channelBuffers.setListener(channel, _onHandlePlatformMessage);
- }
-
- /// The name of the channel where all messages should be intercepted.
- final String channel;
-
- void _onHandlePlatformMessage(ByteData? data, PlatformMessageResponseCallback _) {
- view.platformDispatcher.sendPlatformMessage(channel, data, null);
- }
-
- @override
- void unmount() {
- channelBuffers.clearListener(channel);
- super.unmount();
- }
-}
diff --git a/engine/src/flutter/testing/ios_scenario_app/lib/src/scenarios.dart b/engine/src/flutter/testing/ios_scenario_app/lib/src/scenarios.dart
index 01ffcf3..8447a34 100644
--- a/engine/src/flutter/testing/ios_scenario_app/lib/src/scenarios.dart
+++ b/engine/src/flutter/testing/ios_scenario_app/lib/src/scenarios.dart
@@ -11,7 +11,6 @@
import 'get_bitmap_scenario.dart';
import 'initial_route_reply.dart';
import 'locale_initialization.dart';
-import 'platform_channel_echo.dart';
import 'platform_view.dart';
import 'poppable_screen.dart';
import 'scenario.dart';
@@ -142,8 +141,7 @@
TwoPlatformViewClipPath(view, firstId: _viewId++, secondId: _viewId++),
'two_platform_view_clip_path_multiple_clips': (FlutterView view) =>
TwoPlatformViewClipPathMultipleClips(view, firstId: _viewId++, secondId: _viewId++),
- 'tap_status_bar': (FlutterView view) =>
- EchoPlatformChannelScenario(view, channel: 'flutter/status_bar'),
+ 'tap_status_bar': (FlutterView view) => TouchesScenario(view),
'initial_route_reply': (FlutterView view) => InitialRouteReply(view),
'platform_view_with_continuous_texture': (FlutterView view) =>
PlatformViewWithContinuousTexture(view, id: _viewId++),
diff --git a/packages/flutter/lib/src/cupertino/page_scaffold.dart b/packages/flutter/lib/src/cupertino/page_scaffold.dart
index 1965f7e..2bb6b47 100644
--- a/packages/flutter/lib/src/cupertino/page_scaffold.dart
+++ b/packages/flutter/lib/src/cupertino/page_scaffold.dart
@@ -89,29 +89,10 @@
State<CupertinoPageScaffold> createState() => _CupertinoPageScaffoldState();
}
-class _CupertinoPageScaffoldState extends State<CupertinoPageScaffold> with WidgetsBindingObserver {
- @override
- void initState() {
- super.initState();
- WidgetsBinding.instance.addObserver(this);
- }
-
- @override
- void deactivate() {
- WidgetsBinding.instance.removeObserver(this);
- super.deactivate();
- }
-
- @override
- void activate() {
- super.activate();
- WidgetsBinding.instance.addObserver(this);
- }
-
- @override
- void handleStatusBarTap() {
- super.handleStatusBarTap();
+class _CupertinoPageScaffoldState extends State<CupertinoPageScaffold> {
+ void _handleStatusBarTap() {
final ScrollController? primaryScrollController = PrimaryScrollController.maybeOf(context);
+ // Only act on the scroll controller if it has any attached scroll positions.
if (primaryScrollController != null && primaryScrollController.hasClients) {
primaryScrollController.animateTo(
0.0,
@@ -206,6 +187,15 @@
right: 0.0,
child: MediaQuery.withNoTextScaling(child: widget.navigationBar!),
),
+ // Add a touch handler the size of the status bar on top of all contents
+ // to handle scroll to top by status bar taps.
+ Positioned(
+ top: 0.0,
+ left: 0.0,
+ right: 0.0,
+ height: existingMediaQuery.padding.top,
+ child: GestureDetector(excludeFromSemantics: true, onTap: _handleStatusBarTap),
+ ),
],
),
),
diff --git a/packages/flutter/lib/src/material/scaffold.dart b/packages/flutter/lib/src/material/scaffold.dart
index a8ebc63..c157cc5 100644
--- a/packages/flutter/lib/src/material/scaffold.dart
+++ b/packages/flutter/lib/src/material/scaffold.dart
@@ -72,6 +72,7 @@
floatingActionButton,
drawer,
endDrawer,
+ statusBar,
}
/// Manages [SnackBar]s and [MaterialBanner]s for descendant [Scaffold]s.
@@ -1272,6 +1273,11 @@
}());
}
+ if (hasChild(_ScaffoldSlot.statusBar)) {
+ layoutChild(_ScaffoldSlot.statusBar, fullWidthConstraints.tighten(height: minInsets.top));
+ positionChild(_ScaffoldSlot.statusBar, Offset.zero);
+ }
+
if (hasChild(_ScaffoldSlot.drawer)) {
layoutChild(_ScaffoldSlot.drawer, BoxConstraints.tight(size));
positionChild(_ScaffoldSlot.drawer, Offset.zero);
@@ -2187,8 +2193,7 @@
///
/// Can display [BottomSheet]s. Retrieve a [ScaffoldState] from the current
/// [BuildContext] using [Scaffold.of].
-class ScaffoldState extends State<Scaffold>
- with TickerProviderStateMixin, RestorationMixin, WidgetsBindingObserver {
+class ScaffoldState extends State<Scaffold> with TickerProviderStateMixin, RestorationMixin {
@override
String? get restorationId => widget.restorationId;
@@ -2738,13 +2743,10 @@
// iOS FEATURES - status bar tap, back gesture
- // On iOS, if `primary` is true, tapping the status bar scrolls the app's primary scrollable
+ // On iOS and macOS, if `primary` is true, tapping the status bar scrolls the app's primary scrollable
// to the top. We implement this by looking up the primary scroll controller and
// scrolling it to the top when tapped.
- @override
- void handleStatusBarTap() {
- super.handleStatusBarTap();
- assert(widget.primary);
+ void _handleStatusBarTap() {
final ScrollController? primaryScrollController = PrimaryScrollController.maybeOf(context);
if (primaryScrollController != null && primaryScrollController.hasClients) {
primaryScrollController.animateTo(
@@ -2785,9 +2787,6 @@
);
_bottomSheetScrimAnimationController = AnimationController(vsync: this);
- if (widget.primary) {
- WidgetsBinding.instance.addObserver(this);
- }
}
@protected
@@ -2829,13 +2828,6 @@
_updatePersistentBottomSheet();
}
}
- switch ((oldWidget.primary, widget.primary)) {
- case (true, false):
- WidgetsBinding.instance.removeObserver(this);
- case (false, true):
- WidgetsBinding.instance.addObserver(this);
- case (true, true) || (false, false):
- }
}
@protected
@@ -2857,20 +2849,6 @@
super.didChangeDependencies();
}
- @override
- void deactivate() {
- WidgetsBinding.instance.removeObserver(this);
- super.deactivate();
- }
-
- @override
- void activate() {
- super.activate();
- if (widget.primary) {
- WidgetsBinding.instance.addObserver(this);
- }
- }
-
@protected
@override
void dispose() {
@@ -3172,6 +3150,33 @@
removeBottomPadding: true,
);
+ switch (themeData.platform) {
+ case TargetPlatform.iOS:
+ case TargetPlatform.macOS:
+ if (!widget.primary) {
+ break;
+ }
+ _addIfNonNull(
+ children,
+ GestureDetector(
+ behavior: HitTestBehavior.opaque,
+ onTap: _handleStatusBarTap,
+ // iOS accessibility automatically adds scroll-to-top to the clock in the status bar
+ excludeFromSemantics: true,
+ ),
+ _ScaffoldSlot.statusBar,
+ removeLeftPadding: false,
+ removeTopPadding: true,
+ removeRightPadding: false,
+ removeBottomPadding: true,
+ );
+ case TargetPlatform.android:
+ case TargetPlatform.fuchsia:
+ case TargetPlatform.linux:
+ case TargetPlatform.windows:
+ break;
+ }
+
if (_endDrawerOpened.value) {
_buildDrawer(children, textDirection);
_buildEndDrawer(children, textDirection);
diff --git a/packages/flutter/lib/src/services/binding.dart b/packages/flutter/lib/src/services/binding.dart
index 2b397b8..b17de46 100644
--- a/packages/flutter/lib/src/services/binding.dart
+++ b/packages/flutter/lib/src/services/binding.dart
@@ -436,7 +436,9 @@
_systemContextMenuClient!.handleCustomContextMenuAction(callbackId);
case 'SystemChrome.systemUIChange':
final args = methodCall.arguments as List<dynamic>;
- await _systemUiChangeCallback?.call(args[0] as bool);
+ if (_systemUiChangeCallback != null) {
+ await _systemUiChangeCallback!(args[0] as bool);
+ }
case 'System.requestAppExit':
return <String, dynamic>{'response': (await handleRequestAppExit()).name};
default:
diff --git a/packages/flutter/lib/src/services/system_channels.dart b/packages/flutter/lib/src/services/system_channels.dart
index 83c70be..4f7e94c 100644
--- a/packages/flutter/lib/src/services/system_channels.dart
+++ b/packages/flutter/lib/src/services/system_channels.dart
@@ -183,26 +183,6 @@
JSONMethodCodec(),
);
- /// An unidirectional JSON [MethodChannel] for receiving status bar related
- /// events from iOS.
- ///
- /// The only method this channel receives is `handleScrollToTop` which
- /// is called on iOS when the user taps the status bar to scroll a scroll view
- /// to the top.
- ///
- /// Typically you should not subscribe to this channel directly. The events are
- /// dispatched to registered [WidgetsBindingObserver]s via the
- /// [WidgetsBindingObserver.handleStatusBarTap] callback.
- ///
- /// See also:
- ///
- /// * [WidgetsBindingObserver.handleStatusBarTap], the widgets library callback
- /// for dispatching the status bar tap event to the widget tree.
- static const OptionalMethodChannel statusBar = OptionalMethodChannel(
- 'flutter/status_bar',
- JSONMethodCodec(),
- );
-
/// A [MethodChannel] for handling text processing actions.
///
/// This channel exposes the text processing feature for supported platforms.
diff --git a/packages/flutter/lib/src/widgets/binding.dart b/packages/flutter/lib/src/widgets/binding.dart
index 3eb0be7..bf3556f 100644
--- a/packages/flutter/lib/src/widgets/binding.dart
+++ b/packages/flutter/lib/src/widgets/binding.dart
@@ -152,22 +152,6 @@
/// predictive back feature.
void handleCancelBackGesture() {}
- /// Called when the user taps the status bar on iOS, to scroll a scroll
- /// view to the top.
- ///
- /// This event should usually only be handled by at most one scroll view, so
- /// implementer(s) of this callback must coordinate to determine the most
- /// suitable scroll view for handling this event.
- ///
- /// This callback is only called on iOS. The default implementation provided by
- /// [WidgetsBindingObserver] does nothing.
- ///
- /// See also:
- ///
- /// * [Scaffold] and [CupertinoPageScaffold] which use this callback to implement
- /// iOS scroll-to-top.
- void handleStatusBarTap() {}
-
/// Called when the host tells the application to push a new route onto the
/// navigator.
///
@@ -477,7 +461,6 @@
platformDispatcher.onLocaleChanged = handleLocaleChanged;
SystemChannels.navigation.setMethodCallHandler(_handleNavigationInvocation);
SystemChannels.backGesture.setMethodCallHandler(_handleBackGestureInvocation);
- SystemChannels.statusBar.setMethodCallHandler(_handleStatusBarActions);
assert(() {
FlutterErrorDetails.propertiesTransformers.add(debugTransformDebugCreator);
return true;
@@ -1003,24 +986,6 @@
}
}
- Future<void> _handleStatusBarActions(MethodCall call) async {
- assert(call.method == 'handleScrollToTop');
- for (final observer in List<WidgetsBindingObserver>.of(_observers)) {
- try {
- observer.handleStatusBarTap();
- } catch (exception, stack) {
- final details = FlutterErrorDetails(
- exception: exception,
- stack: stack,
- library: 'widgets library',
- context: ErrorDescription('handling status bar action'),
- );
- FlutterError.reportError(details);
- // No error widget possible here since it wouldn't have a view to render into.
- }
- }
- }
-
/// Called when the system pops the current route.
///
/// This first notifies the binding observers (using
diff --git a/packages/flutter/test/cupertino/scaffold_test.dart b/packages/flutter/test/cupertino/scaffold_test.dart
index 84d1668..78eb57d 100644
--- a/packages/flutter/test/cupertino/scaffold_test.dart
+++ b/packages/flutter/test/cupertino/scaffold_test.dart
@@ -5,8 +5,6 @@
import 'dart:typed_data';
import 'package:flutter/cupertino.dart';
-import 'package:flutter/services.dart' show SystemChannels;
-import 'package:flutter/src/services/message_codecs.dart';
import 'package:flutter_test/flutter_test.dart';
import '../image_data.dart';
@@ -426,6 +424,64 @@
expect(decoration.color, const Color(0xFF010203));
});
+ testWidgets('Lists in CupertinoPageScaffold scroll to the top when status bar tapped', (
+ WidgetTester tester,
+ ) async {
+ await tester.pumpWidget(
+ CupertinoApp(
+ builder: (BuildContext context, Widget? child) {
+ // Acts as a 20px status bar at the root of the app.
+ return MediaQuery(
+ data: MediaQuery.of(context).copyWith(padding: const EdgeInsets.only(top: 20)),
+ child: child!,
+ );
+ },
+ home: CupertinoPageScaffold(
+ // Default nav bar is translucent.
+ navigationBar: const CupertinoNavigationBar(middle: Text('Title')),
+ child: ListView.builder(
+ itemExtent: 50,
+ itemBuilder: (BuildContext context, int index) => Text(index.toString()),
+ ),
+ ),
+ ),
+ );
+ // Top media query padding 20 + translucent nav bar 44.
+ expect(tester.getTopLeft(find.text('0')).dy, 64);
+ expect(tester.getTopLeft(find.text('6')).dy, 364);
+
+ await tester.fling(
+ find.text('5'), // Find some random text on the screen.
+ const Offset(0, -200),
+ 20,
+ );
+
+ await tester.pumpAndSettle();
+
+ expect(tester.getTopLeft(find.text('6')).dy, moreOrLessEquals(166.833, epsilon: 0.1));
+ expect(
+ tester.getTopLeft(find.text('12')).dy,
+ moreOrLessEquals(466.8333333333334, epsilon: 0.1),
+ );
+
+ // The media query top padding is 20. Tapping at 20 should do nothing.
+ await tester.tapAt(const Offset(400, 20));
+ await tester.pumpAndSettle();
+ expect(tester.getTopLeft(find.text('6')).dy, moreOrLessEquals(166.833, epsilon: 0.1));
+ expect(
+ tester.getTopLeft(find.text('12')).dy,
+ moreOrLessEquals(466.8333333333334, epsilon: 0.1),
+ );
+
+ // Tap 1 pixel higher.
+ await tester.tapAt(const Offset(400, 19));
+ await tester.pump();
+ await tester.pump(const Duration(milliseconds: 500));
+ expect(tester.getTopLeft(find.text('0')).dy, 64);
+ expect(tester.getTopLeft(find.text('6')).dy, 364);
+ expect(find.text('12'), findsNothing);
+ });
+
testWidgets('resizeToAvoidBottomInset is supported even when no navigationBar', (
WidgetTester tester,
) async {
@@ -532,36 +588,6 @@
);
});
- testWidgets('Tap the status bar scrolls to top', (WidgetTester tester) async {
- final scrollController = ScrollController(initialScrollOffset: 1000);
- addTearDown(scrollController.dispose);
- await tester.pumpWidget(
- CupertinoApp(
- home: Builder(
- builder: (BuildContext context) {
- return PrimaryScrollController(
- controller: scrollController,
- child: const CupertinoPageScaffold(
- child: SingleChildScrollView(primary: true, child: SizedBox(height: 12345)),
- ),
- );
- },
- ),
- ),
- );
- final ByteData message = const JSONMethodCodec().encodeMethodCall(
- const MethodCall('handleScrollToTop'),
- );
- tester.binding.defaultBinaryMessenger.handlePlatformMessage(
- SystemChannels.statusBar.name,
- message,
- (ByteData? data) {},
- );
- await tester.pumpAndSettle();
-
- expect(scrollController.offset, 0.0);
- });
-
testWidgets('CupertinoPageScaffold does not crash at zero area', (WidgetTester tester) async {
await tester.pumpWidget(
const CupertinoApp(
diff --git a/packages/flutter/test/material/scaffold_test.dart b/packages/flutter/test/material/scaffold_test.dart
index 475da35..738fbdc 100644
--- a/packages/flutter/test/material/scaffold_test.dart
+++ b/packages/flutter/test/material/scaffold_test.dart
@@ -598,6 +598,23 @@
}
testWidgets(
+ 'Tapping the status bar scrolls to top',
+ (WidgetTester tester) async {
+ await tester.pumpWidget(buildStatusBarTestApp(debugDefaultTargetPlatformOverride));
+ final ScrollableState scrollable = tester.state(find.byType(Scrollable));
+ scrollable.position.jumpTo(500.0);
+ expect(scrollable.position.pixels, equals(500.0));
+ await tester.tapAt(const Offset(100.0, 10.0));
+ await tester.pumpAndSettle();
+ expect(scrollable.position.pixels, equals(0.0));
+ },
+ variant: const TargetPlatformVariant(<TargetPlatform>{
+ TargetPlatform.iOS,
+ TargetPlatform.macOS,
+ }),
+ );
+
+ testWidgets(
'No status bar when primary is false',
(WidgetTester tester) async {
await tester.pumpWidget(
@@ -675,43 +692,51 @@
}),
);
- testWidgets('Tapping the status bar scrolls to top with ease out curve animation', (
- WidgetTester tester,
- ) async {
- const duration = 1000;
- final stops = <double>[0.842, 0.959, 0.993, 1.0];
- const double scrollOffset = 1000;
+ testWidgets(
+ 'Tapping the status bar scrolls to top with ease out curve animation',
+ (WidgetTester tester) async {
+ const duration = 1000;
+ final stops = <double>[0.842, 0.959, 0.993, 1.0];
+ const double scrollOffset = 1000;
- await tester.pumpWidget(buildStatusBarTestApp(null));
- final ScrollableState scrollable = tester.state(find.byType(Scrollable));
- scrollable.position.jumpTo(scrollOffset);
+ await tester.pumpWidget(buildStatusBarTestApp(debugDefaultTargetPlatformOverride));
+ final ScrollableState scrollable = tester.state(find.byType(Scrollable));
+ scrollable.position.jumpTo(scrollOffset);
+ await tester.tapAt(const Offset(100.0, 10.0));
- final ByteData message = const JSONMethodCodec().encodeMethodCall(
- const MethodCall('handleScrollToTop'),
- );
- tester.binding.defaultBinaryMessenger.handlePlatformMessage(
- SystemChannels.statusBar.name,
- message,
- (ByteData? data) {},
- );
+ await tester.pump(Duration.zero);
+ expect(scrollable.position.pixels, equals(scrollOffset));
- await tester.pump(Duration.zero);
- expect(scrollable.position.pixels, equals(scrollOffset));
+ for (var i = 0; i < stops.length; i++) {
+ await tester.pump(Duration(milliseconds: duration ~/ stops.length));
+ // Scroll pixel position is very long double, compare with floored int
+ // pixel position
+ expect(scrollable.position.pixels.toInt(), equals((scrollOffset * (1 - stops[i])).toInt()));
+ }
- for (var i = 0; i < stops.length; i++) {
- await tester.pump(Duration(milliseconds: duration ~/ stops.length));
- // Scroll pixel position is very long double, compare with floored int
- // pixel position
- expect(
- scrollable.position.pixels.toInt(),
- equals((scrollOffset * (1 - stops[i])).toInt()),
- reason: 'stop $i',
- );
- }
+ // Finally stops at the top.
+ expect(scrollable.position.pixels, equals(0.0));
+ },
+ variant: const TargetPlatformVariant(<TargetPlatform>{
+ TargetPlatform.iOS,
+ TargetPlatform.macOS,
+ }),
+ );
- // Finally stops at the top.
- expect(scrollable.position.pixels, equals(0.0));
- });
+ testWidgets(
+ 'Tapping the status bar does not scroll to top',
+ (WidgetTester tester) async {
+ await tester.pumpWidget(buildStatusBarTestApp(TargetPlatform.android));
+ final ScrollableState scrollable = tester.state(find.byType(Scrollable));
+ scrollable.position.jumpTo(500.0);
+ expect(scrollable.position.pixels, equals(500.0));
+ await tester.tapAt(const Offset(100.0, 10.0));
+ await tester.pump();
+ await tester.pump(const Duration(seconds: 1));
+ expect(scrollable.position.pixels, equals(500.0));
+ },
+ variant: const TargetPlatformVariant(<TargetPlatform>{TargetPlatform.android}),
+ );
testWidgets('Bottom sheet cannot overlap app bar', (WidgetTester tester) async {
final Key sheetKey = UniqueKey();
diff --git a/packages/flutter/test/widgets/binding_test.dart b/packages/flutter/test/widgets/binding_test.dart
index 1e88039..2cc4b85 100644
--- a/packages/flutter/test/widgets/binding_test.dart
+++ b/packages/flutter/test/widgets/binding_test.dart
@@ -360,12 +360,6 @@
WidgetsBinding.instance.addObserver(this);
return Future<AppExitResponse>.value(AppExitResponse.exit);
}
-
- @override
- void handleStatusBarTap() {
- assert(active);
- WidgetsBinding.instance.addObserver(this);
- }
}
void main() {