Flutter iOS Interactive Keyboard: Fixing Animation Issue (#44514)
This PR addresses an issue with the animation of the keyboard. In iOS 16.0 a delay was included in UIView becomeFirstResponder where the areAnimationsEnabled boolean is no longer immediately read. In response to this issue a delay is added that allows for the animation to be properly disabled.
Design Document:
https://docs.google.com/document/d/1-T7_0mSkXzPaWxveeypIzzzAdyo-EEuP5V84161foL4/edit?pli=1
Issues Address:
https://github.com/flutter/flutter/issues/57609
[C++, Objective-C, Java style guides]: https://github.com/flutter/engine/blob/main/CONTRIBUTING.md#style
diff --git a/shell/platform/darwin/ios/framework/Source/FlutterTextInputPlugin.mm b/shell/platform/darwin/ios/framework/Source/FlutterTextInputPlugin.mm
index c263b37..a4fb7a0 100644
--- a/shell/platform/darwin/ios/framework/Source/FlutterTextInputPlugin.mm
+++ b/shell/platform/darwin/ios/framework/Source/FlutterTextInputPlugin.mm
@@ -21,6 +21,10 @@
// it is activated.
static constexpr double kUITextInputAccessibilityEnablingDelaySeconds = 0.5;
+// A delay before reenabling the UIView areAnimationsEnabled to YES
+// in order for becomeFirstResponder to receive the proper value
+static const NSTimeInterval kKeyboardAnimationDelaySeconds = 0.1;
+
// The "canonical" invalid CGRect, similar to CGRectNull, used to
// indicate a CGRect involved in firstRectForRange calculation is
// invalid. The specific value is chosen so that if firstRectForRange
@@ -2369,8 +2373,13 @@
- (void)showKeyboardAndRemoveScreenshot {
[UIView setAnimationsEnabled:NO];
[_cachedFirstResponder becomeFirstResponder];
- [UIView setAnimationsEnabled:YES];
- [self dismissKeyboardScreenshot];
+ // UIKit does not immediately access the areAnimationsEnabled Boolean so a delay is needed before
+ // returned
+ dispatch_after(dispatch_time(DISPATCH_TIME_NOW, kKeyboardAnimationDelaySeconds * NSEC_PER_SEC),
+ dispatch_get_main_queue(), ^{
+ [UIView setAnimationsEnabled:YES];
+ [self dismissKeyboardScreenshot];
+ });
}
- (void)handlePointerMove:(CGFloat)pointerY {
diff --git a/shell/platform/darwin/ios/framework/Source/FlutterTextInputPluginTest.mm b/shell/platform/darwin/ios/framework/Source/FlutterTextInputPluginTest.mm
index d341013..edc95d8 100644
--- a/shell/platform/darwin/ios/framework/Source/FlutterTextInputPluginTest.mm
+++ b/shell/platform/darwin/ios/framework/Source/FlutterTextInputPluginTest.mm
@@ -75,6 +75,8 @@
- (UIView*)hostView;
- (void)addToInputParentViewIfNeeded:(FlutterTextInputView*)inputView;
- (void)startLiveTextInput;
+- (void)showKeyboardAndRemoveScreenshot;
+
@end
@interface FlutterTextInputPluginTest : XCTestCase
@@ -2890,5 +2892,25 @@
}];
textInputPlugin.cachedFirstResponder = nil;
}
+- (void)testInteractiveKeyboardShowKeyboardAndRemoveScreenshotAnimationIsNotImmediatelyEnable {
+ [UIView setAnimationsEnabled:YES];
+ [textInputPlugin showKeyboardAndRemoveScreenshot];
+ XCTAssertFalse(
+ UIView.areAnimationsEnabled,
+ @"The animation should still be disabled following showKeyboardAndRemoveScreenshot");
+}
+
+- (void)testInteractiveKeyboardShowKeyboardAndRemoveScreenshotAnimationIsReenabledAfterDelay {
+ [UIView setAnimationsEnabled:YES];
+ [textInputPlugin showKeyboardAndRemoveScreenshot];
+
+ NSPredicate* predicate = [NSPredicate predicateWithBlock:^BOOL(id item, NSDictionary* bindings) {
+ // This will be enabled after a delay
+ return UIView.areAnimationsEnabled;
+ }];
+ XCTNSPredicateExpectation* expectation =
+ [[XCTNSPredicateExpectation alloc] initWithPredicate:predicate object:nil];
+ [self waitForExpectations:@[ expectation ] timeout:10.0];
+}
@end