Skip to content
This repository was archived by the owner on Feb 25, 2025. It is now read-only.

Commit 97c628b

Browse files
committed
[ios][platform_view]force reset forwarding recognizer state when its stuck
1 parent dfb115d commit 97c628b

2 files changed

Lines changed: 146 additions & 1 deletion

File tree

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

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2782,6 +2782,111 @@ - (void)testFlutterPlatformViewTouchesCancelledEventAreForcedToBeCancelled {
27822782
flutterPlatformViewsController->Reset();
27832783
}
27842784

2785+
- (void)testFlutterPlatformViewTouchesEndedOrTouchesCancelledEventDoesNotFailTheGestureRecognizer {
2786+
flutter::FlutterPlatformViewsTestMockPlatformViewDelegate mock_delegate;
2787+
2788+
flutter::TaskRunners runners(/*label=*/self.name.UTF8String,
2789+
/*platform=*/GetDefaultTaskRunner(),
2790+
/*raster=*/GetDefaultTaskRunner(),
2791+
/*ui=*/GetDefaultTaskRunner(),
2792+
/*io=*/GetDefaultTaskRunner());
2793+
auto flutterPlatformViewsController = std::make_shared<flutter::PlatformViewsController>();
2794+
flutterPlatformViewsController->SetTaskRunner(GetDefaultTaskRunner());
2795+
auto platform_view = std::make_unique<flutter::PlatformViewIOS>(
2796+
/*delegate=*/mock_delegate,
2797+
/*rendering_api=*/mock_delegate.settings_.enable_impeller
2798+
? flutter::IOSRenderingAPI::kMetal
2799+
: flutter::IOSRenderingAPI::kSoftware,
2800+
/*platform_views_controller=*/flutterPlatformViewsController,
2801+
/*task_runners=*/runners,
2802+
/*worker_task_runner=*/nil,
2803+
/*is_gpu_disabled_jsync_switch=*/std::make_shared<fml::SyncSwitch>());
2804+
2805+
FlutterPlatformViewsTestMockFlutterPlatformFactory* factory =
2806+
[[FlutterPlatformViewsTestMockFlutterPlatformFactory alloc] init];
2807+
flutterPlatformViewsController->RegisterViewFactory(
2808+
factory, @"MockFlutterPlatformView",
2809+
FlutterPlatformViewGestureRecognizersBlockingPolicyEager);
2810+
FlutterResult result = ^(id result) {
2811+
};
2812+
flutterPlatformViewsController->OnMethodCall(
2813+
[FlutterMethodCall
2814+
methodCallWithMethodName:@"create"
2815+
arguments:@{@"id" : @2, @"viewType" : @"MockFlutterPlatformView"}],
2816+
result);
2817+
2818+
XCTAssertNotNil(gMockPlatformView);
2819+
2820+
// Find touch inteceptor view
2821+
UIView* touchInteceptorView = gMockPlatformView;
2822+
while (touchInteceptorView != nil &&
2823+
![touchInteceptorView isKindOfClass:[FlutterTouchInterceptingView class]]) {
2824+
touchInteceptorView = touchInteceptorView.superview;
2825+
}
2826+
XCTAssertNotNil(touchInteceptorView);
2827+
2828+
// Find ForwardGestureRecognizer
2829+
__block UIGestureRecognizer* forwardGestureRecognizer = nil;
2830+
for (UIGestureRecognizer* gestureRecognizer in touchInteceptorView.gestureRecognizers) {
2831+
if ([gestureRecognizer isKindOfClass:NSClassFromString(@"ForwardingGestureRecognizer")]) {
2832+
forwardGestureRecognizer = gestureRecognizer;
2833+
break;
2834+
}
2835+
}
2836+
id flutterViewContoller = OCMClassMock([FlutterViewController class]);
2837+
2838+
flutterPlatformViewsController->SetFlutterViewController(flutterViewContoller);
2839+
2840+
NSSet* touches1 = [NSSet setWithObject:@1];
2841+
id event1 = OCMClassMock([UIEvent class]);
2842+
XCTAssert(forwardGestureRecognizer.state == UIGestureRecognizerStatePossible,
2843+
@"Forwarding gesture recognizer must start with possible state.");
2844+
[forwardGestureRecognizer touchesBegan:touches1 withEvent:event1];
2845+
[forwardGestureRecognizer touchesEnded:touches1 withEvent:event1];
2846+
XCTAssert(forwardGestureRecognizer.state == UIGestureRecognizerStateFailed,
2847+
@"Forwarding gesture recognizer must end with failed state.");
2848+
2849+
XCTestExpectation* touchEndedExpectation =
2850+
[self expectationWithDescription:@"Wait for gesture recognizer's state change."];
2851+
dispatch_async(dispatch_get_main_queue(), ^{
2852+
// Re-query forward gesture recognizer since it's recreated.
2853+
for (UIGestureRecognizer* gestureRecognizer in touchInteceptorView.gestureRecognizers) {
2854+
if ([gestureRecognizer isKindOfClass:NSClassFromString(@"ForwardingGestureRecognizer")]) {
2855+
forwardGestureRecognizer = gestureRecognizer;
2856+
break;
2857+
}
2858+
}
2859+
XCTAssert(forwardGestureRecognizer.state == UIGestureRecognizerStatePossible,
2860+
@"Forwarding gesture recognizer must be reset to possible state.");
2861+
[touchEndedExpectation fulfill];
2862+
});
2863+
[self waitForExpectationsWithTimeout:30 handler:nil];
2864+
2865+
XCTAssert(forwardGestureRecognizer.state == UIGestureRecognizerStatePossible,
2866+
@"Forwarding gesture recognizer must start with possible state.");
2867+
[forwardGestureRecognizer touchesBegan:touches1 withEvent:event1];
2868+
[forwardGestureRecognizer touchesCancelled:touches1 withEvent:event1];
2869+
XCTAssert(forwardGestureRecognizer.state == UIGestureRecognizerStateFailed,
2870+
@"Forwarding gesture recognizer must end with failed state.");
2871+
XCTestExpectation* touchCancelledExpectation =
2872+
[self expectationWithDescription:@"Wait for gesture recognizer's state change."];
2873+
dispatch_async(dispatch_get_main_queue(), ^{
2874+
// Re-query forward gesture recognizer since it's recreated.
2875+
for (UIGestureRecognizer* gestureRecognizer in touchInteceptorView.gestureRecognizers) {
2876+
if ([gestureRecognizer isKindOfClass:NSClassFromString(@"ForwardingGestureRecognizer")]) {
2877+
forwardGestureRecognizer = gestureRecognizer;
2878+
break;
2879+
}
2880+
}
2881+
XCTAssert(forwardGestureRecognizer.state == UIGestureRecognizerStatePossible,
2882+
@"Forwarding gesture recognizer must be reset to possible state.");
2883+
[touchCancelledExpectation fulfill];
2884+
});
2885+
[self waitForExpectationsWithTimeout:30 handler:nil];
2886+
2887+
flutterPlatformViewsController->Reset();
2888+
}
2889+
27852890
- (void)testFlutterPlatformViewControllerSubmitFrameWithoutFlutterViewNotCrashing {
27862891
flutter::FlutterPlatformViewsTestMockPlatformViewDelegate mock_delegate;
27872892

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

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -526,7 +526,7 @@ @interface FlutterDelayingGestureRecognizer : UIGestureRecognizer <UIGestureReco
526526
// setting the state to `UIGestureRecognizerStateEnded`.
527527
@property(nonatomic) BOOL touchedEndedWithoutBlocking;
528528

529-
@property(nonatomic, readonly) UIGestureRecognizer* forwardingRecognizer;
529+
@property(nonatomic) UIGestureRecognizer* forwardingRecognizer;
530530

531531
- (instancetype)initWithTarget:(id)target
532532
action:(SEL)action
@@ -547,6 +547,7 @@ @interface ForwardingGestureRecognizer : UIGestureRecognizer <UIGestureRecognize
547547
- (instancetype)initWithTarget:(id)target
548548
platformViewsController:
549549
(fml::WeakPtr<flutter::PlatformViewsController>)platformViewsController;
550+
- (ForwardingGestureRecognizer*)recreateRecognizerWithTarget:(id)target;
550551
@end
551552

552553
@interface FlutterTouchInterceptingView ()
@@ -586,6 +587,20 @@ - (instancetype)initWithEmbeddedView:(UIView*)embeddedView
586587
return self;
587588
}
588589

590+
- (void)forceResetForwardingGestureRecognizerState {
591+
// When iPad pencil is involved in a finger touch gesture, the gesture is not reset to "possible"
592+
// state, which causes subsequent touches to be blocked. As a workaround, we force reset the state
593+
// by recreating the forwarding gesture recognizer. See:
594+
// https://github.com/flutter/flutter/issues/136244
595+
ForwardingGestureRecognizer* oldForwardingRecognizer =
596+
(ForwardingGestureRecognizer*)self.delayingRecognizer.forwardingRecognizer;
597+
ForwardingGestureRecognizer* newForwardingRecognizer =
598+
[oldForwardingRecognizer recreateRecognizerWithTarget:self];
599+
[self removeGestureRecognizer:oldForwardingRecognizer];
600+
self.delayingRecognizer.forwardingRecognizer = newForwardingRecognizer;
601+
[self addGestureRecognizer:newForwardingRecognizer];
602+
}
603+
589604
- (void)releaseGesture {
590605
self.delayingRecognizer.state = UIGestureRecognizerStateFailed;
591606
}
@@ -715,6 +730,11 @@ - (instancetype)initWithTarget:(id)target
715730
return self;
716731
}
717732

733+
- (ForwardingGestureRecognizer*)recreateRecognizerWithTarget:(id)target {
734+
return [[ForwardingGestureRecognizer alloc] initWithTarget:target
735+
platformViewsController:std::move(_platformViewsController)];
736+
}
737+
718738
- (void)touchesBegan:(NSSet*)touches withEvent:(UIEvent*)event {
719739
FML_DCHECK(_currentTouchPointersCount >= 0);
720740
if (_currentTouchPointersCount == 0) {
@@ -741,6 +761,16 @@ - (void)touchesEnded:(NSSet*)touches withEvent:(UIEvent*)event {
741761
if (_currentTouchPointersCount == 0) {
742762
self.state = UIGestureRecognizerStateFailed;
743763
_flutterViewController.reset(nil);
764+
__weak __typeof(self) weakSelf = self;
765+
dispatch_async(dispatch_get_main_queue(), ^{
766+
__strong __typeof(weakSelf) strongSelf = weakSelf;
767+
if (!strongSelf) {
768+
return;
769+
}
770+
if (strongSelf.state != UIGestureRecognizerStatePossible) {
771+
[(FlutterTouchInterceptingView*)strongSelf.view forceResetForwardingGestureRecognizerState];
772+
}
773+
});
744774
}
745775
}
746776

@@ -755,6 +785,16 @@ - (void)touchesCancelled:(NSSet*)touches withEvent:(UIEvent*)event {
755785
if (_currentTouchPointersCount == 0) {
756786
self.state = UIGestureRecognizerStateFailed;
757787
_flutterViewController.reset(nil);
788+
__weak __typeof(self) weakSelf = self;
789+
dispatch_async(dispatch_get_main_queue(), ^{
790+
__strong __typeof(weakSelf) strongSelf = weakSelf;
791+
if (!strongSelf) {
792+
return;
793+
}
794+
if (strongSelf.state != UIGestureRecognizerStatePossible) {
795+
[(FlutterTouchInterceptingView*)strongSelf.view forceResetForwardingGestureRecognizerState];
796+
}
797+
});
758798
}
759799
}
760800

0 commit comments

Comments
 (0)