AsyncDisplayKit+Debug.m 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755
  1. //
  2. // AsyncDisplayKit+Debug.m
  3. // AsyncDisplayKit
  4. //
  5. // Created by Hannah Troisi on 3/7/16.
  6. //
  7. // Copyright (c) 2014-present, Facebook, Inc. All rights reserved.
  8. // This source code is licensed under the BSD-style license found in the
  9. // LICENSE file in the root directory of this source tree. An additional grant
  10. // of patent rights can be found in the PATENTS file in the same directory.
  11. //
  12. #import "AsyncDisplayKit+Debug.h"
  13. #import "ASDisplayNode+Subclasses.h"
  14. #import "ASDisplayNodeExtras.h"
  15. #import "ASWeakSet.h"
  16. #import "UIImage+ASConvenience.h"
  17. #import <AsyncDisplayKit/ASLayout.h>
  18. #import <AsyncDisplayKit/ASDisplayNode+Subclasses.h>
  19. #import <AsyncDisplayKit/CoreGraphics+ASConvenience.h>
  20. #import <AsyncDisplayKit/ASDisplayNodeExtras.h>
  21. #import <AsyncDisplayKit/ASTextNode.h>
  22. #import <AsyncDisplayKit/ASRangeController.h>
  23. #pragma mark - ASImageNode (Debugging)
  24. static BOOL __shouldShowImageScalingOverlay = NO;
  25. @implementation ASImageNode (Debugging)
  26. + (void)setShouldShowImageScalingOverlay:(BOOL)show;
  27. {
  28. __shouldShowImageScalingOverlay = show;
  29. }
  30. + (BOOL)shouldShowImageScalingOverlay
  31. {
  32. return __shouldShowImageScalingOverlay;
  33. }
  34. @end
  35. #pragma mark - ASControlNode (DebuggingInternal)
  36. static BOOL __enableHitTestDebug = NO;
  37. @interface ASControlNode (DebuggingInternal)
  38. - (ASImageNode *)debugHighlightOverlay;
  39. @end
  40. @implementation ASControlNode (Debugging)
  41. + (void)setEnableHitTestDebug:(BOOL)enable
  42. {
  43. __enableHitTestDebug = enable;
  44. }
  45. + (BOOL)enableHitTestDebug
  46. {
  47. return __enableHitTestDebug;
  48. }
  49. // layout method required ONLY when hitTestDebug is enabled
  50. - (void)layout
  51. {
  52. [super layout];
  53. if ([ASControlNode enableHitTestDebug]) {
  54. // Construct hitTestDebug highlight overlay frame indicating tappable area of a node, which can be restricted by two things:
  55. // (1) Any parent's tapable area (its own bounds + hitTestSlop) may restrict the desired tappable area expansion using
  56. // hitTestSlop of a child as UIKit event delivery (hitTest:) will not search sub-hierarchies if one of our parents does
  57. // not return YES for pointInside:. To circumvent this restriction, a developer will need to set / adjust the hitTestSlop
  58. // on the limiting parent. This is indicated in the overlay by a dark GREEN edge. This is an ACTUAL restriction.
  59. // (2) Any parent's .clipToBounds. If a parent is clipping, we cannot show the overlay outside that area
  60. // (although it still respond to touch). To indicate that the overlay cannot accurately display the true tappable area,
  61. // the overlay will have an ORANGE edge. This is a VISUALIZATION restriction.
  62. CGRect intersectRect = UIEdgeInsetsInsetRect(self.bounds, [self hitTestSlop]);
  63. UIRectEdge clippedEdges = UIRectEdgeNone;
  64. UIRectEdge clipsToBoundsClippedEdges = UIRectEdgeNone;
  65. CALayer *layer = self.layer;
  66. CALayer *intersectLayer = layer;
  67. CALayer *intersectSuperlayer = layer.superlayer;
  68. // FIXED: Stop climbing hierarchy if UIScrollView is encountered (its offset bounds origin may make it seem like our events
  69. // will be clipped when scrolling will actually reveal them (because this process will not re-run due to scrolling))
  70. while (intersectSuperlayer && ![intersectSuperlayer.delegate respondsToSelector:@selector(contentOffset)]) {
  71. // Get parent's tappable area
  72. CGRect parentHitRect = intersectSuperlayer.bounds;
  73. BOOL parentClipsToBounds = NO;
  74. // If parent is a node, tappable area may be expanded by hitTestSlop
  75. ASDisplayNode *parentNode = ASLayerToDisplayNode(intersectSuperlayer);
  76. if (parentNode) {
  77. UIEdgeInsets parentSlop = [parentNode hitTestSlop];
  78. // If parent has hitTestSlop, expand tappable area (if parent doesn't clipToBounds)
  79. if (!UIEdgeInsetsEqualToEdgeInsets(UIEdgeInsetsZero, parentSlop)) {
  80. parentClipsToBounds = parentNode.clipsToBounds;
  81. if (!parentClipsToBounds) {
  82. parentHitRect = UIEdgeInsetsInsetRect(parentHitRect, [parentNode hitTestSlop]);
  83. }
  84. }
  85. }
  86. // Convert our current rect to parent coordinates
  87. CGRect intersectRectInParentCoordinates = [intersectSuperlayer convertRect:intersectRect fromLayer:intersectLayer];
  88. // Intersect rect with the parent's tappable area rect
  89. intersectRect = CGRectIntersection(parentHitRect, intersectRectInParentCoordinates);
  90. if (!CGSizeEqualToSize(parentHitRect.size, intersectRectInParentCoordinates.size)) {
  91. clippedEdges = [self setEdgesOfIntersectionForChildRect:intersectRectInParentCoordinates
  92. parentRect:parentHitRect rectEdge:clippedEdges];
  93. if (parentClipsToBounds) {
  94. clipsToBoundsClippedEdges = [self setEdgesOfIntersectionForChildRect:intersectRectInParentCoordinates
  95. parentRect:parentHitRect rectEdge:clipsToBoundsClippedEdges];
  96. }
  97. }
  98. // move up hierarchy
  99. intersectLayer = intersectSuperlayer;
  100. intersectSuperlayer = intersectLayer.superlayer;
  101. }
  102. // produce final overlay image (or fill background if edges aren't restricted)
  103. CGRect finalRect = [intersectLayer convertRect:intersectRect toLayer:layer];
  104. UIColor *fillColor = [[UIColor greenColor] colorWithAlphaComponent:0.4];
  105. ASImageNode *debugOverlay = [self debugHighlightOverlay];
  106. // determine if edges are clipped and if so, highlight the restricted edges
  107. if (clippedEdges == UIRectEdgeNone) {
  108. debugOverlay.backgroundColor = fillColor;
  109. } else {
  110. const CGFloat borderWidth = 2.0;
  111. UIColor *borderColor = [[UIColor orangeColor] colorWithAlphaComponent:0.8];
  112. UIColor *clipsBorderColor = [UIColor colorWithRed:30/255.0 green:90/255.0 blue:50/255.0 alpha:0.7];
  113. CGRect imgRect = CGRectMake(0, 0, 2.0 * borderWidth + 1.0, 2.0 * borderWidth + 1.0);
  114. UIGraphicsBeginImageContext(imgRect.size);
  115. [fillColor setFill];
  116. UIRectFill(imgRect);
  117. [self drawEdgeIfClippedWithEdges:clippedEdges color:clipsBorderColor borderWidth:borderWidth imgRect:imgRect];
  118. [self drawEdgeIfClippedWithEdges:clipsToBoundsClippedEdges color:borderColor borderWidth:borderWidth imgRect:imgRect];
  119. UIImage *debugHighlightImage = UIGraphicsGetImageFromCurrentImageContext();
  120. UIGraphicsEndImageContext();
  121. UIEdgeInsets edgeInsets = UIEdgeInsetsMake(borderWidth, borderWidth, borderWidth, borderWidth);
  122. debugOverlay.image = [debugHighlightImage resizableImageWithCapInsets:edgeInsets resizingMode:UIImageResizingModeStretch];
  123. debugOverlay.backgroundColor = nil;
  124. }
  125. debugOverlay.frame = finalRect;
  126. }
  127. }
  128. - (UIRectEdge)setEdgesOfIntersectionForChildRect:(CGRect)childRect parentRect:(CGRect)parentRect rectEdge:(UIRectEdge)rectEdge
  129. {
  130. // determine which edges of childRect are outside parentRect (and thus will be clipped)
  131. if (childRect.origin.y < parentRect.origin.y) {
  132. rectEdge |= UIRectEdgeTop;
  133. }
  134. if (childRect.origin.x < parentRect.origin.x) {
  135. rectEdge |= UIRectEdgeLeft;
  136. }
  137. if (CGRectGetMaxY(childRect) > CGRectGetMaxY(parentRect)) {
  138. rectEdge |= UIRectEdgeBottom;
  139. }
  140. if (CGRectGetMaxX(childRect) > CGRectGetMaxX(parentRect)) {
  141. rectEdge |= UIRectEdgeRight;
  142. }
  143. return rectEdge;
  144. }
  145. - (void)drawEdgeIfClippedWithEdges:(UIRectEdge)rectEdge color:(UIColor *)color borderWidth:(CGFloat)borderWidth imgRect:(CGRect)imgRect
  146. {
  147. [color setFill];
  148. // highlight individual edges of overlay if edge is restricted by parentRect
  149. // so that the developer is aware that increasing hitTestSlop will not result in an expanded tappable area
  150. if (rectEdge & UIRectEdgeTop) {
  151. UIRectFill(CGRectMake(0.0, 0.0, imgRect.size.width, borderWidth));
  152. }
  153. if (rectEdge & UIRectEdgeLeft) {
  154. UIRectFill(CGRectMake(0.0, 0.0, borderWidth, imgRect.size.height));
  155. }
  156. if (rectEdge & UIRectEdgeBottom) {
  157. UIRectFill(CGRectMake(0.0, imgRect.size.height - borderWidth, imgRect.size.width, borderWidth));
  158. }
  159. if (rectEdge & UIRectEdgeRight) {
  160. UIRectFill(CGRectMake(imgRect.size.width - borderWidth, 0.0, borderWidth, imgRect.size.height));
  161. }
  162. }
  163. @end
  164. #pragma mark - ASRangeController (Debugging)
  165. @interface _ASRangeDebugOverlayView : UIView
  166. + (instancetype)sharedInstance;
  167. - (void)addRangeController:(ASRangeController *)rangeController;
  168. - (void)updateRangeController:(ASRangeController *)controller
  169. withScrollableDirections:(ASScrollDirection)scrollableDirections
  170. scrollDirection:(ASScrollDirection)direction
  171. rangeMode:(ASLayoutRangeMode)mode
  172. displayTuningParameters:(ASRangeTuningParameters)displayTuningParameters
  173. preloadTuningParameters:(ASRangeTuningParameters)preloadTuningParameters
  174. interfaceState:(ASInterfaceState)interfaceState;
  175. @end
  176. @interface _ASRangeDebugBarView : UIView
  177. @property (nonatomic, weak) ASRangeController *rangeController;
  178. @property (nonatomic, assign) BOOL destroyOnLayout;
  179. @property (nonatomic, strong) NSString *debugString;
  180. - (instancetype)initWithRangeController:(ASRangeController *)rangeController;
  181. - (void)updateWithVisibleRatio:(CGFloat)visibleRatio
  182. displayRatio:(CGFloat)displayRatio
  183. leadingDisplayRatio:(CGFloat)leadingDisplayRatio
  184. preloadRatio:(CGFloat)preloadRatio
  185. leadingpreloadRatio:(CGFloat)leadingpreloadRatio
  186. direction:(ASScrollDirection)direction;
  187. @end
  188. static BOOL __shouldShowRangeDebugOverlay = NO;
  189. @implementation ASRangeController (Debugging)
  190. + (void)setShouldShowRangeDebugOverlay:(BOOL)show
  191. {
  192. __shouldShowRangeDebugOverlay = show;
  193. }
  194. + (BOOL)shouldShowRangeDebugOverlay
  195. {
  196. return __shouldShowRangeDebugOverlay;
  197. }
  198. + (void)layoutDebugOverlayIfNeeded
  199. {
  200. [[_ASRangeDebugOverlayView sharedInstance] setNeedsLayout];
  201. }
  202. - (void)addRangeControllerToRangeDebugOverlay
  203. {
  204. [[_ASRangeDebugOverlayView sharedInstance] addRangeController:self];
  205. }
  206. - (void)updateRangeController:(ASRangeController *)controller
  207. withScrollableDirections:(ASScrollDirection)scrollableDirections
  208. scrollDirection:(ASScrollDirection)direction
  209. rangeMode:(ASLayoutRangeMode)mode
  210. displayTuningParameters:(ASRangeTuningParameters)displayTuningParameters
  211. preloadTuningParameters:(ASRangeTuningParameters)preloadTuningParameters
  212. interfaceState:(ASInterfaceState)interfaceState
  213. {
  214. [[_ASRangeDebugOverlayView sharedInstance] updateRangeController:controller
  215. withScrollableDirections:scrollableDirections
  216. scrollDirection:direction
  217. rangeMode:mode
  218. displayTuningParameters:displayTuningParameters
  219. preloadTuningParameters:preloadTuningParameters
  220. interfaceState:interfaceState];
  221. }
  222. @end
  223. #pragma mark _ASRangeDebugOverlayView
  224. @interface _ASRangeDebugOverlayView () <UIGestureRecognizerDelegate>
  225. @end
  226. @implementation _ASRangeDebugOverlayView
  227. {
  228. NSMutableArray *_rangeControllerViews;
  229. NSInteger _newControllerCount;
  230. NSInteger _removeControllerCount;
  231. BOOL _animating;
  232. }
  233. + (UIWindow *)keyWindow
  234. {
  235. // hack to work around app extensions not having UIApplication...not sure of a better way to do this?
  236. return [[NSClassFromString(@"UIApplication") sharedApplication] keyWindow];
  237. }
  238. + (instancetype)sharedInstance
  239. {
  240. static _ASRangeDebugOverlayView *__rangeDebugOverlay = nil;
  241. if (!__rangeDebugOverlay && [ASRangeController shouldShowRangeDebugOverlay]) {
  242. __rangeDebugOverlay = [[self alloc] initWithFrame:CGRectZero];
  243. [[self keyWindow] addSubview:__rangeDebugOverlay];
  244. }
  245. return __rangeDebugOverlay;
  246. }
  247. #define OVERLAY_INSET 10
  248. #define OVERLAY_SCALE 3
  249. - (instancetype)initWithFrame:(CGRect)frame
  250. {
  251. self = [super initWithFrame:frame];
  252. if (self) {
  253. _rangeControllerViews = [[NSMutableArray alloc] init];
  254. self.backgroundColor = [[UIColor blackColor] colorWithAlphaComponent:0.4];
  255. self.layer.zPosition = 1000;
  256. self.clipsToBounds = YES;
  257. CGSize windowSize = [[[self class] keyWindow] bounds].size;
  258. self.frame = CGRectMake(windowSize.width - (windowSize.width / OVERLAY_SCALE) - OVERLAY_INSET, windowSize.height - OVERLAY_INSET,
  259. windowSize.width / OVERLAY_SCALE, 0.0);
  260. UIPanGestureRecognizer *panGR = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(rangeDebugOverlayWasPanned:)];
  261. [self addGestureRecognizer:panGR];
  262. }
  263. return self;
  264. }
  265. #define BAR_THICKNESS 24
  266. - (void)layoutSubviews
  267. {
  268. [super layoutSubviews];
  269. [UIView animateWithDuration:0.2 delay:0.0 options:UIViewAnimationOptionBeginFromCurrentState animations:^{
  270. [self layoutToFitAllBarsExcept:0];
  271. } completion:^(BOOL finished) {
  272. }];
  273. }
  274. - (void)layoutToFitAllBarsExcept:(NSInteger)barsToClip
  275. {
  276. CGSize boundsSize = self.bounds.size;
  277. CGFloat totalHeight = 0.0;
  278. CGRect barRect = CGRectMake(0, boundsSize.height - BAR_THICKNESS, self.bounds.size.width, BAR_THICKNESS);
  279. NSMutableArray *displayedBars = [NSMutableArray array];
  280. for (_ASRangeDebugBarView *barView in [_rangeControllerViews copy]) {
  281. barView.frame = barRect;
  282. ASInterfaceState interfaceState = [barView.rangeController.dataSource interfaceStateForRangeController:barView.rangeController];
  283. if (!(interfaceState & (ASInterfaceStateVisible))) {
  284. if (barView.destroyOnLayout && barView.alpha == 0.0) {
  285. [_rangeControllerViews removeObjectIdenticalTo:barView];
  286. [barView removeFromSuperview];
  287. } else {
  288. barView.alpha = 0.0;
  289. }
  290. } else {
  291. assert(!barView.destroyOnLayout); // In this case we should not have a visible interfaceState
  292. barView.alpha = 1.0;
  293. totalHeight += BAR_THICKNESS;
  294. barRect.origin.y -= BAR_THICKNESS;
  295. [displayedBars addObject:barView];
  296. }
  297. }
  298. if (totalHeight > 0) {
  299. totalHeight -= (BAR_THICKNESS * barsToClip);
  300. }
  301. if (barsToClip == 0) {
  302. CGRect overlayFrame = self.frame;
  303. CGFloat heightChange = (overlayFrame.size.height - totalHeight);
  304. overlayFrame.origin.y += heightChange;
  305. overlayFrame.size.height = totalHeight;
  306. self.frame = overlayFrame;
  307. for (_ASRangeDebugBarView *barView in displayedBars) {
  308. [self offsetYOrigin:-heightChange forView:barView];
  309. }
  310. }
  311. }
  312. - (void)setOrigin:(CGPoint)origin forView:(UIView *)view
  313. {
  314. CGRect newFrame = view.frame;
  315. newFrame.origin = origin;
  316. view.frame = newFrame;
  317. }
  318. - (void)offsetYOrigin:(CGFloat)offset forView:(UIView *)view
  319. {
  320. CGRect newFrame = view.frame;
  321. newFrame.origin = CGPointMake(newFrame.origin.x, newFrame.origin.y + offset);
  322. view.frame = newFrame;
  323. }
  324. - (void)addRangeController:(ASRangeController *)rangeController
  325. {
  326. for (_ASRangeDebugBarView *rangeView in _rangeControllerViews) {
  327. if (rangeView.rangeController == rangeController) {
  328. return;
  329. }
  330. }
  331. _ASRangeDebugBarView *rangeView = [[_ASRangeDebugBarView alloc] initWithRangeController:rangeController];
  332. [_rangeControllerViews addObject:rangeView];
  333. [self addSubview:rangeView];
  334. if (!_animating) {
  335. [self layoutToFitAllBarsExcept:1];
  336. }
  337. [UIView animateWithDuration:0.2 delay:0.0 options:UIViewAnimationOptionBeginFromCurrentState animations:^{
  338. _animating = YES;
  339. [self layoutToFitAllBarsExcept:0];
  340. } completion:^(BOOL finished) {
  341. _animating = NO;
  342. }];
  343. }
  344. - (void)updateRangeController:(ASRangeController *)controller
  345. withScrollableDirections:(ASScrollDirection)scrollableDirections
  346. scrollDirection:(ASScrollDirection)scrollDirection
  347. rangeMode:(ASLayoutRangeMode)rangeMode
  348. displayTuningParameters:(ASRangeTuningParameters)displayTuningParameters
  349. preloadTuningParameters:(ASRangeTuningParameters)preloadTuningParameters
  350. interfaceState:(ASInterfaceState)interfaceState;
  351. {
  352. _ASRangeDebugBarView *viewToUpdate = [self barViewForRangeController:controller];
  353. CGRect boundsRect = self.bounds;
  354. CGRect visibleRect = CGRectExpandToRangeWithScrollableDirections(boundsRect, ASRangeTuningParametersZero, scrollableDirections, scrollDirection);
  355. CGRect displayRect = CGRectExpandToRangeWithScrollableDirections(boundsRect, displayTuningParameters, scrollableDirections, scrollDirection);
  356. CGRect preloadRect = CGRectExpandToRangeWithScrollableDirections(boundsRect, preloadTuningParameters, scrollableDirections, scrollDirection);
  357. // figure out which is biggest and assume that is full bounds
  358. BOOL displayRangeLargerThanPreload = NO;
  359. CGFloat visibleRatio = 0;
  360. CGFloat displayRatio = 0;
  361. CGFloat preloadRatio = 0;
  362. CGFloat leadingDisplayTuningRatio = 0;
  363. CGFloat leadingPreloadTuningRatio = 0;
  364. if (!((displayTuningParameters.leadingBufferScreenfuls + displayTuningParameters.trailingBufferScreenfuls) == 0)) {
  365. leadingDisplayTuningRatio = displayTuningParameters.leadingBufferScreenfuls / (displayTuningParameters.leadingBufferScreenfuls + displayTuningParameters.trailingBufferScreenfuls);
  366. }
  367. if (!((preloadTuningParameters.leadingBufferScreenfuls + preloadTuningParameters.trailingBufferScreenfuls) == 0)) {
  368. leadingPreloadTuningRatio = preloadTuningParameters.leadingBufferScreenfuls / (preloadTuningParameters.leadingBufferScreenfuls + preloadTuningParameters.trailingBufferScreenfuls);
  369. }
  370. if (ASScrollDirectionContainsVerticalDirection(scrollDirection)) {
  371. if (displayRect.size.height >= preloadRect.size.height) {
  372. displayRangeLargerThanPreload = YES;
  373. } else {
  374. displayRangeLargerThanPreload = NO;
  375. }
  376. if (displayRangeLargerThanPreload) {
  377. visibleRatio = visibleRect.size.height / displayRect.size.height;
  378. displayRatio = 1.0;
  379. preloadRatio = preloadRect.size.height / displayRect.size.height;
  380. } else {
  381. visibleRatio = visibleRect.size.height / preloadRect.size.height;
  382. displayRatio = displayRect.size.height / preloadRect.size.height;
  383. preloadRatio = 1.0;
  384. }
  385. } else {
  386. if (displayRect.size.width >= preloadRect.size.width) {
  387. displayRangeLargerThanPreload = YES;
  388. } else {
  389. displayRangeLargerThanPreload = NO;
  390. }
  391. if (displayRangeLargerThanPreload) {
  392. visibleRatio = visibleRect.size.width / displayRect.size.width;
  393. displayRatio = 1.0;
  394. preloadRatio = preloadRect.size.width / displayRect.size.width;
  395. } else {
  396. visibleRatio = visibleRect.size.width / preloadRect.size.width;
  397. displayRatio = displayRect.size.width / preloadRect.size.width;
  398. preloadRatio = 1.0;
  399. }
  400. }
  401. [viewToUpdate updateWithVisibleRatio:visibleRatio
  402. displayRatio:displayRatio
  403. leadingDisplayRatio:leadingDisplayTuningRatio
  404. preloadRatio:preloadRatio
  405. leadingpreloadRatio:leadingPreloadTuningRatio
  406. direction:scrollDirection];
  407. [self setNeedsLayout];
  408. }
  409. - (_ASRangeDebugBarView *)barViewForRangeController:(ASRangeController *)controller
  410. {
  411. _ASRangeDebugBarView *rangeControllerBarView = nil;
  412. for (_ASRangeDebugBarView *rangeView in [[_rangeControllerViews reverseObjectEnumerator] allObjects]) {
  413. // remove barView if its rangeController has been deleted
  414. if (!rangeView.rangeController) {
  415. rangeView.destroyOnLayout = YES;
  416. [self setNeedsLayout];
  417. }
  418. ASInterfaceState interfaceState = [rangeView.rangeController.dataSource interfaceStateForRangeController:rangeView.rangeController];
  419. if (!(interfaceState & (ASInterfaceStateVisible | ASInterfaceStateDisplay))) {
  420. [self setNeedsLayout];
  421. }
  422. if ([rangeView.rangeController isEqual:controller]) {
  423. rangeControllerBarView = rangeView;
  424. }
  425. }
  426. return rangeControllerBarView;
  427. }
  428. #define MIN_VISIBLE_INSET 40
  429. - (void)rangeDebugOverlayWasPanned:(UIPanGestureRecognizer *)recognizer
  430. {
  431. CGPoint translation = [recognizer translationInView:recognizer.view];
  432. CGFloat newCenterX = recognizer.view.center.x + translation.x;
  433. CGFloat newCenterY = recognizer.view.center.y + translation.y;
  434. CGSize boundsSize = recognizer.view.bounds.size;
  435. CGSize superBoundsSize = recognizer.view.superview.bounds.size;
  436. CGFloat minAllowableX = -boundsSize.width / 2.0 + MIN_VISIBLE_INSET;
  437. CGFloat maxAllowableX = superBoundsSize.width + boundsSize.width / 2.0 - MIN_VISIBLE_INSET;
  438. if (newCenterX > maxAllowableX) {
  439. newCenterX = maxAllowableX;
  440. } else if (newCenterX < minAllowableX) {
  441. newCenterX = minAllowableX;
  442. }
  443. CGFloat minAllowableY = -boundsSize.height / 2.0 + MIN_VISIBLE_INSET;
  444. CGFloat maxAllowableY = superBoundsSize.height + boundsSize.height / 2.0 - MIN_VISIBLE_INSET;
  445. if (newCenterY > maxAllowableY) {
  446. newCenterY = maxAllowableY;
  447. } else if (newCenterY < minAllowableY) {
  448. newCenterY = minAllowableY;
  449. }
  450. recognizer.view.center = CGPointMake(newCenterX, newCenterY);
  451. [recognizer setTranslation:CGPointMake(0, 0) inView:recognizer.view];
  452. }
  453. @end
  454. #pragma mark _ASRangeDebugBarView
  455. @implementation _ASRangeDebugBarView
  456. {
  457. ASTextNode *_debugText;
  458. ASTextNode *_leftDebugText;
  459. ASTextNode *_rightDebugText;
  460. ASImageNode *_visibleRect;
  461. ASImageNode *_displayRect;
  462. ASImageNode *_preloadRect;
  463. CGFloat _visibleRatio;
  464. CGFloat _displayRatio;
  465. CGFloat _preloadRatio;
  466. CGFloat _leadingDisplayRatio;
  467. CGFloat _leadingpreloadRatio;
  468. ASScrollDirection _scrollDirection;
  469. BOOL _firstLayoutOfRects;
  470. }
  471. - (instancetype)initWithRangeController:(ASRangeController *)rangeController
  472. {
  473. self = [super initWithFrame:CGRectZero];
  474. if (self) {
  475. _firstLayoutOfRects = YES;
  476. _rangeController = rangeController;
  477. _debugText = [self createDebugTextNode];
  478. _leftDebugText = [self createDebugTextNode];
  479. _rightDebugText = [self createDebugTextNode];
  480. _preloadRect = [self createRangeNodeWithColor:[UIColor orangeColor]];
  481. _displayRect = [self createRangeNodeWithColor:[UIColor yellowColor]];
  482. _visibleRect = [self createRangeNodeWithColor:[UIColor greenColor]];
  483. }
  484. return self;
  485. }
  486. #define HORIZONTAL_INSET 10
  487. - (void)layoutSubviews
  488. {
  489. [super layoutSubviews];
  490. CGSize boundsSize = self.bounds.size;
  491. CGFloat subCellHeight = 9.0;
  492. [self setBarDebugLabelsWithSize:subCellHeight];
  493. [self setBarSubviewOrder];
  494. CGRect rect = CGRectIntegral(CGRectMake(0, 0, boundsSize.width, floorf(boundsSize.height / 2.0)));
  495. rect.size = [_debugText layoutThatFits:ASSizeRangeMake(CGSizeZero, CGSizeMake(CGFLOAT_MAX, CGFLOAT_MAX))].size;
  496. rect.origin.x = (boundsSize.width - rect.size.width) / 2.0;
  497. _debugText.frame = rect;
  498. rect.origin.y += rect.size.height;
  499. rect.origin.x = 0;
  500. rect.size = CGSizeMake(HORIZONTAL_INSET, boundsSize.height / 2.0);
  501. _leftDebugText.frame = rect;
  502. rect.origin.x = boundsSize.width - HORIZONTAL_INSET;
  503. _rightDebugText.frame = rect;
  504. CGFloat visibleDimension = (boundsSize.width - 2 * HORIZONTAL_INSET) * _visibleRatio;
  505. CGFloat displayDimension = (boundsSize.width - 2 * HORIZONTAL_INSET) * _displayRatio;
  506. CGFloat preloadDimension = (boundsSize.width - 2 * HORIZONTAL_INSET) * _preloadRatio;
  507. CGFloat visiblePoint = 0;
  508. CGFloat displayPoint = 0;
  509. CGFloat preloadPoint = 0;
  510. BOOL displayLargerThanPreload = (_displayRatio == 1.0) ? YES : NO;
  511. if (ASScrollDirectionContainsLeft(_scrollDirection) || ASScrollDirectionContainsUp(_scrollDirection)) {
  512. if (displayLargerThanPreload) {
  513. visiblePoint = (displayDimension - visibleDimension) * _leadingDisplayRatio;
  514. preloadPoint = visiblePoint - (preloadDimension - visibleDimension) * _leadingpreloadRatio;
  515. } else {
  516. visiblePoint = (preloadDimension - visibleDimension) * _leadingpreloadRatio;
  517. displayPoint = visiblePoint - (displayDimension - visibleDimension) * _leadingDisplayRatio;
  518. }
  519. } else if (ASScrollDirectionContainsRight(_scrollDirection) || ASScrollDirectionContainsDown(_scrollDirection)) {
  520. if (displayLargerThanPreload) {
  521. visiblePoint = (displayDimension - visibleDimension) * (1 - _leadingDisplayRatio);
  522. preloadPoint = visiblePoint - (preloadDimension - visibleDimension) * (1 - _leadingpreloadRatio);
  523. } else {
  524. visiblePoint = (preloadDimension - visibleDimension) * (1 - _leadingpreloadRatio);
  525. displayPoint = visiblePoint - (displayDimension - visibleDimension) * (1 - _leadingDisplayRatio);
  526. }
  527. }
  528. BOOL animate = !_firstLayoutOfRects;
  529. [UIView animateWithDuration:animate ? 0.3 : 0.0 delay:0.0 options:UIViewAnimationOptionLayoutSubviews animations:^{
  530. _visibleRect.frame = CGRectMake(HORIZONTAL_INSET + visiblePoint, rect.origin.y, visibleDimension, subCellHeight);
  531. _displayRect.frame = CGRectMake(HORIZONTAL_INSET + displayPoint, rect.origin.y, displayDimension, subCellHeight);
  532. _preloadRect.frame = CGRectMake(HORIZONTAL_INSET + preloadPoint, rect.origin.y, preloadDimension, subCellHeight);
  533. } completion:^(BOOL finished) {}];
  534. if (!animate) {
  535. _visibleRect.alpha = _displayRect.alpha = _preloadRect.alpha = 0;
  536. [UIView animateWithDuration:0.3 animations:^{
  537. _visibleRect.alpha = _displayRect.alpha = _preloadRect.alpha = 1;
  538. }];
  539. }
  540. _firstLayoutOfRects = NO;
  541. }
  542. - (void)updateWithVisibleRatio:(CGFloat)visibleRatio
  543. displayRatio:(CGFloat)displayRatio
  544. leadingDisplayRatio:(CGFloat)leadingDisplayRatio
  545. preloadRatio:(CGFloat)preloadRatio
  546. leadingpreloadRatio:(CGFloat)leadingpreloadRatio
  547. direction:(ASScrollDirection)scrollDirection
  548. {
  549. _visibleRatio = visibleRatio;
  550. _displayRatio = displayRatio;
  551. _leadingDisplayRatio = leadingDisplayRatio;
  552. _preloadRatio = preloadRatio;
  553. _leadingpreloadRatio = leadingpreloadRatio;
  554. _scrollDirection = scrollDirection;
  555. [self setNeedsLayout];
  556. }
  557. - (void)setBarSubviewOrder
  558. {
  559. if (_preloadRatio == 1.0) {
  560. [self sendSubviewToBack:_preloadRect.view];
  561. } else {
  562. [self sendSubviewToBack:_displayRect.view];
  563. }
  564. [self bringSubviewToFront:_visibleRect.view];
  565. }
  566. - (void)setBarDebugLabelsWithSize:(CGFloat)size
  567. {
  568. if (!_debugString) {
  569. _debugString = [[_rangeController dataSource] nameForRangeControllerDataSource];
  570. }
  571. if (_debugString) {
  572. _debugText.attributedText = [_ASRangeDebugBarView whiteAttributedStringFromString:_debugString withSize:size];
  573. }
  574. if (ASScrollDirectionContainsVerticalDirection(_scrollDirection)) {
  575. _leftDebugText.attributedText = [_ASRangeDebugBarView whiteAttributedStringFromString:@"▲" withSize:size];
  576. _rightDebugText.attributedText = [_ASRangeDebugBarView whiteAttributedStringFromString:@"▼" withSize:size];
  577. } else if (ASScrollDirectionContainsHorizontalDirection(_scrollDirection)) {
  578. _leftDebugText.attributedText = [_ASRangeDebugBarView whiteAttributedStringFromString:@"◀︎" withSize:size];
  579. _rightDebugText.attributedText = [_ASRangeDebugBarView whiteAttributedStringFromString:@"▶︎" withSize:size];
  580. }
  581. _leftDebugText.hidden = (_scrollDirection != ASScrollDirectionLeft && _scrollDirection != ASScrollDirectionUp);
  582. _rightDebugText.hidden = (_scrollDirection != ASScrollDirectionRight && _scrollDirection != ASScrollDirectionDown);
  583. }
  584. - (ASTextNode *)createDebugTextNode
  585. {
  586. ASTextNode *label = [[ASTextNode alloc] init];
  587. [self addSubnode:label];
  588. return label;
  589. }
  590. #define RANGE_BAR_CORNER_RADIUS 3
  591. #define RANGE_BAR_BORDER_WIDTH 1
  592. - (ASImageNode *)createRangeNodeWithColor:(UIColor *)color
  593. {
  594. ASImageNode *rangeBarImageNode = [[ASImageNode alloc] init];
  595. rangeBarImageNode.image = [UIImage as_resizableRoundedImageWithCornerRadius:RANGE_BAR_CORNER_RADIUS
  596. cornerColor:[UIColor clearColor]
  597. fillColor:[color colorWithAlphaComponent:0.5]
  598. borderColor:[[UIColor blackColor] colorWithAlphaComponent:0.9]
  599. borderWidth:RANGE_BAR_BORDER_WIDTH
  600. roundedCorners:UIRectCornerAllCorners
  601. scale:[[UIScreen mainScreen] scale]];
  602. [self addSubnode:rangeBarImageNode];
  603. return rangeBarImageNode;
  604. }
  605. + (NSAttributedString *)whiteAttributedStringFromString:(NSString *)string withSize:(CGFloat)size
  606. {
  607. NSDictionary *attributes = @{NSForegroundColorAttributeName : [UIColor whiteColor],
  608. NSFontAttributeName : [UIFont systemFontOfSize:size]};
  609. return [[NSAttributedString alloc] initWithString:string attributes:attributes];
  610. }
  611. @end