diff --git a/shell/platform/darwin/ios/framework/Source/FlutterViewController.mm b/shell/platform/darwin/ios/framework/Source/FlutterViewController.mm index 1831b4d2f546b..4709511a8b64f 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterViewController.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterViewController.mm @@ -34,6 +34,8 @@ static constexpr int kMicrosecondsPerSecond = 1000 * 1000; static constexpr CGFloat kScrollViewContentSize = 2.0; +static constexpr fml::TimeDelta kKeyboardAnimationUpdateViewportMetricsDelay = + fml::TimeDelta::FromMilliseconds(1); static NSString* const kFlutterRestorationStateAppData = @"FlutterRestorationStateAppData"; @@ -1661,15 +1663,19 @@ - (void)setupKeyboardAnimationVsyncClient { flutterViewController.get()->_viewportMetrics.physical_view_inset_bottom = flutterViewController.get() .keyboardAnimationView.layer.presentationLayer.frame.origin.y; - [flutterViewController updateViewportMetricsIfNeeded]; + [flutterViewController keyboardAnimationDelayUpdateViewportMetrics]; } } else { - fml::TimeDelta timeElapsed = recorder.get()->GetVsyncTargetTime() - + // Because updateViewportMetrics will work in next frame and the frame will + // present in next next frame, so here should add a time of one frame interval + // to get correct animation target time. + fml::TimeDelta frameInterval = recorder->GetVsyncTargetTime() - recorder->GetVsyncStartTime(); + fml::TimeDelta timeElapsed = recorder.get()->GetVsyncTargetTime() + frameInterval - flutterViewController.get().keyboardAnimationStartTime; flutterViewController.get()->_viewportMetrics.physical_view_inset_bottom = [[flutterViewController keyboardSpringAnimation] curveFunction:timeElapsed.ToSecondsF()]; - [flutterViewController updateViewportMetricsIfNeeded]; + [flutterViewController keyboardAnimationDelayUpdateViewportMetrics]; } }; flutter::Shell& shell = [_engine.get() shell]; @@ -1682,6 +1688,29 @@ - (void)setupKeyboardAnimationVsyncClient { [_keyboardAnimationVSyncClient await]; } +- (void)keyboardAnimationDelayUpdateViewportMetrics { + // Because the keyboard animation is driven by vsync callback in platform + // thread, and it is extremely closed to vsync callback in UI thread, so it maybe + // run before the vsync process callback in UI thread. + // And if this happens, will cause jitter behavior in rendering, so adding a + // tiny delay to keep the updateViewportMetrics signal run after the vsync + // process callback in UI thread to keep things smooth. + flutter::Shell& shell = [_engine.get() shell]; + shell.GetTaskRunners().GetPlatformTaskRunner()->PostDelayedTask( + [weakSelf = [self getWeakPtr]] { + if (!weakSelf) { + return; + } + fml::scoped_nsobject flutterViewController( + [(FlutterViewController*)weakSelf.get() retain]); + if (!flutterViewController) { + return; + } + [flutterViewController updateViewportMetricsIfNeeded]; + }, + kKeyboardAnimationUpdateViewportMetricsDelay); +} + - (void)invalidateKeyboardAnimationVSyncClient { [_keyboardAnimationVSyncClient invalidate]; [_keyboardAnimationVSyncClient release]; diff --git a/shell/platform/darwin/ios/framework/Source/FlutterViewControllerTest.mm b/shell/platform/darwin/ios/framework/Source/FlutterViewControllerTest.mm index b02109c236672..19444182db269 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterViewControllerTest.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterViewControllerTest.mm @@ -139,6 +139,7 @@ - (void)setupKeyboardAnimationVsyncClient; - (UIView*)keyboardAnimationView; - (SpringAnimation*)keyboardSpringAnimation; - (void)setupKeyboardSpringAnimationIfNeeded:(CAAnimation*)keyboardAnimation; +- (void)keyboardAnimationDelayUpdateViewportMetrics; - (void)ensureViewportMetricsIsCorrect; - (void)invalidateKeyboardAnimationVSyncClient; - (void)addInternalPlugins; @@ -185,6 +186,26 @@ - (id)setupMockMainScreenAndView:(FlutterViewController*)viewControllerMock return mockView; } +- (void)testKeyboardAnimationDelayUpdateViewportMetricsWillWorkCorrectly { + FlutterEngine* mockEngine = OCMPartialMock([[FlutterEngine alloc] init]); + [mockEngine createShell:@"" libraryURI:@"" initialRoute:nil]; + FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:mockEngine + nibName:nil + bundle:nil]; + FlutterViewController* viewControllerMock = OCMPartialMock(viewController); + CGRect viewFrame = UIScreen.mainScreen.bounds; + [self setupMockMainScreenAndView:viewControllerMock viewFrame:viewFrame convertedFrame:viewFrame]; + + [viewControllerMock keyboardAnimationDelayUpdateViewportMetrics]; + + // Expect the updateViewportMetrics will invoke after some time. + XCTestExpectation* expectation = [self expectationWithDescription:@"delay update viewport"]; + OCMStub([viewControllerMock updateViewportMetricsIfNeeded]).andDo(^(NSInvocation* invocation) { + [expectation fulfill]; + }); + [self waitForExpectationsWithTimeout:5.0 handler:nil]; +} + - (void)testViewDidLoadWillInvokeCreateTouchRateCorrectionVSyncClient { FlutterEngine* engine = [[FlutterEngine alloc] init]; [engine runWithEntrypoint:nil];