mirror of
https://github.com/flutter/flutter.git
synced 2026-02-20 02:29:02 +08:00
[ios][platform_view]force reset forwarding recognizer state when its stuck (flutter/engine#55958)
This PR force reset forwarding recognizer state when it's stuck at failed state, by recreating the recognizer (we are not able to reset the state back to possible - it has to be reset by UIKit). This is a tricky one since pencil and finger triggers exactly the same callbacks. It turns out that when pencil is involved after finger interaction, the platform view's "forwarding" gesture recognizer is stuck at failed state. This seems to be an iOS bug, because according to [the API doc](https://developer.apple.com/documentation/uikit/uigesturerecognizerstate/uigesturerecognizerstatefailed?language=objc), it should be reset back to "possible" state: > No action message is sent and the gesture recognizer is reset to [UIGestureRecognizerStatePossible](https://developer.apple.com/documentation/uikit/uigesturerecognizerstate/uigesturerecognizerstatepossible?language=objc). However, when iPad pencil is involved, the state is not reset. I tried to KVO the state property, and wasn't able to capture the change. This means the state change very likely happened internally within the recognizer via the backing ivar of the state property. Fixes https://github.com/flutter/flutter/issues/136244 *If you had to change anything in the [flutter/tests] repo, include a link to the migration guide as per the [breaking change policy].* [C++, Objective-C, Java style guides]: https://github.com/flutter/engine/blob/main/CONTRIBUTING.md#style
This commit is contained in:
parent
57a7c7a5fe
commit
3e89cd79b5
@ -2782,6 +2782,111 @@ fml::RefPtr<fml::TaskRunner> GetDefaultTaskRunner() {
|
||||
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;
|
||||
|
||||
|
||||
@ -526,7 +526,7 @@ static BOOL _preparedOnce = NO;
|
||||
// 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
|
||||
@ -547,6 +547,7 @@ static BOOL _preparedOnce = NO;
|
||||
- (instancetype)initWithTarget:(id)target
|
||||
platformViewsController:
|
||||
(fml::WeakPtr<flutter::PlatformViewsController>)platformViewsController;
|
||||
- (ForwardingGestureRecognizer*)recreateRecognizerWithTarget:(id)target;
|
||||
@end
|
||||
|
||||
@interface FlutterTouchInterceptingView ()
|
||||
@ -586,6 +587,20 @@ static BOOL _preparedOnce = NO;
|
||||
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;
|
||||
}
|
||||
@ -715,6 +730,11 @@ static BOOL _preparedOnce = NO;
|
||||
return self;
|
||||
}
|
||||
|
||||
- (ForwardingGestureRecognizer*)recreateRecognizerWithTarget:(id)target {
|
||||
return [[ForwardingGestureRecognizer alloc] initWithTarget:target
|
||||
platformViewsController:std::move(_platformViewsController)];
|
||||
}
|
||||
|
||||
- (void)touchesBegan:(NSSet*)touches withEvent:(UIEvent*)event {
|
||||
FML_DCHECK(_currentTouchPointersCount >= 0);
|
||||
if (_currentTouchPointersCount == 0) {
|
||||
@ -741,6 +761,7 @@ static BOOL _preparedOnce = NO;
|
||||
if (_currentTouchPointersCount == 0) {
|
||||
self.state = UIGestureRecognizerStateFailed;
|
||||
_flutterViewController.reset(nil);
|
||||
[self forceResetStateIfNeeded];
|
||||
}
|
||||
}
|
||||
|
||||
@ -755,9 +776,23 @@ static BOOL _preparedOnce = NO;
|
||||
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 {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user