Skip to content

Commit

Permalink
useAnimatedKeyboard fixes (iOS) (#6755)
Browse files Browse the repository at this point in the history
<!-- Thanks for submitting a pull request! We appreciate you spending
the time to work on these changes. Please follow the template so that
the reviewers can easily understand what the code changes affect. -->

## Summary

fix: return correct height for detached keyboard on iOS (fixes #5584,
#6440)
fix: don't clobber keyboard height when keyboard did not animate on iOS
fix: convert keyboard position to window coordinate space on iOS (Stage
Manager compatibility)
fix: don't prematurely update keyboard height during dismissal on iOS
fix: don't jump to height when iOS keyboard is rapidly opened/closed
(fixes #6727)
fix: correct height when keyboard opened from shortcuts bar on iOS

## Test plan

Can be tested with the useAnimatedKeyboard example.

1. Enable iPad for the example project.
2. Launch the useAnimatedKeyboard example.
3. Open the keyboard, then undock or float the keyboard. The view should
not transform when undocked or floating.
4. Dock the keyboard
5. Hide the keyboard, note that the view should now be transformed by
the keyboard height
6. Enable Reduce Motion in Accessibility, and Prefers Cross-Fade
Transitions
7. Open the keyboard, note that he transform should work as expected
  • Loading branch information
mhoran authored Jan 23, 2025
1 parent f16eefa commit 7ab0e09
Showing 1 changed file with 91 additions and 45 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ @implementation REAKeyboardEventObserver {
float _targetKeyboardHeight;
REAUIView *_keyboardView;
bool _isKeyboardObserverAttached;
bool _isInteractiveDismissalCanceled;
}

- (instancetype)init
Expand All @@ -35,6 +36,7 @@ - (instancetype)init
_state = UNKNOWN;
_animationStartTimestamp = 0;
_isKeyboardObserverAttached = false;
_isInteractiveDismissalCanceled = false;
NSNotificationCenter *notificationCenter = [NSNotificationCenter defaultCenter];

[notificationCenter addObserver:self
Expand Down Expand Up @@ -169,31 +171,13 @@ - (float)getAnimatingKeyboardHeight
return keyboardHeight;
}

- (float)getStaticKeyboardHeight
{
CGRect measuringFrame = _measuringView.frame;
CGFloat keyboardHeight = measuringFrame.size.height;
return keyboardHeight;
}

- (void)updateKeyboardFrame
{
CGFloat keyboardHeight = 0;
bool isKeyboardAnimationRunning = [self hasAnyAnimation:_measuringView];
if (isKeyboardAnimationRunning) {
keyboardHeight = [self getAnimatingKeyboardHeight];
} else {
// measuring view is no longer running an animation, we should settle in OPEN/CLOSE state
if (_state == OPENING || _state == CLOSING) {
_state = _state == OPENING ? OPEN : CLOSED;
}
if (_state == OPEN || _state == CLOSED) {
keyboardHeight = [self getStaticKeyboardHeight];
}
// stop display link updates if no animation is running
[[self getDisplayLink] setPaused:YES];
CGFloat keyboardHeight = [self getAnimatingKeyboardHeight];
[self runListeners:keyboardHeight];
}
[self runListeners:keyboardHeight];
}

- (void)keyboardWillChangeFrame:(NSNotification *)notification
Expand All @@ -202,37 +186,51 @@ - (void)keyboardWillChangeFrame:(NSNotification *)notification
CGRect beginFrame = [[userInfo objectForKey:UIKeyboardFrameBeginUserInfoKey] CGRectValue];
CGRect endFrame = [[userInfo objectForKey:UIKeyboardFrameEndUserInfoKey] CGRectValue];
NSTimeInterval animationDuration = [[userInfo objectForKey:UIKeyboardAnimationDurationUserInfoKey] doubleValue];
CGSize windowSize = [[[UIApplication sharedApplication] delegate] window].frame.size;
auto window = [[[UIApplication sharedApplication] delegate] window];

_initialKeyboardHeight = windowSize.height - beginFrame.origin.y;
_targetKeyboardHeight = windowSize.height - endFrame.origin.y;
/*
The keyboard frame is in the screen's coordinate space and must first be converted
to the window coordinate space in order to support Slide Over and Stage Manager on
iPadOS.
*/
beginFrame = [window convertRect:beginFrame fromCoordinateSpace:window.screen.coordinateSpace];
endFrame = [window convertRect:endFrame fromCoordinateSpace:window.screen.coordinateSpace];

/*
This may seem a bit confusing, but usually, the state should be either OPENED or CLOSED.
However, if it shows as OPENING, it means that the interactive dismissal was canceled.
*/
bool isInteractiveMode = _state == OPENING;
if (_targetKeyboardHeight > 0 && _state != OPEN) {
_state = OPENING;
} else if (_targetKeyboardHeight == 0 && _state != CLOSED) {
_state = CLOSING;
}
In some cases, such as when the user has enabled "prefer cross-fade transitions" or
when the keyboard is floating, the begin or end frame is an empty rectangle. In
such cases we should treat the keyboard as closed, rather than at (0, 0).
*/
CGRect beginFrameIntersection = CGRectIntersection(window.bounds, beginFrame);
_initialKeyboardHeight =
CGRectIsEmpty(beginFrameIntersection) ? 0 : CGRectGetMaxY(window.bounds) - CGRectGetMinY(beginFrameIntersection);

CGRect endFrameIntersection = CGRectIntersection(window.bounds, endFrame);
_targetKeyboardHeight =
CGRectIsEmpty(endFrameIntersection) ? 0 : CGRectGetMaxY(window.bounds) - CGRectGetMinY(endFrameIntersection);

bool forceAnimation = false;
auto keyboardView = [self getKeyboardView];
bool hasKeyboardAnimation = [self hasAnyAnimation:keyboardView];
if (isInteractiveMode) {
// This condition can be met after canceling interactive dismissal.
_initialKeyboardHeight = windowSize.height - keyboardView.frame.origin.y;
if (_state == CLOSING) {
_targetKeyboardHeight = 0;
} else if (_isInteractiveDismissalCanceled && keyboardView) {
// Prefer the keyboard view frame over notification frame which may not be current.
beginFrameIntersection = CGRectIntersection(window.bounds, keyboardView.frame);
_initialKeyboardHeight = CGRectIsEmpty(beginFrameIntersection)
? 0
: CGRectGetMaxY(window.bounds) - CGRectGetMinY(beginFrameIntersection);
forceAnimation = true;
}
_isInteractiveDismissalCanceled = false;

if (hasKeyboardAnimation || isInteractiveMode) {
bool hasKeyboardAnimation = [self hasAnyAnimation:keyboardView];
if (hasKeyboardAnimation || forceAnimation) {
_measuringView.frame = CGRectMake(0, -1, 0, _initialKeyboardHeight);
[UIView animateWithDuration:animationDuration
animations:^{
self->_measuringView.frame = CGRectMake(0, -1, 0, self->_targetKeyboardHeight);
}];
[self runUpdater];
} else {
[self runListeners:_targetKeyboardHeight];
}
}

Expand All @@ -249,8 +247,8 @@ - (int)subscribeForKeyboardEvents:(KeyboardEventListenerBlock)listener
if ([self->_listeners count] == 0) {
NSNotificationCenter *notificationCenter = [NSNotificationCenter defaultCenter];
[notificationCenter addObserver:self
selector:@selector(keyboardWillChangeFrame:)
name:UIKeyboardWillChangeFrameNotification
selector:@selector(keyboardWillShow:)
name:UIKeyboardWillShowNotification
object:nil];
[notificationCenter addObserver:self
selector:@selector(keyboardDidShow:)
Expand All @@ -260,6 +258,10 @@ - (int)subscribeForKeyboardEvents:(KeyboardEventListenerBlock)listener
selector:@selector(keyboardWillHide:)
name:UIKeyboardWillHideNotification
object:nil];
[notificationCenter addObserver:self
selector:@selector(keyboardDidHide:)
name:UIKeyboardDidHideNotification
object:nil];
}

[self->_listeners setObject:listener forKey:listenerId];
Expand All @@ -274,9 +276,10 @@ - (void)unsubscribeFromKeyboardEvents:(int)listenerId
[self->_listeners removeObjectForKey:_listenerId];
if ([self->_listeners count] == 0) {
NSNotificationCenter *notificationCenter = [NSNotificationCenter defaultCenter];
[notificationCenter removeObserver:self name:UIKeyboardWillChangeFrameNotification object:nil];
[notificationCenter removeObserver:self name:UIKeyboardWillShowNotification object:nil];
[notificationCenter removeObserver:self name:UIKeyboardDidShowNotification object:nil];
[notificationCenter removeObserver:self name:UIKeyboardWillHideNotification object:nil];
[notificationCenter removeObserver:self name:UIKeyboardDidHideNotification object:nil];
}
});
}
Expand All @@ -291,12 +294,30 @@ - (void)cleanupListeners
});
}

- (void)keyboardWillShow:(NSNotification *)notification
{
_state = OPENING;
[self keyboardWillChangeFrame:notification];
}

- (void)keyboardDidShow:(NSNotification *)notification
{
[[self getDisplayLink] setPaused:YES];

auto window = [[[UIApplication sharedApplication] delegate] window];
auto keyboardView = [self getKeyboardView];
if (keyboardView) {
CGRect frameIntersection = CGRectIntersection(window.bounds, keyboardView.frame);
_targetKeyboardHeight =
CGRectIsEmpty(frameIntersection) ? 0 : CGRectGetMaxY(window.bounds) - CGRectGetMinY(frameIntersection);
}
_state = OPEN;
[self runListeners:_targetKeyboardHeight];

if (_isKeyboardObserverAttached) {
return;
}
if (auto keyboardView = [self getKeyboardView]) {
if (keyboardView) {
[_keyboardView addObserver:self
forKeyPath:@"center"
options:NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew
Expand All @@ -311,23 +332,48 @@ - (void)keyboardWillHide:(NSNotification *)notification
[_keyboardView removeObserver:self forKeyPath:@"center"];
_isKeyboardObserverAttached = false;
}

_state = CLOSING;
[self keyboardWillChangeFrame:notification];
}

- (void)keyboardDidHide:(NSNotification *)notification
{
[[self getDisplayLink] setPaused:YES];

_targetKeyboardHeight = 0;
_state = CLOSED;
[self runListeners:_targetKeyboardHeight];
}

- (void)updateKeyboardHeightDuringInteractiveDismiss:(CGPoint)oldKeyboardFrame
newKeyboardFrame:(CGPoint)newKeyboardFrame
{
auto keyboardView = [self getKeyboardView];
if (!keyboardView) {
return;
}

bool hasKeyboardAnimation = [self hasAnyAnimation:keyboardView];
if (hasKeyboardAnimation) {
float keyboardHeight = keyboardView.frame.size.height;
/*
If the keyboard is animating, height will be updated by the notification observers.
If the keyboard state is CLOSED, height is 0, or the keyboard is floating (keyboard
width != window width), don't update to prevent jumping to intermediate state.
*/
if (hasKeyboardAnimation || _state == CLOSED || keyboardHeight == 0 ||
CGRectGetWidth(keyboardView.frame) != CGRectGetWidth(keyboardView.window.bounds)) {
return;
}

float windowHeight = keyboardView.window.bounds.size.height;
float keyboardHeight = keyboardView.frame.size.height;
float visibleKeyboardHeight = windowHeight - (newKeyboardFrame.y - keyboardHeight / 2);
if (oldKeyboardFrame.y > newKeyboardFrame.y) {
_state = OPENING;
_isInteractiveDismissalCanceled = true;
} else if (oldKeyboardFrame.y < newKeyboardFrame.y) {
_state = CLOSING;
_isInteractiveDismissalCanceled = false;
}
[self runListeners:visibleKeyboardHeight];
}
Expand Down

0 comments on commit 7ab0e09

Please sign in to comment.