From 21f38d7a48cc17fd88f680bdbd11a6e951df4fb6 Mon Sep 17 00:00:00 2001 From: Chris Bracken Date: Tue, 31 Jan 2017 13:42:58 -0800 Subject: [PATCH] Support scroll to top on iOS statusbar touches (flutter/engine#3375) On iOS, when a tap is detected in the status bar, provide a means to pass that touch event through to one or more FlutterViewControllers to trigger a scroll to top. In iOS apps, scroll to top should occur under the following conditions: 1. There is one and only one UIScrollView visible with scrollsToTop == YES. 2. The status-bar is in standard height mode, not in double-height mode. In double-height mode, the expected behaviour is to trigger a switch to the application associated with the double-height status bar. 3. A tap or a drag gesture occurs that is entirely constrained to the status bar frame. (We currently only handle the tap scenario). Unfortunately, AppDelegates only get touchesBegan events for status bar taps, though get get touchesBegan and touchesEnded events for drags within the status bar frame. As such, we currently synthesise the touchesEnded event for taps. --- .../framework/Headers/FlutterViewController.h | 2 ++ .../framework/Source/FlutterViewController.mm | 33 +++++++++++++++++++ 2 files changed, 35 insertions(+) diff --git a/engine/src/flutter/shell/platform/darwin/ios/framework/Headers/FlutterViewController.h b/engine/src/flutter/shell/platform/darwin/ios/framework/Headers/FlutterViewController.h index d6b07feb42..df78e9c33e 100644 --- a/engine/src/flutter/shell/platform/darwin/ios/framework/Headers/FlutterViewController.h +++ b/engine/src/flutter/shell/platform/darwin/ios/framework/Headers/FlutterViewController.h @@ -37,6 +37,8 @@ FLUTTER_EXPORT - (void)removeAsyncMessageListener: (NSObject*)listener; +- (void)handleStatusBarTouches:(UIEvent *)event; + @end __BEGIN_DECLS 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 6ba0559dc1..bbf0965794 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 @@ -271,6 +271,11 @@ static inline PointerChangeMapperPhase PointerChangePhaseFromUITouchPhase( } - (void)dispatchTouches:(NSSet*)touches phase:(UITouchPhase)phase { + // Note: we cannot rely on touch.phase, since in some cases, e.g., + // handleStatusBarTouches, we synthesize touches from existing events. + // + // TODO(cbracken) consider creating out own class with the touch fields we + // need. auto eventTypePhase = PointerChangePhaseFromUITouchPhase(phase); const CGFloat scale = [UIScreen mainScreen].scale; auto packet = std::make_unique(touches.count); @@ -498,6 +503,34 @@ static inline PointerChangeMapperPhase PointerChangePhaseFromUITouchPhase( [super dealloc]; } +#pragma mark - Status Bar touch event handling + +// Standard iOS status bar height in pixels. +constexpr CGFloat kStandardStatusBarHeight = 20.0; + +- (void)handleStatusBarTouches:(UIEvent *)event { + // If the status bar is double-height, don't handle status bar taps. iOS + // should open the app associated with the status bar. + CGRect statusBarFrame = [UIApplication sharedApplication].statusBarFrame; + if (statusBarFrame.size.height != kStandardStatusBarHeight) { + return; + } + + // If we detect a touch in the status bar, synthesize a fake touch begin/end. + for (UITouch *touch in event.allTouches) { + if (touch.phase == UITouchPhaseBegan && touch.tapCount > 0) { + CGPoint windowLoc = [touch locationInView:nil]; + CGPoint screenLoc = [touch.window convertPoint:windowLoc toWindow:nil]; + if (CGRectContainsPoint(statusBarFrame, screenLoc)) { + NSSet *statusbarTouches = [NSSet setWithObject:touch]; + [self dispatchTouches:statusbarTouches phase:UITouchPhaseBegan]; + [self dispatchTouches:statusbarTouches phase:UITouchPhaseEnded]; + return; + } + } + } +} + #pragma mark - Status bar style - (UIStatusBarStyle)preferredStatusBarStyle {