Skip to content

Commit

Permalink
PlatformViewsController always make sure the touch events are finished (
Browse files Browse the repository at this point in the history
  • Loading branch information
Chris Yang committed Nov 13, 2020
1 parent ecfe5ae commit 92e5a95
Show file tree
Hide file tree
Showing 2 changed files with 212 additions and 6 deletions.
28 changes: 22 additions & 6 deletions shell/platform/darwin/ios/framework/Source/FlutterPlatformViews.mm
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;
}

- (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);
}
}

- (BOOL)gestureRecognizer:(UIGestureRecognizer*)gestureRecognizer
Expand Down
190 changes: 190 additions & 0 deletions shell/platform/darwin/ios/framework/Source/FlutterPlatformViewsTest.mm
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

0 comments on commit 92e5a95

Please sign in to comment.