diff --git a/shell/platform/darwin/ios/framework/Headers/FlutterPlugin.h b/shell/platform/darwin/ios/framework/Headers/FlutterPlugin.h index 8092217fcfe35..287301dbc2dbc 100644 --- a/shell/platform/darwin/ios/framework/Headers/FlutterPlugin.h +++ b/shell/platform/darwin/ios/framework/Headers/FlutterPlugin.h @@ -225,6 +225,32 @@ typedef void (*FlutterPluginRegistrantCallback)(NSObject* - (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* - (void)registerViewFactory:(NSObject*)factory withId:(NSString*)factoryId; +/** + * Registers a `FlutterPlatformViewFactory` for creation of platform views. + * + * Plugins can expose a `UIView` for embedding in Flutter apps by registering a view factory. + * + * @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*)factory + withId:(NSString*)factoryId + gestureRecognizersBlockingPolicy: + (FlutterPlatformViewGestureRecognizersBlockingPolicy)gestureRecognizersBlockingPolicy; + /** * Publishes a value for external use of the plugin. * diff --git a/shell/platform/darwin/ios/framework/Source/FlutterEngine.mm b/shell/platform/darwin/ios/framework/Source/FlutterEngine.mm index ec22caa532f45..7a8034156864f 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterEngine.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterEngine.mm @@ -741,7 +741,17 @@ - (NSString*)lookupKeyForAsset:(NSString*)asset fromPackage:(NSString*)package { - (void)registerViewFactory:(NSObject*)factory withId:(NSString*)factoryId { - [_flutterEngine platformViewsController] -> RegisterViewFactory(factory, factoryId); + [self registerViewFactory:factory + withId:factoryId + gestureRecognizersBlockingPolicy:FlutterPlatformViewGestureRecognizersBlockingPolicyEager]; +} + +- (void)registerViewFactory:(NSObject*)factory + withId:(NSString*)factoryId + gestureRecognizersBlockingPolicy: + (FlutterPlatformViewGestureRecognizersBlockingPolicy)gestureRecognizersBlockingPolicy { + [_flutterEngine platformViewsController] -> RegisterViewFactory(factory, factoryId, + gestureRecognizersBlockingPolicy); } @end diff --git a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm index b4e4e4273602a..e7a30d61add75 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm @@ -86,8 +86,10 @@ views_[viewId] = fml::scoped_nsobject>([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([touch_interceptor retain]); @@ -149,11 +151,13 @@ void FlutterPlatformViewsController::RegisterViewFactory( NSObject* factory, - NSString* factoryId) { + NSString* factoryId, + FlutterPlatformViewGestureRecognizersBlockingPolicy gestureRecognizerBlockingPolicy) { std::string idString([factoryId UTF8String]); FML_CHECK(factories_.count(idString) == 0); factories_[idString] = fml::scoped_nsobject>([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 + +// 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 _delayingRecognizer; + FlutterPlatformViewGestureRecognizersBlockingPolicy _blockingPolicy; } - (instancetype)initWithEmbeddedView:(UIView*)embeddedView - flutterViewController:(UIViewController*)flutterViewController { + flutterViewController:(UIViewController*)flutterViewController + gestureRecognizersBlockingPolicy: + (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*)touches withEvent:(UIEvent*)event { + self.touchedEndedWithoutBlocking = NO; + [super touchesBegan:touches withEvent:event]; +} + +- (void)touchesEnded:(NSSet*)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; } diff --git a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.h b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.h index c8daeaa605946..bdd013c98e07f 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.h +++ b/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.h @@ -11,6 +11,7 @@ #include "flutter/shell/platform/darwin/common/framework/Headers/FlutterBinaryMessenger.h" #include "flutter/shell/platform/darwin/common/framework/Headers/FlutterChannels.h" #include "flutter/shell/platform/darwin/ios/framework/Headers/FlutterPlatformViews.h" +#include "flutter/shell/platform/darwin/ios/framework/Headers/FlutterPlugin.h" // A UIView that is used as the parent for embedded UIViews. // @@ -19,7 +20,9 @@ // 2. Dispatching all events that are hittested to the embedded view to the FlutterView. @interface FlutterTouchInterceptingView : UIView - (instancetype)initWithEmbeddedView:(UIView*)embeddedView - flutterViewController:(UIViewController*)flutterViewController; + flutterViewController:(UIViewController*)flutterViewController + gestureRecognizersBlockingPolicy: + (FlutterPlatformViewGestureRecognizersBlockingPolicy)blockingPolicy; // Stop delaying any active touch sequence (and let it arrive the embedded view). - (void)releaseGesture; @@ -80,7 +83,10 @@ class FlutterPlatformViewsController { void SetFlutterViewController(UIViewController* flutter_view_controller); - void RegisterViewFactory(NSObject* factory, NSString* factoryId); + void RegisterViewFactory( + NSObject* factory, + NSString* factoryId, + FlutterPlatformViewGestureRecognizersBlockingPolicy gestureRecognizerBlockingPolicy); void SetFrameSize(SkISize frame_size); @@ -152,6 +158,10 @@ class FlutterPlatformViewsController { // Only compoiste platform views in this set. std::unordered_set views_to_recomposite_; + // The FlutterPlatformViewGestureRecognizersBlockingPolicy for each type of platform view. + std::map + gesture_recognizers_blocking_policies; + std::map> picture_recorders_; void OnCreate(FlutterMethodCall* call, FlutterResult& result); diff --git a/testing/scenario_app/android/app/src/main/java/io/flutter/plugins/GeneratedPluginRegistrant.java b/testing/scenario_app/android/app/src/main/java/io/flutter/plugins/GeneratedPluginRegistrant.java new file mode 100644 index 0000000000000..d007606a44d83 --- /dev/null +++ b/testing/scenario_app/android/app/src/main/java/io/flutter/plugins/GeneratedPluginRegistrant.java @@ -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; + } +} diff --git a/testing/scenario_app/ios/Runner/GeneratedPluginRegistrant.h b/testing/scenario_app/ios/Runner/GeneratedPluginRegistrant.h new file mode 100644 index 0000000000000..ed9a5c61691e5 --- /dev/null +++ b/testing/scenario_app/ios/Runner/GeneratedPluginRegistrant.h @@ -0,0 +1,17 @@ +// +// Generated file. Do not edit. +// + +#ifndef GeneratedPluginRegistrant_h +#define GeneratedPluginRegistrant_h + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface GeneratedPluginRegistrant : NSObject ++ (void)registerWithRegistry:(NSObject*)registry; +@end + +NS_ASSUME_NONNULL_END +#endif /* GeneratedPluginRegistrant_h */ diff --git a/testing/scenario_app/ios/Runner/GeneratedPluginRegistrant.m b/testing/scenario_app/ios/Runner/GeneratedPluginRegistrant.m new file mode 100644 index 0000000000000..60dfa42b328db --- /dev/null +++ b/testing/scenario_app/ios/Runner/GeneratedPluginRegistrant.m @@ -0,0 +1,12 @@ +// +// Generated file. Do not edit. +// + +#import "GeneratedPluginRegistrant.h" + +@implementation GeneratedPluginRegistrant + ++ (void)registerWithRegistry:(NSObject*)registry { +} + +@end diff --git a/testing/scenario_app/ios/Scenarios/Scenarios.xcodeproj/project.pbxproj b/testing/scenario_app/ios/Scenarios/Scenarios.xcodeproj/project.pbxproj index fd490352a2256..5da90fd32a674 100644 --- a/testing/scenario_app/ios/Scenarios/Scenarios.xcodeproj/project.pbxproj +++ b/testing/scenario_app/ios/Scenarios/Scenarios.xcodeproj/project.pbxproj @@ -50,6 +50,7 @@ 6816DBAC2318696600A51400 /* golden_platform_view_opacity_iPhone SE_simulator.png in Resources */ = {isa = PBXBuildFile; fileRef = 6816DBA72318696600A51400 /* golden_platform_view_opacity_iPhone SE_simulator.png */; }; 6816DBAD2318696600A51400 /* golden_platform_view_cliprect_iPhone SE_simulator.png in Resources */ = {isa = PBXBuildFile; fileRef = 6816DBA82318696600A51400 /* golden_platform_view_cliprect_iPhone SE_simulator.png */; }; 6816DBAE2318696600A51400 /* golden_platform_view_cliprrect_iPhone SE_simulator.png in Resources */ = {isa = PBXBuildFile; fileRef = 6816DBA92318696600A51400 /* golden_platform_view_cliprrect_iPhone SE_simulator.png */; }; + 68A5B63423EB71D300BDBCDB /* PlatformViewGestureRecognizerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 68A5B63323EB71D300BDBCDB /* PlatformViewGestureRecognizerTests.m */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -156,6 +157,7 @@ 6816DBA72318696600A51400 /* golden_platform_view_opacity_iPhone SE_simulator.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "golden_platform_view_opacity_iPhone SE_simulator.png"; sourceTree = ""; }; 6816DBA82318696600A51400 /* golden_platform_view_cliprect_iPhone SE_simulator.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "golden_platform_view_cliprect_iPhone SE_simulator.png"; sourceTree = ""; }; 6816DBA92318696600A51400 /* golden_platform_view_cliprrect_iPhone SE_simulator.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "golden_platform_view_cliprrect_iPhone SE_simulator.png"; sourceTree = ""; }; + 68A5B63323EB71D300BDBCDB /* PlatformViewGestureRecognizerTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = PlatformViewGestureRecognizerTests.m; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -269,6 +271,7 @@ 6816DBA02317573300A51400 /* GoldenImage.m */, 6816DBA22318358200A51400 /* PlatformViewGoldenTestManager.h */, 6816DBA32318358200A51400 /* PlatformViewGoldenTestManager.m */, + 68A5B63323EB71D300BDBCDB /* PlatformViewGestureRecognizerTests.m */, ); path = ScenariosUITests; sourceTree = ""; @@ -458,6 +461,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 68A5B63423EB71D300BDBCDB /* PlatformViewGestureRecognizerTests.m in Sources */, 6816DBA12317573300A51400 /* GoldenImage.m in Sources */, 6816DB9E231750ED00A51400 /* GoldenPlatformViewTests.m in Sources */, 6816DBA42318358200A51400 /* PlatformViewGoldenTestManager.m in Sources */, diff --git a/testing/scenario_app/ios/Scenarios/Scenarios/AppDelegate.m b/testing/scenario_app/ios/Scenarios/Scenarios/AppDelegate.m index 86a601cc19495..8805ad80bd644 100644 --- a/testing/scenario_app/ios/Scenarios/Scenarios/AppDelegate.m +++ b/testing/scenario_app/ios/Scenarios/Scenarios/AppDelegate.m @@ -25,7 +25,8 @@ - (BOOL)application:(UIApplication*)application // This argument is used by the XCUITest for Platform Views so that the app // under test will create platform views. - // The launchArgsMap should match the one in the `PlatformVieGoldenTestManager`. + // If the test is one of the platform view golden tests, + // the launchArgsMap should match the one in the `PlatformVieGoldenTestManager`. NSDictionary* launchArgsMap = @{ @"--platform-view" : @"platform_view", @"--platform-view-multiple" : @"platform_view_multiple", @@ -37,18 +38,21 @@ - (BOOL)application:(UIApplication*)application @"--platform-view-transform" : @"platform_view_transform", @"--platform-view-opacity" : @"platform_view_opacity", @"--platform-view-rotate" : @"platform_view_rotate", + @"--gesture-reject-after-touches-ended" : @"platform_view_gesture_reject_after_touches_ended", + @"--gesture-reject-eager" : @"platform_view_gesture_reject_eager", + @"--gesture-accept" : @"platform_view_gesture_accept", }; - __block NSString* goldenTestName = nil; + __block NSString* platformViewTestName = nil; [launchArgsMap enumerateKeysAndObjectsUsingBlock:^(NSString* argument, NSString* testName, BOOL* stop) { if ([[[NSProcessInfo processInfo] arguments] containsObject:argument]) { - goldenTestName = testName; + platformViewTestName = testName; *stop = YES; } }]; - if (goldenTestName) { - [self readyContextForPlatformViewTests:goldenTestName]; + if (platformViewTestName) { + [self readyContextForPlatformViewTests:platformViewTestName]; } else if ([[[NSProcessInfo processInfo] arguments] containsObject:@"--screen-before-flutter"]) { self.window.rootViewController = [[ScreenBeforeFlutter alloc] initWithEngineRunCompletion:nil]; } else { @@ -76,7 +80,13 @@ - (void)readyContextForPlatformViewTests:(NSString*)scenarioIdentifier { [[TextPlatformViewFactory alloc] initWithMessenger:flutterViewController.binaryMessenger]; NSObject* registrar = [flutterViewController.engine registrarForPlugin:@"scenarios/TextPlatformViewPlugin"]; - [registrar registerViewFactory:textPlatformViewFactory withId:@"scenarios/textPlatformView"]; + [registrar registerViewFactory:textPlatformViewFactory + withId:@"scenarios/textPlatformView" + gestureRecognizersBlockingPolicy:FlutterPlatformViewGestureRecognizersBlockingPolicyEager]; + [registrar registerViewFactory:textPlatformViewFactory + withId:@"scenarios/textPlatformView_blockPolicyUntilTouchesEnded" + gestureRecognizersBlockingPolicy: + FlutterPlatformViewGestureRecognizersBlockingPolicyWaitUntilTouchesEnded]; self.window.rootViewController = flutterViewController; } diff --git a/testing/scenario_app/ios/Scenarios/Scenarios/TextPlatformView.m b/testing/scenario_app/ios/Scenarios/Scenarios/TextPlatformView.m index c75b5c815d0d6..e01bca6c61e67 100644 --- a/testing/scenario_app/ios/Scenarios/Scenarios/TextPlatformView.m +++ b/testing/scenario_app/ios/Scenarios/Scenarios/TextPlatformView.m @@ -4,6 +4,34 @@ #import "TextPlatformView.h" +@protocol TestGestureRecognizerDelegate + +- (void)gestureTouchesBegan; +- (void)gestureTouchesEnded; + +@end + +@interface TestTapGestureRecognizer : UITapGestureRecognizer + +@property(weak, nonatomic) + NSObject* testTapGestureRecognizerDelegate; + +@end + +@implementation TestTapGestureRecognizer + +- (void)touchesBegan:(NSSet*)touches withEvent:(UIEvent*)event { + [self.testTapGestureRecognizerDelegate gestureTouchesBegan]; + [super touchesBegan:touches withEvent:event]; +} + +- (void)touchesEnded:(NSSet*)touches withEvent:(UIEvent*)event { + [self.testTapGestureRecognizerDelegate gestureTouchesEnded]; + [super touchesEnded:touches withEvent:event]; +} + +@end + @implementation TextPlatformViewFactory { NSObject* _messenger; } @@ -32,8 +60,11 @@ - (instancetype)initWithMessenger:(NSObject*)messenger { @end +@interface TextPlatformView () + +@end + @implementation TextPlatformView { - int64_t _viewId; UITextView* _textView; FlutterMethodChannel* _channel; } @@ -43,12 +74,19 @@ - (instancetype)initWithFrame:(CGRect)frame arguments:(id _Nullable)args binaryMessenger:(NSObject*)messenger { if ([super init]) { - _viewId = viewId; _textView = [[UITextView alloc] initWithFrame:CGRectMake(50.0, 50.0, 250.0, 100.0)]; _textView.textColor = UIColor.blueColor; _textView.backgroundColor = UIColor.lightGrayColor; [_textView setFont:[UIFont systemFontOfSize:52]]; _textView.text = args; + _textView.accessibilityIdentifier = @"platform_view"; + + TestTapGestureRecognizer* gestureRecognizer = + [[TestTapGestureRecognizer alloc] initWithTarget:self action:@selector(platformViewTapped)]; + + [_textView addGestureRecognizer:gestureRecognizer]; + gestureRecognizer.testTapGestureRecognizerDelegate = self; + _textView.accessibilityLabel = @""; } return self; } @@ -57,7 +95,19 @@ - (UIView*)view { return _textView; } -// - (void)onMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result { -// } +- (void)platformViewTapped { + _textView.accessibilityLabel = + [_textView.accessibilityLabel stringByAppendingString:@"-platformViewTapped"]; +} + +- (void)gestureTouchesBegan { + _textView.accessibilityLabel = + [_textView.accessibilityLabel stringByAppendingString:@"-gestureTouchesBegan"]; +} + +- (void)gestureTouchesEnded { + _textView.accessibilityLabel = + [_textView.accessibilityLabel stringByAppendingString:@"-gestureTouchesEnded"]; +} @end diff --git a/testing/scenario_app/ios/Scenarios/ScenariosUITests/PlatformViewGestureRecognizerTests.m b/testing/scenario_app/ios/Scenarios/ScenariosUITests/PlatformViewGestureRecognizerTests.m new file mode 100644 index 0000000000000..d791210f22707 --- /dev/null +++ b/testing/scenario_app/ios/Scenarios/ScenariosUITests/PlatformViewGestureRecognizerTests.m @@ -0,0 +1,118 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#import + +static const NSInteger kSecondsToWaitForPlatformView = 30; + +@interface PlatformViewGestureRecognizerTests : XCTestCase + +@end + +@implementation PlatformViewGestureRecognizerTests + +- (void)setUp { + self.continueAfterFailure = NO; +} + +- (void)testRejectPolicyUtilTouchesEnded { + XCUIApplication* app = [[XCUIApplication alloc] init]; + app.launchArguments = @[ @"--gesture-reject-after-touches-ended" ]; + [app launch]; + + NSPredicate* predicateToFindPlatformView = + [NSPredicate predicateWithBlock:^BOOL(id _Nullable evaluatedObject, + NSDictionary* _Nullable bindings) { + XCUIElement* element = evaluatedObject; + return [element.identifier isEqualToString:@"platform_view"]; + }]; + XCUIElement* platformView = [app.textViews elementMatchingPredicate:predicateToFindPlatformView]; + if (![platformView waitForExistenceWithTimeout:kSecondsToWaitForPlatformView]) { + NSLog(@"%@", app.debugDescription); + XCTFail(@"Failed due to not able to find any platformView with %@ seconds", + @(kSecondsToWaitForPlatformView)); + } + + XCTAssertNotNil(platformView); + XCTAssertEqualObjects(platformView.label, @""); + + NSPredicate* predicate = + [NSPredicate predicateWithFormat:@"label == %@", @"-gestureTouchesBegan-gestureTouchesEnded"]; + XCTNSPredicateExpectation* expection = + [[XCTNSPredicateExpectation alloc] initWithPredicate:predicate object:platformView]; + + [platformView tap]; + [self waitForExpectations:@[ expection ] timeout:kSecondsToWaitForPlatformView]; + XCTAssertEqualObjects(platformView.label, @"-gestureTouchesBegan-gestureTouchesEnded"); +} + +- (void)testRejectPolicyEager { + XCUIApplication* app = [[XCUIApplication alloc] init]; + app.launchArguments = @[ @"--gesture-reject-eager" ]; + [app launch]; + + NSPredicate* predicateToFindPlatformView = + [NSPredicate predicateWithBlock:^BOOL(id _Nullable evaluatedObject, + NSDictionary* _Nullable bindings) { + XCUIElement* element = evaluatedObject; + return [element.identifier isEqualToString:@"platform_view"]; + }]; + XCUIElement* platformView = [app.textViews elementMatchingPredicate:predicateToFindPlatformView]; + if (![platformView waitForExistenceWithTimeout:kSecondsToWaitForPlatformView]) { + NSLog(@"%@", app.debugDescription); + XCTFail(@"Failed due to not able to find any platformView with %@ seconds", + @(kSecondsToWaitForPlatformView)); + } + + XCTAssertNotNil(platformView); + XCTAssertEqualObjects(platformView.label, @""); + + NSPredicate* predicate = + [NSPredicate predicateWithBlock:^BOOL(id _Nullable evaluatedObject, + NSDictionary* _Nullable bindings) { + XCUIElement* view = (XCUIElement*)evaluatedObject; + return [view.label containsString:@"-gestureTouchesBegan"]; + }]; + XCTNSPredicateExpectation* expection = + [[XCTNSPredicateExpectation alloc] initWithPredicate:predicate object:platformView]; + + [platformView tap]; + [self waitForExpectations:@[ expection ] timeout:kSecondsToWaitForPlatformView]; + XCTAssertTrue([platformView.label containsString:@"-gestureTouchesBegan"]); +} + +- (void)testAccept { + XCUIApplication* app = [[XCUIApplication alloc] init]; + app.launchArguments = @[ @"--gesture-accept" ]; + [app launch]; + + NSPredicate* predicateToFindPlatformView = + [NSPredicate predicateWithBlock:^BOOL(id _Nullable evaluatedObject, + NSDictionary* _Nullable bindings) { + XCUIElement* element = evaluatedObject; + return [element.identifier isEqualToString:@"platform_view"]; + }]; + XCUIElement* platformView = [app.textViews elementMatchingPredicate:predicateToFindPlatformView]; + if (![platformView waitForExistenceWithTimeout:kSecondsToWaitForPlatformView]) { + NSLog(@"%@", app.debugDescription); + XCTFail(@"Failed due to not able to find any platformView with %@ seconds", + @(kSecondsToWaitForPlatformView)); + } + + XCTAssertNotNil(platformView); + XCTAssertEqualObjects(platformView.label, @""); + + NSPredicate* predicate = [NSPredicate + predicateWithFormat:@"label == %@", + @"-gestureTouchesBegan-gestureTouchesEnded-platformViewTapped"]; + XCTNSPredicateExpectation* expection = + [[XCTNSPredicateExpectation alloc] initWithPredicate:predicate object:platformView]; + + [platformView tap]; + [self waitForExpectations:@[ expection ] timeout:kSecondsToWaitForPlatformView]; + XCTAssertEqualObjects(platformView.label, + @"-gestureTouchesBegan-gestureTouchesEnded-platformViewTapped"); +} + +@end diff --git a/testing/scenario_app/lib/main.dart b/testing/scenario_app/lib/main.dart index 2f0a7688702f2..de647de5fb7c3 100644 --- a/testing/scenario_app/lib/main.dart +++ b/testing/scenario_app/lib/main.dart @@ -26,6 +26,9 @@ Map _scenarios = { 'platform_view_multiple_background_foreground': MultiPlatformViewBackgroundForegroundScenario(window, firstId: 8, secondId: 9), 'poppable_screen': PoppableScreenScenario(window), 'platform_view_rotate': PlatformViewScenario(window, 'Rotate Platform View', id: 10), + 'platform_view_gesture_reject_eager': PlatformViewForTouchIOSScenario(window, 'platform view touch', id: 11, accept: false), + 'platform_view_gesture_accept': PlatformViewForTouchIOSScenario(window, 'platform view touch', id: 11, accept: true), + 'platform_view_gesture_reject_after_touches_ended': PlatformViewForTouchIOSScenario(window, 'platform view touch', id: 11, accept: false, rejectUntilTouchesEnded: true), }; Scenario _currentScenario = _scenarios['animated_color_square']; diff --git a/testing/scenario_app/lib/src/platform_view.dart b/testing/scenario_app/lib/src/platform_view.dart index 05efe52fa8b25..6726cdf3f3ec7 100644 --- a/testing/scenario_app/lib/src/platform_view.dart +++ b/testing/scenario_app/lib/src/platform_view.dart @@ -281,6 +281,67 @@ class PlatformViewOpacityScenario extends PlatformViewScenario { } } +/// A simple platform view for testing touch events from iOS. +class PlatformViewForTouchIOSScenario extends Scenario + with _BasePlatformViewScenarioMixin { + + int _viewId; + bool _accept; + /// Creates the PlatformView scenario. + /// + /// The [window] parameter must not be null. + PlatformViewForTouchIOSScenario(Window window, String text, {int id = 0, bool accept, bool rejectUntilTouchesEnded = false}) + : assert(window != null), + _accept = accept, + _viewId = id, + super(window) { + if (rejectUntilTouchesEnded) { + createPlatformView(window, text, id, viewType: 'scenarios/textPlatformView_blockPolicyUntilTouchesEnded'); + } else { + createPlatformView(window, text, id); + } + } + + @override + void onBeginFrame(Duration duration) { + final SceneBuilder builder = SceneBuilder(); + + builder.pushOffset(0, 0); + finishBuilderByAddingPlatformViewAndPicture(builder, 11); + } + + @override + void onPointerDataPacket(PointerDataPacket packet) { + if (packet.data.first.change == PointerChange.add) { + String method = 'rejectGesture'; + if (_accept) { + method = 'acceptGesture'; + } + const int _valueString = 7; + const int _valueInt32 = 3; + const int _valueMap = 13; + final Uint8List message = Uint8List.fromList([ + _valueString, + method.length, + ...utf8.encode(method), + _valueMap, + 1, + _valueString, + 'id'.length, + ...utf8.encode('id'), + _valueInt32, + ..._to32(_viewId), + ]); + window.sendPlatformMessage( + 'flutter/platform_views', + message.buffer.asByteData(), + (ByteData response) {}, + ); + } + + } +} + mixin _BasePlatformViewScenarioMixin on Scenario { int _textureId; @@ -289,7 +350,7 @@ mixin _BasePlatformViewScenarioMixin on Scenario { /// It prepare a TextPlatformView so it can be added to the SceneBuilder in `onBeginFrame`. /// Call this method in the constructor of the platform view related scenarios /// to perform necessary set up. - void createPlatformView(Window window, String text, int id) { + void createPlatformView(Window window, String text, int id, {String viewType = 'scenarios/textPlatformView'}) { const int _valueInt32 = 3; const int _valueFloat64 = 6; const int _valueString = 7; @@ -313,8 +374,8 @@ mixin _BasePlatformViewScenarioMixin on Scenario { 'viewType'.length, ...utf8.encode('viewType'), _valueString, - 'scenarios/textPlatformView'.length, - ...utf8.encode('scenarios/textPlatformView'), + viewType.length, + ...utf8.encode(viewType), if (Platform.isAndroid) ...[ _valueString, 'width'.length,