mirror of
https://github.com/flutter/flutter.git
synced 2026-02-20 02:29:02 +08:00
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:
parent
491ce39b6d
commit
68dccebb48
@ -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 {
|
||||
|
||||
@ -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"
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user