From 89f00aee008d5a50d2503fc4d9654443246a0498 Mon Sep 17 00:00:00 2001 From: Amir Hardon Date: Thu, 25 Oct 2018 15:34:21 -0700 Subject: [PATCH 1/2] Delay embedded UIViews touch events until the framework says so. This allows the framework to decide whether and when a touch event sequence arrives to the embedded view. Which gives the framework the ability to manage hit testing and gesture disambiguation for embedded UIViews. We achieve this by wrapping each embedded UIView with another UIView that has a custom UIGestureRecognizer that delays touch events from being delivered, and another UIGestureRecognizer that makes sure to let Flutter know of events that are hit tested to the embedded view. --- .../framework/Source/FlutterPlatformViews.mm | 159 +++++++++++++++++- .../Source/FlutterPlatformViews_Internal.h | 15 +- 2 files changed, 170 insertions(+), 4 deletions(-) diff --git a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm index 48e175ef04e80..8c1ac39b33100 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm @@ -10,6 +10,8 @@ #include "flutter/fml/platform/darwin/scoped_nsobject.h" #include "flutter/shell/platform/darwin/ios/framework/Headers/FlutterChannels.h" +#import + namespace shell { FlutterPlatformViewsController::FlutterPlatformViewsController( @@ -30,6 +32,8 @@ OnCreate(call, result); } else if ([[call method] isEqualToString:@"dispose"]) { OnDispose(call, result); + } else if ([[call method] isEqualToString:@"acceptGesture"]) { + OnAcceptGesture(call, result); } else { result(FlutterMethodNotImplemented); } @@ -57,9 +61,10 @@ } // TODO(amirh): decode and pass the creation args. - views_[viewId] = fml::scoped_nsobject([[factory createWithFrame:CGRectZero - viewIdentifier:viewId - arguments:nil] retain]); + TouchInterceptingView* view = [[[TouchInterceptingView alloc] + initWithSubView:[factory createWithFrame:CGRectZero viewIdentifier:viewId arguments:nil] + flutterView:flutter_view_] autorelease]; + views_[viewId] = fml::scoped_nsobject([view retain]); FlutterView* flutter_view = flutter_view_.get(); [flutter_view addSubview:views_[viewId].get()]; @@ -83,6 +88,24 @@ result(nil); } +void FlutterPlatformViewsController::OnAcceptGesture(FlutterMethodCall* call, + FlutterResult& result) { + NSDictionary* args = [call arguments]; + int64_t viewId = [args[@"id"] longLongValue]; + + if (views_[viewId] == nil) { + result([FlutterError errorWithCode:@"unknown_view" + message:@"trying to set gesture state for an unknown view" + details:[NSString stringWithFormat:@"view id: '%lld'", viewId]]); + return; + } + + TouchInterceptingView* view = views_[viewId].get(); + [view releaseGesture]; + + result(nil); +} + void FlutterPlatformViewsController::RegisterViewFactory( NSObject* factory, NSString* factoryId) { @@ -107,3 +130,133 @@ } } // namespace shell + +// This recognizers delays touch events from being dispatched to the responder chain until it failed +// recognizing a gesture. +// +// We only fail this recognizer when asked to do so by the Flutter framework (which does so by +// 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 +@end + +// While the DelayingGestureRecognizer is preventing touches from hitting the responder chain +// the touch events are not arriving to the FlutterView (and thus not arriving to the Flutter +// framework). We use this gesture recognizer to dispatch the events directly to the FlutterView +// while during this phase. +// +// If the Flutter framework decides to dispatch events to the embedded view, we fail the +// DelayingGestureRecognizer which sends the events up the responder chain. But since the events +// are handled by the embedded view they are not delivered to the Flutter framework in this phase +// as well. So during this phase as well the ForwardingGestureRecognizer dispatched the events +// directly to the FlutterView. +@interface ForwardingGestureRecognizer : UIGestureRecognizer +- (instancetype)initWithTarget:(id)target flutterView:(UIView*)flutterView; +@end + +@implementation TouchInterceptingView { + ForwardingGestureRecognizer* forwardingRecognizer; + DelayingGestureRecognizer* delayingRecognizer; +} +- (instancetype)initWithSubView:(UIView*)embeddedView flutterView:(UIView*)flutterView { + self = [super initWithFrame:embeddedView.frame]; + if (self) { + self.multipleTouchEnabled = YES; + embeddedView.autoresizingMask = + (UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight); + + [self addSubview:embeddedView]; + + forwardingRecognizer = + [[[ForwardingGestureRecognizer alloc] initWithTarget:self + flutterView:flutterView] autorelease]; + + delayingRecognizer = [[[DelayingGestureRecognizer alloc] initWithTarget:self + action:nil] autorelease]; + + [self addGestureRecognizer:delayingRecognizer]; + [self addGestureRecognizer:forwardingRecognizer]; + } + return self; +} + +- (void)releaseGesture { + delayingRecognizer.state = UIGestureRecognizerStateFailed; +} +@end + +@implementation DelayingGestureRecognizer +- (instancetype)initWithTarget:(id)target action:(SEL)action { + self = [super initWithTarget:target action:action]; + if (self) { + self.delaysTouchesBegan = YES; + self.delegate = self; + } + return self; +} + +- (BOOL)gestureRecognizer:(UIGestureRecognizer*)gestureRecognizer + shouldBeRequiredToFailByGestureRecognizer:(UIGestureRecognizer*)otherGestureRecognizer { + return otherGestureRecognizer != self; +} + +- (BOOL)gestureRecognizer:(UIGestureRecognizer*)gestureRecognizer + shouldRequireFailureOfGestureRecognizer:(UIGestureRecognizer*)otherGestureRecognizer { + 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)touchesCancelled:(NSSet*)touches withEvent:(UIEvent*)event { + self.state = UIGestureRecognizerStateCancelled; +} +@end + +@implementation ForwardingGestureRecognizer { + UIView* _flutterView; +} + +- (instancetype)initWithTarget:(id)target flutterView:(UIView*)flutterView { + self = [super initWithTarget:target action:nil]; + if (self) { + self.delegate = self; + _flutterView = flutterView; + } + return self; +} + +- (void)touchesBegan:(NSSet*)touches withEvent:(UIEvent*)event { + [_flutterView touchesBegan:touches withEvent:event]; +} + +- (void)touchesMoved:(NSSet*)touches withEvent:(UIEvent*)event { + [_flutterView touchesMoved:touches withEvent:event]; +} + +- (void)touchesEnded:(NSSet*)touches withEvent:(UIEvent*)event { + [_flutterView touchesEnded:touches withEvent:event]; + self.state = UIGestureRecognizerStateRecognized; +} + +- (void)touchesCancelled:(NSSet*)touches withEvent:(UIEvent*)event { + [_flutterView touchesCancelled:touches withEvent:event]; + self.state = UIGestureRecognizerStateCancelled; +} + +- (BOOL)gestureRecognizer:(UIGestureRecognizer*)gestureRecognizer + shouldRecognizeSimultaneouslyWithGestureRecognizer: + (UIGestureRecognizer*)otherGestureRecognizer { + return YES; +} +@end diff --git a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.h b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.h index 494d1fe5f1c16..604b2a6a89355 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.h +++ b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.h @@ -13,6 +13,18 @@ #include "flutter/shell/platform/darwin/ios/framework/Headers/FlutterChannels.h" #include "flutter/shell/platform/darwin/ios/framework/Headers/FlutterPlatformViews.h" +// A UIView that is used as the parent for embedded UIViews. +// +// This view has 2 roles: +// 1. Delay or prevent touch events from arriving the embedded view. +// 2. Dispatching all events that are hittested to the embedded view to the FlutterView. +@interface TouchInterceptingView : UIView +- (instancetype)initWithSubView:(UIView*)embeddedView flutterView:(UIView*)flutterView; + +// Stop delaying any active touch sequence (and let it arrive the embedded view). +- (void)releaseGesture; +@end + namespace shell { class FlutterPlatformViewsController : public flow::ExternalViewEmbedder { @@ -28,11 +40,12 @@ class FlutterPlatformViewsController : public flow::ExternalViewEmbedder { fml::scoped_nsobject channel_; fml::scoped_nsobject flutter_view_; std::map>> factories_; - std::map> views_; + std::map> views_; void OnMethodCall(FlutterMethodCall* call, FlutterResult& result); void OnCreate(FlutterMethodCall* call, FlutterResult& result); void OnDispose(FlutterMethodCall* call, FlutterResult& result); + void OnAcceptGesture(FlutterMethodCall* call, FlutterResult& result); FML_DISALLOW_COPY_AND_ASSIGN(FlutterPlatformViewsController); }; From 8fc7c93c5fc427724670995f96b237787f62eff2 Mon Sep 17 00:00:00 2001 From: Amir Hardon Date: Tue, 30 Oct 2018 17:53:48 -0700 Subject: [PATCH 2/2] review comments followup --- .../framework/Source/FlutterPlatformViews.mm | 35 ++++++++++--------- .../Source/FlutterPlatformViews_Internal.h | 6 ++-- 2 files changed, 22 insertions(+), 19 deletions(-) diff --git a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm index 8c1ac39b33100..6e9895ce07626 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm @@ -61,10 +61,10 @@ } // TODO(amirh): decode and pass the creation args. - TouchInterceptingView* view = [[[TouchInterceptingView alloc] - initWithSubView:[factory createWithFrame:CGRectZero viewIdentifier:viewId arguments:nil] - flutterView:flutter_view_] autorelease]; - views_[viewId] = fml::scoped_nsobject([view retain]); + FlutterTouchInterceptingView* view = [[[FlutterTouchInterceptingView alloc] + initWithEmbeddedView:[factory createWithFrame:CGRectZero viewIdentifier:viewId arguments:nil] + flutterView:flutter_view_] autorelease]; + views_[viewId] = fml::scoped_nsobject([view retain]); FlutterView* flutter_view = flutter_view_.get(); [flutter_view addSubview:views_[viewId].get()]; @@ -100,7 +100,7 @@ return; } - TouchInterceptingView* view = views_[viewId].get(); + FlutterTouchInterceptingView* view = views_[viewId].get(); [view releaseGesture]; result(nil); @@ -154,11 +154,10 @@ @interface ForwardingGestureRecognizer : UIGestureRecognizer _delayingRecognizer; } -- (instancetype)initWithSubView:(UIView*)embeddedView flutterView:(UIView*)flutterView { +- (instancetype)initWithEmbeddedView:(UIView*)embeddedView flutterView:(UIView*)flutterView { self = [super initWithFrame:embeddedView.frame]; if (self) { self.multipleTouchEnabled = YES; @@ -167,21 +166,20 @@ - (instancetype)initWithSubView:(UIView*)embeddedView flutterView:(UIView*)flutt [self addSubview:embeddedView]; - forwardingRecognizer = + ForwardingGestureRecognizer* forwardingRecognizer = [[[ForwardingGestureRecognizer alloc] initWithTarget:self flutterView:flutterView] autorelease]; - delayingRecognizer = [[[DelayingGestureRecognizer alloc] initWithTarget:self - action:nil] autorelease]; + _delayingRecognizer.reset([[DelayingGestureRecognizer alloc] initWithTarget:self action:nil]); - [self addGestureRecognizer:delayingRecognizer]; + [self addGestureRecognizer:_delayingRecognizer.get()]; [self addGestureRecognizer:forwardingRecognizer]; } return self; } - (void)releaseGesture { - delayingRecognizer.state = UIGestureRecognizerStateFailed; + _delayingRecognizer.get().state = UIGestureRecognizerStateFailed; } @end @@ -219,11 +217,16 @@ - (void)touchesEnded:(NSSet*)touches withEvent:(UIEvent*)event { } - (void)touchesCancelled:(NSSet*)touches withEvent:(UIEvent*)event { - self.state = UIGestureRecognizerStateCancelled; + self.state = UIGestureRecognizerStateRecognized; } @end @implementation ForwardingGestureRecognizer { + // We can't dispatch events to the framework without this back pointer. + // This is a weak reference, the ForwardingGestureRecognizer is owned by the + // FlutterTouchInterceptingView which is strong referenced only by the FlutterView. + // So this is safe as when FlutterView is deallocated the reference to ForwardingGestureRecognizer + // will go away. UIView* _flutterView; } @@ -251,7 +254,7 @@ - (void)touchesEnded:(NSSet*)touches withEvent:(UIEvent*)event { - (void)touchesCancelled:(NSSet*)touches withEvent:(UIEvent*)event { [_flutterView touchesCancelled:touches withEvent:event]; - self.state = UIGestureRecognizerStateCancelled; + self.state = UIGestureRecognizerStateRecognized; } - (BOOL)gestureRecognizer:(UIGestureRecognizer*)gestureRecognizer diff --git a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.h b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.h index 604b2a6a89355..e8f7103f1c1db 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.h +++ b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.h @@ -18,8 +18,8 @@ // This view has 2 roles: // 1. Delay or prevent touch events from arriving the embedded view. // 2. Dispatching all events that are hittested to the embedded view to the FlutterView. -@interface TouchInterceptingView : UIView -- (instancetype)initWithSubView:(UIView*)embeddedView flutterView:(UIView*)flutterView; +@interface FlutterTouchInterceptingView : UIView +- (instancetype)initWithEmbeddedView:(UIView*)embeddedView flutterView:(UIView*)flutterView; // Stop delaying any active touch sequence (and let it arrive the embedded view). - (void)releaseGesture; @@ -40,7 +40,7 @@ class FlutterPlatformViewsController : public flow::ExternalViewEmbedder { fml::scoped_nsobject channel_; fml::scoped_nsobject flutter_view_; std::map>> factories_; - std::map> views_; + std::map> views_; void OnMethodCall(FlutterMethodCall* call, FlutterResult& result); void OnCreate(FlutterMethodCall* call, FlutterResult& result);