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 all 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 |
---|---|---|
|
@@ -65,6 +65,7 @@ @interface FlutterViewController () <FlutterBinaryMessenger, UIScrollViewDelegat | |
*/ | ||
@property(nonatomic, assign) double targetViewInsetBottom; | ||
@property(nonatomic, retain) VSyncClient* keyboardAnimationVSyncClient; | ||
@property(nonatomic, assign) BOOL isKeyboardInOrTransitioningFromBackground; | ||
|
||
/// VSyncClient for touch events delivery frame rate correction. | ||
/// | ||
|
@@ -315,6 +316,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 +594,16 @@ - (UIView*)keyboardAnimationView { | |
return _keyboardAnimationView.get(); | ||
} | ||
|
||
- (UIScreen*)mainScreenIfViewLoaded { | ||
if (@available(iOS 13.0, *)) { | ||
if (self.viewIfLoaded == nil) { | ||
FML_LOG(WARNING) << "Trying to access the view before it is loaded."; | ||
} | ||
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"]; | ||
|
@@ -873,6 +889,7 @@ - (void)dealloc { | |
|
||
- (void)applicationBecameActive:(NSNotification*)notification { | ||
TRACE_EVENT0("flutter", "applicationBecameActive"); | ||
self.isKeyboardInOrTransitioningFromBackground = NO; | ||
if (_viewportMetrics.physical_width) { | ||
[self surfaceUpdated:YES]; | ||
} | ||
|
@@ -891,6 +908,7 @@ - (void)applicationWillTerminate:(NSNotification*)notification { | |
|
||
- (void)applicationDidEnterBackground:(NSNotification*)notification { | ||
TRACE_EVENT0("flutter", "applicationDidEnterBackground"); | ||
self.isKeyboardInOrTransitioningFromBackground = YES; | ||
[self surfaceUpdated:NO]; | ||
[self goToApplicationLifecycle:@"AppLifecycleState.paused"]; | ||
} | ||
|
@@ -1272,65 +1290,207 @@ - (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. This notification also happens | ||
// when Minimized/Expanded Shortcuts bar is dropped after dragging (the keyboard's end frame will | ||
// be CGRectZero). | ||
[self handleKeyboardNotification:notification]; | ||
} | ||
|
||
- (void)keyboardWillChangeFrame:(NSNotification*)notification { | ||
NSDictionary* info = [notification userInfo]; | ||
// Immediately prior to a change in keyboard frame, this notification is triggered. | ||
// Sometimes when the keyboard is being hidden or undocked, this notification's keyboard's end | ||
// frame is not yet entirely out of screen, which is why we also use | ||
// UIKeyboardWillHideNotification. | ||
[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
|
||
// This notification might not occur when the keyboard is changed from docked to floating, which | ||
// is why we also use UIKeyboardWillChangeFrameNotification. | ||
[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]; | ||
} | ||
|
||
- (BOOL)shouldIgnoreKeyboardNotification:(NSNotification*)notification { | ||
// Don't ignore UIKeyboardWillHideNotification notifications. | ||
vashworth marked this conversation as resolved.
Show resolved
Hide resolved
|
||
// Even if the notification is triggered in the background or by a different app/view controller, | ||
// we want to always handle this notification to avoid inaccurate inset when in a mulitasking mode | ||
// or when switching between apps. | ||
if (notification.name == UIKeyboardWillHideNotification) { | ||
return NO; | ||
} | ||
|
||
// Get the animation duration | ||
NSTimeInterval duration = | ||
[[info objectForKey:UIKeyboardAnimationDurationUserInfoKey] doubleValue]; | ||
// Ignore notification when keyboard's dimensions and position are all zeroes for | ||
// UIKeyboardWillChangeFrameNotification. This happens when keyboard is dragged. Do not ignore if | ||
// the notification is UIKeyboardWillShowNotification, as CGRectZero for that notfication only | ||
// occurs when Minimized/Expanded Shortcuts Bar is dropped after dragging, which we later use to | ||
// categorize it as floating. | ||
NSDictionary* info = notification.userInfo; | ||
CGRect keyboardFrame = [info[UIKeyboardFrameEndUserInfoKey] CGRectValue]; | ||
if (notification.name == UIKeyboardWillChangeFrameNotification && | ||
CGRectEqualToRect(keyboardFrame, CGRectZero)) { | ||
return YES; | ||
} | ||
|
||
// 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; | ||
// When keyboard's height or width is set to 0, don't ignore. This does not happen | ||
// often but can happen sometimes when switching between multitasking modes. | ||
if (CGRectIsEmpty(keyboardFrame)) { | ||
return NO; | ||
} | ||
|
||
// Ignore keyboard notifications related to other apps or view controllers. | ||
if ([self isKeyboardNotificationForDifferentView:notification]) { | ||
return YES; | ||
} | ||
|
||
if (@available(iOS 13.0, *)) { | ||
// noop | ||
} else { | ||
self.targetViewInsetBottom = 0; | ||
// If OS version is less than 13, ignore notification if the app is in the background | ||
// or is transitioning from the background. In older versions, when switching between | ||
// apps with the keyboard open in the secondary app, notifications are sent when | ||
// the app is in the background/transitioning from background as if they belong | ||
// to the app and as if the keyboard is showing even though it is not. | ||
if (self.isKeyboardInOrTransitioningFromBackground) { | ||
return YES; | ||
} | ||
} | ||
[self startKeyBoardAnimation:duration]; | ||
} | ||
|
||
- (void)keyboardWillBeHidden:(NSNotification*)notification { | ||
NSDictionary* info = [notification userInfo]; | ||
return NO; | ||
} | ||
|
||
// Ignore keyboard notifications related to other apps. | ||
- (BOOL)isKeyboardNotificationForDifferentView:(NSNotification*)notification { | ||
NSDictionary* info = notification.userInfo; | ||
// Keyboard notifications related to other apps. | ||
// If the UIKeyboardIsLocalUserInfoKey key doesn't exist (this should not happen after iOS 8), | ||
// proceed as if it was local so that the notification is not ignored. | ||
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) { | ||
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? 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. 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; | ||
} | ||
|
||
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:(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 a Minimized/Expanded | ||
// Shortcuts Bar that has been dropped after dragging, which we categorize as 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 mainScreenIfViewLoaded].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 (which is why CGRectIntersectRect can't be used). 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 mainScreenIfViewLoaded].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 mainScreenIfViewLoaded].coordinateSpace]; | ||
CGRect intersection = CGRectIntersection(keyboardFrame, viewRectRelativeToScreen); | ||
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 mainScreenIfViewLoaded].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.