-
Notifications
You must be signed in to change notification settings - Fork 6k
Delay embedded UIViews touch events until the framework says so. #6665
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -10,6 +10,8 @@ | |
| #include "flutter/fml/platform/darwin/scoped_nsobject.h" | ||
| #include "flutter/shell/platform/darwin/ios/framework/Headers/FlutterChannels.h" | ||
|
|
||
| #import <UIKit/UIGestureRecognizerSubclass.h> | ||
|
|
||
| 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<UIView>([[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<TouchInterceptingView>([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<NSString*, id>* 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<FlutterPlatformViewFactory>* 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 <UIGestureRecognizerDelegate> | ||
| @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 <UIGestureRecognizerDelegate> | ||
| - (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 | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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<FlutterMethodChannel> channel_; | ||
| fml::scoped_nsobject<FlutterView> flutter_view_; | ||
| std::map<std::string, fml::scoped_nsobject<NSObject<FlutterPlatformViewFactory>>> factories_; | ||
| std::map<int64_t, fml::scoped_nsobject<UIView>> views_; | ||
| std::map<int64_t, fml::scoped_nsobject<TouchInterceptingView>> 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); | ||
| }; | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: Prefix the names of all private iOS classes with the
Flutterprefix.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done