YYTextKeyboardManager.m 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522
  1. //
  2. // YYTextKeyboardManager.m
  3. // YYKit <https://github.com/ibireme/YYKit>
  4. //
  5. // Created by ibireme on 15/6/3.
  6. // Copyright (c) 2015 ibireme.
  7. //
  8. // This source code is licensed under the MIT-style license found in the
  9. // LICENSE file in the root directory of this source tree.
  10. //
  11. #import "YYTextKeyboardManager.h"
  12. #import "UIApplication+YYAdd.h"
  13. #import <objc/runtime.h>
  14. static int _YYTextKeyboardViewFrameObserverKey;
  15. /// Observer for view's frame/bounds/center/transform
  16. @interface _YYTextKeyboardViewFrameObserver : NSObject
  17. @property (nonatomic, copy) void (^notifyBlock)(UIView *keyboard);
  18. - (void)addToKeyboardView:(UIView *)keyboardView;
  19. + (instancetype)observerForView:(UIView *)keyboardView;
  20. @end
  21. @implementation _YYTextKeyboardViewFrameObserver {
  22. __unsafe_unretained UIView *_keyboardView;
  23. }
  24. - (void)addToKeyboardView:(UIView *)keyboardView {
  25. if (_keyboardView == keyboardView) return;
  26. if (_keyboardView) {
  27. [self removeFrameObserver];
  28. objc_setAssociatedObject(_keyboardView, &_YYTextKeyboardViewFrameObserverKey, nil, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
  29. }
  30. _keyboardView = keyboardView;
  31. if (keyboardView) {
  32. [self addFrameObserver];
  33. }
  34. objc_setAssociatedObject(keyboardView, &_YYTextKeyboardViewFrameObserverKey, self, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
  35. }
  36. - (void)removeFrameObserver {
  37. [_keyboardView removeObserver:self forKeyPath:@"frame"];
  38. [_keyboardView removeObserver:self forKeyPath:@"center"];
  39. [_keyboardView removeObserver:self forKeyPath:@"bounds"];
  40. [_keyboardView removeObserver:self forKeyPath:@"transform"];
  41. _keyboardView = nil;
  42. }
  43. - (void)addFrameObserver {
  44. if (!_keyboardView) return;
  45. [_keyboardView addObserver:self forKeyPath:@"frame" options:kNilOptions context:NULL];
  46. [_keyboardView addObserver:self forKeyPath:@"center" options:kNilOptions context:NULL];
  47. [_keyboardView addObserver:self forKeyPath:@"bounds" options:kNilOptions context:NULL];
  48. [_keyboardView addObserver:self forKeyPath:@"transform" options:kNilOptions context:NULL];
  49. }
  50. - (void)dealloc {
  51. [self removeFrameObserver];
  52. }
  53. + (instancetype)observerForView:(UIView *)keyboardView {
  54. if (!keyboardView) return nil;
  55. return objc_getAssociatedObject(keyboardView, &_YYTextKeyboardViewFrameObserverKey);
  56. }
  57. - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
  58. BOOL isPrior = [[change objectForKey:NSKeyValueChangeNotificationIsPriorKey] boolValue];
  59. if (isPrior) return;
  60. NSKeyValueChange changeKind = [[change objectForKey:NSKeyValueChangeKindKey] integerValue];
  61. if (changeKind != NSKeyValueChangeSetting) return;
  62. id newVal = [change objectForKey:NSKeyValueChangeNewKey];
  63. if (newVal == [NSNull null]) newVal = nil;
  64. if (_notifyBlock) {
  65. _notifyBlock(_keyboardView);
  66. }
  67. }
  68. @end
  69. @implementation YYTextKeyboardManager {
  70. NSHashTable *_observers;
  71. CGRect _fromFrame;
  72. BOOL _fromVisible;
  73. UIInterfaceOrientation _fromOrientation;
  74. CGRect _notificationFromFrame;
  75. CGRect _notificationToFrame;
  76. NSTimeInterval _notificationDuration;
  77. UIViewAnimationCurve _notificationCurve;
  78. BOOL _hasNotification;
  79. CGRect _observedToFrame;
  80. BOOL _hasObservedChange;
  81. BOOL _lastIsNotification;
  82. }
  83. - (instancetype)init {
  84. @throw [NSException exceptionWithName:@"YYTextKeyboardManager init error" reason:@"Use 'defaultManager' to get instance." userInfo:nil];
  85. return [super init];
  86. }
  87. - (instancetype)_init {
  88. self = [super init];
  89. _observers = [[NSHashTable alloc] initWithOptions:NSPointerFunctionsWeakMemory|NSPointerFunctionsObjectPointerPersonality capacity:0];
  90. [[NSNotificationCenter defaultCenter] addObserver:self
  91. selector:@selector(_keyboardFrameWillChangeNotification:)
  92. name:UIKeyboardWillChangeFrameNotification
  93. object:nil];
  94. // for iPad (iOS 9)
  95. if ([UIDevice currentDevice].systemVersion.floatValue >= 9) {
  96. [[NSNotificationCenter defaultCenter] addObserver:self
  97. selector:@selector(_keyboardFrameDidChangeNotification:)
  98. name:UIKeyboardDidChangeFrameNotification
  99. object:nil];
  100. }
  101. return self;
  102. }
  103. - (void)_initFrameObserver {
  104. UIView *keyboardView = self.keyboardView;
  105. if (!keyboardView) return;
  106. __weak typeof(self) _self = self;
  107. _YYTextKeyboardViewFrameObserver *observer = [_YYTextKeyboardViewFrameObserver observerForView:keyboardView];
  108. if (!observer) {
  109. observer = [_YYTextKeyboardViewFrameObserver new];
  110. observer.notifyBlock = ^(UIView *keyboard) {
  111. [_self _keyboardFrameChanged:keyboard];
  112. };
  113. [observer addToKeyboardView:keyboardView];
  114. }
  115. }
  116. - (void)dealloc {
  117. [[NSNotificationCenter defaultCenter] removeObserver:self];
  118. }
  119. + (instancetype)defaultManager {
  120. static YYTextKeyboardManager *mgr = nil;
  121. static dispatch_once_t onceToken;
  122. dispatch_once(&onceToken, ^{
  123. if (![UIApplication isAppExtension]) {
  124. mgr = [[self alloc] _init];
  125. }
  126. });
  127. return mgr;
  128. }
  129. + (void)load {
  130. dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
  131. [self defaultManager];
  132. });
  133. }
  134. - (void)addObserver:(id<YYTextKeyboardObserver>)observer {
  135. if (!observer) return;
  136. [_observers addObject:observer];
  137. }
  138. - (void)removeObserver:(id<YYTextKeyboardObserver>)observer {
  139. if (!observer) return;
  140. [_observers removeObject:observer];
  141. }
  142. - (UIWindow *)keyboardWindow {
  143. UIApplication *app = [UIApplication sharedExtensionApplication];
  144. if (!app) return nil;
  145. UIWindow *window = nil;
  146. for (window in app.windows) {
  147. if ([self _getKeyboardViewFromWindow:window]) return window;
  148. }
  149. window = app.keyWindow;
  150. if ([self _getKeyboardViewFromWindow:window]) return window;
  151. NSMutableArray *kbWindows = nil;
  152. for (window in app.windows) {
  153. NSString *windowName = NSStringFromClass(window.class);
  154. if ([self _systemVersion] < 9) {
  155. // UITextEffectsWindow
  156. if (windowName.length == 19 &&
  157. [windowName hasPrefix:@"UI"] &&
  158. [windowName hasSuffix:@"TextEffectsWindow"]) {
  159. if (!kbWindows) kbWindows = [NSMutableArray new];
  160. [kbWindows addObject:window];
  161. }
  162. } else {
  163. // UIRemoteKeyboardWindow
  164. if (windowName.length == 22 &&
  165. [windowName hasPrefix:@"UI"] &&
  166. [windowName hasSuffix:@"RemoteKeyboardWindow"]) {
  167. if (!kbWindows) kbWindows = [NSMutableArray new];
  168. [kbWindows addObject:window];
  169. }
  170. }
  171. }
  172. if (kbWindows.count == 1) {
  173. return kbWindows.firstObject;
  174. }
  175. return nil;
  176. }
  177. - (UIView *)keyboardView {
  178. UIApplication *app = [UIApplication sharedExtensionApplication];
  179. if (!app) return nil;
  180. UIWindow *window = nil;
  181. UIView *view = nil;
  182. for (window in app.windows) {
  183. view = [self _getKeyboardViewFromWindow:window];
  184. if (view) return view;
  185. }
  186. window = app.keyWindow;
  187. view = [self _getKeyboardViewFromWindow:window];
  188. if (view) return view;
  189. return nil;
  190. }
  191. - (BOOL)isKeyboardVisible {
  192. UIWindow *window = self.keyboardWindow;
  193. if (!window) return NO;
  194. UIView *view = self.keyboardView;
  195. if (!view) return NO;
  196. CGRect rect = CGRectIntersection(window.bounds, view.frame);
  197. if (CGRectIsNull(rect)) return NO;
  198. if (CGRectIsInfinite(rect)) return NO;
  199. return rect.size.width > 0 && rect.size.height > 0;
  200. }
  201. - (CGRect)keyboardFrame {
  202. UIView *keyboard = [self keyboardView];
  203. if (!keyboard) return CGRectNull;
  204. CGRect frame = CGRectNull;
  205. UIWindow *window = keyboard.window;
  206. if (window) {
  207. frame = [window convertRect:keyboard.frame toWindow:nil];
  208. } else {
  209. frame = keyboard.frame;
  210. }
  211. return frame;
  212. }
  213. #pragma mark - private
  214. - (CGFloat)_systemVersion {
  215. static CGFloat v;
  216. static dispatch_once_t onceToken;
  217. dispatch_once(&onceToken, ^{
  218. v = [UIDevice currentDevice].systemVersion.floatValue;
  219. });
  220. return v;
  221. }
  222. - (UIView *)_getKeyboardViewFromWindow:(UIWindow *)window {
  223. /*
  224. iOS 6/7:
  225. UITextEffectsWindow
  226. UIPeripheralHostView << keyboard
  227. iOS 8:
  228. UITextEffectsWindow
  229. UIInputSetContainerView
  230. UIInputSetHostView << keyboard
  231. iOS 9:
  232. UIRemoteKeyboardWindow
  233. UIInputSetContainerView
  234. UIInputSetHostView << keyboard
  235. */
  236. if (!window) return nil;
  237. // Get the window
  238. NSString *windowName = NSStringFromClass(window.class);
  239. if ([self _systemVersion] < 9) {
  240. // UITextEffectsWindow
  241. if (windowName.length != 19) return nil;
  242. if (![windowName hasPrefix:@"UI"]) return nil;
  243. if (![windowName hasSuffix:@"TextEffectsWindow"]) return nil;
  244. } else {
  245. // UIRemoteKeyboardWindow
  246. if (windowName.length != 22) return nil;
  247. if (![windowName hasPrefix:@"UI"]) return nil;
  248. if (![windowName hasSuffix:@"RemoteKeyboardWindow"]) return nil;
  249. }
  250. // Get the view
  251. if ([self _systemVersion] < 8) {
  252. // UIPeripheralHostView
  253. for (UIView *view in window.subviews) {
  254. NSString *viewName = NSStringFromClass(view.class);
  255. if (viewName.length != 20) continue;
  256. if (![viewName hasPrefix:@"UI"]) continue;
  257. if (![viewName hasSuffix:@"PeripheralHostView"]) continue;
  258. return view;
  259. }
  260. } else {
  261. // UIInputSetContainerView
  262. for (UIView *view in window.subviews) {
  263. NSString *viewName = NSStringFromClass(view.class);
  264. if (viewName.length != 23) continue;
  265. if (![viewName hasPrefix:@"UI"]) continue;
  266. if (![viewName hasSuffix:@"InputSetContainerView"]) continue;
  267. // UIInputSetHostView
  268. for (UIView *subView in view.subviews) {
  269. NSString *subViewName = NSStringFromClass(subView.class);
  270. if (subViewName.length != 18) continue;
  271. if (![subViewName hasPrefix:@"UI"]) continue;
  272. if (![subViewName hasSuffix:@"InputSetHostView"]) continue;
  273. return subView;
  274. }
  275. }
  276. }
  277. return nil;
  278. }
  279. - (void)_keyboardFrameWillChangeNotification:(NSNotification *)notif {
  280. if (![notif.name isEqualToString:UIKeyboardWillChangeFrameNotification]) return;
  281. NSDictionary *info = notif.userInfo;
  282. if (!info) return;
  283. [self _initFrameObserver];
  284. NSValue *beforeValue = info[UIKeyboardFrameBeginUserInfoKey];
  285. NSValue *afterValue = info[UIKeyboardFrameEndUserInfoKey];
  286. NSNumber *curveNumber = info[UIKeyboardAnimationCurveUserInfoKey];
  287. NSNumber *durationNumber = info[UIKeyboardAnimationDurationUserInfoKey];
  288. CGRect before = beforeValue.CGRectValue;
  289. CGRect after = afterValue.CGRectValue;
  290. UIViewAnimationCurve curve = curveNumber.integerValue;
  291. NSTimeInterval duration = durationNumber.doubleValue;
  292. // ignore zero end frame
  293. if (after.size.width <= 0 && after.size.height <= 0) return;
  294. _notificationFromFrame = before;
  295. _notificationToFrame = after;
  296. _notificationCurve = curve;
  297. _notificationDuration = duration;
  298. _hasNotification = YES;
  299. _lastIsNotification = YES;
  300. [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(_notifyAllObservers) object:nil];
  301. if (duration == 0) {
  302. [self performSelector:@selector(_notifyAllObservers) withObject:nil afterDelay:0 inModes:@[NSRunLoopCommonModes]];
  303. } else {
  304. [self _notifyAllObservers];
  305. }
  306. }
  307. - (void)_keyboardFrameDidChangeNotification:(NSNotification *)notif {
  308. if (![notif.name isEqualToString:UIKeyboardDidChangeFrameNotification]) return;
  309. NSDictionary *info = notif.userInfo;
  310. if (!info) return;
  311. [self _initFrameObserver];
  312. NSValue *afterValue = info[UIKeyboardFrameEndUserInfoKey];
  313. CGRect after = afterValue.CGRectValue;
  314. // ignore zero end frame
  315. if (after.size.width <= 0 && after.size.height <= 0) return;
  316. _notificationToFrame = after;
  317. _notificationCurve = UIViewAnimationCurveEaseInOut;
  318. _notificationDuration = 0;
  319. _hasNotification = YES;
  320. _lastIsNotification = YES;
  321. [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(_notifyAllObservers) object:nil];
  322. [self performSelector:@selector(_notifyAllObservers) withObject:nil afterDelay:0 inModes:@[NSRunLoopCommonModes]];
  323. }
  324. - (void)_keyboardFrameChanged:(UIView *)keyboard {
  325. if (keyboard != self.keyboardView) return;
  326. UIWindow *window = keyboard.window;
  327. if (window) {
  328. _observedToFrame = [window convertRect:keyboard.frame toWindow:nil];
  329. } else {
  330. _observedToFrame = keyboard.frame;
  331. }
  332. _hasObservedChange = YES;
  333. _lastIsNotification = NO;
  334. [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(_notifyAllObservers) object:nil];
  335. [self performSelector:@selector(_notifyAllObservers) withObject:nil afterDelay:0 inModes:@[NSRunLoopCommonModes]];
  336. }
  337. - (void)_notifyAllObservers {
  338. UIApplication *app = [UIApplication sharedExtensionApplication];
  339. if (!app) return;
  340. UIView *keyboard = self.keyboardView;
  341. UIWindow *window = keyboard.window;
  342. if (!window) {
  343. window = app.keyWindow;
  344. }
  345. if (!window) {
  346. window = app.windows.firstObject;
  347. }
  348. YYTextKeyboardTransition trans = {0};
  349. // from
  350. if (_fromFrame.size.width == 0 && _fromFrame.size.height == 0) { // first notify
  351. _fromFrame.size.width = window.bounds.size.width;
  352. _fromFrame.size.height = trans.toFrame.size.height;
  353. _fromFrame.origin.x = trans.toFrame.origin.x;
  354. _fromFrame.origin.y = window.bounds.size.height;
  355. }
  356. trans.fromFrame = _fromFrame;
  357. trans.fromVisible = _fromVisible;
  358. // to
  359. if (_lastIsNotification || (_hasObservedChange && CGRectEqualToRect(_observedToFrame, _notificationToFrame))) {
  360. trans.toFrame = _notificationToFrame;
  361. trans.animationDuration = _notificationDuration;
  362. trans.animationCurve = _notificationCurve;
  363. trans.animationOption = _notificationCurve << 16;
  364. // Fix iPad(iOS7) keyboard frame error after rorate device when the keyboard is not docked to bottom.
  365. if (((int)[self _systemVersion]) == 7) {
  366. UIInterfaceOrientation ori = app.statusBarOrientation;
  367. if (_fromOrientation != UIInterfaceOrientationUnknown && _fromOrientation != ori) {
  368. switch (ori) {
  369. case UIInterfaceOrientationPortrait: {
  370. if (CGRectGetMaxY(trans.toFrame) != window.frame.size.height) {
  371. trans.toFrame.origin.y -= trans.toFrame.size.height;
  372. }
  373. } break;
  374. case UIInterfaceOrientationPortraitUpsideDown: {
  375. if (CGRectGetMinY(trans.toFrame) != 0) {
  376. trans.toFrame.origin.y += trans.toFrame.size.height;
  377. }
  378. } break;
  379. case UIInterfaceOrientationLandscapeLeft: {
  380. if (CGRectGetMaxX(trans.toFrame) != window.frame.size.width) {
  381. trans.toFrame.origin.x -= trans.toFrame.size.width;
  382. }
  383. } break;
  384. case UIInterfaceOrientationLandscapeRight: {
  385. if (CGRectGetMinX(trans.toFrame) != 0) {
  386. trans.toFrame.origin.x += trans.toFrame.size.width;
  387. }
  388. } break;
  389. default: break;
  390. }
  391. }
  392. }
  393. } else {
  394. trans.toFrame = _observedToFrame;
  395. }
  396. if (window && trans.toFrame.size.width > 0 && trans.toFrame.size.height > 0) {
  397. CGRect rect = CGRectIntersection(window.bounds, trans.toFrame);
  398. if (!CGRectIsNull(rect) && !CGRectIsEmpty(rect)) {
  399. trans.toVisible = YES;
  400. }
  401. }
  402. if (!CGRectEqualToRect(trans.toFrame, _fromFrame)) {
  403. for (id<YYTextKeyboardObserver> observer in _observers.copy) {
  404. if ([observer respondsToSelector:@selector(keyboardChangedWithTransition:)]) {
  405. [observer keyboardChangedWithTransition:trans];
  406. }
  407. }
  408. }
  409. _hasNotification = NO;
  410. _hasObservedChange = NO;
  411. _fromFrame = trans.toFrame;
  412. _fromVisible = trans.toVisible;
  413. _fromOrientation = app.statusBarOrientation;
  414. }
  415. - (CGRect)convertRect:(CGRect)rect toView:(UIView *)view {
  416. UIApplication *app = [UIApplication sharedExtensionApplication];
  417. if (!app) return CGRectZero;
  418. if (CGRectIsNull(rect)) return rect;
  419. if (CGRectIsInfinite(rect)) return rect;
  420. UIWindow *mainWindow = app.keyWindow;
  421. if (!mainWindow) mainWindow = app.windows.firstObject;
  422. if (!mainWindow) { // no window ?!
  423. if (view) {
  424. [view convertRect:rect fromView:nil];
  425. } else {
  426. return rect;
  427. }
  428. }
  429. rect = [mainWindow convertRect:rect fromWindow:nil];
  430. if (!view) return [mainWindow convertRect:rect toWindow:nil];
  431. if (view == mainWindow) return rect;
  432. UIWindow *toWindow = [view isKindOfClass:[UIWindow class]] ? (id)view : view.window;
  433. if (!mainWindow || !toWindow) return [mainWindow convertRect:rect toView:view];
  434. if (mainWindow == toWindow) return [mainWindow convertRect:rect toView:view];
  435. // in different window
  436. rect = [mainWindow convertRect:rect toView:mainWindow];
  437. rect = [toWindow convertRect:rect fromWindow:mainWindow];
  438. rect = [view convertRect:rect fromView:toWindow];
  439. return rect;
  440. }
  441. @end