Skip to content

Commit e97dc01

Browse files
Chris YangNoamDev
authored andcommitted
iOS platform view gesture blocking policy. (flutter#15940)
1 parent 4f430d6 commit e97dc01

File tree

13 files changed

+437
-21
lines changed

13 files changed

+437
-21
lines changed

shell/platform/darwin/ios/framework/Headers/FlutterPlugin.h

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -238,6 +238,32 @@ typedef void (*FlutterPluginRegistrantCallback)(NSObject<FlutterPluginRegistry>*
238238
- (void)detachFromEngineForRegistrar:(NSObject<FlutterPluginRegistrar>*)registrar;
239239
@end
240240

241+
#pragma mark -
242+
/***************************************************************************************************
243+
* How the UIGestureRecognizers of a platform view are blocked.
244+
*
245+
* UIGestureRecognizers of platform views can be blocked based on decisions made by the
246+
* Flutter Framework (e.g. When an interact-able widget is covering the platform view).
247+
*/
248+
typedef enum {
249+
/**
250+
* Flutter blocks all the UIGestureRecognizers on the platform view as soon as it
251+
* decides they should be blocked.
252+
*
253+
* With this policy, only the `touchesBegan` method for all the UIGestureRecognizers is guaranteed
254+
* to be called.
255+
*/
256+
FlutterPlatformViewGestureRecognizersBlockingPolicyEager,
257+
/**
258+
* Flutter blocks the platform view's UIGestureRecognizers from recognizing only after
259+
* touchesEnded was invoked.
260+
*
261+
* This results in the platform view's UIGestureRecognizers seeing the entire touch sequence,
262+
* but never recognizing the gesture (and never invoking actions).
263+
*/
264+
FlutterPlatformViewGestureRecognizersBlockingPolicyWaitUntilTouchesEnded,
265+
} FlutterPlatformViewGestureRecognizersBlockingPolicy;
266+
241267
#pragma mark -
242268
/***************************************************************************************************
243269
*Registration context for a single `FlutterPlugin`, providing a one stop shop
@@ -277,6 +303,23 @@ typedef void (*FlutterPluginRegistrantCallback)(NSObject<FlutterPluginRegistry>*
277303
- (void)registerViewFactory:(NSObject<FlutterPlatformViewFactory>*)factory
278304
withId:(NSString*)factoryId;
279305

306+
/**
307+
* Registers a `FlutterPlatformViewFactory` for creation of platform views.
308+
*
309+
* Plugins can expose a `UIView` for embedding in Flutter apps by registering a view factory.
310+
*
311+
* @param factory The view factory that will be registered.
312+
* @param factoryId A unique identifier for the factory, the Dart code of the Flutter app can use
313+
* this identifier to request creation of a `UIView` by the registered factory.
314+
* @param gestureBlockingPolicy How UIGestureRecognizers on the platform views are
315+
* blocked.
316+
*
317+
*/
318+
- (void)registerViewFactory:(NSObject<FlutterPlatformViewFactory>*)factory
319+
withId:(NSString*)factoryId
320+
gestureRecognizersBlockingPolicy:
321+
(FlutterPlatformViewGestureRecognizersBlockingPolicy)gestureRecognizersBlockingPolicy;
322+
280323
/**
281324
* Publishes a value for external use of the plugin.
282325
*

shell/platform/darwin/ios/framework/Source/FlutterEngine.mm

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -761,7 +761,17 @@ - (NSString*)lookupKeyForAsset:(NSString*)asset fromPackage:(NSString*)package {
761761

762762
- (void)registerViewFactory:(NSObject<FlutterPlatformViewFactory>*)factory
763763
withId:(NSString*)factoryId {
764-
[_flutterEngine platformViewsController] -> RegisterViewFactory(factory, factoryId);
764+
[self registerViewFactory:factory
765+
withId:factoryId
766+
gestureRecognizersBlockingPolicy:FlutterPlatformViewGestureRecognizersBlockingPolicyEager];
767+
}
768+
769+
- (void)registerViewFactory:(NSObject<FlutterPlatformViewFactory>*)factory
770+
withId:(NSString*)factoryId
771+
gestureRecognizersBlockingPolicy:
772+
(FlutterPlatformViewGestureRecognizersBlockingPolicy)gestureRecognizersBlockingPolicy {
773+
[_flutterEngine platformViewsController] -> RegisterViewFactory(factory, factoryId,
774+
gestureRecognizersBlockingPolicy);
765775
}
766776

767777
@end

shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm

Lines changed: 60 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -86,8 +86,10 @@
8686
views_[viewId] = fml::scoped_nsobject<NSObject<FlutterPlatformView>>([embedded_view retain]);
8787

8888
FlutterTouchInterceptingView* touch_interceptor = [[[FlutterTouchInterceptingView alloc]
89-
initWithEmbeddedView:embedded_view.view
90-
flutterViewController:flutter_view_controller_.get()] autorelease];
89+
initWithEmbeddedView:embedded_view.view
90+
flutterViewController:flutter_view_controller_.get()
91+
gestureRecognizersBlockingPolicy:gesture_recognizers_blocking_policies[viewType]]
92+
autorelease];
9193

9294
touch_interceptors_[viewId] =
9395
fml::scoped_nsobject<FlutterTouchInterceptingView>([touch_interceptor retain]);
@@ -149,11 +151,13 @@
149151

150152
void FlutterPlatformViewsController::RegisterViewFactory(
151153
NSObject<FlutterPlatformViewFactory>* factory,
152-
NSString* factoryId) {
154+
NSString* factoryId,
155+
FlutterPlatformViewGestureRecognizersBlockingPolicy gestureRecognizerBlockingPolicy) {
153156
std::string idString([factoryId UTF8String]);
154157
FML_CHECK(factories_.count(idString) == 0);
155158
factories_[idString] =
156159
fml::scoped_nsobject<NSObject<FlutterPlatformViewFactory>>([factory retain]);
160+
gesture_recognizers_blocking_policies[idString] = gestureRecognizerBlockingPolicy;
157161
}
158162

159163
void FlutterPlatformViewsController::SetFrameSize(SkISize frame_size) {
@@ -513,6 +517,15 @@
513517
// invoking an acceptGesture method on the platform_views channel). And this is how we allow the
514518
// Flutter framework to delay or prevent the embedded view from getting a touch sequence.
515519
@interface DelayingGestureRecognizer : UIGestureRecognizer <UIGestureRecognizerDelegate>
520+
521+
// Indicates that if the `DelayingGestureRecognizer`'s state should be set to
522+
// `UIGestureRecognizerStateEnded` during next `touchesEnded` call.
523+
@property(nonatomic) bool shouldEndInNextTouchesEnded;
524+
525+
// Indicates that the `DelayingGestureRecognizer`'s `touchesEnded` has been invoked without
526+
// setting the state to `UIGestureRecognizerStateEnded`.
527+
@property(nonatomic) bool touchedEndedWithoutBlocking;
528+
516529
- (instancetype)initWithTarget:(id)target
517530
action:(SEL)action
518531
forwardingRecognizer:(UIGestureRecognizer*)forwardingRecognizer;
@@ -535,9 +548,12 @@ - (instancetype)initWithTarget:(id)target
535548

536549
@implementation FlutterTouchInterceptingView {
537550
fml::scoped_nsobject<DelayingGestureRecognizer> _delayingRecognizer;
551+
FlutterPlatformViewGestureRecognizersBlockingPolicy _blockingPolicy;
538552
}
539553
- (instancetype)initWithEmbeddedView:(UIView*)embeddedView
540-
flutterViewController:(UIViewController*)flutterViewController {
554+
flutterViewController:(UIViewController*)flutterViewController
555+
gestureRecognizersBlockingPolicy:
556+
(FlutterPlatformViewGestureRecognizersBlockingPolicy)blockingPolicy {
541557
self = [super initWithFrame:embeddedView.frame];
542558
if (self) {
543559
self.multipleTouchEnabled = YES;
@@ -554,6 +570,7 @@ - (instancetype)initWithEmbeddedView:(UIView*)embeddedView
554570
initWithTarget:self
555571
action:nil
556572
forwardingRecognizer:forwardingRecognizer]);
573+
_blockingPolicy = blockingPolicy;
557574

558575
[self addGestureRecognizer:_delayingRecognizer.get()];
559576
[self addGestureRecognizer:forwardingRecognizer];
@@ -566,7 +583,27 @@ - (void)releaseGesture {
566583
}
567584

568585
- (void)blockGesture {
569-
_delayingRecognizer.get().state = UIGestureRecognizerStateEnded;
586+
switch (_blockingPolicy) {
587+
case FlutterPlatformViewGestureRecognizersBlockingPolicyEager:
588+
// We block all other gesture recognizers immediately in this policy.
589+
_delayingRecognizer.get().state = UIGestureRecognizerStateEnded;
590+
break;
591+
case FlutterPlatformViewGestureRecognizersBlockingPolicyWaitUntilTouchesEnded:
592+
if (_delayingRecognizer.get().touchedEndedWithoutBlocking) {
593+
// If touchesEnded of the `DelayingGesureRecognizer` has been already invoked,
594+
// we want to set the state of the `DelayingGesureRecognizer` to
595+
// `UIGestureRecognizerStateEnded` as soon as possible.
596+
_delayingRecognizer.get().state = UIGestureRecognizerStateEnded;
597+
} else {
598+
// If touchesEnded of the `DelayingGesureRecognizer` has not been invoked,
599+
// We will set a flag to notify the `DelayingGesureRecognizer` to set the state to
600+
// `UIGestureRecognizerStateEnded` when touchesEnded is called.
601+
_delayingRecognizer.get().shouldEndInNextTouchesEnded = YES;
602+
}
603+
break;
604+
default:
605+
break;
606+
}
570607
}
571608

572609
// 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
596633
self = [super initWithTarget:target action:action];
597634
if (self) {
598635
self.delaysTouchesBegan = YES;
636+
self.delaysTouchesEnded = YES;
599637
self.delegate = self;
638+
self.shouldEndInNextTouchesEnded = NO;
639+
self.touchedEndedWithoutBlocking = NO;
600640
_forwardingRecognizer.reset([forwardingRecognizer retain]);
601641
}
602642
return self;
@@ -614,6 +654,21 @@ - (BOOL)gestureRecognizer:(UIGestureRecognizer*)gestureRecognizer
614654
return otherGestureRecognizer == self;
615655
}
616656

657+
- (void)touchesBegan:(NSSet<UITouch*>*)touches withEvent:(UIEvent*)event {
658+
self.touchedEndedWithoutBlocking = NO;
659+
[super touchesBegan:touches withEvent:event];
660+
}
661+
662+
- (void)touchesEnded:(NSSet<UITouch*>*)touches withEvent:(UIEvent*)event {
663+
if (self.shouldEndInNextTouchesEnded) {
664+
self.state = UIGestureRecognizerStateEnded;
665+
self.shouldEndInNextTouchesEnded = NO;
666+
} else {
667+
self.touchedEndedWithoutBlocking = YES;
668+
}
669+
[super touchesEnded:touches withEvent:event];
670+
}
671+
617672
- (void)touchesCancelled:(NSSet*)touches withEvent:(UIEvent*)event {
618673
self.state = UIGestureRecognizerStateFailed;
619674
}

shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.h

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
#include "flutter/shell/platform/darwin/common/framework/Headers/FlutterBinaryMessenger.h"
1212
#include "flutter/shell/platform/darwin/common/framework/Headers/FlutterChannels.h"
1313
#include "flutter/shell/platform/darwin/ios/framework/Headers/FlutterPlatformViews.h"
14+
#include "flutter/shell/platform/darwin/ios/framework/Headers/FlutterPlugin.h"
1415

1516
// A UIView that is used as the parent for embedded UIViews.
1617
//
@@ -19,7 +20,9 @@
1920
// 2. Dispatching all events that are hittested to the embedded view to the FlutterView.
2021
@interface FlutterTouchInterceptingView : UIView
2122
- (instancetype)initWithEmbeddedView:(UIView*)embeddedView
22-
flutterViewController:(UIViewController*)flutterViewController;
23+
flutterViewController:(UIViewController*)flutterViewController
24+
gestureRecognizersBlockingPolicy:
25+
(FlutterPlatformViewGestureRecognizersBlockingPolicy)blockingPolicy;
2326

2427
// Stop delaying any active touch sequence (and let it arrive the embedded view).
2528
- (void)releaseGesture;
@@ -80,7 +83,10 @@ class FlutterPlatformViewsController {
8083

8184
void SetFlutterViewController(UIViewController* flutter_view_controller);
8285

83-
void RegisterViewFactory(NSObject<FlutterPlatformViewFactory>* factory, NSString* factoryId);
86+
void RegisterViewFactory(
87+
NSObject<FlutterPlatformViewFactory>* factory,
88+
NSString* factoryId,
89+
FlutterPlatformViewGestureRecognizersBlockingPolicy gestureRecognizerBlockingPolicy);
8490

8591
void SetFrameSize(SkISize frame_size);
8692

@@ -152,6 +158,10 @@ class FlutterPlatformViewsController {
152158
// Only compoiste platform views in this set.
153159
std::unordered_set<int64_t> views_to_recomposite_;
154160

161+
// The FlutterPlatformViewGestureRecognizersBlockingPolicy for each type of platform view.
162+
std::map<std::string, FlutterPlatformViewGestureRecognizersBlockingPolicy>
163+
gesture_recognizers_blocking_policies;
164+
155165
std::map<int64_t, std::unique_ptr<SkPictureRecorder>> picture_recorders_;
156166

157167
void OnCreate(FlutterMethodCall* call, FlutterResult& result);
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package io.flutter.plugins;
2+
3+
import io.flutter.plugin.common.PluginRegistry;
4+
5+
/**
6+
* Generated file. Do not edit.
7+
*/
8+
public final class GeneratedPluginRegistrant {
9+
public static void registerWith(PluginRegistry registry) {
10+
if (alreadyRegisteredWith(registry)) {
11+
return;
12+
}
13+
}
14+
15+
private static boolean alreadyRegisteredWith(PluginRegistry registry) {
16+
final String key = GeneratedPluginRegistrant.class.getCanonicalName();
17+
if (registry.hasPlugin(key)) {
18+
return true;
19+
}
20+
registry.registrarFor(key);
21+
return false;
22+
}
23+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
//
2+
// Generated file. Do not edit.
3+
//
4+
5+
#ifndef GeneratedPluginRegistrant_h
6+
#define GeneratedPluginRegistrant_h
7+
8+
#import <Flutter/Flutter.h>
9+
10+
NS_ASSUME_NONNULL_BEGIN
11+
12+
@interface GeneratedPluginRegistrant : NSObject
13+
+ (void)registerWithRegistry:(NSObject<FlutterPluginRegistry>*)registry;
14+
@end
15+
16+
NS_ASSUME_NONNULL_END
17+
#endif /* GeneratedPluginRegistrant_h */
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
//
2+
// Generated file. Do not edit.
3+
//
4+
5+
#import "GeneratedPluginRegistrant.h"
6+
7+
@implementation GeneratedPluginRegistrant
8+
9+
+ (void)registerWithRegistry:(NSObject<FlutterPluginRegistry>*)registry {
10+
}
11+
12+
@end

testing/scenario_app/ios/Scenarios/Scenarios.xcodeproj/project.pbxproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@
5050
6816DBAC2318696600A51400 /* golden_platform_view_opacity_iPhone SE_simulator.png in Resources */ = {isa = PBXBuildFile; fileRef = 6816DBA72318696600A51400 /* golden_platform_view_opacity_iPhone SE_simulator.png */; };
5151
6816DBAD2318696600A51400 /* golden_platform_view_cliprect_iPhone SE_simulator.png in Resources */ = {isa = PBXBuildFile; fileRef = 6816DBA82318696600A51400 /* golden_platform_view_cliprect_iPhone SE_simulator.png */; };
5252
6816DBAE2318696600A51400 /* golden_platform_view_cliprrect_iPhone SE_simulator.png in Resources */ = {isa = PBXBuildFile; fileRef = 6816DBA92318696600A51400 /* golden_platform_view_cliprrect_iPhone SE_simulator.png */; };
53+
68A5B63423EB71D300BDBCDB /* PlatformViewGestureRecognizerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 68A5B63323EB71D300BDBCDB /* PlatformViewGestureRecognizerTests.m */; };
5354
/* End PBXBuildFile section */
5455

5556
/* Begin PBXContainerItemProxy section */
@@ -156,6 +157,7 @@
156157
6816DBA72318696600A51400 /* golden_platform_view_opacity_iPhone SE_simulator.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "golden_platform_view_opacity_iPhone SE_simulator.png"; sourceTree = "<group>"; };
157158
6816DBA82318696600A51400 /* golden_platform_view_cliprect_iPhone SE_simulator.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "golden_platform_view_cliprect_iPhone SE_simulator.png"; sourceTree = "<group>"; };
158159
6816DBA92318696600A51400 /* golden_platform_view_cliprrect_iPhone SE_simulator.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "golden_platform_view_cliprrect_iPhone SE_simulator.png"; sourceTree = "<group>"; };
160+
68A5B63323EB71D300BDBCDB /* PlatformViewGestureRecognizerTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = PlatformViewGestureRecognizerTests.m; sourceTree = "<group>"; };
159161
/* End PBXFileReference section */
160162

161163
/* Begin PBXFrameworksBuildPhase section */
@@ -269,6 +271,7 @@
269271
6816DBA02317573300A51400 /* GoldenImage.m */,
270272
6816DBA22318358200A51400 /* PlatformViewGoldenTestManager.h */,
271273
6816DBA32318358200A51400 /* PlatformViewGoldenTestManager.m */,
274+
68A5B63323EB71D300BDBCDB /* PlatformViewGestureRecognizerTests.m */,
272275
);
273276
path = ScenariosUITests;
274277
sourceTree = "<group>";
@@ -458,6 +461,7 @@
458461
isa = PBXSourcesBuildPhase;
459462
buildActionMask = 2147483647;
460463
files = (
464+
68A5B63423EB71D300BDBCDB /* PlatformViewGestureRecognizerTests.m in Sources */,
461465
6816DBA12317573300A51400 /* GoldenImage.m in Sources */,
462466
6816DB9E231750ED00A51400 /* GoldenPlatformViewTests.m in Sources */,
463467
6816DBA42318358200A51400 /* PlatformViewGoldenTestManager.m in Sources */,

testing/scenario_app/ios/Scenarios/Scenarios/AppDelegate.m

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,8 @@ - (BOOL)application:(UIApplication*)application
2525

2626
// This argument is used by the XCUITest for Platform Views so that the app
2727
// under test will create platform views.
28-
// The launchArgsMap should match the one in the `PlatformVieGoldenTestManager`.
28+
// If the test is one of the platform view golden tests,
29+
// the launchArgsMap should match the one in the `PlatformVieGoldenTestManager`.
2930
NSDictionary<NSString*, NSString*>* launchArgsMap = @{
3031
@"--platform-view" : @"platform_view",
3132
@"--platform-view-multiple" : @"platform_view_multiple",
@@ -37,18 +38,21 @@ - (BOOL)application:(UIApplication*)application
3738
@"--platform-view-transform" : @"platform_view_transform",
3839
@"--platform-view-opacity" : @"platform_view_opacity",
3940
@"--platform-view-rotate" : @"platform_view_rotate",
41+
@"--gesture-reject-after-touches-ended" : @"platform_view_gesture_reject_after_touches_ended",
42+
@"--gesture-reject-eager" : @"platform_view_gesture_reject_eager",
43+
@"--gesture-accept" : @"platform_view_gesture_accept",
4044
};
41-
__block NSString* goldenTestName = nil;
45+
__block NSString* platformViewTestName = nil;
4246
[launchArgsMap
4347
enumerateKeysAndObjectsUsingBlock:^(NSString* argument, NSString* testName, BOOL* stop) {
4448
if ([[[NSProcessInfo processInfo] arguments] containsObject:argument]) {
45-
goldenTestName = testName;
49+
platformViewTestName = testName;
4650
*stop = YES;
4751
}
4852
}];
4953

50-
if (goldenTestName) {
51-
[self readyContextForPlatformViewTests:goldenTestName];
54+
if (platformViewTestName) {
55+
[self readyContextForPlatformViewTests:platformViewTestName];
5256
} else if ([[[NSProcessInfo processInfo] arguments] containsObject:@"--screen-before-flutter"]) {
5357
self.window.rootViewController = [[ScreenBeforeFlutter alloc] initWithEngineRunCompletion:nil];
5458
} else {
@@ -76,7 +80,13 @@ - (void)readyContextForPlatformViewTests:(NSString*)scenarioIdentifier {
7680
[[TextPlatformViewFactory alloc] initWithMessenger:flutterViewController.binaryMessenger];
7781
NSObject<FlutterPluginRegistrar>* registrar =
7882
[flutterViewController.engine registrarForPlugin:@"scenarios/TextPlatformViewPlugin"];
79-
[registrar registerViewFactory:textPlatformViewFactory withId:@"scenarios/textPlatformView"];
83+
[registrar registerViewFactory:textPlatformViewFactory
84+
withId:@"scenarios/textPlatformView"
85+
gestureRecognizersBlockingPolicy:FlutterPlatformViewGestureRecognizersBlockingPolicyEager];
86+
[registrar registerViewFactory:textPlatformViewFactory
87+
withId:@"scenarios/textPlatformView_blockPolicyUntilTouchesEnded"
88+
gestureRecognizersBlockingPolicy:
89+
FlutterPlatformViewGestureRecognizersBlockingPolicyWaitUntilTouchesEnded];
8090
self.window.rootViewController = flutterViewController;
8191
}
8292

0 commit comments

Comments
 (0)