| // Copyright 2016-present the Material Components for iOS authors. All Rights Reserved. |
| // |
| // Licensed under the Apache License, Version 2.0 (the "License"); |
| // you may not use this file except in compliance with the License. |
| // You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS IS" BASIS, |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| // See the License for the specific language governing permissions and |
| // limitations under the License. |
| |
| #import "MDCKeyboardWatcher.h" |
| |
| #import <CoreGraphics/CoreGraphics.h> |
| |
| #import "MaterialApplication.h" |
| |
| NS_ASSUME_NONNULL_BEGIN |
| |
| NSString *const MDCKeyboardWatcherKeyboardWillShowNotification = |
| @"MDCKeyboardWatcherKeyboardWillShowNotification"; |
| NSString *const MDCKeyboardWatcherKeyboardWillHideNotification = |
| @"MDCKeyboardWatcherKeyboardWillHideNotification"; |
| NSString *const MDCKeyboardWatcherKeyboardWillChangeFrameNotification = |
| @"MDCKeyboardWatcherKeyboardWillChangeFrameNotification"; |
| |
| static MDCKeyboardWatcher *_sKeyboardWatcher; |
| |
| @interface MDCKeyboardWatcher () |
| |
| /** The keyboard's frame, in rotation-compensated screen coordinates. */ |
| @property(nonatomic) CGRect keyboardFrame; |
| |
| @end |
| |
| @implementation MDCKeyboardWatcher |
| |
| // Because at the time of writing, there is no public API for answering the question: "Is the |
| // keyboard currently showing?", we must watch the keyboard's show/hide notifications and maintain |
| // that state on our own. The only time early enough to start watching the keyboard is at +load, so |
| // we must create a watcher then. |
| + (void)load { |
| @autoreleasepool { |
| _sKeyboardWatcher = [[MDCKeyboardWatcher alloc] init]; |
| } |
| } |
| |
| + (instancetype)sharedKeyboardWatcher { |
| return _sKeyboardWatcher; |
| } |
| |
| - (instancetype)init { |
| self = [super init]; |
| if (self) { |
| NSNotificationCenter *defaultCenter = [NSNotificationCenter defaultCenter]; |
| [defaultCenter addObserver:self |
| selector:@selector(keyboardWillShow:) |
| name:UIKeyboardWillShowNotification |
| object:nil]; |
| |
| [defaultCenter addObserver:self |
| selector:@selector(keyboardWillHide:) |
| name:UIKeyboardWillHideNotification |
| object:nil]; |
| |
| [defaultCenter addObserver:self |
| selector:@selector(keyboardWillChangeFrame:) |
| name:UIKeyboardWillChangeFrameNotification |
| object:nil]; |
| } |
| |
| return self; |
| } |
| |
| #pragma mark - Keyboard Notifications |
| |
| - (void)updateKeyboardOffsetWithKeyboardUserInfo:(NSDictionary *)userInfo { |
| // On iOS 8, the window orientation is corrected logically after transforms, so there is |
| // no need to swap the width and height like we had to on iOS 7 and below.. |
| CGRect keyboardRect = [userInfo[UIKeyboardFrameEndUserInfoKey] CGRectValue]; |
| |
| // iOS 8 doesn't notify us of a new keyboard rect when a keyboard dock or undocks to/from the |
| // bottom of the screen. The more common case of no frame is a keyboard undocking and moving |
| // around, so on iOS 8 we'll take a missing frame to indicate an undocked keyboard. Unfortunately |
| // when the keyboard is re-docked, we won't know, and won't be able to re-update the offset. |
| // This also means that our "failure" mode is at the bottom of the screen, so we shouldn't get |
| // into a situation where the offset is too far up with no keyboard on screen. |
| if (CGRectIsEmpty(keyboardRect)) { |
| // Set the offset to zero, as if the keyboard was undocked. |
| self.keyboardFrame = CGRectZero; |
| return; |
| } |
| |
| #if defined(TARGET_OS_VISION) && TARGET_OS_VISION |
| // For code review, use the review queue listed inĀ go/material-visionos-review. |
| |
| // The keyboard on visionOS is undocked |
| self.keyboardFrame = CGRectZero; |
| #else |
| CGRect keyWindowBounds = [UIApplication mdc_safeSharedApplication].keyWindow.bounds; |
| CGRect screenBounds = [[UIScreen mainScreen] bounds]; |
| CGRect intersection = CGRectIntersection(screenBounds, keyboardRect); |
| |
| // If the extent of the keyboard is at or below the bottom of the screen it is docked. |
| // This handles the case of an external keyboard on iOS8+ where the entire frame of the keyboard |
| // view is used, but on the top, the input accessory section is show. |
| BOOL dockedKeyboard = CGRectGetMaxY(keyWindowBounds) <= CGRectGetMaxY(keyboardRect); |
| |
| // If the bottom of the keyboard isn't at the bottom of the screen, then it is undocked, and we |
| // shouldn't try to account for it. |
| if (dockedKeyboard && !CGRectIsEmpty(intersection)) { |
| self.keyboardFrame = intersection; |
| } else { |
| self.keyboardFrame = CGRectZero; |
| } |
| #endif |
| } |
| |
| - (CGFloat)visibleKeyboardHeight { |
| return CGRectGetHeight(self.keyboardFrame); |
| } |
| |
| + (NSTimeInterval)animationDurationFromKeyboardNotification:(NSNotification *)notification { |
| if (![notification.name isEqualToString:MDCKeyboardWatcherKeyboardWillShowNotification] && |
| ![notification.name isEqualToString:MDCKeyboardWatcherKeyboardWillHideNotification] && |
| ![notification.name isEqualToString:MDCKeyboardWatcherKeyboardWillChangeFrameNotification]) { |
| NSAssert(NO, @"Cannot extract the animation duration from a non-keyboard notification."); |
| |
| return 0.0; |
| } |
| |
| NSNumber *animationDurationNumber = notification.userInfo[UIKeyboardAnimationDurationUserInfoKey]; |
| NSTimeInterval animationDuration = (NSTimeInterval)[animationDurationNumber doubleValue]; |
| |
| return animationDuration; |
| } |
| |
| /** Convert UIViewAnimationCurve to UIViewAnimationOptions */ |
| static UIViewAnimationOptions animationOptionsWithCurve(UIViewAnimationCurve animationCurve) { |
| switch (animationCurve) { |
| case UIViewAnimationCurveEaseInOut: |
| return UIViewAnimationOptionCurveEaseInOut; |
| case UIViewAnimationCurveEaseIn: |
| return UIViewAnimationOptionCurveEaseIn; |
| case UIViewAnimationCurveEaseOut: |
| return UIViewAnimationOptionCurveEaseOut; |
| case UIViewAnimationCurveLinear: |
| return UIViewAnimationOptionCurveLinear; |
| } |
| |
| // UIKit unpredictably returns values that aren't declared in UIViewAnimationCurve, so we can't |
| // assert here. |
| // UIKeyboardWillChangeFrameNotification can post with a curve of 7. |
| // Based on how UIViewAnimationOptions are defined in UIView.h, (animationCurve << 16) may an |
| // be acceptable return value for unrecognized curves. |
| return UIViewAnimationOptionCurveEaseInOut; |
| } |
| |
| + (UIViewAnimationOptions)animationCurveOptionFromKeyboardNotification: |
| (NSNotification *)notification { |
| if (![notification.name isEqualToString:MDCKeyboardWatcherKeyboardWillShowNotification] && |
| ![notification.name isEqualToString:MDCKeyboardWatcherKeyboardWillHideNotification] && |
| ![notification.name isEqualToString:MDCKeyboardWatcherKeyboardWillChangeFrameNotification]) { |
| NSAssert(NO, @"Cannot extract the animation curve option from a non-keyboard notification."); |
| |
| return UIViewAnimationOptionCurveEaseInOut; |
| } |
| |
| NSNumber *animationCurveNumber = notification.userInfo[UIKeyboardAnimationCurveUserInfoKey]; |
| UIViewAnimationCurve animationCurve = (UIViewAnimationCurve)[animationCurveNumber integerValue]; |
| UIViewAnimationOptions animationCurveOption = animationOptionsWithCurve(animationCurve); |
| |
| return animationCurveOption; |
| } |
| |
| #pragma mark - Notifications |
| |
| - (void)keyboardWillShow:(NSNotification *)notification { |
| [self updateKeyboardOffsetWithKeyboardUserInfo:notification.userInfo]; |
| [[NSNotificationCenter defaultCenter] |
| postNotificationName:MDCKeyboardWatcherKeyboardWillShowNotification |
| object:self |
| userInfo:notification.userInfo]; |
| } |
| |
| - (void)keyboardWillChangeFrame:(NSNotification *)notification { |
| [self updateKeyboardOffsetWithKeyboardUserInfo:notification.userInfo]; |
| [[NSNotificationCenter defaultCenter] |
| postNotificationName:MDCKeyboardWatcherKeyboardWillChangeFrameNotification |
| object:self |
| userInfo:notification.userInfo]; |
| } |
| |
| - (void)keyboardWillHide:(NSNotification *)notification { |
| // If we are going to be hidden, it doesn't matter what the keyboard rects are, the keyboard |
| // offset is 0. This applies in scenarios where a keyboard is showing on a view controller inside |
| // of a navigation controller, and that controller is popped. Instead of the normal keyboard |
| // vertical dismiss animation, the keyboard actually slides away with the view controller. In that |
| // scenario, the keyboard dictionaries do not reflect the keyboard going to the bottom of the |
| // screen. As such, we need to take into account the extra knowledge that the keyboard is being |
| // hidden, and drive the keyboard offset that way. |
| self.keyboardFrame = CGRectZero; |
| [[NSNotificationCenter defaultCenter] |
| postNotificationName:MDCKeyboardWatcherKeyboardWillHideNotification |
| object:self |
| userInfo:notification.userInfo]; |
| } |
| |
| @end |
| |
| NS_ASSUME_NONNULL_END |