//
|
// YYTextKeyboardManager.m
|
// YYText <https://github.com/ibireme/YYText>
|
//
|
// Created by ibireme on 15/6/3.
|
// Copyright (c) 2015 ibireme.
|
//
|
// This source code is licensed under the MIT-style license found in the
|
// LICENSE file in the root directory of this source tree.
|
//
|
|
#import "YYTextKeyboardManager.h"
|
#import "YYTextUtilities.h"
|
#import <objc/runtime.h>
|
|
|
static int _YYTextKeyboardViewFrameObserverKey;
|
|
/// Observer for view's frame/bounds/center/transform
|
@interface _YYTextKeyboardViewFrameObserver : NSObject
|
@property (nonatomic, copy) void (^notifyBlock)(UIView *keyboard);
|
- (void)addToKeyboardView:(UIView *)keyboardView;
|
+ (instancetype)observerForView:(UIView *)keyboardView;
|
@end
|
|
|
@implementation _YYTextKeyboardViewFrameObserver {
|
__unsafe_unretained UIView *_keyboardView;
|
}
|
- (void)addToKeyboardView:(UIView *)keyboardView {
|
if (_keyboardView == keyboardView) return;
|
if (_keyboardView) {
|
[self removeFrameObserver];
|
objc_setAssociatedObject(_keyboardView, &_YYTextKeyboardViewFrameObserverKey, nil, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
|
}
|
_keyboardView = keyboardView;
|
if (keyboardView) {
|
[self addFrameObserver];
|
}
|
objc_setAssociatedObject(keyboardView, &_YYTextKeyboardViewFrameObserverKey, self, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
|
}
|
|
- (void)removeFrameObserver {
|
[_keyboardView removeObserver:self forKeyPath:@"frame"];
|
[_keyboardView removeObserver:self forKeyPath:@"center"];
|
[_keyboardView removeObserver:self forKeyPath:@"bounds"];
|
[_keyboardView removeObserver:self forKeyPath:@"transform"];
|
_keyboardView = nil;
|
}
|
|
- (void)addFrameObserver {
|
if (!_keyboardView) return;
|
[_keyboardView addObserver:self forKeyPath:@"frame" options:kNilOptions context:NULL];
|
[_keyboardView addObserver:self forKeyPath:@"center" options:kNilOptions context:NULL];
|
[_keyboardView addObserver:self forKeyPath:@"bounds" options:kNilOptions context:NULL];
|
[_keyboardView addObserver:self forKeyPath:@"transform" options:kNilOptions context:NULL];
|
}
|
|
- (void)dealloc {
|
[self removeFrameObserver];
|
}
|
|
+ (instancetype)observerForView:(UIView *)keyboardView {
|
if (!keyboardView) return nil;
|
return objc_getAssociatedObject(keyboardView, &_YYTextKeyboardViewFrameObserverKey);
|
}
|
|
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
|
|
BOOL isPrior = [[change objectForKey:NSKeyValueChangeNotificationIsPriorKey] boolValue];
|
if (isPrior) return;
|
|
NSKeyValueChange changeKind = [[change objectForKey:NSKeyValueChangeKindKey] integerValue];
|
if (changeKind != NSKeyValueChangeSetting) return;
|
|
id newVal = [change objectForKey:NSKeyValueChangeNewKey];
|
if (newVal == [NSNull null]) newVal = nil;
|
|
if (_notifyBlock) {
|
_notifyBlock(_keyboardView);
|
}
|
}
|
|
@end
|
|
|
|
@implementation YYTextKeyboardManager {
|
NSHashTable *_observers;
|
|
CGRect _fromFrame;
|
BOOL _fromVisible;
|
UIInterfaceOrientation _fromOrientation;
|
|
CGRect _notificationFromFrame;
|
CGRect _notificationToFrame;
|
NSTimeInterval _notificationDuration;
|
UIViewAnimationCurve _notificationCurve;
|
BOOL _hasNotification;
|
|
CGRect _observedToFrame;
|
BOOL _hasObservedChange;
|
|
BOOL _lastIsNotification;
|
}
|
|
- (instancetype)init {
|
@throw [NSException exceptionWithName:@"YYTextKeyboardManager init error" reason:@"Use 'defaultManager' to get instance." userInfo:nil];
|
return [super init];
|
}
|
|
- (instancetype)_init {
|
self = [super init];
|
_observers = [[NSHashTable alloc] initWithOptions:NSPointerFunctionsWeakMemory|NSPointerFunctionsObjectPointerPersonality capacity:0];
|
[[NSNotificationCenter defaultCenter] addObserver:self
|
selector:@selector(_keyboardFrameWillChangeNotification:)
|
name:UIKeyboardWillChangeFrameNotification
|
object:nil];
|
// for iPad (iOS 9)
|
if ([UIDevice currentDevice].systemVersion.floatValue >= 9) {
|
[[NSNotificationCenter defaultCenter] addObserver:self
|
selector:@selector(_keyboardFrameDidChangeNotification:)
|
name:UIKeyboardDidChangeFrameNotification
|
object:nil];
|
}
|
return self;
|
}
|
|
- (void)_initFrameObserver {
|
UIView *keyboardView = self.keyboardView;
|
if (!keyboardView) return;
|
__weak typeof(self) _self = self;
|
_YYTextKeyboardViewFrameObserver *observer = [_YYTextKeyboardViewFrameObserver observerForView:keyboardView];
|
if (!observer) {
|
observer = [_YYTextKeyboardViewFrameObserver new];
|
observer.notifyBlock = ^(UIView *keyboard) {
|
[_self _keyboardFrameChanged:keyboard];
|
};
|
[observer addToKeyboardView:keyboardView];
|
}
|
}
|
|
- (void)dealloc {
|
[[NSNotificationCenter defaultCenter] removeObserver:self];
|
}
|
|
+ (instancetype)defaultManager {
|
static YYTextKeyboardManager *mgr = nil;
|
static dispatch_once_t onceToken;
|
dispatch_once(&onceToken, ^{
|
if (!YYTextIsAppExtension()) {
|
mgr = [[self alloc] _init];
|
}
|
});
|
return mgr;
|
}
|
|
+ (void)load {
|
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
|
[self defaultManager];
|
});
|
}
|
|
- (void)addObserver:(id<YYTextKeyboardObserver>)observer {
|
if (!observer) return;
|
[_observers addObject:observer];
|
}
|
|
- (void)removeObserver:(id<YYTextKeyboardObserver>)observer {
|
if (!observer) return;
|
[_observers removeObject:observer];
|
}
|
|
- (UIWindow *)keyboardWindow {
|
UIApplication *app = YYTextSharedApplication();
|
if (!app) return nil;
|
|
UIWindow *window = nil;
|
for (window in app.windows) {
|
if ([self _getKeyboardViewFromWindow:window]) return window;
|
}
|
window = app.keyWindow;
|
if ([self _getKeyboardViewFromWindow:window]) return window;
|
|
NSMutableArray *kbWindows = nil;
|
for (window in app.windows) {
|
NSString *windowName = NSStringFromClass(window.class);
|
if ([self _systemVersion] < 9) {
|
// UITextEffectsWindow
|
if (windowName.length == 19 &&
|
[windowName hasPrefix:@"UI"] &&
|
[windowName hasSuffix:@"TextEffectsWindow"]) {
|
if (!kbWindows) kbWindows = [NSMutableArray new];
|
[kbWindows addObject:window];
|
}
|
} else {
|
// UIRemoteKeyboardWindow
|
if (windowName.length == 22 &&
|
[windowName hasPrefix:@"UI"] &&
|
[windowName hasSuffix:@"RemoteKeyboardWindow"]) {
|
if (!kbWindows) kbWindows = [NSMutableArray new];
|
[kbWindows addObject:window];
|
}
|
}
|
}
|
|
if (kbWindows.count == 1) {
|
return kbWindows.firstObject;
|
}
|
return nil;
|
}
|
|
- (UIView *)keyboardView {
|
UIApplication *app = YYTextSharedApplication();
|
if (!app) return nil;
|
|
UIWindow *window = nil;
|
UIView *view = nil;
|
for (window in app.windows) {
|
view = [self _getKeyboardViewFromWindow:window];
|
if (view) return view;
|
}
|
window = app.keyWindow;
|
view = [self _getKeyboardViewFromWindow:window];
|
if (view) return view;
|
return nil;
|
}
|
|
- (BOOL)isKeyboardVisible {
|
UIWindow *window = self.keyboardWindow;
|
if (!window) return NO;
|
UIView *view = self.keyboardView;
|
if (!view) return NO;
|
CGRect rect = CGRectIntersection(window.bounds, view.frame);
|
if (CGRectIsNull(rect)) return NO;
|
if (CGRectIsInfinite(rect)) return NO;
|
return rect.size.width > 0 && rect.size.height > 0;
|
}
|
|
- (CGRect)keyboardFrame {
|
UIView *keyboard = [self keyboardView];
|
if (!keyboard) return CGRectNull;
|
|
CGRect frame = CGRectNull;
|
UIWindow *window = keyboard.window;
|
if (window) {
|
frame = [window convertRect:keyboard.frame toWindow:nil];
|
} else {
|
frame = keyboard.frame;
|
}
|
return frame;
|
}
|
|
#pragma mark - private
|
|
- (double)_systemVersion {
|
static double v;
|
static dispatch_once_t onceToken;
|
dispatch_once(&onceToken, ^{
|
v = [UIDevice currentDevice].systemVersion.doubleValue;
|
});
|
return v;
|
}
|
|
- (UIView *)_getKeyboardViewFromWindow:(UIWindow *)window {
|
/*
|
iOS 6/7:
|
UITextEffectsWindow
|
UIPeripheralHostView << keyboard
|
|
iOS 8:
|
UITextEffectsWindow
|
UIInputSetContainerView
|
UIInputSetHostView << keyboard
|
|
iOS 9:
|
UIRemoteKeyboardWindow
|
UIInputSetContainerView
|
UIInputSetHostView << keyboard
|
*/
|
if (!window) return nil;
|
|
// Get the window
|
NSString *windowName = NSStringFromClass(window.class);
|
if ([self _systemVersion] < 9) {
|
// UITextEffectsWindow
|
if (windowName.length != 19) return nil;
|
if (![windowName hasPrefix:@"UI"]) return nil;
|
if (![windowName hasSuffix:@"TextEffectsWindow"]) return nil;
|
} else {
|
// UIRemoteKeyboardWindow
|
if (windowName.length != 22) return nil;
|
if (![windowName hasPrefix:@"UI"]) return nil;
|
if (![windowName hasSuffix:@"RemoteKeyboardWindow"]) return nil;
|
}
|
|
// Get the view
|
if ([self _systemVersion] < 8) {
|
// UIPeripheralHostView
|
for (UIView *view in window.subviews) {
|
NSString *viewName = NSStringFromClass(view.class);
|
if (viewName.length != 20) continue;
|
if (![viewName hasPrefix:@"UI"]) continue;
|
if (![viewName hasSuffix:@"PeripheralHostView"]) continue;
|
return view;
|
}
|
} else {
|
// UIInputSetContainerView
|
for (UIView *view in window.subviews) {
|
NSString *viewName = NSStringFromClass(view.class);
|
if (viewName.length != 23) continue;
|
if (![viewName hasPrefix:@"UI"]) continue;
|
if (![viewName hasSuffix:@"InputSetContainerView"]) continue;
|
// UIInputSetHostView
|
for (UIView *subView in view.subviews) {
|
NSString *subViewName = NSStringFromClass(subView.class);
|
if (subViewName.length != 18) continue;
|
if (![subViewName hasPrefix:@"UI"]) continue;
|
if (![subViewName hasSuffix:@"InputSetHostView"]) continue;
|
return subView;
|
}
|
}
|
}
|
|
return nil;
|
}
|
|
- (void)_keyboardFrameWillChangeNotification:(NSNotification *)notif {
|
if (![notif.name isEqualToString:UIKeyboardWillChangeFrameNotification]) return;
|
NSDictionary *info = notif.userInfo;
|
if (!info) return;
|
|
[self _initFrameObserver];
|
|
NSValue *beforeValue = info[UIKeyboardFrameBeginUserInfoKey];
|
NSValue *afterValue = info[UIKeyboardFrameEndUserInfoKey];
|
NSNumber *curveNumber = info[UIKeyboardAnimationCurveUserInfoKey];
|
NSNumber *durationNumber = info[UIKeyboardAnimationDurationUserInfoKey];
|
|
CGRect before = beforeValue.CGRectValue;
|
CGRect after = afterValue.CGRectValue;
|
UIViewAnimationCurve curve = curveNumber.integerValue;
|
NSTimeInterval duration = durationNumber.doubleValue;
|
|
// ignore zero end frame
|
if (after.size.width <= 0 && after.size.height <= 0) return;
|
|
_notificationFromFrame = before;
|
_notificationToFrame = after;
|
_notificationCurve = curve;
|
_notificationDuration = duration;
|
_hasNotification = YES;
|
_lastIsNotification = YES;
|
|
[NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(_notifyAllObservers) object:nil];
|
if (duration == 0) {
|
[self performSelector:@selector(_notifyAllObservers) withObject:nil afterDelay:0 inModes:@[NSRunLoopCommonModes]];
|
} else {
|
[self _notifyAllObservers];
|
}
|
}
|
|
- (void)_keyboardFrameDidChangeNotification:(NSNotification *)notif {
|
if (![notif.name isEqualToString:UIKeyboardDidChangeFrameNotification]) return;
|
NSDictionary *info = notif.userInfo;
|
if (!info) return;
|
|
[self _initFrameObserver];
|
|
NSValue *afterValue = info[UIKeyboardFrameEndUserInfoKey];
|
CGRect after = afterValue.CGRectValue;
|
|
// ignore zero end frame
|
if (after.size.width <= 0 && after.size.height <= 0) return;
|
|
_notificationToFrame = after;
|
_notificationCurve = UIViewAnimationCurveEaseInOut;
|
_notificationDuration = 0;
|
_hasNotification = YES;
|
_lastIsNotification = YES;
|
|
[NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(_notifyAllObservers) object:nil];
|
[self performSelector:@selector(_notifyAllObservers) withObject:nil afterDelay:0 inModes:@[NSRunLoopCommonModes]];
|
}
|
|
- (void)_keyboardFrameChanged:(UIView *)keyboard {
|
if (keyboard != self.keyboardView) return;
|
|
UIWindow *window = keyboard.window;
|
if (window) {
|
_observedToFrame = [window convertRect:keyboard.frame toWindow:nil];
|
} else {
|
_observedToFrame = keyboard.frame;
|
}
|
_hasObservedChange = YES;
|
_lastIsNotification = NO;
|
|
[NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(_notifyAllObservers) object:nil];
|
[self performSelector:@selector(_notifyAllObservers) withObject:nil afterDelay:0 inModes:@[NSRunLoopCommonModes]];
|
}
|
|
- (void)_notifyAllObservers {
|
UIApplication *app = YYTextSharedApplication();
|
if (!app) return;
|
|
UIView *keyboard = self.keyboardView;
|
UIWindow *window = keyboard.window;
|
if (!window) {
|
window = app.keyWindow;
|
}
|
if (!window) {
|
window = app.windows.firstObject;
|
}
|
|
YYTextKeyboardTransition trans = {0};
|
|
// from
|
if (_fromFrame.size.width == 0 && _fromFrame.size.height == 0) { // first notify
|
_fromFrame.size.width = window.bounds.size.width;
|
_fromFrame.size.height = trans.toFrame.size.height;
|
_fromFrame.origin.x = trans.toFrame.origin.x;
|
_fromFrame.origin.y = window.bounds.size.height;
|
}
|
trans.fromFrame = _fromFrame;
|
trans.fromVisible = _fromVisible;
|
|
// to
|
if (_lastIsNotification || (_hasObservedChange && CGRectEqualToRect(_observedToFrame, _notificationToFrame))) {
|
trans.toFrame = _notificationToFrame;
|
trans.animationDuration = _notificationDuration;
|
trans.animationCurve = _notificationCurve;
|
trans.animationOption = _notificationCurve << 16;
|
|
// Fix iPad(iOS7) keyboard frame error after rorate device when the keyboard is not docked to bottom.
|
if (((int)[self _systemVersion]) == 7) {
|
UIInterfaceOrientation ori = app.statusBarOrientation;
|
if (_fromOrientation != UIInterfaceOrientationUnknown && _fromOrientation != ori) {
|
switch (ori) {
|
case UIInterfaceOrientationPortrait: {
|
if (CGRectGetMaxY(trans.toFrame) != window.frame.size.height) {
|
trans.toFrame.origin.y -= trans.toFrame.size.height;
|
}
|
} break;
|
case UIInterfaceOrientationPortraitUpsideDown: {
|
if (CGRectGetMinY(trans.toFrame) != 0) {
|
trans.toFrame.origin.y += trans.toFrame.size.height;
|
}
|
} break;
|
case UIInterfaceOrientationLandscapeLeft: {
|
if (CGRectGetMaxX(trans.toFrame) != window.frame.size.width) {
|
trans.toFrame.origin.x -= trans.toFrame.size.width;
|
}
|
} break;
|
case UIInterfaceOrientationLandscapeRight: {
|
if (CGRectGetMinX(trans.toFrame) != 0) {
|
trans.toFrame.origin.x += trans.toFrame.size.width;
|
}
|
} break;
|
default: break;
|
}
|
}
|
}
|
} else {
|
trans.toFrame = _observedToFrame;
|
}
|
|
if (window && trans.toFrame.size.width > 0 && trans.toFrame.size.height > 0) {
|
CGRect rect = CGRectIntersection(window.bounds, trans.toFrame);
|
if (!CGRectIsNull(rect) && !CGRectIsEmpty(rect)) {
|
trans.toVisible = YES;
|
}
|
}
|
|
if (!CGRectEqualToRect(trans.toFrame, _fromFrame)) {
|
for (id<YYTextKeyboardObserver> observer in _observers.copy) {
|
if ([observer respondsToSelector:@selector(keyboardChangedWithTransition:)]) {
|
[observer keyboardChangedWithTransition:trans];
|
}
|
}
|
}
|
|
_hasNotification = NO;
|
_hasObservedChange = NO;
|
_fromFrame = trans.toFrame;
|
_fromVisible = trans.toVisible;
|
_fromOrientation = app.statusBarOrientation;
|
}
|
|
- (CGRect)convertRect:(CGRect)rect toView:(UIView *)view {
|
UIApplication *app = YYTextSharedApplication();
|
if (!app) return CGRectZero;
|
|
if (CGRectIsNull(rect)) return rect;
|
if (CGRectIsInfinite(rect)) return rect;
|
|
UIWindow *mainWindow = app.keyWindow;
|
if (!mainWindow) mainWindow = app.windows.firstObject;
|
if (!mainWindow) { // no window ?!
|
if (view) {
|
[view convertRect:rect fromView:nil];
|
} else {
|
return rect;
|
}
|
}
|
|
rect = [mainWindow convertRect:rect fromWindow:nil];
|
if (!view) return [mainWindow convertRect:rect toWindow:nil];
|
if (view == mainWindow) return rect;
|
|
UIWindow *toWindow = [view isKindOfClass:[UIWindow class]] ? (id)view : view.window;
|
if (!mainWindow || !toWindow) return [mainWindow convertRect:rect toView:view];
|
if (mainWindow == toWindow) return [mainWindow convertRect:rect toView:view];
|
|
// in different window
|
rect = [mainWindow convertRect:rect toView:mainWindow];
|
rect = [toWindow convertRect:rect fromWindow:mainWindow];
|
rect = [view convertRect:rect fromView:toWindow];
|
return rect;
|
}
|
|
@end
|