diff --git a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterTextInputPlugin.mm b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterTextInputPlugin.mm index a4fb7a04d82..0d6ecbe9b63 100644 --- a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterTextInputPlugin.mm +++ b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterTextInputPlugin.mm @@ -22,9 +22,12 @@ static const char kTextAffinityUpstream[] = "TextAffinity.upstream"; static constexpr double kUITextInputAccessibilityEnablingDelaySeconds = 0.5; // A delay before reenabling the UIView areAnimationsEnabled to YES -// in order for becomeFirstResponder to receive the proper value +// in order for becomeFirstResponder to receive the proper value. static const NSTimeInterval kKeyboardAnimationDelaySeconds = 0.1; +// A time set for the screenshot to animate back to the assigned position. +static const NSTimeInterval kKeyboardAnimationTimeToCompleteion = 0.3; + // 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 @@ -2234,6 +2237,8 @@ static BOOL IsSelectionRectBoundaryCloserToPoint(CGPoint point, @property(nonatomic, strong) UIView* keyboardView; @property(nonatomic, strong) UIView* cachedFirstResponder; @property(nonatomic, assign) CGRect keyboardRect; +@property(nonatomic, assign) CGFloat previousPointerYPosition; +@property(nonatomic, assign) CGFloat pointerYVelocity; @end @implementation FlutterTextInputPlugin { @@ -2340,28 +2345,32 @@ static BOOL IsSelectionRectBoundaryCloserToPoint(CGPoint point, } - (void)handlePointerUp:(CGFloat)pointerY { - // View must be loaded at this point. - UIScreen* screen = _viewController.flutterScreenIfViewLoaded; - CGFloat screenHeight = screen.bounds.size.height; - CGFloat keyboardHeight = _keyboardRect.size.height; - BOOL shouldDismissKeyboard = (screenHeight - (keyboardHeight / 2)) < pointerY; - [UIView animateWithDuration:0.3f - animations:^{ - double keyboardDestination = - shouldDismissKeyboard ? screenHeight : screenHeight - keyboardHeight; - _keyboardViewContainer.frame = CGRectMake( - 0, keyboardDestination, _viewController.flutterScreenIfViewLoaded.bounds.size.width, - _keyboardViewContainer.frame.size.height); - } - completion:^(BOOL finished) { - if (shouldDismissKeyboard) { - [self.textInputDelegate flutterTextInputView:self.activeView - didResignFirstResponderWithTextInputClient:self.activeView.textInputClient]; - [self dismissKeyboardScreenshot]; - } else { - [self showKeyboardAndRemoveScreenshot]; + if (_keyboardView.superview != nil) { + // Done to avoid the issue of a pointer up done without a screenshot + // View must be loaded at this point. + UIScreen* screen = _viewController.flutterScreenIfViewLoaded; + CGFloat screenHeight = screen.bounds.size.height; + CGFloat keyboardHeight = _keyboardRect.size.height; + // Negative velocity indicates a downward movement + BOOL shouldDismissKeyboardBasedOnVelocity = _pointerYVelocity < 0; + [UIView animateWithDuration:kKeyboardAnimationTimeToCompleteion + animations:^{ + double keyboardDestination = + shouldDismissKeyboardBasedOnVelocity ? screenHeight : screenHeight - keyboardHeight; + _keyboardViewContainer.frame = CGRectMake( + 0, keyboardDestination, _viewController.flutterScreenIfViewLoaded.bounds.size.width, + _keyboardViewContainer.frame.size.height); } - }]; + completion:^(BOOL finished) { + if (shouldDismissKeyboardBasedOnVelocity) { + [self.textInputDelegate flutterTextInputView:self.activeView + didResignFirstResponderWithTextInputClient:self.activeView.textInputClient]; + [self dismissKeyboardScreenshot]; + } else { + [self showKeyboardAndRemoveScreenshot]; + } + }]; + } } - (void)dismissKeyboardScreenshot { @@ -2395,13 +2404,16 @@ static BOOL IsSelectionRectBoundaryCloserToPoint(CGPoint point, [self hideKeyboardWithoutAnimationAndAvoidCursorDismissUpdate]; } else { [self setKeyboardContainerHeight:pointerY]; + _pointerYVelocity = _previousPointerYPosition - pointerY; } } else { if (_keyboardView.superview != nil) { // Keeps keyboard at proper height. _keyboardViewContainer.frame = _keyboardRect; + _pointerYVelocity = _previousPointerYPosition - pointerY; } } + _previousPointerYPosition = pointerY; } - (void)setKeyboardContainerHeight:(CGFloat)pointerY { diff --git a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterTextInputPluginTest.mm b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterTextInputPluginTest.mm index edc95d8b3b4..5a214281eb5 100644 --- a/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterTextInputPluginTest.mm +++ b/engine/src/flutter/shell/platform/darwin/ios/framework/Source/FlutterTextInputPluginTest.mm @@ -2655,6 +2655,17 @@ FLUTTER_ASSERT_ARC } - (void)testInteractiveKeyboardDidResignFirstResponderDelegateisCalledAfterDismissedKeyboard { + NSSet* scenes = UIApplication.sharedApplication.connectedScenes; + XCTAssertEqual(scenes.count, 1UL, @"There must only be 1 scene for test"); + UIScene* scene = scenes.anyObject; + XCTAssert([scene isKindOfClass:[UIWindowScene class]], @"Must be a window scene for test"); + UIWindowScene* windowScene = (UIWindowScene*)scene; + XCTAssert(windowScene.windows.count > 0, @"There must be at least 1 window for test"); + UIWindow* window = windowScene.windows[0]; + [window addSubview:viewController.view]; + + [viewController loadView]; + XCTestExpectation* expectation = [[XCTestExpectation alloc] initWithDescription: @"didResignFirstResponder is called after screenshot keyboard dismissed."]; @@ -2687,7 +2698,7 @@ FLUTTER_ASSERT_ARC result:^(id _Nullable result){ }]; - [self waitForExpectations:@[ expectation ] timeout:1.0]; + [self waitForExpectations:@[ expectation ] timeout:2.0]; textInputPlugin.cachedFirstResponder = nil; } @@ -2833,6 +2844,12 @@ FLUTTER_ASSERT_ARC [textInputPlugin handleMethodCall:subsequentMoveCall result:^(id _Nullable result){ }]; + FlutterMethodCall* upwardVelocityMoveCall = + [FlutterMethodCall methodCallWithMethodName:@"TextInput.onPointerMoveForInteractiveKeyboard" + arguments:@{@"pointerY" : @(500)}]; + [textInputPlugin handleMethodCall:upwardVelocityMoveCall + result:^(id _Nullable result){ + }]; FlutterMethodCall* pointerUpCall = [FlutterMethodCall methodCallWithMethodName:@"TextInput.onPointerUpForInteractiveKeyboard"