Skip to content
This repository was archived by the owner on Feb 25, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
105 changes: 105 additions & 0 deletions shell/platform/darwin/ios/framework/Source/FlutterPlatformViewsTest.mm
Original file line number Diff line number Diff line change
Expand Up @@ -2782,6 +2782,111 @@ - (void)testFlutterPlatformViewTouchesCancelledEventAreForcedToBeCancelled {
flutterPlatformViewsController->Reset();
}

- (void)testFlutterPlatformViewTouchesEndedOrTouchesCancelledEventDoesNotFailTheGestureRecognizer {
flutter::FlutterPlatformViewsTestMockPlatformViewDelegate mock_delegate;

flutter::TaskRunners runners(/*label=*/self.name.UTF8String,
/*platform=*/GetDefaultTaskRunner(),
/*raster=*/GetDefaultTaskRunner(),
/*ui=*/GetDefaultTaskRunner(),
/*io=*/GetDefaultTaskRunner());
auto flutterPlatformViewsController = std::make_shared<flutter::PlatformViewsController>();
flutterPlatformViewsController->SetTaskRunner(GetDefaultTaskRunner());
auto platform_view = std::make_unique<flutter::PlatformViewIOS>(
/*delegate=*/mock_delegate,
/*rendering_api=*/mock_delegate.settings_.enable_impeller
? flutter::IOSRenderingAPI::kMetal
: flutter::IOSRenderingAPI::kSoftware,
/*platform_views_controller=*/flutterPlatformViewsController,
/*task_runners=*/runners,
/*worker_task_runner=*/nil,
/*is_gpu_disabled_jsync_switch=*/std::make_shared<fml::SyncSwitch>());

FlutterPlatformViewsTestMockFlutterPlatformFactory* factory =
[[FlutterPlatformViewsTestMockFlutterPlatformFactory alloc] init];
flutterPlatformViewsController->RegisterViewFactory(
factory, @"MockFlutterPlatformView",
FlutterPlatformViewGestureRecognizersBlockingPolicyEager);
FlutterResult result = ^(id result) {
};
flutterPlatformViewsController->OnMethodCall(
[FlutterMethodCall
methodCallWithMethodName:@"create"
arguments:@{@"id" : @2, @"viewType" : @"MockFlutterPlatformView"}],
result);

XCTAssertNotNil(gMockPlatformView);

// Find touch inteceptor view
UIView* touchInteceptorView = gMockPlatformView;
while (touchInteceptorView != nil &&
![touchInteceptorView isKindOfClass:[FlutterTouchInterceptingView class]]) {
touchInteceptorView = touchInteceptorView.superview;
}
XCTAssertNotNil(touchInteceptorView);

// Find ForwardGestureRecognizer
__block UIGestureRecognizer* forwardGestureRecognizer = nil;
for (UIGestureRecognizer* gestureRecognizer in touchInteceptorView.gestureRecognizers) {
if ([gestureRecognizer isKindOfClass:NSClassFromString(@"ForwardingGestureRecognizer")]) {
forwardGestureRecognizer = gestureRecognizer;
break;
}
}
id flutterViewContoller = OCMClassMock([FlutterViewController class]);

flutterPlatformViewsController->SetFlutterViewController(flutterViewContoller);

NSSet* touches1 = [NSSet setWithObject:@1];
id event1 = OCMClassMock([UIEvent class]);
XCTAssert(forwardGestureRecognizer.state == UIGestureRecognizerStatePossible,
@"Forwarding gesture recognizer must start with possible state.");
[forwardGestureRecognizer touchesBegan:touches1 withEvent:event1];
[forwardGestureRecognizer touchesEnded:touches1 withEvent:event1];
XCTAssert(forwardGestureRecognizer.state == UIGestureRecognizerStateFailed,
@"Forwarding gesture recognizer must end with failed state.");

XCTestExpectation* touchEndedExpectation =
[self expectationWithDescription:@"Wait for gesture recognizer's state change."];
dispatch_async(dispatch_get_main_queue(), ^{
// Re-query forward gesture recognizer since it's recreated.
for (UIGestureRecognizer* gestureRecognizer in touchInteceptorView.gestureRecognizers) {
if ([gestureRecognizer isKindOfClass:NSClassFromString(@"ForwardingGestureRecognizer")]) {
forwardGestureRecognizer = gestureRecognizer;
break;
}
}
XCTAssert(forwardGestureRecognizer.state == UIGestureRecognizerStatePossible,
@"Forwarding gesture recognizer must be reset to possible state.");
[touchEndedExpectation fulfill];
});
[self waitForExpectationsWithTimeout:30 handler:nil];

XCTAssert(forwardGestureRecognizer.state == UIGestureRecognizerStatePossible,
@"Forwarding gesture recognizer must start with possible state.");
[forwardGestureRecognizer touchesBegan:touches1 withEvent:event1];
[forwardGestureRecognizer touchesCancelled:touches1 withEvent:event1];
XCTAssert(forwardGestureRecognizer.state == UIGestureRecognizerStateFailed,
@"Forwarding gesture recognizer must end with failed state.");
XCTestExpectation* touchCancelledExpectation =
[self expectationWithDescription:@"Wait for gesture recognizer's state change."];
dispatch_async(dispatch_get_main_queue(), ^{
// Re-query forward gesture recognizer since it's recreated.
for (UIGestureRecognizer* gestureRecognizer in touchInteceptorView.gestureRecognizers) {
if ([gestureRecognizer isKindOfClass:NSClassFromString(@"ForwardingGestureRecognizer")]) {
forwardGestureRecognizer = gestureRecognizer;
break;
}
}
XCTAssert(forwardGestureRecognizer.state == UIGestureRecognizerStatePossible,
@"Forwarding gesture recognizer must be reset to possible state.");
[touchCancelledExpectation fulfill];
});
[self waitForExpectationsWithTimeout:30 handler:nil];

flutterPlatformViewsController->Reset();
}

- (void)testFlutterPlatformViewControllerSubmitFrameWithoutFlutterViewNotCrashing {
flutter::FlutterPlatformViewsTestMockPlatformViewDelegate mock_delegate;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -526,7 +526,7 @@ @interface FlutterDelayingGestureRecognizer : UIGestureRecognizer <UIGestureReco
// setting the state to `UIGestureRecognizerStateEnded`.
@property(nonatomic) BOOL touchedEndedWithoutBlocking;

@property(nonatomic, readonly) UIGestureRecognizer* forwardingRecognizer;
@property(nonatomic) UIGestureRecognizer* forwardingRecognizer;

- (instancetype)initWithTarget:(id)target
action:(SEL)action
Expand All @@ -547,6 +547,7 @@ @interface ForwardingGestureRecognizer : UIGestureRecognizer <UIGestureRecognize
- (instancetype)initWithTarget:(id)target
platformViewsController:
(fml::WeakPtr<flutter::PlatformViewsController>)platformViewsController;
- (ForwardingGestureRecognizer*)recreateRecognizerWithTarget:(id)target;
@end

@interface FlutterTouchInterceptingView ()
Expand Down Expand Up @@ -586,6 +587,20 @@ - (instancetype)initWithEmbeddedView:(UIView*)embeddedView
return self;
}

- (void)forceResetForwardingGestureRecognizerState {
// When iPad pencil is involved in a finger touch gesture, the gesture is not reset to "possible"
// state and is stuck on "failed" state, which causes subsequent touches to be blocked. As a
// workaround, we force reset the state by recreating the forwarding gesture recognizer. See:
// https://github.com/flutter/flutter/issues/136244
ForwardingGestureRecognizer* oldForwardingRecognizer =
(ForwardingGestureRecognizer*)self.delayingRecognizer.forwardingRecognizer;
ForwardingGestureRecognizer* newForwardingRecognizer =
[oldForwardingRecognizer recreateRecognizerWithTarget:self];
self.delayingRecognizer.forwardingRecognizer = newForwardingRecognizer;
[self removeGestureRecognizer:oldForwardingRecognizer];
[self addGestureRecognizer:newForwardingRecognizer];
}

- (void)releaseGesture {
self.delayingRecognizer.state = UIGestureRecognizerStateFailed;
}
Expand Down Expand Up @@ -715,6 +730,11 @@ - (instancetype)initWithTarget:(id)target
return self;
}

- (ForwardingGestureRecognizer*)recreateRecognizerWithTarget:(id)target {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it be possible for ForwardingGestureRecognizer to instead implement NSCopying?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Talked to @hellohuanlin, this isn't feasible because of the move which makes the old recognizer unusable.

return [[ForwardingGestureRecognizer alloc] initWithTarget:target
platformViewsController:std::move(_platformViewsController)];
}

- (void)touchesBegan:(NSSet*)touches withEvent:(UIEvent*)event {
FML_DCHECK(_currentTouchPointersCount >= 0);
if (_currentTouchPointersCount == 0) {
Expand All @@ -741,6 +761,7 @@ - (void)touchesEnded:(NSSet*)touches withEvent:(UIEvent*)event {
if (_currentTouchPointersCount == 0) {
self.state = UIGestureRecognizerStateFailed;
_flutterViewController.reset(nil);
[self forceResetStateIfNeeded];
}
}

Expand All @@ -755,9 +776,23 @@ - (void)touchesCancelled:(NSSet*)touches withEvent:(UIEvent*)event {
if (_currentTouchPointersCount == 0) {
self.state = UIGestureRecognizerStateFailed;
_flutterViewController.reset(nil);
[self forceResetStateIfNeeded];
}
}

- (void)forceResetStateIfNeeded {
__weak ForwardingGestureRecognizer* weakSelf = self;
dispatch_async(dispatch_get_main_queue(), ^{
ForwardingGestureRecognizer* strongSelf = weakSelf;
if (!strongSelf) {
return;
}
if (strongSelf.state != UIGestureRecognizerStatePossible) {
[(FlutterTouchInterceptingView*)strongSelf.view forceResetForwardingGestureRecognizerState];
}
});
}

- (BOOL)gestureRecognizer:(UIGestureRecognizer*)gestureRecognizer
shouldRecognizeSimultaneouslyWithGestureRecognizer:
(UIGestureRecognizer*)otherGestureRecognizer {
Expand Down