-
Notifications
You must be signed in to change notification settings - Fork 6k
iOS platform view gesture blocking policy. #15940
Changes from 10 commits
53f0144
cda277d
c251e27
e9e8e90
c6bd198
96f28c0
9cf53c5
7b06bb3
ef6f470
839c3f7
705a105
4df622a
f262ca0
488f394
6f67a00
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 |
|---|---|---|
|
|
@@ -225,6 +225,32 @@ typedef void (*FlutterPluginRegistrantCallback)(NSObject<FlutterPluginRegistry>* | |
| - (void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result; | ||
| @end | ||
|
|
||
| #pragma mark - | ||
| /*************************************************************************************************** | ||
| * How the UIGestureRecognizers of a platform view are blocked. | ||
| * | ||
| * UIGestureRecognizers of platform views can be blocked based on decisions made by the | ||
| * Flutter Framework (e.g. When an interact-able widget is covering the platform view). | ||
| */ | ||
| typedef enum { | ||
| /** | ||
| * Flutter blocks all the UIGestureRecognizers on the platform view as soon as it | ||
| * decides they should be blocked. | ||
| * | ||
| * With this policy, only the `touchesBegan` method for all the UIGestureRecognizers is guaranteed | ||
| * to be called. | ||
| */ | ||
| FlutterPlatformViewGestureRecognizersBlockingPolicyEager, | ||
| /** | ||
| * Flutter blocks the platform view's UIGestureRecognizers from recognizing only after | ||
| * touchesEnded was invoked. | ||
| * | ||
| * This results in the platform view's UIGestureRecognizers seeing the entire touch sequence, | ||
| * but never recognizing the gesture (and never invoking actions). | ||
| */ | ||
| FlutterPlatformViewGestureRecognizersBlockingPolicyWaitUntilTouchesEnded, | ||
| } FlutterPlatformViewGestureRecognizersBlockingPolicy; | ||
|
|
||
| #pragma mark - | ||
| /*************************************************************************************************** | ||
| *Registration context for a single `FlutterPlugin`, providing a one stop shop | ||
|
|
@@ -264,6 +290,23 @@ typedef void (*FlutterPluginRegistrantCallback)(NSObject<FlutterPluginRegistry>* | |
| - (void)registerViewFactory:(NSObject<FlutterPlatformViewFactory>*)factory | ||
| withId:(NSString*)factoryId; | ||
|
|
||
| /** | ||
| * Registers a `FlutterPlatformViewFactory` for creation of platform views. | ||
| * | ||
| * Plugins expose `UIView` for embedding in Flutter apps by registering a view factory. | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: can expose a
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is copied from the method above, should I apply the same fix there in this PR too? |
||
| * | ||
| * @param factory The view factory that will be registered. | ||
| * @param factoryId A unique identifier for the factory, the Dart code of the Flutter app can use | ||
| * this identifier to request creation of a `UIView` by the registered factory. | ||
| * @param gestureBlockingPolicy How UIGestureRecognizers on the platform views are | ||
| * blocked. | ||
| * | ||
| */ | ||
| - (void)registerViewFactory:(NSObject<FlutterPlatformViewFactory>*)factory | ||
| withId:(NSString*)factoryId | ||
| gestureRecognizersBlockingPolicy: | ||
| (FlutterPlatformViewGestureRecognizersBlockingPolicy)gestureRecognizersBlockingPolicy; | ||
|
|
||
| /** | ||
| * Publishes a value for external use of the plugin. | ||
| * | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -86,8 +86,10 @@ | |
| views_[viewId] = fml::scoped_nsobject<NSObject<FlutterPlatformView>>([embedded_view retain]); | ||
|
|
||
| FlutterTouchInterceptingView* touch_interceptor = [[[FlutterTouchInterceptingView alloc] | ||
| initWithEmbeddedView:embedded_view.view | ||
| flutterViewController:flutter_view_controller_.get()] autorelease]; | ||
| initWithEmbeddedView:embedded_view.view | ||
| flutterViewController:flutter_view_controller_.get() | ||
| gestureRecognizersBlockingPolicy:gesture_recognizers_blocking_policies[viewType]] | ||
| autorelease]; | ||
|
|
||
| touch_interceptors_[viewId] = | ||
| fml::scoped_nsobject<FlutterTouchInterceptingView>([touch_interceptor retain]); | ||
|
|
@@ -149,11 +151,13 @@ | |
|
|
||
| void FlutterPlatformViewsController::RegisterViewFactory( | ||
| NSObject<FlutterPlatformViewFactory>* factory, | ||
| NSString* factoryId) { | ||
| NSString* factoryId, | ||
| FlutterPlatformViewGestureRecognizersBlockingPolicy gestureRecognizerBlockingPolicy) { | ||
| std::string idString([factoryId UTF8String]); | ||
| FML_CHECK(factories_.count(idString) == 0); | ||
| factories_[idString] = | ||
| fml::scoped_nsobject<NSObject<FlutterPlatformViewFactory>>([factory retain]); | ||
| gesture_recognizers_blocking_policies[idString] = gestureRecognizerBlockingPolicy; | ||
| } | ||
|
|
||
| void FlutterPlatformViewsController::SetFrameSize(SkISize frame_size) { | ||
|
|
@@ -513,6 +517,15 @@ | |
| // 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> | ||
|
|
||
| // Indicates that if the `DelayingGestureRecognizer`'s state should be set to | ||
| // `UIGestureRecognizerStateEnded` during next `touchesEnded` call. | ||
| @property(nonatomic) bool shouldEndInNextTouchesEnded; | ||
|
|
||
| // Indicates that the `DelayingGestureRecognizer`'s `touchesEnded` has been invoked without | ||
| // setting the state to `UIGestureRecognizerStateEnded`. | ||
| @property(nonatomic) bool touchedEndedWithoutBlocking; | ||
|
|
||
| - (instancetype)initWithTarget:(id)target | ||
| action:(SEL)action | ||
| forwardingRecognizer:(UIGestureRecognizer*)forwardingRecognizer; | ||
|
|
@@ -535,9 +548,12 @@ - (instancetype)initWithTarget:(id)target | |
|
|
||
| @implementation FlutterTouchInterceptingView { | ||
| fml::scoped_nsobject<DelayingGestureRecognizer> _delayingRecognizer; | ||
| FlutterPlatformViewGestureRecognizersBlockingPolicy _blockingPolicy; | ||
| } | ||
| - (instancetype)initWithEmbeddedView:(UIView*)embeddedView | ||
| flutterViewController:(UIViewController*)flutterViewController { | ||
| flutterViewController:(UIViewController*)flutterViewController | ||
| gestureRecognizersBlockingPolicy: | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. is this the auto indentation?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, indented by the formatter |
||
| (FlutterPlatformViewGestureRecognizersBlockingPolicy)blockingPolicy { | ||
| self = [super initWithFrame:embeddedView.frame]; | ||
| if (self) { | ||
| self.multipleTouchEnabled = YES; | ||
|
|
@@ -554,6 +570,7 @@ - (instancetype)initWithEmbeddedView:(UIView*)embeddedView | |
| initWithTarget:self | ||
| action:nil | ||
| forwardingRecognizer:forwardingRecognizer]); | ||
| _blockingPolicy = blockingPolicy; | ||
|
|
||
| [self addGestureRecognizer:_delayingRecognizer.get()]; | ||
| [self addGestureRecognizer:forwardingRecognizer]; | ||
|
|
@@ -566,7 +583,27 @@ - (void)releaseGesture { | |
| } | ||
|
|
||
| - (void)blockGesture { | ||
| _delayingRecognizer.get().state = UIGestureRecognizerStateEnded; | ||
| switch (_blockingPolicy) { | ||
| case FlutterPlatformViewGestureRecognizersBlockingPolicyEager: | ||
| // We block all other gesture recognizers immediately in this policy. | ||
| _delayingRecognizer.get().state = UIGestureRecognizerStateEnded; | ||
| break; | ||
| case FlutterPlatformViewGestureRecognizersBlockingPolicyWaitUntilTouchesEnded: | ||
| if (_delayingRecognizer.get().touchedEndedWithoutBlocking) { | ||
| // If touchesEnded of the `DelayingGesureRecognizer` has been already invoked, | ||
| // we want to set the state of the `DelayingGesureRecognizer` to | ||
| // `UIGestureRecognizerStateEnded` as soon as possible. | ||
| _delayingRecognizer.get().state = UIGestureRecognizerStateEnded; | ||
| } else { | ||
| // If touchesEnded of the `DelayingGesureRecognizer` has not been invoked, | ||
| // We will set a flag to notify the `DelayingGesureRecognizer` to set the state to | ||
| // `UIGestureRecognizerStateEnded` when touchesEnded is called. | ||
| _delayingRecognizer.get().shouldEndInNextTouchesEnded = YES; | ||
| } | ||
| break; | ||
| default: | ||
| break; | ||
| } | ||
| } | ||
|
|
||
| // We want the intercepting view to consume the touches and not pass the touches up to the parent | ||
|
|
@@ -596,7 +633,10 @@ - (instancetype)initWithTarget:(id)target | |
| self = [super initWithTarget:target action:action]; | ||
| if (self) { | ||
| self.delaysTouchesBegan = YES; | ||
| self.delaysTouchesEnded = YES; | ||
| self.delegate = self; | ||
| self.shouldEndInNextTouchesEnded = NO; | ||
| self.touchedEndedWithoutBlocking = NO; | ||
| _forwardingRecognizer.reset([forwardingRecognizer retain]); | ||
| } | ||
| return self; | ||
|
|
@@ -614,6 +654,21 @@ - (BOOL)gestureRecognizer:(UIGestureRecognizer*)gestureRecognizer | |
| return otherGestureRecognizer == self; | ||
| } | ||
|
|
||
| - (void)touchesBegan:(NSSet<UITouch*>*)touches withEvent:(UIEvent*)event { | ||
| self.touchedEndedWithoutBlocking = NO; | ||
| [super touchesBegan:touches withEvent:event]; | ||
| } | ||
|
|
||
| - (void)touchesEnded:(NSSet<UITouch*>*)touches withEvent:(UIEvent*)event { | ||
| if (self.shouldEndInNextTouchesEnded) { | ||
| self.state = UIGestureRecognizerStateEnded; | ||
| self.shouldEndInNextTouchesEnded = NO; | ||
| } else { | ||
| self.touchedEndedWithoutBlocking = YES; | ||
| } | ||
| [super touchesEnded:touches withEvent:event]; | ||
| } | ||
|
|
||
| - (void)touchesCancelled:(NSSet*)touches withEvent:(UIEvent*)event { | ||
| self.state = UIGestureRecognizerStateFailed; | ||
| } | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,23 @@ | ||
| package io.flutter.plugins; | ||
|
|
||
| import io.flutter.plugin.common.PluginRegistry; | ||
|
|
||
| /** | ||
| * Generated file. Do not edit. | ||
| */ | ||
| public final class GeneratedPluginRegistrant { | ||
| public static void registerWith(PluginRegistry registry) { | ||
| if (alreadyRegisteredWith(registry)) { | ||
| return; | ||
| } | ||
| } | ||
|
|
||
| private static boolean alreadyRegisteredWith(PluginRegistry registry) { | ||
| final String key = GeneratedPluginRegistrant.class.getCanonicalName(); | ||
| if (registry.hasPlugin(key)) { | ||
| return true; | ||
| } | ||
| registry.registrarFor(key); | ||
| return false; | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,17 @@ | ||
| // | ||
| // Generated file. Do not edit. | ||
| // | ||
|
|
||
| #ifndef GeneratedPluginRegistrant_h | ||
| #define GeneratedPluginRegistrant_h | ||
|
|
||
| #import <Flutter/Flutter.h> | ||
|
|
||
| NS_ASSUME_NONNULL_BEGIN | ||
|
|
||
| @interface GeneratedPluginRegistrant : NSObject | ||
| + (void)registerWithRegistry:(NSObject<FlutterPluginRegistry>*)registry; | ||
| @end | ||
|
|
||
| NS_ASSUME_NONNULL_END | ||
| #endif /* GeneratedPluginRegistrant_h */ |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| // | ||
| // Generated file. Do not edit. | ||
| // | ||
|
|
||
| #import "GeneratedPluginRegistrant.h" | ||
|
|
||
| @implementation GeneratedPluginRegistrant | ||
|
|
||
| + (void)registerWithRegistry:(NSObject<FlutterPluginRegistry>*)registry { | ||
| } | ||
|
|
||
| @end |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -48,7 +48,21 @@ - (BOOL)application:(UIApplication*)application | |
| }]; | ||
|
|
||
| if (goldenTestName) { | ||
| [self readyContextForPlatformViewTests:goldenTestName]; | ||
| [self | ||
| readyContextForPlatformViewTests:goldenTestName | ||
| gestureRecognizersBlockingPolicy:FlutterPlatformViewGestureRecognizersBlockingPolicyEager]; | ||
| } else if ([[[NSProcessInfo processInfo] arguments] containsObject:@"--gesture-reject"]) { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I find this logic a little hard to follow, what do you think about always registering 2 platform views factories (each with a different blocking policy), and then have the test case themselves determine which one is used?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. done |
||
| FlutterPlatformViewGestureRecognizersBlockingPolicy policy = | ||
| FlutterPlatformViewGestureRecognizersBlockingPolicyEager; | ||
| if ([[[NSProcessInfo processInfo] arguments] containsObject:@"--until-touches-ended"]) { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can we just have a different teat-name for this instead of an additional argument? e.g
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. done |
||
| policy = FlutterPlatformViewGestureRecognizersBlockingPolicyWaitUntilTouchesEnded; | ||
| } | ||
| [self readyContextForPlatformViewTests:@"platform_view_touch_reject" | ||
| gestureRecognizersBlockingPolicy:policy]; | ||
| } else if ([[[NSProcessInfo processInfo] arguments] containsObject:@"--gesture-accept"]) { | ||
| [self readyContextForPlatformViewTests:@"platform_view_touch_accept" | ||
| gestureRecognizersBlockingPolicy: | ||
| FlutterPlatformViewGestureRecognizersBlockingPolicyWaitUntilTouchesEnded]; | ||
| } else if ([[[NSProcessInfo processInfo] arguments] containsObject:@"--screen-before-flutter"]) { | ||
| self.window.rootViewController = [[ScreenBeforeFlutter alloc] initWithEngineRunCompletion:nil]; | ||
| } else { | ||
|
|
@@ -59,7 +73,9 @@ - (BOOL)application:(UIApplication*)application | |
| return [super application:application didFinishLaunchingWithOptions:launchOptions]; | ||
| } | ||
|
|
||
| - (void)readyContextForPlatformViewTests:(NSString*)scenarioIdentifier { | ||
| - (void)readyContextForPlatformViewTests:(NSString*)scenarioIdentifier | ||
| gestureRecognizersBlockingPolicy: | ||
| (FlutterPlatformViewGestureRecognizersBlockingPolicy)policy { | ||
| FlutterEngine* engine = [[FlutterEngine alloc] initWithName:@"PlatformViewTest" project:nil]; | ||
| [engine runWithEntrypoint:nil]; | ||
|
|
||
|
|
@@ -76,7 +92,9 @@ - (void)readyContextForPlatformViewTests:(NSString*)scenarioIdentifier { | |
| [[TextPlatformViewFactory alloc] initWithMessenger:flutterViewController.binaryMessenger]; | ||
| NSObject<FlutterPluginRegistrar>* registrar = | ||
| [flutterViewController.engine registrarForPlugin:@"scenarios/TextPlatformViewPlugin"]; | ||
| [registrar registerViewFactory:textPlatformViewFactory withId:@"scenarios/textPlatformView"]; | ||
| [registrar registerViewFactory:textPlatformViewFactory | ||
| withId:@"scenarios/textPlatformView" | ||
| gestureRecognizersBlockingPolicy:policy]; | ||
| self.window.rootViewController = flutterViewController; | ||
| } | ||
|
|
||
|
|
||
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.
typos: framework, policy, implemented,
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