Skip to content
This repository was archived by the owner on Feb 25, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
160 changes: 132 additions & 28 deletions shell/platform/darwin/ios/framework/Source/FlutterViewController.mm
Original file line number Diff line number Diff line change
Expand Up @@ -315,6 +315,11 @@ - (void)setupNotificationCenterObservers {
name:UIKeyboardWillChangeFrameNotification
object:nil];

[center addObserver:self
selector:@selector(keyboardWillShowNotification:)
name:UIKeyboardWillShowNotification
object:nil];

[center addObserver:self
selector:@selector(keyboardWillBeHidden:)
name:UIKeyboardWillHideNotification
Expand Down Expand Up @@ -1272,43 +1277,67 @@ - (void)updateViewportPadding {

#pragma mark - Keyboard events

- (void)keyboardWillChangeFrame:(NSNotification*)notification {
- (void)keyboardWillShowNotification:(NSNotification*)notification {
// Immediately prior to a docked keyboard being shown or when a keyboard goes from
// undocked/floating to docked, this notification is triggered

NSDictionary* info = [notification userInfo];
CGRect keyboardFrame = [[info objectForKey:UIKeyboardFrameEndUserInfoKey] CGRectValue];
bool isEmpty = CGRectIsEmpty(keyboardFrame);

// Ignore keyboard notifications related to other apps.
id isLocal = info[UIKeyboardIsLocalUserInfoKey];
if (isLocal && ![isLocal boolValue]) {
return;
}
// If keyboard is empty, bypass check if it's from another app
if (isEmpty == false) {
// Ignore keyboard notifications related to other apps.
id isLocal = info[UIKeyboardIsLocalUserInfoKey];
if (isLocal && ![isLocal boolValue]) {
return;
}

// Ignore keyboard notifications if engine’s viewController is not current viewController.
if ([_engine.get() viewController] != self) {
return;
// Ignore keyboard notifications if engine’s viewController is not current viewController.
if ([_engine.get() viewController] != self) {
return;
}
}

CGRect keyboardFrame = [[info objectForKey:UIKeyboardFrameEndUserInfoKey] CGRectValue];
CGRect screenRect = [[UIScreen mainScreen] bounds];

// Get the animation duration
// In Slide Over view, the keyboard's dimensions/position does not include the space
// below the app, even though the keyboard may be at the bottom of the screen.
// To handle, shift the Y origin by the amount of space below the app.
CGFloat screenHeight = CGRectGetHeight(screenRect);
CGFloat screenWidth = CGRectGetWidth(screenRect);
CGFloat appHeight = CGRectGetHeight(self.view.window.frame);
CGFloat appWidth = CGRectGetWidth(self.view.window.frame);
if (self.view.safeAreaInsets.bottom > 0 && appWidth < screenWidth) {
// In Slide Over view, the app is vertically centered with space above and below,
// which is why we divide by 2 to get the space below.
keyboardFrame.origin.y += (screenHeight - appHeight) / 2;
}

// If keyboard is within the screen and it's not empty, calculate and set the inset.
// If keyboard is not within the screen (it's usually below), set inset to 0.
// If keyboard frame is empty (usually because it was dragged and dropped), set inset to 0.
CGFloat calculatedInset = 0;
if (CGRectIntersectsRect(keyboardFrame, screenRect) && !isEmpty) {
calculatedInset = [self calculateKeyboardInset:screenRect keyboardFrame:keyboardFrame];
}

// avoid double triggering startKeyBoardAnimation
if (self.targetViewInsetBottom == calculatedInset) {
return;
}

self.targetViewInsetBottom = calculatedInset;
NSTimeInterval duration =
[[info objectForKey:UIKeyboardAnimationDurationUserInfoKey] doubleValue];

// Considering the iPad's split keyboard, Flutter needs to check if the keyboard frame is present
// in the screen to see if the keyboard is visible.
if (CGRectIntersectsRect(keyboardFrame, screenRect)) {
CGFloat bottom = CGRectGetHeight(keyboardFrame);
CGFloat scale = [UIScreen mainScreen].scale;
// The keyboard is treated as an inset since we want to effectively reduce the window size by
// the keyboard height. The Dart side will compute a value accounting for the keyboard-consuming
// bottom padding.
self.targetViewInsetBottom = bottom * scale;
} else {
self.targetViewInsetBottom = 0;
}
[self startKeyBoardAnimation:duration];
}

- (void)keyboardWillBeHidden:(NSNotification*)notification {
- (void)keyboardWillChangeFrame:(NSNotification*)notification {
// Immediately prior to a change in keyboard frame, this notification is triggered.
// There are some cases where UIKeyboardWillShowNotification & UIKeyboardWillHideNotification
// do not act as expected and this is used to catch those cases.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does the change frame notification work in all cases? Maybe we should eliminate UIKeyboardWillShowNotification and UIKeyboardWillHideNotification if the change frame notification is more reliable?

Could be a nice refactor in a follow up PR if we can use frame change notification for all cases.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So none of them are very reliable which is why I used all three of them. Here's some of the use cases for example

  • When predictive-only/minimized keyboard is dragged, it sends a UIKeyboardWillShowNotification notification where the keyboard = CGRectZero, which we use to determine it's "floating" and should have inset 0 . Whereas in UIKeyboardWillChangeFrameNotification we skip when keyboard = CGRectZero, because otherwise it will set inset to 0 prematurely in some cases like when you dragged a docked keyboard but don't actually change it from docked.
  • When keyboard is changed from docked to floating, it's supposed to send a UIKeyboardWillHideNotification notification but it doesn't, so we handle in UIKeyboardWillChangeFrameNotification instead
  • When keyboard goes from docked to undocked, the UIKeyboardWillChangeFrameNotification position will not be completely below the screen so it will still have an inset, which is at least one reason why we also need UIKeyboardWillHideNotification

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, gotcha. I think logic this complex deserves a design documentation :) And we should have a comment in the code to link to the design doc. (The same way PlatformView should have and never did :p )

I think for this PR, it would be nice to explain "some cases" a little more in the comments. Or add a link in the code to your comment here: #37719 (comment)


NSDictionary* info = [notification userInfo];

// Ignore keyboard notifications related to other apps.
Expand All @@ -1322,10 +1351,85 @@ - (void)keyboardWillBeHidden:(NSNotification*)notification {
return;
}

CGRect keyboardFrame = [[info objectForKey:UIKeyboardFrameEndUserInfoKey] CGRectValue];

// Ignore notification when keyboard has zero width/height
// This happens when keyboard is dragged.
if (CGRectIsEmpty(keyboardFrame)) {
return;
}

CGRect screenRect = [[UIScreen mainScreen] bounds];
CGFloat screenHeight = CGRectGetHeight(screenRect);
CGRect keyboardBeginFrame = [[info objectForKey:UIKeyboardFrameBeginUserInfoKey] CGRectValue];
CGFloat keyboardBeginWidth = CGRectGetWidth(keyboardBeginFrame);

// Ignore notification when keyboard is in process of being rotated.
// When the keyboard's width at the beginning of the animation equals the screen's
// current height, we can assume the keyboard was rotated.
if (screenHeight == keyboardBeginWidth) {
return;
}

// In Slide Over view, the keyboard's dimensions/position does not include the space
// below the app, even though the keyboard may be at the bottom of the screen.
// To handle, shift the Y origin by the amount of space below the app.
CGFloat screenWidth = CGRectGetWidth(screenRect);
CGFloat appHeight = CGRectGetHeight(self.view.window.frame);
CGFloat appWidth = CGRectGetWidth(self.view.window.frame);
if (self.view.safeAreaInsets.bottom > 0 && appWidth < screenWidth) {
// In Slide Over view, the app is vertically centered with space above and below,
// which is why we divide by 2 to get the space below.
keyboardFrame.origin.y += (screenHeight - appHeight) / 2;
}

// If the keyboard is partially or fully showing at the bottom of the screen,
// calculate and set the inset.
// When the keyboard goes from docked to the floating small keyboard, it sometimes
// does not send a UIKeyboardWillHideNotification notification as expected.
// To handle, if the keyboard is above the bottom of the screen, set the inset to 0.
// If keyboard is not within the screen (it's usually below), set inset to 0.
CGFloat calculatedInset = 0;
CGFloat keyboardBottom = CGRectGetMaxY(keyboardFrame);
if (keyboardBottom >= screenHeight && CGRectIntersectsRect(keyboardFrame, screenRect)) {
calculatedInset = [self calculateKeyboardInset:screenRect keyboardFrame:keyboardFrame];
}

// avoid double triggering startKeyBoardAnimation
if (self.targetViewInsetBottom == calculatedInset) {
return;
}

self.targetViewInsetBottom = calculatedInset;
NSTimeInterval duration =
[[info objectForKey:UIKeyboardAnimationDurationUserInfoKey] doubleValue];
[self startKeyBoardAnimation:duration];
}

- (CGFloat)calculateKeyboardInset:(CGRect)screenRect keyboardFrame:(CGRect)keyboardFrame {
// Sometimes when rotating orientation, the keyboard height will be higher than it really is.
// So calculate how much of the keyboard is showing using position.
CGFloat screenHeight = CGRectGetHeight(screenRect);
CGFloat keyboardTop = CGRectGetMinY(keyboardFrame);
CGFloat portionOfKeyboardShowing = screenHeight - keyboardTop;

// The keyboard is treated as an inset since we want to effectively reduce the window size by
// the keyboard height. The Dart side will compute a value accounting for the keyboard-consuming
// bottom padding.
CGFloat scale = [UIScreen mainScreen].scale;
CGFloat calculatedInset = portionOfKeyboardShowing * scale;

return calculatedInset;
}

- (void)keyboardWillBeHidden:(NSNotification*)notification {
// When keyboard is hidden or undocked, this notification will be triggered
if (self.targetViewInsetBottom != 0) {
// Ensure the keyboard will be dismissed. Just like the keyboardWillChangeFrame,
// keyboardWillBeHidden is also in an animation block in iOS sdk, so we don't need to set the
// animation curve. Related issue: https://github.com/flutter/flutter/issues/99951
// Ensure the keyboard will be dismissed. Just like keyboardWillShowNotification
// and keyboardWillChangeFrame, keyboardWillBeHidden is also in an animation
// block in iOS sdk, so we don't need to set the animation curve.
// Related issue: https://github.com/flutter/flutter/issues/99951
NSDictionary* info = [notification userInfo];
self.targetViewInsetBottom = 0;
NSTimeInterval duration =
[[info objectForKey:UIKeyboardAnimationDurationUserInfoKey] doubleValue];
Expand Down
Loading