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
auto-submit
merged 17 commits into
flutter-team-archive:main
from
vashworth:keyboard_fixes
Dec 14, 2022
Merged
Changes from 7 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
| 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*)getMainScreen { | ||
| if (@available(iOS 13.0, *)) { | ||
| return self.view.window.windowScene.screen; | ||
| } | ||
| return UIScreen.mainScreen; | ||
| } | ||
|
|
||
| - (BOOL)loadDefaultSplashScreenView { | ||
| NSString* launchscreenName = | ||
| [[[NSBundle mainBundle] infoDictionary] objectForKey:@"UILaunchStoryboardName"]; | ||
|
|
@@ -1272,65 +1284,220 @@ - (void)updateViewportPadding { | |
|
|
||
| #pragma mark - Keyboard events | ||
|
|
||
| typedef NS_ENUM(NSInteger, FlutterKeyboardMode) { | ||
|
vashworth marked this conversation as resolved.
Outdated
|
||
| FlutterKeyboardModeHidden = 0, | ||
| FlutterKeyboardModeDocked = 1, | ||
| FlutterKeyboardModeFloating = 2, | ||
| }; | ||
|
|
||
| - (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 notificationName:UIKeyboardWillShowNotification]; | ||
|
vashworth marked this conversation as resolved.
Outdated
|
||
| } | ||
|
|
||
| - (void)keyboardWillChangeFrame:(NSNotification*)notification { | ||
| NSDictionary* info = [notification userInfo]; | ||
| // 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. | ||
| [self handleKeyboardNotification:notification | ||
| notificationName:UIKeyboardWillChangeFrameNotification]; | ||
| } | ||
|
|
||
| // Ignore keyboard notifications related to other apps. | ||
| id isLocal = info[UIKeyboardIsLocalUserInfoKey]; | ||
| if (isLocal && ![isLocal boolValue]) { | ||
| return; | ||
| } | ||
| - (void)keyboardWillBeHidden:(NSNotification*)notification { | ||
| // When keyboard is hidden or undocked, this notification will be triggered. | ||
|
vashworth marked this conversation as resolved.
|
||
| [self handleKeyboardNotification:notification notificationName:UIKeyboardWillHideNotification]; | ||
| } | ||
|
|
||
| // Ignore keyboard notifications if engine’s viewController is not current viewController. | ||
| if ([_engine.get() viewController] != self) { | ||
| - (void)handleKeyboardNotification:(NSNotification*)notification | ||
| notificationName:(NSNotificationName)notificationName { | ||
| BOOL ignoreNotification = [self shouldIgnoreKeyboardNotification:notification | ||
| notificationName:notificationName]; | ||
| if (ignoreNotification) { | ||
|
vashworth marked this conversation as resolved.
Outdated
|
||
| return; | ||
| } | ||
|
|
||
| NSDictionary* info = notification.userInfo; | ||
| CGRect keyboardFrame = [[info objectForKey:UIKeyboardFrameEndUserInfoKey] CGRectValue]; | ||
|
vashworth marked this conversation as resolved.
Outdated
|
||
| CGRect screenRect = [[UIScreen mainScreen] bounds]; | ||
| FlutterKeyboardMode keyboardMode = [self calculateKeyboardAttachMode:keyboardFrame | ||
| notificationName:notificationName]; | ||
| CGFloat calculatedInset = [self calculateKeyboardInset:keyboardFrame keyboardMode:keyboardMode]; | ||
|
|
||
| // Get the animation duration | ||
| // Avoid double triggering startKeyBoardAnimation. | ||
|
vashworth marked this conversation as resolved.
|
||
| 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 { | ||
| NSDictionary* info = [notification userInfo]; | ||
| - (BOOL)shouldIgnoreKeyboardNotification:(NSNotification*)notification | ||
| notificationName:(NSNotificationName)notificationName { | ||
|
vashworth marked this conversation as resolved.
Outdated
|
||
| BOOL isKeyboardNotificationForThisView = [self isKeyboardNotificationForThisView:notification]; | ||
| BOOL isKeyboardRotated = [self isKeyboardRotated:notification]; | ||
|
vashworth marked this conversation as resolved.
Outdated
|
||
|
|
||
| // Don't ignore UIKeyboardWillHideNotification notifications. | ||
|
vashworth marked this conversation as resolved.
|
||
| if (notificationName == UIKeyboardWillHideNotification) { | ||
| // Skip hide notification when rotation in progress unless triggered by another app. | ||
|
vashworth marked this conversation as resolved.
Outdated
|
||
| if (isKeyboardRotated && isKeyboardNotificationForThisView) { | ||
| return YES; | ||
| } | ||
| return NO; | ||
| } | ||
|
|
||
| // 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 objectForKey:UIKeyboardFrameEndUserInfoKey] CGRectValue]; | ||
|
vashworth marked this conversation as resolved.
Outdated
|
||
| if (notificationName == UIKeyboardWillChangeFrameNotification && | ||
| CGRectEqualToRect(keyboardFrame, CGRectZero)) { | ||
| return YES; | ||
| } | ||
|
|
||
| // Don't ignore other times a keyboard's height or width is 0. | ||
|
vashworth marked this conversation as resolved.
Outdated
|
||
| if (CGRectIsEmpty(keyboardFrame)) { | ||
| return NO; | ||
| } | ||
|
|
||
| // Ignore keyboard notifications related to other apps. | ||
|
vashworth marked this conversation as resolved.
Outdated
|
||
| if (!isKeyboardNotificationForThisView) { | ||
| return YES; | ||
| } | ||
|
|
||
| // Ignore notification when keyboard is in process of being rotated. | ||
| if (isKeyboardRotated) { | ||
| return YES; | ||
| } | ||
|
|
||
| return NO; | ||
| } | ||
|
|
||
| - (BOOL)isKeyboardNotificationForThisView:(NSNotification*)notification { | ||
| NSDictionary* info = notification.userInfo; | ||
| // Keyboard notifications related to other apps. | ||
| id isLocal = info[UIKeyboardIsLocalUserInfoKey]; | ||
| if (isLocal && ![isLocal boolValue]) { | ||
| return; | ||
| return NO; | ||
| } | ||
|
|
||
| // 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 NO; | ||
| } | ||
| return YES; | ||
| } | ||
|
|
||
| - (BOOL)isKeyboardRotated:(NSNotification*)notification { | ||
| // When the keyboard's width at the beginning of the animation equals the screen's | ||
| // current height, we can assume the keyboard was rotated. | ||
|
vashworth marked this conversation as resolved.
Outdated
|
||
| NSDictionary* info = notification.userInfo; | ||
| CGRect screenRect = [self getMainScreen].bounds; | ||
| CGFloat screenHeight = CGRectGetHeight(screenRect); | ||
| CGRect keyboardEndFrame = [[info objectForKey:UIKeyboardFrameEndUserInfoKey] CGRectValue]; | ||
| CGRect keyboardBeginFrame = [[info objectForKey:UIKeyboardFrameBeginUserInfoKey] CGRectValue]; | ||
| CGFloat keyboardBeginWidth = CGRectGetWidth(keyboardBeginFrame); | ||
| if (CGRectEqualToRect(keyboardEndFrame, CGRectZero)) { | ||
| return NO; | ||
| } | ||
| if (screenHeight == keyboardBeginWidth) { | ||
| return YES; | ||
| } | ||
| return NO; | ||
| } | ||
|
|
||
| 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]; | ||
| - (FlutterKeyboardMode)calculateKeyboardAttachMode:(CGRect)keyboardFrame | ||
| notificationName:(NSNotificationName)notificationName { | ||
| // There are multiple types of keyboard: docked, undocked, split, split docked, | ||
| // floating, predictive-only, minimized. This function will categorize | ||
| // the keyboard as one of the following modes: docked, floating, or hidden. | ||
| // Docked mode includes docked, split docked, predictive-only (when opening via click), | ||
| // and minimized (when opened via click). | ||
| // Floating includes undocked, split, floating, predictive-only (when dragged and dropped), | ||
| // and minimized (when dragged and dropped). | ||
| if (notificationName == 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 getMainScreen].bounds; | ||
| CGFloat keyboardWidth = CGRectGetWidth(keyboardFrame); | ||
| CGFloat screenWidth = CGRectGetWidth(screenRect); | ||
|
|
||
| // If keyboard is not full width, it's floating. | ||
| if (keyboardWidth != screenWidth) { | ||
| return FlutterKeyboardModeFloating; | ||
| } | ||
|
|
||
| CGFloat screenHeight = CGRectGetHeight(screenRect); | ||
| CGRect adjustedKeyboardFrame = keyboardFrame; | ||
| adjustedKeyboardFrame.origin.y += [self calculateMultitaskingAdjustment:screenRect | ||
| keyboardFrame:keyboardFrame]; | ||
| CGFloat adjustedKeyboardBottom = CGRectGetMaxY(adjustedKeyboardFrame); | ||
|
|
||
| // If the keyboard is above the bottom of the screen, it's floating. | ||
| if (adjustedKeyboardBottom < screenHeight) { | ||
| return FlutterKeyboardModeFloating; | ||
| } | ||
| // If the keyboard is partially or fully showing at the bottom of the screen, it's docked. | ||
| if (CGRectIntersectsRect(adjustedKeyboardFrame, screenRect)) { | ||
| return FlutterKeyboardModeDocked; | ||
| } | ||
|
luckysmg marked this conversation as resolved.
Outdated
|
||
| 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.view.traitCollection.userInterfaceIdiom == UIUserInterfaceIdiomPad && | ||
| self.view.traitCollection.horizontalSizeClass == UIUserInterfaceSizeClassCompact && | ||
| self.view.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 viewFrameInScreen = [self.view convertRect:self.view.frame | ||
| toCoordinateSpace:[self getMainScreen].coordinateSpace]; | ||
| CGFloat viewBottom = CGRectGetMaxY(viewFrameInScreen); | ||
| 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 viewFrameInScreen = [self.view convertRect:self.view.frame | ||
| toCoordinateSpace:[self getMainScreen].coordinateSpace]; | ||
| CGRect intersection = CGRectIntersection(keyboardFrame, viewFrameInScreen); | ||
| CGFloat portionOfKeyboardInView = CGRectGetHeight(intersection); | ||
|
|
||
| // 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 getMainScreen].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.
Uh oh!
There was an error while loading. Please reload this page.