AsyncDisplayKit+Debug.m 29 KB

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