diff --git a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterViewController.mm b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterViewController.mm index 2556a5b0635..22cd3061a38 100644 --- a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterViewController.mm +++ b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterViewController.mm @@ -1523,6 +1523,17 @@ static flutter::PointerData::DeviceKind DeviceKindFromTouchType(UITouch* touch) CGRect keyboardFrame = [info[UIKeyboardFrameEndUserInfoKey] CGRectValue]; FlutterKeyboardMode keyboardMode = [self calculateKeyboardAttachMode:notification]; CGFloat calculatedInset = [self calculateKeyboardInset:keyboardFrame keyboardMode:keyboardMode]; + NSTimeInterval duration = [info[UIKeyboardAnimationDurationUserInfoKey] doubleValue]; + + // If the software keyboard is displayed before displaying the PasswordManager prompt, + // UIKeyboardWillHideNotification will occur immediately after UIKeyboardWillShowNotification. + // The duration of the animation will be 0.0, and the calculated inset will be 0.0. + // In this case, it is necessary to cancel the animation and hide the keyboard immediately. + // https://github.com/flutter/flutter/pull/164884 + if (keyboardMode == FlutterKeyboardModeHidden && calculatedInset == 0.0 && duration == 0.0) { + [self hideKeyboardImmediately]; + return; + } // Avoid double triggering startKeyBoardAnimation. if (self.targetViewInsetBottom == calculatedInset) { @@ -1530,7 +1541,6 @@ static flutter::PointerData::DeviceKind DeviceKindFromTouchType(UITouch* touch) } self.targetViewInsetBottom = calculatedInset; - NSTimeInterval duration = [info[UIKeyboardAnimationDurationUserInfoKey] doubleValue]; // Flag for simultaneous compounding animation calls. // This captures animation calls made while the keyboard animation is currently animating. If the @@ -1773,6 +1783,21 @@ static flutter::PointerData::DeviceKind DeviceKindFromTouchType(UITouch* touch) }]; } +- (void)hideKeyboardImmediately { + [self invalidateKeyboardAnimationVSyncClient]; + if (self.keyboardAnimationView) { + [self.keyboardAnimationView.layer removeAllAnimations]; + [self removeKeyboardAnimationView]; + self.keyboardAnimationView = nil; + } + if (self.keyboardSpringAnimation) { + self.keyboardSpringAnimation = nil; + } + // Reset targetViewInsetBottom to 0.0. + self.targetViewInsetBottom = 0.0; + [self ensureViewportMetricsIsCorrect]; +} + - (void)setUpKeyboardSpringAnimationIfNeeded:(CAAnimation*)keyboardAnimation { // If keyboard animation is null or not a spring animation, fallback to DisplayLink tracking. if (keyboardAnimation == nil || ![keyboardAnimation isKindOfClass:[CASpringAnimation class]]) { diff --git a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterViewControllerTest.mm b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterViewControllerTest.mm index 654716535f4..c98b3e2b824 100644 --- a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterViewControllerTest.mm +++ b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterViewControllerTest.mm @@ -142,6 +142,7 @@ extern NSNotificationName const FlutterViewControllerWillDealloc; - (FlutterKeyboardMode)calculateKeyboardAttachMode:(NSNotification*)notification; - (CGFloat)calculateMultitaskingAdjustment:(CGRect)screenRect keyboardFrame:(CGRect)keyboardFrame; - (void)startKeyBoardAnimation:(NSTimeInterval)duration; +- (void)hideKeyboardImmediately; - (UIView*)keyboardAnimationView; - (SpringAnimation*)keyboardSpringAnimation; - (void)setUpKeyboardSpringAnimationIfNeeded:(CAAnimation*)keyboardAnimation; @@ -779,6 +780,57 @@ extern NSNotificationName const FlutterViewControllerWillDealloc; XCTAssertTrue(viewControllerMock.targetViewInsetBottom == 0); } +- (void)testStopKeyBoardAnimationWhenReceivedWillHideNotificationAfterWillShowNotification { + // see: https://github.com/flutter/flutter/issues/112281 + + FlutterEngine* engine = [[FlutterEngine alloc] init]; + [engine runWithEntrypoint:nil]; + FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:engine + nibName:nil + bundle:nil]; + FlutterViewController* viewControllerMock = OCMPartialMock(viewController); + UIScreen* screen = [self setUpMockScreen]; + CGRect viewFrame = screen.bounds; + [self setUpMockView:viewControllerMock + screen:screen + viewFrame:viewFrame + convertedFrame:viewFrame]; + viewControllerMock.targetViewInsetBottom = 0; + + CGFloat screenHeight = screen.bounds.size.height; + CGFloat screenWidth = screen.bounds.size.height; + CGRect keyboardFrame = CGRectMake(0, screenHeight - 320, screenWidth, 320); + BOOL isLocal = YES; + + // Receive will show notification + NSNotification* fakeShowNotification = + [NSNotification notificationWithName:UIKeyboardWillShowNotification + object:nil + userInfo:@{ + @"UIKeyboardFrameEndUserInfoKey" : @(keyboardFrame), + @"UIKeyboardAnimationDurationUserInfoKey" : @0.25, + @"UIKeyboardIsLocalUserInfoKey" : @(isLocal) + }]; + [viewControllerMock handleKeyboardNotification:fakeShowNotification]; + XCTAssertTrue(viewControllerMock.targetViewInsetBottom == 320 * screen.scale); + + // Receive will hide notification + NSNotification* fakeHideNotification = + [NSNotification notificationWithName:UIKeyboardWillHideNotification + object:nil + userInfo:@{ + @"UIKeyboardFrameEndUserInfoKey" : @(keyboardFrame), + @"UIKeyboardAnimationDurationUserInfoKey" : @(0.0), + @"UIKeyboardIsLocalUserInfoKey" : @(isLocal) + }]; + [viewControllerMock handleKeyboardNotification:fakeHideNotification]; + XCTAssertTrue(viewControllerMock.targetViewInsetBottom == 0); + + // Check if the keyboard animation is stopped. + XCTAssertNil(viewControllerMock.keyboardAnimationView); + XCTAssertNil(viewControllerMock.keyboardSpringAnimation); +} + - (void)testEnsureViewportMetricsWillInvokeAndDisplayLinkWillInvalidateInViewDidDisappear { FlutterEngine* mockEngine = OCMPartialMock([[FlutterEngine alloc] init]); [mockEngine createShell:@"" libraryURI:@"" initialRoute:nil];