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
28 changes: 22 additions & 6 deletions shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm
Original file line number Diff line number Diff line change
Expand Up @@ -871,6 +871,11 @@ @implementation ForwardingGestureRecognizer {
fml::WeakPtr<flutter::FlutterPlatformViewsController> _platformViewsController;
// Counting the pointers that has started in one touch sequence.
NSInteger _currentTouchPointersCount;
// We can't dispatch events to the framework without this back pointer.
// This gesture recognizer retains the `FlutterViewController` until the
// end of a gesture sequence, that is all the touches in touchesBegan are concluded
// with |touchesCancelled| or |touchesEnded|.
fml::scoped_nsobject<UIViewController> _flutterViewController;
}

- (instancetype)initWithTarget:(id)target
Expand All @@ -887,30 +892,41 @@ - (instancetype)initWithTarget:(id)target
}

- (void)touchesBegan:(NSSet*)touches withEvent:(UIEvent*)event {
[_platformViewsController->getFlutterViewController() touchesBegan:touches withEvent:event];
FML_DCHECK(_currentTouchPointersCount >= 0);
if (_currentTouchPointersCount == 0) {
// At the start of each gesture sequence, we reset the `_flutterViewController`,
// so that all the touch events in the same sequence are forwarded to the same
// `_flutterViewController`.
_flutterViewController.reset([_platformViewsController->getFlutterViewController() retain]);
}
[_flutterViewController.get() touchesBegan:touches withEvent:event];
_currentTouchPointersCount += touches.count;
Copy link
Contributor

Choose a reason for hiding this comment

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

What's the implication of not incrementing this counter if we have no VC?

Should we be setting to zero in the early return above?

}

- (void)touchesMoved:(NSSet*)touches withEvent:(UIEvent*)event {
[_platformViewsController->getFlutterViewController() touchesMoved:touches withEvent:event];
[_flutterViewController.get() touchesMoved:touches withEvent:event];
}

- (void)touchesEnded:(NSSet*)touches withEvent:(UIEvent*)event {
[_platformViewsController->getFlutterViewController() touchesEnded:touches withEvent:event];
[_flutterViewController.get() touchesEnded:touches withEvent:event];
_currentTouchPointersCount -= touches.count;
// Touches in one touch sequence are sent to the touchesEnded method separately if different
// fingers stop touching the screen at different time. So one touchesEnded method triggering does
// not necessarially mean the touch sequence has ended. We Only set the state to
// UIGestureRecognizerStateFailed when all the touches in the current touch sequence is ended.
if (_currentTouchPointersCount == 0) {
self.state = UIGestureRecognizerStateFailed;
_flutterViewController.reset(nil);
}
}

- (void)touchesCancelled:(NSSet*)touches withEvent:(UIEvent*)event {
[_platformViewsController->getFlutterViewController() touchesCancelled:touches withEvent:event];
_currentTouchPointersCount = 0;
self.state = UIGestureRecognizerStateFailed;
[_flutterViewController.get() touchesCancelled:touches withEvent:event];
_currentTouchPointersCount -= touches.count;
if (_currentTouchPointersCount == 0) {
self.state = UIGestureRecognizerStateFailed;
_flutterViewController.reset(nil);
}
Comment on lines +924 to +929
Copy link
Member

Choose a reason for hiding this comment

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

In the case where we get Begin-Begin-Cancel-End the state will no longer be Failed like it was previously, is that expected?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Begin-Begin-Cancel-End makes the count to 0 at the last End, so it state would still be failed.
I don't think Begin-Begin-Cancel-End would be possible anyway, if a Cancel happens during a sequence, it cancels all the touches had begun.

}

- (BOOL)gestureRecognizer:(UIGestureRecognizer*)gestureRecognizer
Expand Down
190 changes: 190 additions & 0 deletions shell/platform/darwin/ios/framework/Source/FlutterPlatformViewsTest.mm
Original file line number Diff line number Diff line change
Expand Up @@ -568,6 +568,196 @@ - (void)testSetFlutterViewControllerAfterCreateCanStillDispatchTouchEvents {
flutterPlatformViewsController->Reset();
}

- (void)testSetFlutterViewControllerInTheMiddleOfTouchEventShouldStillAllowGesturesToBeHandled {
flutter::FlutterPlatformViewsTestMockPlatformViewDelegate mock_delegate;
auto thread_task_runner = CreateNewThread("FlutterPlatformViewsTest");
flutter::TaskRunners runners(/*label=*/self.name.UTF8String,
/*platform=*/thread_task_runner,
/*raster=*/thread_task_runner,
/*ui=*/thread_task_runner,
/*io=*/thread_task_runner);
auto flutterPlatformViewsController = std::make_shared<flutter::FlutterPlatformViewsController>();
auto platform_view = std::make_unique<flutter::PlatformViewIOS>(
/*delegate=*/mock_delegate,
/*rendering_api=*/flutter::IOSRenderingAPI::kSoftware,
/*platform_views_controller=*/flutterPlatformViewsController,
/*task_runners=*/runners);

FlutterPlatformViewsTestMockFlutterPlatformFactory* factory =
[[FlutterPlatformViewsTestMockFlutterPlatformFactory new] autorelease];
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
UIGestureRecognizer* forwardGectureRecognizer = nil;
for (UIGestureRecognizer* gestureRecognizer in touchInteceptorView.gestureRecognizers) {
if ([gestureRecognizer isKindOfClass:NSClassFromString(@"ForwardingGestureRecognizer")]) {
forwardGectureRecognizer = gestureRecognizer;
break;
}
}
UIViewController* mockFlutterViewContoller = OCMClassMock([UIViewController class]);
{
// ***** Sequence 1, finishing touch event with touchEnded ***** //

flutterPlatformViewsController->SetFlutterViewController(mockFlutterViewContoller);

NSSet* touches1 = OCMClassMock([NSSet class]);
UIEvent* event1 = OCMClassMock([UIEvent class]);
[forwardGectureRecognizer touchesBegan:touches1 withEvent:event1];
OCMVerify([mockFlutterViewContoller touchesBegan:touches1 withEvent:event1]);

flutterPlatformViewsController->SetFlutterViewController(nil);

// Allow the touch events to finish
NSSet* touches2 = OCMClassMock([NSSet class]);
UIEvent* event2 = OCMClassMock([UIEvent class]);
[forwardGectureRecognizer touchesMoved:touches2 withEvent:event2];
OCMVerify([mockFlutterViewContoller touchesMoved:touches2 withEvent:event2]);

NSSet* touches3 = OCMClassMock([NSSet class]);
UIEvent* event3 = OCMClassMock([UIEvent class]);
[forwardGectureRecognizer touchesEnded:touches3 withEvent:event3];
OCMVerify([mockFlutterViewContoller touchesEnded:touches3 withEvent:event3]);

// Now the 2nd touch sequence should not be allowed.
NSSet* touches4 = OCMClassMock([NSSet class]);
UIEvent* event4 = OCMClassMock([UIEvent class]);
mockFlutterViewContoller = OCMClassMock([UIViewController class]);
[forwardGectureRecognizer touchesBegan:touches4 withEvent:event4];
OCMReject([mockFlutterViewContoller touchesBegan:touches4 withEvent:event4]);

NSSet* touches5 = OCMClassMock([NSSet class]);
UIEvent* event5 = OCMClassMock([UIEvent class]);
[forwardGectureRecognizer touchesEnded:touches5 withEvent:event5];
OCMReject([mockFlutterViewContoller touchesEnded:touches5 withEvent:event5]);
}

{
// ***** Sequence 2, finishing touch event with touchCancelled ***** //
flutterPlatformViewsController->SetFlutterViewController(mockFlutterViewContoller);

NSSet* touches1 = OCMClassMock([NSSet class]);
UIEvent* event1 = OCMClassMock([UIEvent class]);
[forwardGectureRecognizer touchesBegan:touches1 withEvent:event1];
OCMVerify([mockFlutterViewContoller touchesBegan:touches1 withEvent:event1]);

flutterPlatformViewsController->SetFlutterViewController(nil);

// Allow the touch events to finish
NSSet* touches2 = OCMClassMock([NSSet class]);
UIEvent* event2 = OCMClassMock([UIEvent class]);
[forwardGectureRecognizer touchesMoved:touches2 withEvent:event2];
OCMVerify([mockFlutterViewContoller touchesMoved:touches2 withEvent:event2]);

NSSet* touches3 = OCMClassMock([NSSet class]);
UIEvent* event3 = OCMClassMock([UIEvent class]);
[forwardGectureRecognizer touchesCancelled:touches3 withEvent:event3];
OCMVerify([mockFlutterViewContoller touchesCancelled:touches3 withEvent:event3]);

// Now the 2nd touch sequence should not be allowed.
NSSet* touches4 = OCMClassMock([NSSet class]);
UIEvent* event4 = OCMClassMock([UIEvent class]);
mockFlutterViewContoller = OCMClassMock([UIViewController class]);
[forwardGectureRecognizer touchesBegan:touches4 withEvent:event4];
OCMReject([mockFlutterViewContoller touchesBegan:touches4 withEvent:event4]);

NSSet* touches5 = OCMClassMock([NSSet class]);
UIEvent* event5 = OCMClassMock([UIEvent class]);
[forwardGectureRecognizer touchesEnded:touches5 withEvent:event5];
OCMReject([mockFlutterViewContoller touchesEnded:touches5 withEvent:event5]);
}

{
// ***** Sequence 3, multile touches in one sequence with setting flutter view controllers in
// between ***** //
flutterPlatformViewsController->SetFlutterViewController(mockFlutterViewContoller);

NSSet* touches1 = OCMClassMock([NSSet class]);
OCMStub([touches1 count]).andReturn(1);
UIEvent* event1 = OCMClassMock([UIEvent class]);
[forwardGectureRecognizer touchesBegan:touches1 withEvent:event1];
OCMVerify([mockFlutterViewContoller touchesBegan:touches1 withEvent:event1]);

UIViewController* mockFlutterViewContoller2 = OCMClassMock([UIViewController class]);
flutterPlatformViewsController->SetFlutterViewController(mockFlutterViewContoller2);

// Touch events should still send to the old FlutterViewController if FlutterViewController
// is updated in between.
NSSet* touches2 = OCMClassMock([NSSet class]);
OCMStub([touches2 count]).andReturn(1);
UIEvent* event2 = OCMClassMock([UIEvent class]);
[forwardGectureRecognizer touchesBegan:touches2 withEvent:event2];
OCMVerify([mockFlutterViewContoller touchesBegan:touches2 withEvent:event2]);
OCMReject([mockFlutterViewContoller2 touchesBegan:touches2 withEvent:event2]);

NSSet* touches3 = OCMClassMock([NSSet class]);
OCMStub([touches3 count]).andReturn(1);
UIEvent* event3 = OCMClassMock([UIEvent class]);
[forwardGectureRecognizer touchesMoved:touches3 withEvent:event3];
OCMVerify([mockFlutterViewContoller touchesMoved:touches3 withEvent:event3]);
OCMReject([mockFlutterViewContoller2 touchesMoved:touches3 withEvent:event3]);

NSSet* touches4 = OCMClassMock([NSSet class]);
OCMStub([touches4 count]).andReturn(1);
UIEvent* event4 = OCMClassMock([UIEvent class]);
[forwardGectureRecognizer touchesEnded:touches4 withEvent:event4];
OCMVerify([mockFlutterViewContoller touchesEnded:touches4 withEvent:event4]);
OCMReject([mockFlutterViewContoller2 touchesEnded:touches4 withEvent:event4]);

NSSet* touches5 = OCMClassMock([NSSet class]);
OCMStub([touches5 count]).andReturn(1);
UIEvent* event5 = OCMClassMock([UIEvent class]);
[forwardGectureRecognizer touchesEnded:touches5 withEvent:event5];
OCMVerify([mockFlutterViewContoller touchesEnded:touches5 withEvent:event5]);
OCMReject([mockFlutterViewContoller2 touchesEnded:touches5 withEvent:event5]);

// Now the 2nd touch sequence should go to the new FlutterViewController

NSSet* touches6 = OCMClassMock([NSSet class]);
OCMStub([touches6 count]).andReturn(1);
UIEvent* event6 = OCMClassMock([UIEvent class]);
[forwardGectureRecognizer touchesBegan:touches6 withEvent:event6];
OCMVerify([mockFlutterViewContoller2 touchesBegan:touches6 withEvent:event6]);
OCMReject([mockFlutterViewContoller touchesBegan:touches6 withEvent:event6]);

// Allow the touch events to finish
NSSet* touches7 = OCMClassMock([NSSet class]);
OCMStub([touches7 count]).andReturn(1);
UIEvent* event7 = OCMClassMock([UIEvent class]);
[forwardGectureRecognizer touchesMoved:touches7 withEvent:event7];
OCMVerify([mockFlutterViewContoller2 touchesMoved:touches7 withEvent:event7]);
OCMReject([mockFlutterViewContoller touchesMoved:touches7 withEvent:event7]);

NSSet* touches8 = OCMClassMock([NSSet class]);
OCMStub([touches8 count]).andReturn(1);
UIEvent* event8 = OCMClassMock([UIEvent class]);
[forwardGectureRecognizer touchesEnded:touches8 withEvent:event8];
OCMVerify([mockFlutterViewContoller2 touchesEnded:touches8 withEvent:event8]);
OCMReject([mockFlutterViewContoller touchesEnded:touches8 withEvent:event8]);
}

flutterPlatformViewsController->Reset();
}

- (void)testFlutterPlatformViewControllerSubmitFrameWithoutFlutterViewNotCrashing {
flutter::FlutterPlatformViewsTestMockPlatformViewDelegate mock_delegate;
auto thread_task_runner = CreateNewThread("FlutterPlatformViewsTest");
Expand Down