Flutter iOS Interactive Keyboard: Fixing Behavior Issue (flutter/engine#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:
https://github.com/flutter/flutter/issues/57609

[C++, Objective-C, Java style guides]: https://github.com/flutter/engine/blob/main/CONTRIBUTING.md#style
This commit is contained in:
Matt2D 2023-08-10 15:02:08 -04:00 committed by GitHub
parent 491ce39b6d
commit 68dccebb48
2 changed files with 52 additions and 23 deletions

View File

@ -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 {

View File

@ -2655,6 +2655,17 @@ FLUTTER_ASSERT_ARC
}
- (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."];
@ -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"