Skip to content

Commit

Permalink
Flutter iOS Interactive Keyboard: Fixing Behavior Issue (#44586)
Browse files Browse the repository at this point in the history
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
  • Loading branch information
Matt2D committed Aug 10, 2023
1 parent 28e52db commit a9be77e
Show file tree
Hide file tree
Showing 2 changed files with 52 additions and 23 deletions.
Expand Up @@ -22,9 +22,12 @@
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
Expand Down Expand Up @@ -2234,6 +2237,8 @@ @interface FlutterTextInputPlugin ()
@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 {
Expand Down Expand Up @@ -2340,28 +2345,32 @@ - (void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result {
}

- (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 {
Expand Down Expand Up @@ -2395,13 +2404,16 @@ - (void)handlePointerMove:(CGFloat)pointerY {
[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 {
Expand Down
Expand Up @@ -2655,6 +2655,17 @@ - (void)testInteractiveKeyboardFindFirstResponderIsNilRecursive {
}

- (void)testInteractiveKeyboardDidResignFirstResponderDelegateisCalledAfterDismissedKeyboard {
NSSet<UIScene*>* 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."];
Expand Down Expand Up @@ -2687,7 +2698,7 @@ - (void)testInteractiveKeyboardDidResignFirstResponderDelegateisCalledAfterDismi
result:^(id _Nullable result){
}];

[self waitForExpectations:@[ expectation ] timeout:1.0];
[self waitForExpectations:@[ expectation ] timeout:2.0];
textInputPlugin.cachedFirstResponder = nil;
}

Expand Down Expand Up @@ -2833,6 +2844,12 @@ - (void)testInteractiveKeyboardKeyboardAnimatesToOriginalPositionalOnPointerUp {
[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"
Expand Down

0 comments on commit a9be77e

Please sign in to comment.