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

Commit cf67f83

Browse files
authored
[iOS][Keyboard] Reland wait vsync on UI thread and update viewport inset to avoid jitter (#43463)
*List which issues are fixed by this PR. You must list at least one issue.* - flutter/flutter#120555 - flutter/flutter#130028 ### New test for crash case: `testKeyboardAnimationWillNotCrashWhenEngineDestroyed` ### The diff with original PR: Use `dispatch_async(dispatch_get_main_queue()` to switch to platform thread.
1 parent 7b826a7 commit cf67f83

6 files changed

Lines changed: 158 additions & 67 deletions

File tree

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

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

338-
- (fml::RefPtr<fml::TaskRunner>)RasterTaskRunner {
338+
- (fml::RefPtr<fml::TaskRunner>)uiTaskRunner {
339+
FML_DCHECK(_shell);
340+
return _shell->GetTaskRunners().GetUITaskRunner();
341+
}
342+
343+
- (fml::RefPtr<fml::TaskRunner>)rasterTaskRunner {
339344
FML_DCHECK(_shell);
340345
return _shell->GetTaskRunners().GetRasterTaskRunner();
341346
}

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: 72 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.
@@ -575,8 +575,8 @@ - (void)installFirstFrameCallback {
575575
// Start on the platform thread.
576576
weakPlatformView->SetNextFrameCallback([weakSelf = [self getWeakPtr],
577577
platformTaskRunner = [_engine.get() platformTaskRunner],
578-
RasterTaskRunner = [_engine.get() RasterTaskRunner]]() {
579-
FML_DCHECK(RasterTaskRunner->RunsTasksOnCurrentThread());
578+
rasterTaskRunner = [_engine.get() rasterTaskRunner]]() {
579+
FML_DCHECK(rasterTaskRunner->RunsTasksOnCurrentThread());
580580
// Get callback on raster thread and jump back to platform thread.
581581
platformTaskRunner->PostTask([weakSelf]() {
582582
if (weakSelf) {
@@ -1605,7 +1605,55 @@ - (void)startKeyBoardAnimation:(NSTimeInterval)duration {
16051605

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

16111659
[UIView animateWithDuration:duration
@@ -1648,45 +1696,27 @@ - (void)setupKeyboardSpringAnimationIfNeeded:(CAAnimation*)keyboardAnimation {
16481696
toValue:self.targetViewInsetBottom]);
16491697
}
16501698

1651-
- (void)setupKeyboardAnimationVsyncClient {
1652-
auto callback = [weakSelf =
1653-
[self getWeakPtr]](std::unique_ptr<flutter::FrameTimingsRecorder> recorder) {
1654-
if (!weakSelf) {
1655-
return;
1656-
}
1657-
fml::scoped_nsobject<FlutterViewController> flutterViewController(
1658-
[(FlutterViewController*)weakSelf.get() retain]);
1659-
if (!flutterViewController) {
1660-
return;
1661-
}
1662-
1663-
if ([flutterViewController keyboardAnimationView].superview == nil) {
1664-
// Ensure the keyboardAnimationView is in view hierarchy when animation running.
1665-
[flutterViewController.get().view addSubview:[flutterViewController keyboardAnimationView]];
1666-
}
1667-
1668-
if ([flutterViewController keyboardSpringAnimation] == nil) {
1669-
if (flutterViewController.get().keyboardAnimationView.layer.presentationLayer) {
1670-
flutterViewController.get()->_viewportMetrics.physical_view_inset_bottom =
1671-
flutterViewController.get()
1672-
.keyboardAnimationView.layer.presentationLayer.frame.origin.y;
1673-
[flutterViewController updateViewportMetricsIfNeeded];
1674-
}
1675-
} else {
1676-
fml::TimeDelta timeElapsed = recorder.get()->GetVsyncTargetTime() -
1677-
flutterViewController.get().keyboardAnimationStartTime;
1678-
1679-
flutterViewController.get()->_viewportMetrics.physical_view_inset_bottom =
1680-
[[flutterViewController keyboardSpringAnimation] curveFunction:timeElapsed.ToSecondsF()];
1681-
[flutterViewController updateViewportMetricsIfNeeded];
1682-
}
1683-
};
1684-
flutter::Shell& shell = [_engine.get() shell];
1699+
- (void)setupKeyboardAnimationVsyncClient:
1700+
(FlutterKeyboardAnimationCallback)keyboardAnimationCallback {
1701+
if (!keyboardAnimationCallback) {
1702+
return;
1703+
}
16851704
NSAssert(_keyboardAnimationVSyncClient == nil,
16861705
@"_keyboardAnimationVSyncClient must be nil when setup");
1687-
_keyboardAnimationVSyncClient =
1688-
[[VSyncClient alloc] initWithTaskRunner:shell.GetTaskRunners().GetPlatformTaskRunner()
1689-
callback:callback];
1706+
1707+
// Make sure the new viewport metrics get sent after the begin frame event has processed.
1708+
fml::scoped_nsprotocol<FlutterKeyboardAnimationCallback> animationCallback(
1709+
[keyboardAnimationCallback copy]);
1710+
auto uiCallback = [animationCallback](std::unique_ptr<flutter::FrameTimingsRecorder> recorder) {
1711+
fml::TimeDelta frameInterval = recorder->GetVsyncTargetTime() - recorder->GetVsyncStartTime();
1712+
fml::TimePoint keyboardAnimationTargetTime = recorder->GetVsyncTargetTime() + frameInterval;
1713+
dispatch_async(dispatch_get_main_queue(), ^(void) {
1714+
animationCallback.get()(keyboardAnimationTargetTime);
1715+
});
1716+
};
1717+
1718+
_keyboardAnimationVSyncClient = [[VSyncClient alloc] initWithTaskRunner:[_engine uiTaskRunner]
1719+
callback:uiCallback];
16901720
_keyboardAnimationVSyncClient.allowPauseAfterVsync = NO;
16911721
[_keyboardAnimationVSyncClient await];
16921722
}

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

Lines changed: 47 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];
@@ -450,6 +440,44 @@ - (void)testShouldIgnoreKeyboardNotification {
450440
XCTAssertTrue(shouldIgnore == YES);
451441
}
452442
}
443+
- (void)testKeyboardAnimationWillNotCrashWhenEngineDestroyed {
444+
FlutterEngine* engine = [[FlutterEngine alloc] init];
445+
[engine runWithEntrypoint:nil];
446+
FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:engine
447+
nibName:nil
448+
bundle:nil];
449+
[viewController setupKeyboardAnimationVsyncClient:^(fml::TimePoint){
450+
}];
451+
[engine destroyContext];
452+
}
453+
454+
- (void)testKeyboardAnimationWillWaitUIThreadVsync {
455+
// We need to make sure the new viewport metrics get sent after the
456+
// begin frame event has processed. And this test is to expect that the callback
457+
// will sync with UI thread. So just simulate a lot of works on UI thread and
458+
// test the keyboard animation callback will execute until UI task completed.
459+
// Related issue: https://github.com/flutter/flutter/issues/120555.
460+
461+
FlutterEngine* engine = [[FlutterEngine alloc] init];
462+
[engine runWithEntrypoint:nil];
463+
FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:engine
464+
nibName:nil
465+
bundle:nil];
466+
// Post a task to UI thread to block the thread.
467+
const int delayTime = 1;
468+
[engine uiTaskRunner]->PostTask([] { sleep(delayTime); });
469+
XCTestExpectation* expectation = [self expectationWithDescription:@"keyboard animation callback"];
470+
471+
__block CFTimeInterval fulfillTime;
472+
FlutterKeyboardAnimationCallback callback = ^(fml::TimePoint targetTime) {
473+
fulfillTime = CACurrentMediaTime();
474+
[expectation fulfill];
475+
};
476+
CFTimeInterval startTime = CACurrentMediaTime();
477+
[viewController setupKeyboardAnimationVsyncClient:callback];
478+
[self waitForExpectationsWithTimeout:5.0 handler:nil];
479+
XCTAssertTrue(fulfillTime - startTime > delayTime);
480+
}
453481

454482
- (void)testCalculateKeyboardAttachMode {
455483
FlutterEngine* mockEngine = OCMPartialMock([[FlutterEngine alloc] init]);
@@ -629,9 +657,9 @@ - (void)testCalculateKeyboardInset {
629657
}
630658

631659
- (void)testHandleKeyboardNotification {
632-
FlutterEngine* mockEngine = OCMPartialMock([[FlutterEngine alloc] init]);
633-
[mockEngine createShell:@"" libraryURI:@"" initialRoute:nil];
634-
FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:mockEngine
660+
FlutterEngine* engine = [[FlutterEngine alloc] init];
661+
[engine runWithEntrypoint:nil];
662+
FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:engine
635663
nibName:nil
636664
bundle:nil];
637665
// keyboard is empty
@@ -652,11 +680,9 @@ - (void)testHandleKeyboardNotification {
652680
[self setupMockMainScreenAndView:viewControllerMock viewFrame:viewFrame convertedFrame:viewFrame];
653681
viewControllerMock.targetViewInsetBottom = 0;
654682
XCTestExpectation* expectation = [self expectationWithDescription:@"update viewport"];
655-
OCMStub([mockEngine updateViewportMetrics:flutter::ViewportMetrics()])
656-
.ignoringNonObjectArgs()
657-
.andDo(^(NSInvocation* invocation) {
658-
[expectation fulfill];
659-
});
683+
OCMStub([viewControllerMock updateViewportMetricsIfNeeded]).andDo(^(NSInvocation* invocation) {
684+
[expectation fulfill];
685+
});
660686

661687
[viewControllerMock handleKeyboardNotification:notification];
662688
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)