From 94b7921b81bbebad9a90cea0bcc95c7e0a9ade60 Mon Sep 17 00:00:00 2001 From: Amir Hardon Date: Wed, 26 Dec 2018 22:11:10 -0800 Subject: [PATCH] Only reject gestures to embedded UIViews when the framework says so. (flutter/engine#7307) Previously the framework could only tell the engine to forward a touch sequence to an embeded UIView between the time touches has started and the time touches ended. This couldn't support gesture arena setups where the gesture is recognized after the touch sequence is complete (e.g a tap competing with a scroll). This change makes it so that a touch gesture is only finally rejected by a platform view when the framework invokes the `rejectGesture` method. This allows the framework to resolve a gesture conflict after the touch sequence was ended. --- .../framework/Source/FlutterPlatformViews.mm | 66 ++++++++++++++----- .../Source/FlutterPlatformViews_Internal.h | 4 ++ 2 files changed, 52 insertions(+), 18 deletions(-) diff --git a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm index 6ca2f334dd..8e1edb9606 100644 --- a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm +++ b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm @@ -29,6 +29,8 @@ void FlutterPlatformViewsController::OnMethodCall(FlutterMethodCall* call, Flutt OnDispose(call, result); } else if ([[call method] isEqualToString:@"acceptGesture"]) { OnAcceptGesture(call, result); + } else if ([[call method] isEqualToString:@"rejectGesture"]) { + OnRejectGesture(call, result); } else { result(FlutterMethodNotImplemented); } @@ -125,6 +127,24 @@ void FlutterPlatformViewsController::OnAcceptGesture(FlutterMethodCall* call, result(nil); } +void FlutterPlatformViewsController::OnRejectGesture(FlutterMethodCall* call, + FlutterResult& result) { + NSDictionary* args = [call arguments]; + int64_t viewId = [args[@"id"] longLongValue]; + + if (views_.count(viewId) == 0) { + result([FlutterError errorWithCode:@"unknown_view" + message:@"trying to set gesture state for an unknown view" + details:[NSString stringWithFormat:@"view id: '%lld'", viewId]]); + return; + } + + FlutterTouchInterceptingView* view = touch_interceptors_[viewId].get(); + [view blockGesture]; + + result(nil); +} + void FlutterPlatformViewsController::RegisterViewFactory( NSObject* factory, NSString* factoryId) { @@ -269,6 +289,9 @@ void FlutterPlatformViewsController::EnsureGLOverlayInitialized( // invoking an acceptGesture method on the platform_views channel). And this is how we allow the // Flutter framework to delay or prevent the embedded view from getting a touch sequence. @interface DelayingGestureRecognizer : UIGestureRecognizer +- (instancetype)initWithTarget:(id)target + action:(SEL)action + forwardingRecognizer:(UIGestureRecognizer*)forwardingRecognizer; @end // While the DelayingGestureRecognizer is preventing touches from hitting the responder chain @@ -301,7 +324,10 @@ void FlutterPlatformViewsController::EnsureGLOverlayInitialized( [[[ForwardingGestureRecognizer alloc] initWithTarget:self flutterView:flutterView] autorelease]; - _delayingRecognizer.reset([[DelayingGestureRecognizer alloc] initWithTarget:self action:nil]); + _delayingRecognizer.reset([[DelayingGestureRecognizer alloc] + initWithTarget:self + action:nil + forwardingRecognizer:forwardingRecognizer]); [self addGestureRecognizer:_delayingRecognizer.get()]; [self addGestureRecognizer:forwardingRecognizer]; @@ -312,21 +338,34 @@ void FlutterPlatformViewsController::EnsureGLOverlayInitialized( - (void)releaseGesture { _delayingRecognizer.get().state = UIGestureRecognizerStateFailed; } + +- (void)blockGesture { + _delayingRecognizer.get().state = UIGestureRecognizerStateEnded; +} + @end -@implementation DelayingGestureRecognizer -- (instancetype)initWithTarget:(id)target action:(SEL)action { +@implementation DelayingGestureRecognizer { + fml::scoped_nsobject _forwardingRecognizer; +} + +- (instancetype)initWithTarget:(id)target + action:(SEL)action + forwardingRecognizer:(UIGestureRecognizer*)forwardingRecognizer { self = [super initWithTarget:target action:action]; if (self) { self.delaysTouchesBegan = YES; self.delegate = self; + _forwardingRecognizer.reset(forwardingRecognizer); } return self; } - (BOOL)gestureRecognizer:(UIGestureRecognizer*)gestureRecognizer shouldBeRequiredToFailByGestureRecognizer:(UIGestureRecognizer*)otherGestureRecognizer { - return otherGestureRecognizer != self; + // The forwarding gesture recognizer should always get all touch events, so it should not be + // required to fail by any other gesture recognizer. + return otherGestureRecognizer != _forwardingRecognizer.get() && otherGestureRecognizer != self; } - (BOOL)gestureRecognizer:(UIGestureRecognizer*)gestureRecognizer @@ -334,21 +373,12 @@ void FlutterPlatformViewsController::EnsureGLOverlayInitialized( return otherGestureRecognizer == self; } -- (void)touchesEnded:(NSSet*)touches withEvent:(UIEvent*)event { - // The gesture has ended, and the delaying gesture recognizer was not failed, we recognize - // the gesture to prevent the touches from being dispatched to the embedded view. - // - // This doesn't work well with gestures that are recognized by the Flutter framework after - // all pointers are up. - // - // TODO(amirh): explore if we can instead set this to recognized when the next touch sequence - // begins, or we can use a framework signal for restarting the recognizers (e.g when the - // gesture arena is resolved). - self.state = UIGestureRecognizerStateRecognized; +- (void)touchesBegan:(NSSet*)touches withEvent:(UIEvent*)event { + self.state = UIGestureRecognizerStateBegan; } - (void)touchesCancelled:(NSSet*)touches withEvent:(UIEvent*)event { - self.state = UIGestureRecognizerStateRecognized; + self.state = UIGestureRecognizerStateCancelled; } @end @@ -380,12 +410,12 @@ void FlutterPlatformViewsController::EnsureGLOverlayInitialized( - (void)touchesEnded:(NSSet*)touches withEvent:(UIEvent*)event { [_flutterView touchesEnded:touches withEvent:event]; - self.state = UIGestureRecognizerStateRecognized; + self.state = UIGestureRecognizerStateEnded; } - (void)touchesCancelled:(NSSet*)touches withEvent:(UIEvent*)event { [_flutterView touchesCancelled:touches withEvent:event]; - self.state = UIGestureRecognizerStateRecognized; + self.state = UIGestureRecognizerStateCancelled; } - (BOOL)gestureRecognizer:(UIGestureRecognizer*)gestureRecognizer diff --git a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.h b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.h index 4173a1ebf6..7c980896bf 100644 --- a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.h +++ b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.h @@ -22,6 +22,9 @@ // Stop delaying any active touch sequence (and let it arrive the embedded view). - (void)releaseGesture; + +// Prevent the touch sequence from ever arriving to the embedded view. +- (void)blockGesture; @end namespace shell { @@ -89,6 +92,7 @@ class FlutterPlatformViewsController { void OnCreate(FlutterMethodCall* call, FlutterResult& result); void OnDispose(FlutterMethodCall* call, FlutterResult& result); void OnAcceptGesture(FlutterMethodCall* call, FlutterResult& result); + void OnRejectGesture(FlutterMethodCall* call, FlutterResult& result); void EnsureOverlayInitialized(int64_t overlay_id); void EnsureGLOverlayInitialized(int64_t overlay_id,