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

Commit a9be77e

Browse files
authored
Flutter iOS Interactive Keyboard: Fixing Behavior Issue (#44586)
This PR addresses an issue with the behavior of the keyboard. Originally the behavior of the keyboard was to see if the pointer was above or below the middle of the keyboards full size and then animate appropriately. However we found that the behavior is instead based on velocity. This PR adjust the code to match this behavior. Design Document: https://docs.google.com/document/d/1-T7_0mSkXzPaWxveeypIzzzAdyo-EEuP5V84161foL4/edit?pli=1 Issues Address: flutter/flutter#57609 [C++, Objective-C, Java style guides]: https://github.com/flutter/engine/blob/main/CONTRIBUTING.md#style
1 parent 28e52db commit a9be77e

File tree

2 files changed

+52
-23
lines changed

2 files changed

+52
-23
lines changed

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

Lines changed: 34 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,12 @@
2222
static constexpr double kUITextInputAccessibilityEnablingDelaySeconds = 0.5;
2323

2424
// A delay before reenabling the UIView areAnimationsEnabled to YES
25-
// in order for becomeFirstResponder to receive the proper value
25+
// in order for becomeFirstResponder to receive the proper value.
2626
static const NSTimeInterval kKeyboardAnimationDelaySeconds = 0.1;
2727

28+
// A time set for the screenshot to animate back to the assigned position.
29+
static const NSTimeInterval kKeyboardAnimationTimeToCompleteion = 0.3;
30+
2831
// The "canonical" invalid CGRect, similar to CGRectNull, used to
2932
// indicate a CGRect involved in firstRectForRange calculation is
3033
// invalid. The specific value is chosen so that if firstRectForRange
@@ -2234,6 +2237,8 @@ @interface FlutterTextInputPlugin ()
22342237
@property(nonatomic, strong) UIView* keyboardView;
22352238
@property(nonatomic, strong) UIView* cachedFirstResponder;
22362239
@property(nonatomic, assign) CGRect keyboardRect;
2240+
@property(nonatomic, assign) CGFloat previousPointerYPosition;
2241+
@property(nonatomic, assign) CGFloat pointerYVelocity;
22372242
@end
22382243

22392244
@implementation FlutterTextInputPlugin {
@@ -2340,28 +2345,32 @@ - (void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result {
23402345
}
23412346

23422347
- (void)handlePointerUp:(CGFloat)pointerY {
2343-
// View must be loaded at this point.
2344-
UIScreen* screen = _viewController.flutterScreenIfViewLoaded;
2345-
CGFloat screenHeight = screen.bounds.size.height;
2346-
CGFloat keyboardHeight = _keyboardRect.size.height;
2347-
BOOL shouldDismissKeyboard = (screenHeight - (keyboardHeight / 2)) < pointerY;
2348-
[UIView animateWithDuration:0.3f
2349-
animations:^{
2350-
double keyboardDestination =
2351-
shouldDismissKeyboard ? screenHeight : screenHeight - keyboardHeight;
2352-
_keyboardViewContainer.frame = CGRectMake(
2353-
0, keyboardDestination, _viewController.flutterScreenIfViewLoaded.bounds.size.width,
2354-
_keyboardViewContainer.frame.size.height);
2355-
}
2356-
completion:^(BOOL finished) {
2357-
if (shouldDismissKeyboard) {
2358-
[self.textInputDelegate flutterTextInputView:self.activeView
2359-
didResignFirstResponderWithTextInputClient:self.activeView.textInputClient];
2360-
[self dismissKeyboardScreenshot];
2361-
} else {
2362-
[self showKeyboardAndRemoveScreenshot];
2348+
if (_keyboardView.superview != nil) {
2349+
// Done to avoid the issue of a pointer up done without a screenshot
2350+
// View must be loaded at this point.
2351+
UIScreen* screen = _viewController.flutterScreenIfViewLoaded;
2352+
CGFloat screenHeight = screen.bounds.size.height;
2353+
CGFloat keyboardHeight = _keyboardRect.size.height;
2354+
// Negative velocity indicates a downward movement
2355+
BOOL shouldDismissKeyboardBasedOnVelocity = _pointerYVelocity < 0;
2356+
[UIView animateWithDuration:kKeyboardAnimationTimeToCompleteion
2357+
animations:^{
2358+
double keyboardDestination =
2359+
shouldDismissKeyboardBasedOnVelocity ? screenHeight : screenHeight - keyboardHeight;
2360+
_keyboardViewContainer.frame = CGRectMake(
2361+
0, keyboardDestination, _viewController.flutterScreenIfViewLoaded.bounds.size.width,
2362+
_keyboardViewContainer.frame.size.height);
23632363
}
2364-
}];
2364+
completion:^(BOOL finished) {
2365+
if (shouldDismissKeyboardBasedOnVelocity) {
2366+
[self.textInputDelegate flutterTextInputView:self.activeView
2367+
didResignFirstResponderWithTextInputClient:self.activeView.textInputClient];
2368+
[self dismissKeyboardScreenshot];
2369+
} else {
2370+
[self showKeyboardAndRemoveScreenshot];
2371+
}
2372+
}];
2373+
}
23652374
}
23662375

23672376
- (void)dismissKeyboardScreenshot {
@@ -2395,13 +2404,16 @@ - (void)handlePointerMove:(CGFloat)pointerY {
23952404
[self hideKeyboardWithoutAnimationAndAvoidCursorDismissUpdate];
23962405
} else {
23972406
[self setKeyboardContainerHeight:pointerY];
2407+
_pointerYVelocity = _previousPointerYPosition - pointerY;
23982408
}
23992409
} else {
24002410
if (_keyboardView.superview != nil) {
24012411
// Keeps keyboard at proper height.
24022412
_keyboardViewContainer.frame = _keyboardRect;
2413+
_pointerYVelocity = _previousPointerYPosition - pointerY;
24032414
}
24042415
}
2416+
_previousPointerYPosition = pointerY;
24052417
}
24062418

24072419
- (void)setKeyboardContainerHeight:(CGFloat)pointerY {

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

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2655,6 +2655,17 @@ - (void)testInteractiveKeyboardFindFirstResponderIsNilRecursive {
26552655
}
26562656

26572657
- (void)testInteractiveKeyboardDidResignFirstResponderDelegateisCalledAfterDismissedKeyboard {
2658+
NSSet<UIScene*>* scenes = UIApplication.sharedApplication.connectedScenes;
2659+
XCTAssertEqual(scenes.count, 1UL, @"There must only be 1 scene for test");
2660+
UIScene* scene = scenes.anyObject;
2661+
XCTAssert([scene isKindOfClass:[UIWindowScene class]], @"Must be a window scene for test");
2662+
UIWindowScene* windowScene = (UIWindowScene*)scene;
2663+
XCTAssert(windowScene.windows.count > 0, @"There must be at least 1 window for test");
2664+
UIWindow* window = windowScene.windows[0];
2665+
[window addSubview:viewController.view];
2666+
2667+
[viewController loadView];
2668+
26582669
XCTestExpectation* expectation = [[XCTestExpectation alloc]
26592670
initWithDescription:
26602671
@"didResignFirstResponder is called after screenshot keyboard dismissed."];
@@ -2687,7 +2698,7 @@ - (void)testInteractiveKeyboardDidResignFirstResponderDelegateisCalledAfterDismi
26872698
result:^(id _Nullable result){
26882699
}];
26892700

2690-
[self waitForExpectations:@[ expectation ] timeout:1.0];
2701+
[self waitForExpectations:@[ expectation ] timeout:2.0];
26912702
textInputPlugin.cachedFirstResponder = nil;
26922703
}
26932704

@@ -2833,6 +2844,12 @@ - (void)testInteractiveKeyboardKeyboardAnimatesToOriginalPositionalOnPointerUp {
28332844
[textInputPlugin handleMethodCall:subsequentMoveCall
28342845
result:^(id _Nullable result){
28352846
}];
2847+
FlutterMethodCall* upwardVelocityMoveCall =
2848+
[FlutterMethodCall methodCallWithMethodName:@"TextInput.onPointerMoveForInteractiveKeyboard"
2849+
arguments:@{@"pointerY" : @(500)}];
2850+
[textInputPlugin handleMethodCall:upwardVelocityMoveCall
2851+
result:^(id _Nullable result){
2852+
}];
28362853

28372854
FlutterMethodCall* pointerUpCall =
28382855
[FlutterMethodCall methodCallWithMethodName:@"TextInput.onPointerUpForInteractiveKeyboard"

0 commit comments

Comments
 (0)