Skip to content
This repository was archived by the owner on Feb 25, 2025. It is now read-only.

Commit a5ff362

Browse files
authored
[iOS][Keyboard] Wait vsync on UI thread and update viewport inset to avoid jitter. (#42312)
Video for after and before this patch: [videos.zip](https://github.com/flutter/engine/files/11231756/videos.zip) - flutter/flutter#120555 The technical problem is: if setViewportMetrics called **before** framework's vsync process callback (rebuild & layout & paint), will cause jitter keyboard animation.So this PR is to keep the time when updateViewportMetrics call to framework side legal. ### Before ![2](https://github.com/flutter/engine/assets/49340347/71198a08-b139-4d6d-87e1-37e536f5d34b) ### After ![1](https://github.com/flutter/engine/assets/49340347/5c9df757-7e39-4855-8c3a-009f5d6438b2)
1 parent 66a5761 commit a5ff362

File tree

6 files changed

+149
-67
lines changed

6 files changed

+149
-67
lines changed

shell/platform/darwin/ios/framework/Source/FlutterEngine.mm

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -333,7 +333,12 @@ - (void)dispatchPointerDataPacket:(std::unique_ptr<flutter::PointerDataPacket>)p
333333
return _shell->GetTaskRunners().GetPlatformTaskRunner();
334334
}
335335

336-
- (fml::RefPtr<fml::TaskRunner>)RasterTaskRunner {
336+
- (fml::RefPtr<fml::TaskRunner>)uiTaskRunner {
337+
FML_DCHECK(_shell);
338+
return _shell->GetTaskRunners().GetUITaskRunner();
339+
}
340+
341+
- (fml::RefPtr<fml::TaskRunner>)rasterTaskRunner {
337342
FML_DCHECK(_shell);
338343
return _shell->GetTaskRunners().GetRasterTaskRunner();
339344
}

shell/platform/darwin/ios/framework/Source/FlutterEngine_Internal.h

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,8 @@ extern NSString* const kFlutterEngineWillDealloc;
3939
- (void)dispatchPointerDataPacket:(std::unique_ptr<flutter::PointerDataPacket>)packet;
4040

4141
- (fml::RefPtr<fml::TaskRunner>)platformTaskRunner;
42-
- (fml::RefPtr<fml::TaskRunner>)RasterTaskRunner;
42+
- (fml::RefPtr<fml::TaskRunner>)uiTaskRunner;
43+
- (fml::RefPtr<fml::TaskRunner>)rasterTaskRunner;
4344

4445
- (fml::WeakPtr<flutter::PlatformView>)platformView;
4546

shell/platform/darwin/ios/framework/Source/FlutterViewController.mm

Lines changed: 73 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -69,11 +69,11 @@ @interface FlutterViewController () <FlutterBinaryMessenger, UIScrollViewDelegat
6969
/**
7070
* Keyboard animation properties
7171
*/
72-
@property(nonatomic, assign) double targetViewInsetBottom;
72+
@property(nonatomic, assign) CGFloat targetViewInsetBottom;
73+
@property(nonatomic, assign) CGFloat originalViewInsetBottom;
7374
@property(nonatomic, retain) VSyncClient* keyboardAnimationVSyncClient;
7475
@property(nonatomic, assign) BOOL keyboardAnimationIsShowing;
7576
@property(nonatomic, assign) fml::TimePoint keyboardAnimationStartTime;
76-
@property(nonatomic, assign) CGFloat originalViewInsetBottom;
7777
@property(nonatomic, assign) BOOL isKeyboardInOrTransitioningFromBackground;
7878

7979
/// VSyncClient for touch events delivery frame rate correction.
@@ -574,8 +574,8 @@ - (void)installFirstFrameCallback {
574574
// Start on the platform thread.
575575
weakPlatformView->SetNextFrameCallback([weakSelf = [self getWeakPtr],
576576
platformTaskRunner = [_engine.get() platformTaskRunner],
577-
RasterTaskRunner = [_engine.get() RasterTaskRunner]]() {
578-
FML_DCHECK(RasterTaskRunner->RunsTasksOnCurrentThread());
577+
rasterTaskRunner = [_engine.get() rasterTaskRunner]]() {
578+
FML_DCHECK(rasterTaskRunner->RunsTasksOnCurrentThread());
579579
// Get callback on raster thread and jump back to platform thread.
580580
platformTaskRunner->PostTask([weakSelf]() {
581581
if (weakSelf) {
@@ -1596,7 +1596,55 @@ - (void)startKeyBoardAnimation:(NSTimeInterval)duration {
15961596

15971597
// Invalidate old vsync client if old animation is not completed.
15981598
[self invalidateKeyboardAnimationVSyncClient];
1599-
[self setupKeyboardAnimationVsyncClient];
1599+
1600+
fml::WeakPtr<FlutterViewController> weakSelf = [self getWeakPtr];
1601+
FlutterKeyboardAnimationCallback keyboardAnimationCallback = ^(
1602+
fml::TimePoint keyboardAnimationTargetTime) {
1603+
if (!weakSelf) {
1604+
return;
1605+
}
1606+
fml::scoped_nsobject<FlutterViewController> flutterViewController(
1607+
[(FlutterViewController*)weakSelf.get() retain]);
1608+
if (!flutterViewController) {
1609+
return;
1610+
}
1611+
1612+
// If the view controller's view is not loaded, bail out.
1613+
if (!flutterViewController.get().isViewLoaded) {
1614+
return;
1615+
}
1616+
// If the view for tracking keyboard animation is nil, means it is not
1617+
// created, bail out.
1618+
if ([flutterViewController keyboardAnimationView] == nil) {
1619+
return;
1620+
}
1621+
// If keyboardAnimationVSyncClient is nil, means the animation ends.
1622+
// And should bail out.
1623+
if (flutterViewController.get().keyboardAnimationVSyncClient == nil) {
1624+
return;
1625+
}
1626+
1627+
if ([flutterViewController keyboardAnimationView].superview == nil) {
1628+
// Ensure the keyboardAnimationView is in view hierarchy when animation running.
1629+
[flutterViewController.get().view addSubview:[flutterViewController keyboardAnimationView]];
1630+
}
1631+
1632+
if ([flutterViewController keyboardSpringAnimation] == nil) {
1633+
if (flutterViewController.get().keyboardAnimationView.layer.presentationLayer) {
1634+
flutterViewController.get()->_viewportMetrics.physical_view_inset_bottom =
1635+
flutterViewController.get()
1636+
.keyboardAnimationView.layer.presentationLayer.frame.origin.y;
1637+
[flutterViewController updateViewportMetricsIfNeeded];
1638+
}
1639+
} else {
1640+
fml::TimeDelta timeElapsed =
1641+
keyboardAnimationTargetTime - flutterViewController.get().keyboardAnimationStartTime;
1642+
flutterViewController.get()->_viewportMetrics.physical_view_inset_bottom =
1643+
[[flutterViewController keyboardSpringAnimation] curveFunction:timeElapsed.ToSecondsF()];
1644+
[flutterViewController updateViewportMetricsIfNeeded];
1645+
}
1646+
};
1647+
[self setupKeyboardAnimationVsyncClient:keyboardAnimationCallback];
16001648
VSyncClient* currentVsyncClient = _keyboardAnimationVSyncClient;
16011649

16021650
[UIView animateWithDuration:duration
@@ -1639,45 +1687,28 @@ - (void)setupKeyboardSpringAnimationIfNeeded:(CAAnimation*)keyboardAnimation {
16391687
toValue:self.targetViewInsetBottom]);
16401688
}
16411689

1642-
- (void)setupKeyboardAnimationVsyncClient {
1643-
auto callback = [weakSelf =
1644-
[self getWeakPtr]](std::unique_ptr<flutter::FrameTimingsRecorder> recorder) {
1645-
if (!weakSelf) {
1646-
return;
1647-
}
1648-
fml::scoped_nsobject<FlutterViewController> flutterViewController(
1649-
[(FlutterViewController*)weakSelf.get() retain]);
1650-
if (!flutterViewController) {
1651-
return;
1652-
}
1653-
1654-
if ([flutterViewController keyboardAnimationView].superview == nil) {
1655-
// Ensure the keyboardAnimationView is in view hierarchy when animation running.
1656-
[flutterViewController.get().view addSubview:[flutterViewController keyboardAnimationView]];
1657-
}
1658-
1659-
if ([flutterViewController keyboardSpringAnimation] == nil) {
1660-
if (flutterViewController.get().keyboardAnimationView.layer.presentationLayer) {
1661-
flutterViewController.get()->_viewportMetrics.physical_view_inset_bottom =
1662-
flutterViewController.get()
1663-
.keyboardAnimationView.layer.presentationLayer.frame.origin.y;
1664-
[flutterViewController updateViewportMetricsIfNeeded];
1665-
}
1666-
} else {
1667-
fml::TimeDelta timeElapsed = recorder.get()->GetVsyncTargetTime() -
1668-
flutterViewController.get().keyboardAnimationStartTime;
1669-
1670-
flutterViewController.get()->_viewportMetrics.physical_view_inset_bottom =
1671-
[[flutterViewController keyboardSpringAnimation] curveFunction:timeElapsed.ToSecondsF()];
1672-
[flutterViewController updateViewportMetricsIfNeeded];
1673-
}
1674-
};
1675-
flutter::Shell& shell = [_engine.get() shell];
1690+
- (void)setupKeyboardAnimationVsyncClient:
1691+
(FlutterKeyboardAnimationCallback)keyboardAnimationCallback {
1692+
if (!keyboardAnimationCallback) {
1693+
return;
1694+
}
16761695
NSAssert(_keyboardAnimationVSyncClient == nil,
16771696
@"_keyboardAnimationVSyncClient must be nil when setup");
1678-
_keyboardAnimationVSyncClient =
1679-
[[VSyncClient alloc] initWithTaskRunner:shell.GetTaskRunners().GetPlatformTaskRunner()
1680-
callback:callback];
1697+
1698+
// Make sure the new viewport metrics get sent after the begin frame event has processed.
1699+
fml::scoped_nsprotocol<FlutterKeyboardAnimationCallback> animationCallback(
1700+
[keyboardAnimationCallback copy]);
1701+
auto uiCallback = [animationCallback,
1702+
engine = _engine](std::unique_ptr<flutter::FrameTimingsRecorder> recorder) {
1703+
fml::TimeDelta frameInterval = recorder->GetVsyncTargetTime() - recorder->GetVsyncStartTime();
1704+
fml::TimePoint keyboardAnimationTargetTime = recorder->GetVsyncTargetTime() + frameInterval;
1705+
[engine platformTaskRunner]->PostTask([animationCallback, keyboardAnimationTargetTime] {
1706+
animationCallback.get()(keyboardAnimationTargetTime);
1707+
});
1708+
};
1709+
1710+
_keyboardAnimationVSyncClient = [[VSyncClient alloc] initWithTaskRunner:[_engine uiTaskRunner]
1711+
callback:uiCallback];
16811712
_keyboardAnimationVSyncClient.allowPauseAfterVsync = NO;
16821713
[_keyboardAnimationVSyncClient await];
16831714
}

shell/platform/darwin/ios/framework/Source/FlutterViewControllerTest.mm

Lines changed: 37 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ - (FlutterTextInputPlugin*)textInputPlugin;
2626
- (void)sendKeyEvent:(const FlutterKeyEvent&)event
2727
callback:(nullable FlutterKeyEventCallback)callback
2828
userData:(nullable void*)userData;
29+
- (fml::RefPtr<fml::TaskRunner>)uiTaskRunner;
2930
@end
3031

3132
/// Sometimes we have to use a custom mock to avoid retain cycles in OCMock.
@@ -135,10 +136,11 @@ - (BOOL)shouldIgnoreKeyboardNotification:(NSNotification*)notification;
135136
- (FlutterKeyboardMode)calculateKeyboardAttachMode:(NSNotification*)notification;
136137
- (CGFloat)calculateMultitaskingAdjustment:(CGRect)screenRect keyboardFrame:(CGRect)keyboardFrame;
137138
- (void)startKeyBoardAnimation:(NSTimeInterval)duration;
138-
- (void)setupKeyboardAnimationVsyncClient;
139139
- (UIView*)keyboardAnimationView;
140140
- (SpringAnimation*)keyboardSpringAnimation;
141141
- (void)setupKeyboardSpringAnimationIfNeeded:(CAAnimation*)keyboardAnimation;
142+
- (void)setupKeyboardAnimationVsyncClient:
143+
(FlutterKeyboardAnimationCallback)keyboardAnimationCallback;
142144
- (void)ensureViewportMetricsIsCorrect;
143145
- (void)invalidateKeyboardAnimationVSyncClient;
144146
- (void)addInternalPlugins;
@@ -197,18 +199,6 @@ - (void)testViewDidLoadWillInvokeCreateTouchRateCorrectionVSyncClient {
197199
OCMVerify([viewControllerMock createTouchRateCorrectionVSyncClientIfNeeded]);
198200
}
199201

200-
- (void)testStartKeyboardAnimationWillInvokeSetupKeyboardAnimationVsyncClient {
201-
FlutterEngine* engine = [[FlutterEngine alloc] init];
202-
[engine runWithEntrypoint:nil];
203-
FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:engine
204-
nibName:nil
205-
bundle:nil];
206-
FlutterViewController* viewControllerMock = OCMPartialMock(viewController);
207-
viewControllerMock.targetViewInsetBottom = 100;
208-
[viewControllerMock startKeyBoardAnimation:0.25];
209-
OCMVerify([viewControllerMock setupKeyboardAnimationVsyncClient]);
210-
}
211-
212202
- (void)testStartKeyboardAnimationWillInvokeSetupKeyboardSpringAnimationIfNeeded {
213203
FlutterEngine* engine = [[FlutterEngine alloc] init];
214204
[engine runWithEntrypoint:nil];
@@ -451,6 +441,34 @@ - (void)testShouldIgnoreKeyboardNotification {
451441
}
452442
}
453443

444+
- (void)testKeyboardAnimationWillWaitUIThreadVsync {
445+
// We need to make sure the new viewport metrics get sent after the
446+
// begin frame event has processed. And this test is to expect that the callback
447+
// will sync with UI thread. So just simulate a lot of works on UI thread and
448+
// test the keyboard animation callback will execute until UI task completed.
449+
// Related issue: https://github.com/flutter/flutter/issues/120555.
450+
451+
FlutterEngine* engine = [[FlutterEngine alloc] init];
452+
[engine runWithEntrypoint:nil];
453+
FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:engine
454+
nibName:nil
455+
bundle:nil];
456+
// Post a task to UI thread to block the thread.
457+
const int delayTime = 1;
458+
[engine uiTaskRunner]->PostTask([] { sleep(delayTime); });
459+
XCTestExpectation* expectation = [self expectationWithDescription:@"keyboard animation callback"];
460+
461+
__block CFTimeInterval fulfillTime;
462+
FlutterKeyboardAnimationCallback callback = ^(fml::TimePoint targetTime) {
463+
fulfillTime = CACurrentMediaTime();
464+
[expectation fulfill];
465+
};
466+
CFTimeInterval startTime = CACurrentMediaTime();
467+
[viewController setupKeyboardAnimationVsyncClient:callback];
468+
[self waitForExpectationsWithTimeout:5.0 handler:nil];
469+
XCTAssertTrue(fulfillTime - startTime > delayTime);
470+
}
471+
454472
- (void)testCalculateKeyboardAttachMode {
455473
FlutterEngine* mockEngine = OCMPartialMock([[FlutterEngine alloc] init]);
456474
[mockEngine createShell:@"" libraryURI:@"" initialRoute:nil];
@@ -629,9 +647,9 @@ - (void)testCalculateKeyboardInset {
629647
}
630648

631649
- (void)testHandleKeyboardNotification {
632-
FlutterEngine* mockEngine = OCMPartialMock([[FlutterEngine alloc] init]);
633-
[mockEngine createShell:@"" libraryURI:@"" initialRoute:nil];
634-
FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:mockEngine
650+
FlutterEngine* engine = [[FlutterEngine alloc] init];
651+
[engine runWithEntrypoint:nil];
652+
FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:engine
635653
nibName:nil
636654
bundle:nil];
637655
// keyboard is empty
@@ -652,11 +670,9 @@ - (void)testHandleKeyboardNotification {
652670
[self setupMockMainScreenAndView:viewControllerMock viewFrame:viewFrame convertedFrame:viewFrame];
653671
viewControllerMock.targetViewInsetBottom = 0;
654672
XCTestExpectation* expectation = [self expectationWithDescription:@"update viewport"];
655-
OCMStub([mockEngine updateViewportMetrics:flutter::ViewportMetrics()])
656-
.ignoringNonObjectArgs()
657-
.andDo(^(NSInvocation* invocation) {
658-
[expectation fulfill];
659-
});
673+
OCMStub([viewControllerMock updateViewportMetricsIfNeeded]).andDo(^(NSInvocation* invocation) {
674+
[expectation fulfill];
675+
});
660676

661677
[viewControllerMock handleKeyboardNotification:notification];
662678
XCTAssertTrue(viewControllerMock.targetViewInsetBottom == 320 * UIScreen.mainScreen.scale);

shell/platform/darwin/ios/framework/Source/FlutterViewControllerTest_mrc.mm

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
#import "flutter/shell/platform/darwin/common/framework/Headers/FlutterMacros.h"
99
#import "flutter/shell/platform/darwin/ios/framework/Headers/FlutterViewController.h"
10+
#import "flutter/shell/platform/darwin/ios/framework/Source/FlutterViewController_Internal.h"
1011
#import "flutter/shell/platform/darwin/ios/framework/Source/vsync_waiter_ios.h"
1112

1213
FLUTTER_ASSERT_NOT_ARC
@@ -31,7 +32,9 @@ @interface FlutterViewController (Testing)
3132
@property(nonatomic, retain) VSyncClient* touchRateCorrectionVSyncClient;
3233

3334
- (void)createTouchRateCorrectionVSyncClientIfNeeded;
34-
- (void)setupKeyboardAnimationVsyncClient;
35+
- (void)setupKeyboardAnimationVsyncClient:
36+
(FlutterKeyboardAnimationCallback)keyboardAnimationCallback;
37+
- (void)startKeyBoardAnimation:(NSTimeInterval)duration;
3538
- (void)triggerTouchRateCorrectionIfNeeded:(NSSet*)touches;
3639

3740
@end
@@ -53,7 +56,9 @@ - (void)testSetupKeyboardAnimationVsyncClientWillCreateNewVsyncClientForFlutterV
5356
FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:engine
5457
nibName:nil
5558
bundle:nil];
56-
[viewController setupKeyboardAnimationVsyncClient];
59+
FlutterKeyboardAnimationCallback callback = ^(fml::TimePoint targetTime) {
60+
};
61+
[viewController setupKeyboardAnimationVsyncClient:callback];
5762
XCTAssertNotNil(viewController.keyboardAnimationVSyncClient);
5863
CADisplayLink* link = [viewController.keyboardAnimationVSyncClient getDisplayLink];
5964
XCTAssertNotNil(link);
@@ -173,4 +178,26 @@ - (void)testTriggerTouchRateCorrectionVSyncClientCorrectly {
173178
XCTAssertFalse(link.isPaused);
174179
}
175180

181+
- (void)testFlutterViewControllerStartKeyboardAnimationWillCreateVsyncClientCorrectly {
182+
FlutterEngine* engine = [[FlutterEngine alloc] init];
183+
[engine runWithEntrypoint:nil];
184+
FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:engine
185+
nibName:nil
186+
bundle:nil];
187+
viewController.targetViewInsetBottom = 100;
188+
[viewController startKeyBoardAnimation:0.25];
189+
XCTAssertNotNil(viewController.keyboardAnimationVSyncClient);
190+
}
191+
192+
- (void)
193+
testSetupKeyboardAnimationVsyncClientWillNotCreateNewVsyncClientWhenKeyboardAnimationCallbackIsNil {
194+
FlutterEngine* engine = [[FlutterEngine alloc] init];
195+
[engine runWithEntrypoint:nil];
196+
FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:engine
197+
nibName:nil
198+
bundle:nil];
199+
[viewController setupKeyboardAnimationVsyncClient:nil];
200+
XCTAssertNil(viewController.keyboardAnimationVSyncClient);
201+
}
202+
176203
@end

shell/platform/darwin/ios/framework/Source/FlutterViewController_Internal.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@ typedef NS_ENUM(NSInteger, FlutterKeyboardMode) {
3333
FlutterKeyboardModeFloating = 2,
3434
};
3535

36+
typedef void (^FlutterKeyboardAnimationCallback)(fml::TimePoint);
37+
3638
@interface FlutterViewController () <FlutterViewResponder>
3739

3840
@property(class, nonatomic, readonly) BOOL accessibilityIsOnOffSwitchLabelsEnabled;

0 commit comments

Comments
 (0)