diff --git a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm index 2eb5f8b95ba80..5594a0c702490 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) { @@ -106,3 +129,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); };