Files
midi_ios/Pods/YYText/YYText/Component/YYTextKeyboardManager.m
2025-08-14 10:07:49 +08:00

522 lines
18 KiB
Objective-C

//
// 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