YBIBScreenRotationHandler.m 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328
  1. //
  2. // YBIBScreenRotationHandler.m
  3. // YBImageBrowserDemo
  4. //
  5. // Created by 波儿菜 on 2019/6/8.
  6. // Copyright © 2019 波儿菜. All rights reserved.
  7. //
  8. #import "YBIBScreenRotationHandler.h"
  9. #import "YBIBUtilities.h"
  10. #import "YBIBCellProtocol.h"
  11. #import "YBImageBrowser+Internal.h"
  12. BOOL YBIBValidDeviceOrientation(UIDeviceOrientation orientation) {
  13. static NSSet *validSet;
  14. static dispatch_once_t onceToken;
  15. dispatch_once(&onceToken, ^{
  16. validSet = [NSSet setWithObjects:@(UIDeviceOrientationPortrait), @(UIDeviceOrientationPortraitUpsideDown), @(UIDeviceOrientationLandscapeLeft), @(UIDeviceOrientationLandscapeRight), nil];
  17. });
  18. return [validSet containsObject:@(orientation)];
  19. }
  20. CGFloat YBIBRotationAngle(UIDeviceOrientation startOrientation, UIDeviceOrientation endOrientation) {
  21. static NSDictionary<NSNumber*, NSNumber*> *angleMap;
  22. static dispatch_once_t onceToken;
  23. dispatch_once(&onceToken, ^{
  24. angleMap = @{@(UIDeviceOrientationPortrait):@(0), @(UIDeviceOrientationPortraitUpsideDown):@(M_PI), @(UIDeviceOrientationLandscapeLeft):@(M_PI_2), @(UIDeviceOrientationLandscapeRight): @(-M_PI_2)};
  25. });
  26. NSNumber *start = angleMap[@(startOrientation)], *end = angleMap[@(endOrientation)];
  27. CGFloat res = CGFLOAT_IS_DOUBLE ? end.doubleValue - start.doubleValue : end.floatValue - start.floatValue;
  28. if (ABS(res) > M_PI) {
  29. return res > 0 ? res - M_PI * 2 : M_PI * 2 + res;
  30. }
  31. return res;
  32. }
  33. static NSUInteger const kMaskNull = 10000;
  34. @interface YBIBScreenRotationHandler ()
  35. @property (nonatomic, assign) BOOL rotating;
  36. @property (nonatomic, assign) UIDeviceOrientation currentOrientation;
  37. @end
  38. @implementation YBIBScreenRotationHandler {
  39. __weak YBImageBrowser *_browser;
  40. CGSize _verticalContainerSize;
  41. CGSize _horizontalContainerSize;
  42. NSInteger _recordPage;
  43. }
  44. #pragma mark - life cycle
  45. - (void)dealloc {
  46. [self clear];
  47. }
  48. - (instancetype)initWithBrowser:(YBImageBrowser *)browser {
  49. if (self = [super init]) {
  50. _browser = browser;
  51. _rotating = NO;
  52. _supportedOrientations = UIInterfaceOrientationMaskAllButUpsideDown;
  53. _rotationDuration = 0.25;
  54. }
  55. return self;
  56. }
  57. #pragma mark - public
  58. - (void)startObserveStatusBarOrientation {
  59. self.currentOrientation = (UIDeviceOrientation)UIApplication.sharedApplication.statusBarOrientation;
  60. [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(applicationDidChangedStatusBarOrientationNotification:) name:UIApplicationDidChangeStatusBarOrientationNotification object:nil];
  61. [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(applicationWillChangeStatusBarOrientationNotification:) name:UIApplicationWillChangeStatusBarOrientationNotification object:nil];
  62. }
  63. - (void)startObserveDeviceOrientation {
  64. [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(deviceOrientationDidChangeNotification:) name:UIDeviceOrientationDidChangeNotification object:nil];
  65. }
  66. - (void)clear {
  67. [[NSNotificationCenter defaultCenter] removeObserver:self];
  68. }
  69. - (void)configContainerSize:(CGSize)size {
  70. if (UIDeviceOrientationIsLandscape(self.currentOrientation)) {
  71. // Now is horizontal.
  72. _verticalContainerSize = CGSizeMake(size.height, size.width);
  73. _horizontalContainerSize = size;
  74. } else {
  75. // Now is vertical.
  76. _verticalContainerSize = size;
  77. _horizontalContainerSize = CGSizeMake(size.height, size.width);
  78. }
  79. }
  80. - (CGSize)containerSizeWithOrientation:(UIDeviceOrientation)orientation {
  81. return UIDeviceOrientationIsLandscape(orientation) ? _horizontalContainerSize : _verticalContainerSize;
  82. }
  83. #pragma mark - private
  84. - (BOOL)supportedOfOrientation:(UIDeviceOrientation)orientation {
  85. if (!YBIBValidDeviceOrientation(orientation)) return NO;
  86. NSMutableSet *set = [NSMutableSet set];
  87. if (_supportedOrientations & UIInterfaceOrientationMaskPortrait) [set addObject:@(UIDeviceOrientationPortrait)];
  88. if (_supportedOrientations & UIInterfaceOrientationMaskPortraitUpsideDown) [set addObject:@(UIDeviceOrientationPortraitUpsideDown)];
  89. if (_supportedOrientations & UIInterfaceOrientationMaskLandscapeRight) [set addObject:@(UIDeviceOrientationLandscapeLeft)];
  90. if (_supportedOrientations & UIInterfaceOrientationMaskLandscapeLeft) [set addObject:@(UIDeviceOrientationLandscapeRight)];
  91. return [set containsObject:@(orientation)];
  92. }
  93. - (BOOL)supportedOnlyOneSystemOrientation {
  94. UIInterfaceOrientationMask mask = [self supportSystemOrientationMask];
  95. return mask == (mask & (-mask));
  96. }
  97. - (void)orientationWillChangeWithExpectOrientation:(UIDeviceOrientation)orientation centerCell:(UICollectionViewCell<YBIBCellProtocol> *)centerCell {
  98. if ([centerCell respondsToSelector:@selector(yb_orientationWillChangeWithExpectOrientation:)]) {
  99. [centerCell yb_orientationWillChangeWithExpectOrientation:orientation];
  100. }
  101. for (id<YBIBToolViewHandler> handler in _browser.toolViewHandlers) {
  102. if ([handler respondsToSelector:@selector(yb_orientationWillChangeWithExpectOrientation:)]) {
  103. [handler yb_orientationWillChangeWithExpectOrientation:orientation];
  104. }
  105. }
  106. }
  107. - (void)orientationChangeAnimationWithExpectOrientation:(UIDeviceOrientation)orientation centerCell:(UICollectionViewCell<YBIBCellProtocol> *)centerCell {
  108. if ([centerCell respondsToSelector:@selector(yb_orientationChangeAnimationWithExpectOrientation:)]) {
  109. [centerCell yb_orientationChangeAnimationWithExpectOrientation:orientation];
  110. [centerCell layoutIfNeeded]; // Compatible with autolayout.
  111. }
  112. for (id<YBIBToolViewHandler> handler in _browser.toolViewHandlers) {
  113. if ([handler respondsToSelector:@selector(yb_orientationChangeAnimationWithExpectOrientation:)]) {
  114. [handler yb_orientationChangeAnimationWithExpectOrientation:orientation];
  115. }
  116. }
  117. }
  118. - (void)orientationDidChangedWithOrientation:(UIDeviceOrientation)orientation centerCell:(UICollectionViewCell<YBIBCellProtocol> *)centerCell {
  119. if ([centerCell respondsToSelector:@selector(yb_orientationDidChangedWithOrientation:)]) {
  120. [centerCell yb_orientationDidChangedWithOrientation:orientation];
  121. }
  122. for (id<YBIBToolViewHandler> handler in _browser.toolViewHandlers) {
  123. if ([handler respondsToSelector:@selector(yb_orientationDidChangedWithOrientation:)]) {
  124. [handler yb_orientationDidChangedWithOrientation:orientation];
  125. }
  126. }
  127. }
  128. #pragma mark - event
  129. - (void)deviceOrientationDidChangeNotification:(NSNotification *)note {
  130. if (![self supportedOnlyOneSystemOrientation]) return;
  131. if (_browser.isTransitioning || self.rotating) return;
  132. UIDeviceOrientation expectOrientation = [UIDevice currentDevice].orientation;
  133. if (expectOrientation == self.currentOrientation || ![self supportedOfOrientation:expectOrientation]) return;
  134. self.rotating = YES;
  135. // Align.
  136. [_browser.collectionView scrollToPage:_browser.currentPage];
  137. // Record current page number before transforming.
  138. NSInteger currentPage = _browser.currentPage;
  139. UIDeviceOrientation statusBarOrientation = (UIDeviceOrientation)UIApplication.sharedApplication.statusBarOrientation;
  140. CGFloat angleStatusBarToExpect = YBIBRotationAngle(statusBarOrientation, expectOrientation);
  141. CGFloat angleCurrentToExpect = YBIBRotationAngle(_currentOrientation, expectOrientation);
  142. CGRect expectBounds = (CGRect){CGPointZero, [self containerSizeWithOrientation:expectOrientation]};
  143. UICollectionViewCell<YBIBCellProtocol> *centerCell = (UICollectionViewCell<YBIBCellProtocol> *)self->_browser.collectionView.centerCell;
  144. // Animate smoothly if bigger rotation angle.
  145. NSTimeInterval duration = self.rotationDuration * (ABS(angleCurrentToExpect) > M_PI_2 ? 2 : 1);
  146. // 'collectionView' transformation.
  147. self->_browser.collectionView.bounds = expectBounds;
  148. self->_browser.collectionView.transform = CGAffineTransformMakeRotation(angleStatusBarToExpect);
  149. centerCell.contentView.transform = CGAffineTransformMakeRotation(-angleCurrentToExpect);
  150. // Reset to prevent the page number change after transforming.
  151. [self->_browser.collectionView scrollToPage:currentPage];
  152. [self orientationWillChangeWithExpectOrientation:expectOrientation centerCell:centerCell];
  153. [UIView animateWithDuration:duration delay:0 options:UIViewAnimationOptionCurveEaseInOut animations:^{
  154. // Maybe the internal UI need to transform.
  155. [self orientationChangeAnimationWithExpectOrientation:expectOrientation centerCell:centerCell];
  156. centerCell.contentView.bounds = expectBounds;
  157. centerCell.contentView.transform = CGAffineTransformIdentity;
  158. self->_browser.containerView.bounds = expectBounds;
  159. self->_browser.containerView.transform = CGAffineTransformMakeRotation(angleStatusBarToExpect);
  160. } completion:^(BOOL finished) {
  161. self.currentOrientation = expectOrientation;
  162. self.rotating = NO;
  163. [self orientationDidChangedWithOrientation:expectOrientation centerCell:centerCell];
  164. }];
  165. }
  166. - (void)applicationWillChangeStatusBarOrientationNotification:(NSNotification *)noti {
  167. if ([self supportedOnlyOneSystemOrientation]) return;
  168. self.rotating = YES;
  169. // Record current page number before transforming.
  170. _recordPage = _browser.currentPage;
  171. UICollectionViewCell<YBIBCellProtocol> *centerCell = (UICollectionViewCell<YBIBCellProtocol> *)self->_browser.collectionView.centerCell;
  172. UIDeviceOrientation expectOrientation = ((NSNumber *)noti.userInfo[UIApplicationStatusBarOrientationUserInfoKey]).integerValue;
  173. [self orientationWillChangeWithExpectOrientation:expectOrientation centerCell:centerCell];
  174. }
  175. - (void)applicationDidChangedStatusBarOrientationNotification:(NSNotification *)noti {
  176. if ([self supportedOnlyOneSystemOrientation]) return;
  177. UIDeviceOrientation expectOrientation = (UIDeviceOrientation)UIApplication.sharedApplication.statusBarOrientation;
  178. UICollectionViewCell<YBIBCellProtocol> *centerCell = (UICollectionViewCell<YBIBCellProtocol> *)self->_browser.collectionView.centerCell;
  179. [self orientationChangeAnimationWithExpectOrientation:expectOrientation centerCell:centerCell];
  180. CGRect expectBounds = (CGRect){CGPointZero, [self containerSizeWithOrientation:expectOrientation]};
  181. self->_browser.collectionView.layout.itemSize = expectBounds.size;
  182. // Reset to prevent the page number change after transforming.
  183. [_browser.collectionView scrollToPage:_recordPage];
  184. self.currentOrientation = expectOrientation;
  185. self.rotating = NO;
  186. [self orientationDidChangedWithOrientation:expectOrientation centerCell:centerCell];
  187. }
  188. #pragma mark - getters & setters
  189. - (void)setRotating:(BOOL)rotating {
  190. _rotating = rotating;
  191. _browser.containerView.userInteractionEnabled = !rotating;
  192. _browser.collectionView.userInteractionEnabled = !rotating;
  193. _browser.collectionView.panGestureRecognizer.enabled = !rotating;
  194. }
  195. #pragma mark - calculate supported orientation of system
  196. - (UIInterfaceOrientationMask)supportSystemOrientationMask {
  197. UIInterfaceOrientationMask limitMask = 0;
  198. // IphoneX series do not support UIInterfaceOrientationMaskPortraitUpsideDown, except selector '-application:supportedInterfaceOrientationsForWindow:' of '[UIApplication sharedApplication].delegate' return 0. Maybe it is BUG of Apple.
  199. BOOL ignoreUpsideDownIfIphoneX = YES;
  200. UIInterfaceOrientationMask delegateMask = [self maskOfApplicationDelegate];
  201. if (delegateMask != kMaskNull) {
  202. if (delegateMask == 0) {
  203. // Apple do.
  204. limitMask = UIInterfaceOrientationMaskAll;
  205. ignoreUpsideDownIfIphoneX = NO;
  206. } else {
  207. limitMask = delegateMask;
  208. }
  209. } else {
  210. // Lower priority.
  211. limitMask = [self maskOfInfoPlist];
  212. }
  213. UIInterfaceOrientationMask supportMask = limitMask & [self maskOfViewController];
  214. if (ignoreUpsideDownIfIphoneX && YBIBIsIphoneXSeries() && (supportMask & UIInterfaceOrientationMaskPortraitUpsideDown)) {
  215. supportMask ^= UIInterfaceOrientationMaskPortraitUpsideDown;
  216. }
  217. return supportMask;
  218. }
  219. - (UIInterfaceOrientationMask)maskOfInfoPlist {
  220. // 'Info.plist' will not change in a process.
  221. static UIInterfaceOrientationMask mask = 0;
  222. static dispatch_once_t onceToken;
  223. dispatch_once(&onceToken, ^{
  224. NSString *path = [[NSBundle mainBundle] pathForResource:@"Info" ofType:@"plist"];
  225. NSMutableDictionary *dict = [NSMutableDictionary dictionaryWithContentsOfFile:path];
  226. NSArray *array = dict[@"UISupportedInterfaceOrientations"];
  227. NSSet *set = [NSSet setWithArray:array];
  228. if ([set containsObject:@"UIInterfaceOrientationPortrait"]) mask |= UIInterfaceOrientationMaskPortrait;
  229. if ([set containsObject:@"UIInterfaceOrientationLandscapeRight"]) mask |= UIInterfaceOrientationMaskLandscapeRight;
  230. if ([set containsObject:@"UIInterfaceOrientationLandscapeLeft"]) mask |= UIInterfaceOrientationMaskLandscapeLeft;
  231. if ([set containsObject:@"UIInterfaceOrientationPortraitUpsideDown"]) mask |= UIInterfaceOrientationMaskPortraitUpsideDown;
  232. });
  233. return mask == 0 ? kMaskNull : mask;
  234. }
  235. - (UIInterfaceOrientationMask)maskOfApplicationDelegate {
  236. UIInterfaceOrientationMask mask = kMaskNull;
  237. id<UIApplicationDelegate> delegate = [UIApplication sharedApplication].delegate;
  238. if ([delegate respondsToSelector:@selector(application:supportedInterfaceOrientationsForWindow:)]) {
  239. mask = [delegate application:[UIApplication sharedApplication] supportedInterfaceOrientationsForWindow:_browser.window];
  240. }
  241. return mask;
  242. }
  243. - (UIInterfaceOrientationMask)maskOfViewController {
  244. UIInterfaceOrientationMask mask = kMaskNull;
  245. // Find the UIViewController whitch 'browser' followed.
  246. UIViewController *target = nil;
  247. id next = _browser;
  248. while (next) {
  249. if ([next isKindOfClass:UIViewController.self]) {
  250. target = next;
  251. break;
  252. }
  253. if ([next isKindOfClass:UIWindow.self]) {
  254. target = YBIBTopControllerByWindow(next);
  255. break;
  256. }
  257. next = [next nextResponder];
  258. }
  259. // Cover directly.
  260. if (target.tabBarController) {
  261. mask = target.tabBarController.shouldAutorotate ? target.tabBarController.supportedInterfaceOrientations : 0;
  262. } else if (target.navigationController) {
  263. mask = target.navigationController.shouldAutorotate ? target.navigationController.supportedInterfaceOrientations : 0;
  264. } else {
  265. mask = target.shouldAutorotate ? target.supportedInterfaceOrientations : 0;
  266. }
  267. return mask;
  268. }
  269. @end