This repository was archived by the owner on Feb 25, 2025. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 6k
Fix issues related to keyboard inset #37719
Merged
Merged
Changes from 12 commits
Commits
Show all changes
17 commits
Select commit
Hold shift + click to select a range
081a812
fix keyboard inset not collapsing when expected
vashworth 8501ff2
fix some formatting
vashworth 0c0cc73
fix issue with rotating with undocked and split keyboard
vashworth 078308c
fix formatting
vashworth 6f16d3d
fix behavior on slide over view
vashworth 79705b4
fix formatting
vashworth 6270428
refactor to make logic more clear
vashworth 084b228
move enum to header file, remove unneeded parameters, syntax fixes, r…
vashworth cbece73
ignore notification if app state is not active, change way it checks …
vashworth 6c4f65e
Merge remote-tracking branch 'upstream/main' into keyboard_fixes
vashworth 39313a7
fix leaking unit test
vashworth 602c703
use viewIfLoaded and update tests to fix mocking
vashworth ea788a1
change ignore logic related to application state to be more specific
vashworth 179f510
Merge remote-tracking branch 'upstream/main' into keyboard_fixes
vashworth 5aa9221
add more comments
vashworth 010e86b
add more comments
vashworth aa6276e
change function name to be more clear, add warning log if view is not…
vashworth File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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 | ||
|
|
@@ -588,6 +593,13 @@ - (UIView*)keyboardAnimationView { | |
| return _keyboardAnimationView.get(); | ||
| } | ||
|
|
||
| - (UIScreen*)mainScreen { | ||
| if (@available(iOS 13.0, *)) { | ||
| return self.viewIfLoaded.window.windowScene.screen; | ||
vashworth marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| } | ||
| return UIScreen.mainScreen; | ||
| } | ||
|
|
||
| - (BOOL)loadDefaultSplashScreenView { | ||
| NSString* launchscreenName = | ||
| [[[NSBundle mainBundle] infoDictionary] objectForKey:@"UILaunchStoryboardName"]; | ||
|
|
@@ -1272,65 +1284,185 @@ - (void)updateViewportPadding { | |
|
|
||
| #pragma mark - Keyboard events | ||
|
|
||
| - (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. | ||
| [self handleKeyboardNotification:notification]; | ||
| } | ||
|
|
||
| - (void)keyboardWillChangeFrame:(NSNotification*)notification { | ||
| NSDictionary* info = [notification userInfo]; | ||
| // Immediately prior to a change in keyboard frame, this notification is triggered. | ||
| [self handleKeyboardNotification:notification]; | ||
| } | ||
|
|
||
| // Ignore keyboard notifications related to other apps. | ||
| id isLocal = info[UIKeyboardIsLocalUserInfoKey]; | ||
| if (isLocal && ![isLocal boolValue]) { | ||
| - (void)keyboardWillBeHidden:(NSNotification*)notification { | ||
| // When keyboard is hidden or undocked, this notification will be triggered. | ||
vashworth marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| [self handleKeyboardNotification:notification]; | ||
| } | ||
|
|
||
| - (void)handleKeyboardNotification:(NSNotification*)notification { | ||
| // See https:://flutter.dev/go/ios-keyboard-calculating-inset for more details | ||
| // on why notifications are used and how things are calculated. | ||
| if ([self shouldIgnoreKeyboardNotification:notification]) { | ||
| return; | ||
| } | ||
|
|
||
| // Ignore keyboard notifications if engine’s viewController is not current viewController. | ||
| if ([_engine.get() viewController] != self) { | ||
| NSDictionary* info = notification.userInfo; | ||
| CGRect keyboardFrame = [info[UIKeyboardFrameEndUserInfoKey] CGRectValue]; | ||
| FlutterKeyboardMode keyboardMode = [self calculateKeyboardAttachMode:notification]; | ||
| CGFloat calculatedInset = [self calculateKeyboardInset:keyboardFrame keyboardMode:keyboardMode]; | ||
|
|
||
| // Avoid double triggering startKeyBoardAnimation. | ||
vashworth marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| if (self.targetViewInsetBottom == calculatedInset) { | ||
| return; | ||
| } | ||
|
|
||
| CGRect keyboardFrame = [[info objectForKey:UIKeyboardFrameEndUserInfoKey] CGRectValue]; | ||
| CGRect screenRect = [[UIScreen mainScreen] bounds]; | ||
| self.targetViewInsetBottom = calculatedInset; | ||
| NSTimeInterval duration = [info[UIKeyboardAnimationDurationUserInfoKey] doubleValue]; | ||
| [self startKeyBoardAnimation:duration]; | ||
| } | ||
|
|
||
| // Get the animation duration | ||
| NSTimeInterval duration = | ||
| [[info objectForKey:UIKeyboardAnimationDurationUserInfoKey] doubleValue]; | ||
| - (BOOL)shouldIgnoreKeyboardNotification:(NSNotification*)notification { | ||
| // Don't ignore UIKeyboardWillHideNotification notifications. | ||
vashworth marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| if (notification.name == UIKeyboardWillHideNotification) { | ||
| return NO; | ||
| } | ||
|
|
||
| // 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; | ||
| // Ignore notification when keyboard's dimensions and position are all zeroes, | ||
| // for UIKeyboardWillChangeFrameNotification. This happens when keyboard is dragged. | ||
| NSDictionary* info = notification.userInfo; | ||
| CGRect keyboardFrame = [info[UIKeyboardFrameEndUserInfoKey] CGRectValue]; | ||
| if (notification.name == UIKeyboardWillChangeFrameNotification && | ||
| CGRectEqualToRect(keyboardFrame, CGRectZero)) { | ||
| return YES; | ||
| } | ||
| [self startKeyBoardAnimation:duration]; | ||
| } | ||
|
|
||
| - (void)keyboardWillBeHidden:(NSNotification*)notification { | ||
| NSDictionary* info = [notification userInfo]; | ||
| // When keyboard's height/width is set to 0 by other app, | ||
| // do not ignore so that the inset will be set to 0. | ||
| if (CGRectIsEmpty(keyboardFrame)) { | ||
| return NO; | ||
| } | ||
|
|
||
| // Ignore keyboard notifications related to other apps. | ||
vashworth marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| if ([self isKeyboardNotificationForDifferentView:notification]) { | ||
| return YES; | ||
| } | ||
|
|
||
| // Ignore notification if the app is not active (meaning it's running in the | ||
| // background, interrupted, or the app is transitioning to or from the background). | ||
| if (UIApplication.sharedApplication.applicationState != UIApplicationStateActive) { | ||
| return YES; | ||
| } | ||
luckysmg marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| return NO; | ||
| } | ||
|
|
||
| - (BOOL)isKeyboardNotificationForDifferentView:(NSNotification*)notification { | ||
| NSDictionary* info = notification.userInfo; | ||
| // Keyboard notifications related to other apps. | ||
| id isLocal = info[UIKeyboardIsLocalUserInfoKey]; | ||
| if (isLocal && ![isLocal boolValue]) { | ||
| return; | ||
| return YES; | ||
vashworth marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| } | ||
|
|
||
| // Ignore keyboard notifications if engine’s viewController is not current viewController. | ||
| // Engine’s viewController is not current viewController. | ||
| if ([_engine.get() viewController] != self) { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. does this happen when an app shows multiple flutter view controller on the same screen?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah, from my understanding this can happen when you're using add-to-app stuff like this flutter/flutter#39036 (comment). This is a good point, though. I didn't test use case like that.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Update: I did test this use case manually and everything appeared to be working correctly. |
||
| return; | ||
| return YES; | ||
| } | ||
| return NO; | ||
| } | ||
|
|
||
| - (FlutterKeyboardMode)calculateKeyboardAttachMode:(NSNotification*)notification { | ||
| // There are multiple types of keyboard: docked, undocked, split, split docked, | ||
| // floating, expanded shortcuts bar, minimized shortcuts bar. This function will categorize | ||
| // the keyboard as one of the following modes: docked, floating, or hidden. | ||
| // Docked mode includes docked, split docked, expanded shortcuts bar (when opening via click), | ||
| // and minimized shortcuts bar (when opened via click). | ||
| // Floating includes undocked, split, floating, expanded shortcuts bar (when dragged and dropped), | ||
| // and minimized shortcuts bar (when dragged and dropped). | ||
| NSDictionary* info = notification.userInfo; | ||
| CGRect keyboardFrame = [info[UIKeyboardFrameEndUserInfoKey] CGRectValue]; | ||
|
|
||
| if (notification.name == UIKeyboardWillHideNotification) { | ||
| return FlutterKeyboardModeHidden; | ||
| } | ||
|
|
||
| // If keyboard's dimensions and position are all zeroes, | ||
| // that means it's been dragged and therefore floating. | ||
| if (CGRectEqualToRect(keyboardFrame, CGRectZero)) { | ||
| return FlutterKeyboardModeFloating; | ||
| } | ||
| // If keyboard's width or height are 0, it's hidden. | ||
| if (CGRectIsEmpty(keyboardFrame)) { | ||
| return FlutterKeyboardModeHidden; | ||
| } | ||
|
|
||
| CGRect screenRect = [self mainScreen].bounds; | ||
| CGRect adjustedKeyboardFrame = keyboardFrame; | ||
| adjustedKeyboardFrame.origin.y += [self calculateMultitaskingAdjustment:screenRect | ||
| keyboardFrame:keyboardFrame]; | ||
|
|
||
| // If the keyboard is partially or fully showing within the screen, it's either docked or | ||
| // floating. Sometimes with custom keyboard extensions, the keyboard's position may be off by a | ||
| // small decimal amount. Round to compare. | ||
| CGRect intersection = CGRectIntersection(adjustedKeyboardFrame, screenRect); | ||
vashworth marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| CGFloat intersectionHeight = CGRectGetHeight(intersection); | ||
| CGFloat intersectionWidth = CGRectGetWidth(intersection); | ||
| if (round(intersectionHeight) > 0 && intersectionWidth > 0) { | ||
| // If the keyboard is above the bottom of the screen, it's floating. | ||
| CGFloat screenHeight = CGRectGetHeight(screenRect); | ||
| CGFloat adjustedKeyboardBottom = CGRectGetMaxY(adjustedKeyboardFrame); | ||
| if (round(adjustedKeyboardBottom) < screenHeight) { | ||
| return FlutterKeyboardModeFloating; | ||
| } | ||
| return FlutterKeyboardModeDocked; | ||
| } | ||
| return FlutterKeyboardModeHidden; | ||
| } | ||
|
|
||
| - (CGFloat)calculateMultitaskingAdjustment:(CGRect)screenRect keyboardFrame:(CGRect)keyboardFrame { | ||
| // In Slide Over mode, the keyboard's frame 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. | ||
| if (self.viewIfLoaded.traitCollection.userInterfaceIdiom == UIUserInterfaceIdiomPad && | ||
| self.viewIfLoaded.traitCollection.horizontalSizeClass == UIUserInterfaceSizeClassCompact && | ||
| self.viewIfLoaded.traitCollection.verticalSizeClass == UIUserInterfaceSizeClassRegular) { | ||
| CGFloat screenHeight = CGRectGetHeight(screenRect); | ||
| CGFloat keyboardBottom = CGRectGetMaxY(keyboardFrame); | ||
|
|
||
| // Stage Manager mode will also meet the above parameters, but it does not handle | ||
| // the keyboard positioning the same way, so skip if keyboard is at bottom of page. | ||
| if (screenHeight == keyboardBottom) { | ||
| return 0; | ||
| } | ||
| CGRect viewRectRelativeToScreen = | ||
| [self.viewIfLoaded convertRect:self.viewIfLoaded.frame | ||
| toCoordinateSpace:[self mainScreen].coordinateSpace]; | ||
| CGFloat viewBottom = CGRectGetMaxY(viewRectRelativeToScreen); | ||
| CGFloat offset = screenHeight - viewBottom; | ||
| if (offset > 0) { | ||
| return offset; | ||
| } | ||
| } | ||
| return 0; | ||
| } | ||
|
|
||
| - (CGFloat)calculateKeyboardInset:(CGRect)keyboardFrame keyboardMode:(NSInteger)keyboardMode { | ||
| // Only docked keyboards will have an inset. | ||
| if (keyboardMode == FlutterKeyboardModeDocked) { | ||
| // Calculate how much of the keyboard intersects with the view. | ||
| CGRect viewRectRelativeToScreen = | ||
| [self.viewIfLoaded convertRect:self.viewIfLoaded.frame | ||
| toCoordinateSpace:[self mainScreen].coordinateSpace]; | ||
| CGRect intersection = CGRectIntersection(keyboardFrame, viewRectRelativeToScreen); | ||
| CGFloat portionOfKeyboardInView = CGRectGetHeight(intersection); | ||
|
|
||
| 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 | ||
| self.targetViewInsetBottom = 0; | ||
| NSTimeInterval duration = | ||
| [[info objectForKey:UIKeyboardAnimationDurationUserInfoKey] doubleValue]; | ||
| [self startKeyBoardAnimation:duration]; | ||
| // 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 = [self mainScreen].scale; | ||
| return portionOfKeyboardInView * scale; | ||
| } | ||
| return 0; | ||
| } | ||
|
|
||
| - (void)startKeyBoardAnimation:(NSTimeInterval)duration { | ||
|
|
||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
thanks for fixing all of these.