diff --git a/shell/platform/darwin/ios/framework/Source/FlutterViewController.mm b/shell/platform/darwin/ios/framework/Source/FlutterViewController.mm index 5aaeb546d502a..8719855a7e5b8 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterViewController.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterViewController.mm @@ -65,6 +65,14 @@ @interface FlutterViewController () (touches.count + touches_to_remove_count); @@ -1120,6 +1135,63 @@ - (void)forceTouchesCancelled:(NSSet*)touches { [self dispatchTouches:touches pointerDataChangeOverride:&cancel event:nullptr]; } +#pragma mark - Touch events rate correction + +- (void)createTouchRateCorrectionVSyncClientIfNeeded { + if (_touchRateCorrectionVSyncClient != nil) { + return; + } + + double displayRefreshRate = [DisplayLinkManager displayRefreshRate]; + const double epsilon = 0.1; + if (displayRefreshRate < 60.0 + epsilon) { // displayRefreshRate <= 60.0 + + // If current device's max frame rate is not larger than 60HZ, the delivery rate of touch events + // is the same with render vsync rate. So it is unnecessary to create + // _touchRateCorrectionVSyncClient to correct touch callback's rate. + return; + } + + flutter::Shell& shell = [_engine.get() shell]; + auto callback = [](std::unique_ptr recorder) { + // Do nothing in this block. Just trigger system to callback touch events with correct rate. + }; + _touchRateCorrectionVSyncClient = + [[VSyncClient alloc] initWithTaskRunner:shell.GetTaskRunners().GetPlatformTaskRunner() + callback:callback]; + _touchRateCorrectionVSyncClient.allowPauseAfterVsync = NO; +} + +- (void)triggerTouchRateCorrectionIfNeeded:(NSSet*)touches { + if (_touchRateCorrectionVSyncClient == nil) { + // If the _touchRateCorrectionVSyncClient is not created, means current devices doesn't + // need to correct the touch rate. So just return. + return; + } + + // As long as there is a touch's phase is UITouchPhaseBegan or UITouchPhaseMoved, + // activate the correction. Otherwise pause the correction. + BOOL isUserInteracting = NO; + for (UITouch* touch in touches) { + if (touch.phase == UITouchPhaseBegan || touch.phase == UITouchPhaseMoved) { + isUserInteracting = YES; + break; + } + } + + if (isUserInteracting && [_engine.get() viewController] == self) { + [_touchRateCorrectionVSyncClient await]; + } else { + [_touchRateCorrectionVSyncClient pause]; + } +} + +- (void)invalidateTouchRateCorrectionVSyncClient { + [_touchRateCorrectionVSyncClient invalidate]; + [_touchRateCorrectionVSyncClient release]; + _touchRateCorrectionVSyncClient = nil; +} + #pragma mark - Handle view resizing - (void)updateViewportMetrics { diff --git a/shell/platform/darwin/ios/framework/Source/FlutterViewControllerTest.mm b/shell/platform/darwin/ios/framework/Source/FlutterViewControllerTest.mm index d3f4de66f8710..cbdcf3cf6974e 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterViewControllerTest.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterViewControllerTest.mm @@ -114,6 +114,7 @@ @interface FlutterViewController (Tests) @property(nonatomic, assign) double targetViewInsetBottom; +- (void)createTouchRateCorrectionVSyncClientIfNeeded; - (void)surfaceUpdated:(BOOL)appeared; - (void)performOrientationUpdate:(UIInterfaceOrientationMask)new_preferences; - (void)handlePressEvent:(FlutterUIPressProxy*)press @@ -160,6 +161,18 @@ - (void)tearDown { self.messageSent = nil; } +- (void)testViewDidLoadWillInvokeCreateTouchRateCorrectionVSyncClient { + FlutterEngine* engine = [[FlutterEngine alloc] init]; + [engine runWithEntrypoint:nil]; + FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:engine + nibName:nil + bundle:nil]; + FlutterViewController* viewControllerMock = OCMPartialMock(viewController); + [viewControllerMock loadView]; + [viewControllerMock viewDidLoad]; + OCMVerify([viewControllerMock createTouchRateCorrectionVSyncClientIfNeeded]); +} + - (void)testStartKeyboardAnimationWillInvokeSetupKeyboardAnimationVsyncClient { FlutterEngine* engine = [[FlutterEngine alloc] init]; [engine runWithEntrypoint:nil]; diff --git a/shell/platform/darwin/ios/framework/Source/FlutterViewControllerTest_mrc.mm b/shell/platform/darwin/ios/framework/Source/FlutterViewControllerTest_mrc.mm index f4cc21706ef47..e18b0b55c9335 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterViewControllerTest_mrc.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterViewControllerTest_mrc.mm @@ -11,6 +11,12 @@ FLUTTER_ASSERT_NOT_ARC +@interface UITouch () + +@property(nonatomic, readwrite) UITouchPhase phase; + +@end + @interface VSyncClient (Testing) - (CADisplayLink*)getDisplayLink; @@ -22,7 +28,11 @@ @interface FlutterViewController (Testing) @property(nonatomic, assign) double targetViewInsetBottom; @property(nonatomic, retain) VSyncClient* keyboardAnimationVSyncClient; +@property(nonatomic, retain) VSyncClient* touchRateCorrectionVSyncClient; + +- (void)createTouchRateCorrectionVSyncClientIfNeeded; - (void)setupKeyboardAnimationVsyncClient; +- (void)triggerTouchRateCorrectionIfNeeded:(NSSet*)touches; @end @@ -56,4 +66,111 @@ - (void)testSetupKeyboardAnimationVsyncClientWillCreateNewVsyncClientForFlutterV } } +- (void) + testCreateTouchRateCorrectionVSyncClientWillCreateVsyncClientWhenRefreshRateIsLargerThan60HZ { + id mockDisplayLinkManager = [OCMockObject mockForClass:[DisplayLinkManager class]]; + double maxFrameRate = 120; + [[[mockDisplayLinkManager stub] andReturnValue:@(maxFrameRate)] displayRefreshRate]; + FlutterEngine* engine = [[FlutterEngine alloc] init]; + [engine runWithEntrypoint:nil]; + FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:engine + nibName:nil + bundle:nil]; + [viewController createTouchRateCorrectionVSyncClientIfNeeded]; + XCTAssertNotNil(viewController.touchRateCorrectionVSyncClient); +} + +- (void)testCreateTouchRateCorrectionVSyncClientWillNotCreateNewVSyncClientWhenClientAlreadyExists { + id mockDisplayLinkManager = [OCMockObject mockForClass:[DisplayLinkManager class]]; + double maxFrameRate = 120; + [[[mockDisplayLinkManager stub] andReturnValue:@(maxFrameRate)] displayRefreshRate]; + + FlutterEngine* engine = [[FlutterEngine alloc] init]; + [engine runWithEntrypoint:nil]; + FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:engine + nibName:nil + bundle:nil]; + [viewController createTouchRateCorrectionVSyncClientIfNeeded]; + VSyncClient* clientBefore = viewController.touchRateCorrectionVSyncClient; + XCTAssertNotNil(clientBefore); + + [viewController createTouchRateCorrectionVSyncClientIfNeeded]; + VSyncClient* clientAfter = viewController.touchRateCorrectionVSyncClient; + XCTAssertNotNil(clientAfter); + + XCTAssertTrue(clientBefore == clientAfter); +} + +- (void)testCreateTouchRateCorrectionVSyncClientWillNotCreateVsyncClientWhenRefreshRateIs60HZ { + id mockDisplayLinkManager = [OCMockObject mockForClass:[DisplayLinkManager class]]; + double maxFrameRate = 60; + [[[mockDisplayLinkManager stub] andReturnValue:@(maxFrameRate)] displayRefreshRate]; + FlutterEngine* engine = [[FlutterEngine alloc] init]; + [engine runWithEntrypoint:nil]; + FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:engine + nibName:nil + bundle:nil]; + [viewController createTouchRateCorrectionVSyncClientIfNeeded]; + XCTAssertNil(viewController.touchRateCorrectionVSyncClient); +} + +- (void)testTriggerTouchRateCorrectionVSyncClientCorrectly { + id mockDisplayLinkManager = [OCMockObject mockForClass:[DisplayLinkManager class]]; + double maxFrameRate = 120; + [[[mockDisplayLinkManager stub] andReturnValue:@(maxFrameRate)] displayRefreshRate]; + FlutterEngine* engine = [[FlutterEngine alloc] init]; + [engine runWithEntrypoint:nil]; + FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:engine + nibName:nil + bundle:nil]; + [viewController loadView]; + [viewController viewDidLoad]; + + VSyncClient* client = viewController.touchRateCorrectionVSyncClient; + CADisplayLink* link = [client getDisplayLink]; + + UITouch* fakeTouchBegan = [[UITouch alloc] init]; + fakeTouchBegan.phase = UITouchPhaseBegan; + + UITouch* fakeTouchMove = [[UITouch alloc] init]; + fakeTouchMove.phase = UITouchPhaseMoved; + + UITouch* fakeTouchEnd = [[UITouch alloc] init]; + fakeTouchEnd.phase = UITouchPhaseEnded; + + UITouch* fakeTouchCancelled = [[UITouch alloc] init]; + fakeTouchCancelled.phase = UITouchPhaseCancelled; + + [viewController + triggerTouchRateCorrectionIfNeeded:[[NSSet alloc] initWithObjects:fakeTouchBegan, nil]]; + XCTAssertFalse(link.isPaused); + + [viewController + triggerTouchRateCorrectionIfNeeded:[[NSSet alloc] initWithObjects:fakeTouchEnd, nil]]; + XCTAssertTrue(link.isPaused); + + [viewController + triggerTouchRateCorrectionIfNeeded:[[NSSet alloc] initWithObjects:fakeTouchMove, nil]]; + XCTAssertFalse(link.isPaused); + + [viewController + triggerTouchRateCorrectionIfNeeded:[[NSSet alloc] initWithObjects:fakeTouchCancelled, nil]]; + XCTAssertTrue(link.isPaused); + + [viewController + triggerTouchRateCorrectionIfNeeded:[[NSSet alloc] + initWithObjects:fakeTouchBegan, fakeTouchEnd, nil]]; + XCTAssertFalse(link.isPaused); + + [viewController + triggerTouchRateCorrectionIfNeeded:[[NSSet alloc] initWithObjects:fakeTouchEnd, + fakeTouchCancelled, nil]]; + XCTAssertTrue(link.isPaused); + + [viewController + triggerTouchRateCorrectionIfNeeded:[[NSSet alloc] + initWithObjects:fakeTouchMove, fakeTouchEnd, nil]]; + XCTAssertFalse(link.isPaused); +} + @end diff --git a/shell/platform/darwin/ios/framework/Source/VsyncWaiterIosTest.mm b/shell/platform/darwin/ios/framework/Source/VsyncWaiterIosTest.mm index 4ac0529808ed7..15d3f6df652ba 100644 --- a/shell/platform/darwin/ios/framework/Source/VsyncWaiterIosTest.mm +++ b/shell/platform/darwin/ios/framework/Source/VsyncWaiterIosTest.mm @@ -116,4 +116,23 @@ - (void)testDoNotSetVariableRefreshRatesIfCADisableMinimumFrameDurationOnPhoneIs [vsyncClient release]; } +- (void)testAwaitAndPauseWillWorkCorrectly { + auto thread_task_runner = CreateNewThread("VsyncWaiterIosTest"); + VSyncClient* vsyncClient = [[[VSyncClient alloc] + initWithTaskRunner:thread_task_runner + callback:[](std::unique_ptr recorder) {}] + autorelease]; + + CADisplayLink* link = [vsyncClient getDisplayLink]; + XCTAssertTrue(link.isPaused); + + [vsyncClient await]; + XCTAssertFalse(link.isPaused); + + [vsyncClient pause]; + XCTAssertTrue(link.isPaused); + + [vsyncClient release]; +} + @end diff --git a/shell/platform/darwin/ios/framework/Source/vsync_waiter_ios.h b/shell/platform/darwin/ios/framework/Source/vsync_waiter_ios.h index 9185fb00978d9..8178f845b8001 100644 --- a/shell/platform/darwin/ios/framework/Source/vsync_waiter_ios.h +++ b/shell/platform/darwin/ios/framework/Source/vsync_waiter_ios.h @@ -45,6 +45,8 @@ - (void)await; +- (void)pause; + - (void)invalidate; - (double)getRefreshRate; diff --git a/shell/platform/darwin/ios/framework/Source/vsync_waiter_ios.mm b/shell/platform/darwin/ios/framework/Source/vsync_waiter_ios.mm index 439ba868ff8b5..63954c9c59b09 100644 --- a/shell/platform/darwin/ios/framework/Source/vsync_waiter_ios.mm +++ b/shell/platform/darwin/ios/framework/Source/vsync_waiter_ios.mm @@ -97,6 +97,10 @@ - (void)await { display_link_.get().paused = NO; } +- (void)pause { + display_link_.get().paused = YES; +} + - (void)onDisplayLink:(CADisplayLink*)link { TRACE_EVENT0("flutter", "VSYNC");