diff --git a/AsyncDisplayKit/ASCollectionNode.mm b/AsyncDisplayKit/ASCollectionNode.mm index c8d7aa2573..510ad4e50b 100644 --- a/AsyncDisplayKit/ASCollectionNode.mm +++ b/AsyncDisplayKit/ASCollectionNode.mm @@ -17,6 +17,7 @@ #import "ASEnvironmentInternal.h" #import "ASInternalHelpers.h" #import "ASCellNode+Internal.h" +#import "AsyncDisplayKit+Debug.h" #pragma mark - _ASCollectionPendingState @@ -171,6 +172,12 @@ [self.view clearFetchedData]; } +- (void)interfaceStateDidChange:(ASInterfaceState)newState fromState:(ASInterfaceState)oldState +{ + [super interfaceStateDidChange:newState fromState:oldState]; + [ASRangeController layoutDebugOverlayIfNeeded]; +} + #if ASRangeControllerLoggingEnabled - (void)visibleStateDidChange:(BOOL)isVisible { diff --git a/AsyncDisplayKit/ASCollectionView.mm b/AsyncDisplayKit/ASCollectionView.mm index 5e71ffd40c..0c581d92b4 100644 --- a/AsyncDisplayKit/ASCollectionView.mm +++ b/AsyncDisplayKit/ASCollectionView.mm @@ -1080,6 +1080,11 @@ static NSString * const kCellReuseIdentifier = @"_ASCollectionViewCell"; return [_dataController nodeAtIndexPath:indexPath]; } +- (NSString *)nameForRangeControllerDataSource +{ + return self.asyncDataSource ? NSStringFromClass([self.asyncDataSource class]) : NSStringFromClass([self class]); +} + #pragma mark - ASRangeControllerDelegate - (void)didBeginUpdatesInRangeController:(ASRangeController *)rangeController diff --git a/AsyncDisplayKit/ASTableNode.mm b/AsyncDisplayKit/ASTableNode.mm index 2f4de2d0cf..f8af1ae0cb 100644 --- a/AsyncDisplayKit/ASTableNode.mm +++ b/AsyncDisplayKit/ASTableNode.mm @@ -16,6 +16,7 @@ #import "ASDisplayNode+Subclasses.h" #import "ASInternalHelpers.h" #import "ASCellNode+Internal.h" +#import "AsyncDisplayKit+Debug.h" #pragma mark - _ASTablePendingState @@ -125,6 +126,12 @@ [self.view clearFetchedData]; } +- (void)interfaceStateDidChange:(ASInterfaceState)newState fromState:(ASInterfaceState)oldState +{ + [super interfaceStateDidChange:newState fromState:oldState]; + [ASRangeController layoutDebugOverlayIfNeeded]; +} + #if ASRangeControllerLoggingEnabled - (void)visibleStateDidChange:(BOOL)isVisible { diff --git a/AsyncDisplayKit/ASTableView.mm b/AsyncDisplayKit/ASTableView.mm index 958530cca9..85d24d199d 100644 --- a/AsyncDisplayKit/ASTableView.mm +++ b/AsyncDisplayKit/ASTableView.mm @@ -937,6 +937,11 @@ static NSString * const kCellReuseIdentifier = @"_ASTableViewCell"; return ASInterfaceStateForDisplayNode(self.tableNode, self.window); } +- (NSString *)nameForRangeControllerDataSource +{ + return self.asyncDataSource ? NSStringFromClass([self.asyncDataSource class]) : NSStringFromClass([self class]); +} + #pragma mark - ASRangeControllerDelegate - (void)didBeginUpdatesInRangeController:(ASRangeController *)rangeController diff --git a/AsyncDisplayKit/AsyncDisplayKit+Debug.h b/AsyncDisplayKit/AsyncDisplayKit+Debug.h index 09c20f2b66..a85dda03fc 100644 --- a/AsyncDisplayKit/AsyncDisplayKit+Debug.h +++ b/AsyncDisplayKit/AsyncDisplayKit+Debug.h @@ -12,6 +12,7 @@ #import "ASControlNode.h" #import "ASImageNode.h" +#import "ASRangeController.h" @interface ASImageNode (Debugging) @@ -29,16 +30,40 @@ @interface ASControlNode (Debugging) /** - Class method to enable a visualization overlay of the tappable area on the ASControlNode. For app debugging purposes only. - NOTE: GESTURE RECOGNIZERS, (including tap gesture recognizers on a control node) WILL NOT BE VISUALIZED!!! - Overlay = translucent GREEN color, - edges that are clipped by the tappable area of any parent (their bounds + hitTestSlop) in the hierarchy = DARK GREEN BORDERED EDGE, - edges that are clipped by clipToBounds = YES of any parent in the hierarchy = ORANGE BORDERED EDGE (may still receive touches beyond - overlay rect, but can't be visualized). - @param enable Specify YES to make this debug feature enabled when messaging the ASControlNode class. + * Class method to enable a visualization overlay of the tappable area on the ASControlNode. For app debugging purposes only. + * NOTE: GESTURE RECOGNIZERS, (including tap gesture recognizers on a control node) WILL NOT BE VISUALIZED!!! + * Overlay = translucent GREEN color, + * edges that are clipped by the tappable area of any parent (their bounds + hitTestSlop) in the hierarchy = DARK GREEN BORDERED EDGE, + * edges that are clipped by clipToBounds = YES of any parent in the hierarchy = ORANGE BORDERED EDGE (may still receive touches beyond + * overlay rect, but can't be visualized). + * @param enable Specify YES to make this debug feature enabled when messaging the ASControlNode class. */ + (void)setEnableHitTestDebug:(BOOL)enable; + (BOOL)enableHitTestDebug; @end +@interface ASRangeController (Debugging) + +/** + * Class method to enable a visualization overlay of the all ASRangeController's tuning parameters. For dev purposes only. + * To use, message ASRangeController in the AppDelegate --> [ASRangeController setShouldShowRangeDebugOverlay:YES]; + * @param enable Specify YES to make this debug feature enabled when messaging the ASRangeController class. + */ ++ (void)setShouldShowRangeDebugOverlay:(BOOL)show; ++ (BOOL)shouldShowRangeDebugOverlay; + ++ (void)layoutDebugOverlayIfNeeded; + +- (void)addRangeControllerToRangeDebugOverlay; + +- (void)updateRangeController:(ASRangeController *)controller + withScrollableDirections:(ASScrollDirection)scrollableDirections + scrollDirection:(ASScrollDirection)direction + rangeMode:(ASLayoutRangeMode)mode + displayTuningParameters:(ASRangeTuningParameters)displayTuningParameters + fetchDataTuningParameters:(ASRangeTuningParameters)fetchDataTuningParameters + interfaceState:(ASInterfaceState)interfaceState; + +@end + diff --git a/AsyncDisplayKit/AsyncDisplayKit+Debug.m b/AsyncDisplayKit/AsyncDisplayKit+Debug.m index 86d77e0f3b..e456ce72f6 100644 --- a/AsyncDisplayKit/AsyncDisplayKit+Debug.m +++ b/AsyncDisplayKit/AsyncDisplayKit+Debug.m @@ -13,6 +13,16 @@ #import "AsyncDisplayKit+Debug.h" #import "ASDisplayNode+Subclasses.h" #import "ASDisplayNodeExtras.h" +#import "ASWeakSet.h" +#import "UIImage+ASConvenience.h" +#import +#import +#import +#import +#import + + +#pragma mark - ASImageNode (Debugging) static BOOL __shouldShowImageScalingOverlay = NO; @@ -30,6 +40,8 @@ static BOOL __shouldShowImageScalingOverlay = NO; @end +#pragma mark - ASControlNode (DebuggingInternal) + static BOOL __enableHitTestDebug = NO; @interface ASControlNode (DebuggingInternal) @@ -191,3 +203,551 @@ static BOOL __enableHitTestDebug = NO; } @end + +#pragma mark - ASRangeController (Debugging) + +@interface _ASRangeDebugOverlayView : UIView + ++ (instancetype)sharedInstance; + +- (void)addRangeController:(ASRangeController *)rangeController; + +- (void)updateRangeController:(ASRangeController *)controller + withScrollableDirections:(ASScrollDirection)scrollableDirections + scrollDirection:(ASScrollDirection)direction + rangeMode:(ASLayoutRangeMode)mode + displayTuningParameters:(ASRangeTuningParameters)displayTuningParameters + fetchDataTuningParameters:(ASRangeTuningParameters)fetchDataTuningParameters + interfaceState:(ASInterfaceState)interfaceState; + +@end + +@interface _ASRangeDebugBarView : UIView + +@property (nonatomic, weak) ASRangeController *rangeController; +@property (nonatomic, assign) BOOL destroyOnLayout; +@property (nonatomic, strong) NSString *debugString; + +- (instancetype)initWithRangeController:(ASRangeController *)rangeController; + +- (void)updateWithVisibleRatio:(CGFloat)visibleRatio + displayRatio:(CGFloat)displayRatio + leadingDisplayRatio:(CGFloat)leadingDisplayRatio + fetchDataRatio:(CGFloat)fetchDataRatio + leadingFetchDataRatio:(CGFloat)leadingFetchDataRatio + direction:(ASScrollDirection)direction; + +@end + +static BOOL __shouldShowRangeDebugOverlay = NO; + +@implementation ASRangeController (Debugging) + ++ (void)setShouldShowRangeDebugOverlay:(BOOL)show +{ + __shouldShowRangeDebugOverlay = show; +} + ++ (BOOL)shouldShowRangeDebugOverlay +{ + return __shouldShowRangeDebugOverlay; +} + ++ (void)layoutDebugOverlayIfNeeded +{ + [[_ASRangeDebugOverlayView sharedInstance] setNeedsLayout]; +} + +- (void)addRangeControllerToRangeDebugOverlay +{ + [[_ASRangeDebugOverlayView sharedInstance] addRangeController:self]; +} + +- (void)updateRangeController:(ASRangeController *)controller + withScrollableDirections:(ASScrollDirection)scrollableDirections + scrollDirection:(ASScrollDirection)direction + rangeMode:(ASLayoutRangeMode)mode + displayTuningParameters:(ASRangeTuningParameters)displayTuningParameters + fetchDataTuningParameters:(ASRangeTuningParameters)fetchDataTuningParameters + interfaceState:(ASInterfaceState)interfaceState +{ + [[_ASRangeDebugOverlayView sharedInstance] updateRangeController:controller + withScrollableDirections:scrollableDirections + scrollDirection:direction + rangeMode:mode + displayTuningParameters:displayTuningParameters + fetchDataTuningParameters:fetchDataTuningParameters + interfaceState:interfaceState]; +} + +@end + + +#pragma mark _ASRangeDebugOverlayView + +@interface _ASRangeDebugOverlayView () +@end + +@implementation _ASRangeDebugOverlayView +{ + NSMutableArray *_rangeControllerViews; + NSInteger _newControllerCount; + NSInteger _removeControllerCount; + BOOL _animating; +} + ++ (UIWindow *)keyWindow +{ + // hack to work around app extensions not having UIApplication...not sure of a better way to do this? + return [[NSClassFromString(@"UIApplication") sharedApplication] keyWindow]; +} + ++ (instancetype)sharedInstance +{ + static _ASRangeDebugOverlayView *__rangeDebugOverlay = nil; + + if (!__rangeDebugOverlay && [ASRangeController shouldShowRangeDebugOverlay]) { + __rangeDebugOverlay = [[self alloc] initWithFrame:CGRectZero]; + [[self keyWindow] addSubview:__rangeDebugOverlay]; + } + + return __rangeDebugOverlay; +} + +#define OVERLAY_INSET 10 +#define OVERLAY_SCALE 3 +- (instancetype)initWithFrame:(CGRect)frame +{ + self = [super initWithFrame:frame]; + + if (self) { + _rangeControllerViews = [[NSMutableArray alloc] init]; + self.backgroundColor = [[UIColor blackColor] colorWithAlphaComponent:0.4]; + self.layer.zPosition = 1000; + self.clipsToBounds = YES; + + CGSize windowSize = [[[self class] keyWindow] bounds].size; + self.frame = CGRectMake(windowSize.width - (windowSize.width / OVERLAY_SCALE) - OVERLAY_INSET, windowSize.height - OVERLAY_INSET, + windowSize.width / OVERLAY_SCALE, 0.0); + + UIPanGestureRecognizer *panGR = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(rangeDebugOverlayWasPanned:)]; + [self addGestureRecognizer:panGR]; + } + + return self; +} + +#define BAR_THICKNESS 24 + +- (void)layoutSubviews +{ + [super layoutSubviews]; + [UIView animateWithDuration:0.2 delay:0.0 options:UIViewAnimationOptionBeginFromCurrentState animations:^{ + [self layoutToFitAllBarsExcept:0]; + } completion:^(BOOL finished) { + + }]; +} + +- (void)layoutToFitAllBarsExcept:(NSInteger)barsToClip +{ + CGSize boundsSize = self.bounds.size; + CGFloat totalHeight = 0.0; + + CGRect barRect = CGRectMake(0, boundsSize.height - BAR_THICKNESS, self.bounds.size.width, BAR_THICKNESS); + NSMutableArray *displayedBars = [NSMutableArray array]; + + for (_ASRangeDebugBarView *barView in [_rangeControllerViews copy]) { + barView.frame = barRect; + + ASInterfaceState interfaceState = [barView.rangeController.dataSource interfaceStateForRangeController:barView.rangeController]; + + if (!(interfaceState & (ASInterfaceStateVisible))) { + if (barView.destroyOnLayout && barView.alpha == 0.0) { + [_rangeControllerViews removeObjectIdenticalTo:barView]; + [barView removeFromSuperview]; + } else { + barView.alpha = 0.0; + } + } else { + assert(!barView.destroyOnLayout); // In this case we should not have a visible interfaceState + barView.alpha = 1.0; + totalHeight += BAR_THICKNESS; + barRect.origin.y -= BAR_THICKNESS; + [displayedBars addObject:barView]; + } + } + + if (totalHeight > 0) { + totalHeight -= (BAR_THICKNESS * barsToClip); + } + + if (barsToClip == 0) { + CGRect overlayFrame = self.frame; + CGFloat heightChange = (overlayFrame.size.height - totalHeight); + + overlayFrame.origin.y += heightChange; + overlayFrame.size.height = totalHeight; + self.frame = overlayFrame; + + for (_ASRangeDebugBarView *barView in displayedBars) { + [self offsetYOrigin:-heightChange forView:barView]; + } + } +} + +- (void)setOrigin:(CGPoint)origin forView:(UIView *)view +{ + CGRect newFrame = view.frame; + newFrame.origin = origin; + view.frame = newFrame; +} + +- (void)offsetYOrigin:(CGFloat)offset forView:(UIView *)view +{ + CGRect newFrame = view.frame; + newFrame.origin = CGPointMake(newFrame.origin.x, newFrame.origin.y + offset); + view.frame = newFrame; +} + +- (void)addRangeController:(ASRangeController *)rangeController +{ + for (_ASRangeDebugBarView *rangeView in _rangeControllerViews) { + if (rangeView.rangeController == rangeController) { + return; + } + } + _ASRangeDebugBarView *rangeView = [[_ASRangeDebugBarView alloc] initWithRangeController:rangeController]; + [_rangeControllerViews addObject:rangeView]; + [self addSubview:rangeView]; + + if (!_animating) { + [self layoutToFitAllBarsExcept:1]; + } + + [UIView animateWithDuration:0.2 delay:0.0 options:UIViewAnimationOptionBeginFromCurrentState animations:^{ + _animating = YES; + [self layoutToFitAllBarsExcept:0]; + } completion:^(BOOL finished) { + _animating = NO; + }]; +} + +- (void)updateRangeController:(ASRangeController *)controller + withScrollableDirections:(ASScrollDirection)scrollableDirections + scrollDirection:(ASScrollDirection)scrollDirection + rangeMode:(ASLayoutRangeMode)rangeMode + displayTuningParameters:(ASRangeTuningParameters)displayTuningParameters + fetchDataTuningParameters:(ASRangeTuningParameters)fetchDataTuningParameters + interfaceState:(ASInterfaceState)interfaceState; +{ + _ASRangeDebugBarView *viewToUpdate = [self barViewForRangeController:controller]; + + CGRect boundsRect = self.bounds; + CGRect visibleRect = CGRectExpandToRangeWithScrollableDirections(boundsRect, ASRangeTuningParametersZero, scrollableDirections, scrollDirection); + CGRect displayRect = CGRectExpandToRangeWithScrollableDirections(boundsRect, displayTuningParameters, scrollableDirections, scrollDirection); + CGRect fetchDataRect = CGRectExpandToRangeWithScrollableDirections(boundsRect, fetchDataTuningParameters, scrollableDirections, scrollDirection); + + // figure out which is biggest and assume that is full bounds + BOOL displayRangeLargerThanFetch = NO; + CGFloat visibleRatio = 0; + CGFloat displayRatio = 0; + CGFloat fetchDataRatio = 0; + CGFloat leadingDisplayTuningRatio = 0; + CGFloat leadingFetchDataTuningRatio = 0; + + if (!((displayTuningParameters.leadingBufferScreenfuls + displayTuningParameters.trailingBufferScreenfuls) == 0)) { + leadingDisplayTuningRatio = displayTuningParameters.leadingBufferScreenfuls / (displayTuningParameters.leadingBufferScreenfuls + displayTuningParameters.trailingBufferScreenfuls); + } + if (!((fetchDataTuningParameters.leadingBufferScreenfuls + fetchDataTuningParameters.trailingBufferScreenfuls) == 0)) { + leadingFetchDataTuningRatio = fetchDataTuningParameters.leadingBufferScreenfuls / (fetchDataTuningParameters.leadingBufferScreenfuls + fetchDataTuningParameters.trailingBufferScreenfuls); + } + + if (ASScrollDirectionContainsVerticalDirection(scrollDirection)) { + + if (displayRect.size.height >= fetchDataRect.size.height) { + displayRangeLargerThanFetch = YES; + } else { + displayRangeLargerThanFetch = NO; + } + + if (displayRangeLargerThanFetch) { + visibleRatio = visibleRect.size.height / displayRect.size.height; + displayRatio = 1.0; + fetchDataRatio = fetchDataRect.size.height / displayRect.size.height; + } else { + visibleRatio = visibleRect.size.height / fetchDataRect.size.height; + displayRatio = displayRect.size.height / fetchDataRect.size.height; + fetchDataRatio = 1.0; + } + + } else { + + if (displayRect.size.width >= fetchDataRect.size.width) { + displayRangeLargerThanFetch = YES; + } else { + displayRangeLargerThanFetch = NO; + } + + if (displayRangeLargerThanFetch) { + visibleRatio = visibleRect.size.width / displayRect.size.width; + displayRatio = 1.0; + fetchDataRatio = fetchDataRect.size.width / displayRect.size.width; + } else { + visibleRatio = visibleRect.size.width / fetchDataRect.size.width; + displayRatio = displayRect.size.width / fetchDataRect.size.width; + fetchDataRatio = 1.0; + } + } + + [viewToUpdate updateWithVisibleRatio:visibleRatio + displayRatio:displayRatio + leadingDisplayRatio:leadingDisplayTuningRatio + fetchDataRatio:fetchDataRatio + leadingFetchDataRatio:leadingFetchDataTuningRatio + direction:scrollDirection]; + + [self setNeedsLayout]; +} + +- (_ASRangeDebugBarView *)barViewForRangeController:(ASRangeController *)controller +{ + _ASRangeDebugBarView *rangeControllerBarView = nil; + + for (_ASRangeDebugBarView *rangeView in [[_rangeControllerViews reverseObjectEnumerator] allObjects]) { + // remove barView if its rangeController has been deleted + if (!rangeView.rangeController) { + rangeView.destroyOnLayout = YES; + [self setNeedsLayout]; + } + ASInterfaceState interfaceState = [rangeView.rangeController.dataSource interfaceStateForRangeController:rangeView.rangeController]; + if (!(interfaceState & (ASInterfaceStateVisible | ASInterfaceStateDisplay))) { + [self setNeedsLayout]; + } + + if ([rangeView.rangeController isEqual:controller]) { + rangeControllerBarView = rangeView; + } + } + + return rangeControllerBarView; +} + +#define MIN_VISIBLE_INSET 40 +- (void)rangeDebugOverlayWasPanned:(UIPanGestureRecognizer *)recognizer +{ + CGPoint translation = [recognizer translationInView:recognizer.view]; + CGFloat newCenterX = recognizer.view.center.x + translation.x; + CGFloat newCenterY = recognizer.view.center.y + translation.y; + CGSize boundsSize = recognizer.view.bounds.size; + CGSize superBoundsSize = recognizer.view.superview.bounds.size; + CGFloat minAllowableX = -boundsSize.width / 2.0 + MIN_VISIBLE_INSET; + CGFloat maxAllowableX = superBoundsSize.width + boundsSize.width / 2.0 - MIN_VISIBLE_INSET; + + if (newCenterX > maxAllowableX) { + newCenterX = maxAllowableX; + } else if (newCenterX < minAllowableX) { + newCenterX = minAllowableX; + } + + CGFloat minAllowableY = -boundsSize.height / 2.0 + MIN_VISIBLE_INSET; + CGFloat maxAllowableY = superBoundsSize.height + boundsSize.height / 2.0 - MIN_VISIBLE_INSET; + + if (newCenterY > maxAllowableY) { + newCenterY = maxAllowableY; + } else if (newCenterY < minAllowableY) { + newCenterY = minAllowableY; + } + + recognizer.view.center = CGPointMake(newCenterX, newCenterY); + [recognizer setTranslation:CGPointMake(0, 0) inView:recognizer.view]; +} + +@end + +#pragma mark _ASRangeDebugBarView + +@implementation _ASRangeDebugBarView +{ + ASTextNode *_debugText; + ASTextNode *_leftDebugText; + ASTextNode *_rightDebugText; + ASImageNode *_visibleRect; + ASImageNode *_displayRect; + ASImageNode *_fetchDataRect; + CGFloat _visibleRatio; + CGFloat _displayRatio; + CGFloat _fetchDataRatio; + CGFloat _leadingDisplayRatio; + CGFloat _leadingFetchDataRatio; + ASScrollDirection _scrollDirection; + BOOL _firstLayoutOfRects; +} + +- (instancetype)initWithRangeController:(ASRangeController *)rangeController +{ + self = [super initWithFrame:CGRectZero]; + if (self) { + _firstLayoutOfRects = YES; + _rangeController = rangeController; + _debugText = [self createDebugTextNode]; + _leftDebugText = [self createDebugTextNode]; + _rightDebugText = [self createDebugTextNode]; + _fetchDataRect = [self createRangeNodeWithColor:[UIColor orangeColor]]; + _displayRect = [self createRangeNodeWithColor:[UIColor yellowColor]]; + _visibleRect = [self createRangeNodeWithColor:[UIColor greenColor]]; + } + + return self; +} + +#define HORIZONTAL_INSET 10 +- (void)layoutSubviews +{ + [super layoutSubviews]; + + CGSize boundsSize = self.bounds.size; + CGFloat subCellHeight = 9.0; + [self setBarDebugLabelsWithSize:subCellHeight]; + [self setBarSubviewOrder]; + + CGRect rect = CGRectIntegral(CGRectMake(0, 0, boundsSize.width, floorf(boundsSize.height / 2.0))); + rect.size = [_debugText measure:CGSizeMake(CGFLOAT_MAX, CGFLOAT_MAX)]; + rect.origin.x = (boundsSize.width - rect.size.width) / 2.0; + _debugText.frame = rect; + rect.origin.y += rect.size.height; + + rect.origin.x = 0; + rect.size = CGSizeMake(HORIZONTAL_INSET, boundsSize.height / 2.0); + _leftDebugText.frame = rect; + + rect.origin.x = boundsSize.width - HORIZONTAL_INSET; + _rightDebugText.frame = rect; + + CGFloat visibleDimension = (boundsSize.width - 2 * HORIZONTAL_INSET) * _visibleRatio; + CGFloat displayDimension = (boundsSize.width - 2 * HORIZONTAL_INSET) * _displayRatio; + CGFloat fetchDataDimension = (boundsSize.width - 2 * HORIZONTAL_INSET) * _fetchDataRatio; + CGFloat visiblePoint = 0; + CGFloat displayPoint = 0; + CGFloat fetchDataPoint = 0; + + BOOL displayLargerThanFetchData = (_displayRatio == 1.0) ? YES : NO; + + if (ASScrollDirectionContainsLeft(_scrollDirection) || ASScrollDirectionContainsUp(_scrollDirection)) { + + if (displayLargerThanFetchData) { + visiblePoint = (displayDimension - visibleDimension) * _leadingDisplayRatio; + fetchDataPoint = visiblePoint - (fetchDataDimension - visibleDimension) * _leadingFetchDataRatio; + } else { + visiblePoint = (fetchDataDimension - visibleDimension) * _leadingFetchDataRatio; + displayPoint = visiblePoint - (displayDimension - visibleDimension) * _leadingDisplayRatio; + } + } else if (ASScrollDirectionContainsRight(_scrollDirection) || ASScrollDirectionContainsDown(_scrollDirection)) { + + if (displayLargerThanFetchData) { + visiblePoint = (displayDimension - visibleDimension) * (1 - _leadingDisplayRatio); + fetchDataPoint = visiblePoint - (fetchDataDimension - visibleDimension) * (1 - _leadingFetchDataRatio); + } else { + visiblePoint = (fetchDataDimension - visibleDimension) * (1 - _leadingFetchDataRatio); + displayPoint = visiblePoint - (displayDimension - visibleDimension) * (1 - _leadingDisplayRatio); + } + } + + BOOL animate = !_firstLayoutOfRects; + [UIView animateWithDuration:animate ? 0.3 : 0.0 delay:0.0 options:UIViewAnimationOptionLayoutSubviews animations:^{ + _visibleRect.frame = CGRectMake(HORIZONTAL_INSET + visiblePoint, rect.origin.y, visibleDimension, subCellHeight); + _displayRect.frame = CGRectMake(HORIZONTAL_INSET + displayPoint, rect.origin.y, displayDimension, subCellHeight); + _fetchDataRect.frame = CGRectMake(HORIZONTAL_INSET + fetchDataPoint, rect.origin.y, fetchDataDimension, subCellHeight); + } completion:^(BOOL finished) {}]; + + if (!animate) { + _visibleRect.alpha = _displayRect.alpha = _fetchDataRect.alpha = 0; + [UIView animateWithDuration:0.3 animations:^{ + _visibleRect.alpha = _displayRect.alpha = _fetchDataRect.alpha = 1; + }]; + } + + _firstLayoutOfRects = NO; +} + +- (void)updateWithVisibleRatio:(CGFloat)visibleRatio + displayRatio:(CGFloat)displayRatio + leadingDisplayRatio:(CGFloat)leadingDisplayRatio + fetchDataRatio:(CGFloat)fetchDataRatio + leadingFetchDataRatio:(CGFloat)leadingFetchDataRatio + direction:(ASScrollDirection)scrollDirection +{ + _visibleRatio = visibleRatio; + _displayRatio = displayRatio; + _leadingDisplayRatio = leadingDisplayRatio; + _fetchDataRatio = fetchDataRatio; + _leadingFetchDataRatio = leadingFetchDataRatio; + _scrollDirection = scrollDirection; + + [self setNeedsLayout]; +} + +- (void)setBarSubviewOrder +{ + if (_fetchDataRatio == 1.0) { + [self sendSubviewToBack:_fetchDataRect.view]; + } else { + [self sendSubviewToBack:_displayRect.view]; + } + + [self bringSubviewToFront:_visibleRect.view]; +} + +- (void)setBarDebugLabelsWithSize:(CGFloat)size +{ + if (!_debugString) { + _debugString = [[_rangeController dataSource] nameForRangeControllerDataSource]; + } + if (_debugString) { + _debugText.attributedString = [_ASRangeDebugBarView whiteAttributedStringFromString:_debugString withSize:size]; + } + + if (ASScrollDirectionContainsVerticalDirection(_scrollDirection)) { + _leftDebugText.attributedText = [_ASRangeDebugBarView whiteAttributedStringFromString:@"▲" withSize:size]; + _rightDebugText.attributedText = [_ASRangeDebugBarView whiteAttributedStringFromString:@"▼" withSize:size]; + } else if (ASScrollDirectionContainsHorizontalDirection(_scrollDirection)) { + _leftDebugText.attributedText = [_ASRangeDebugBarView whiteAttributedStringFromString:@"◀︎" withSize:size]; + _rightDebugText.attributedText = [_ASRangeDebugBarView whiteAttributedStringFromString:@"▶︎" withSize:size]; + } + + _leftDebugText.hidden = (_scrollDirection != ASScrollDirectionLeft && _scrollDirection != ASScrollDirectionUp); + _rightDebugText.hidden = (_scrollDirection != ASScrollDirectionRight && _scrollDirection != ASScrollDirectionDown); +} + +- (ASTextNode *)createDebugTextNode +{ + ASTextNode *label = [[ASTextNode alloc] init]; + [self addSubnode:label]; + return label; +} + +#define RANGE_BAR_CORNER_RADIUS 3 +#define RANGE_BAR_BORDER_WIDTH 1 +- (ASImageNode *)createRangeNodeWithColor:(UIColor *)color +{ + ASImageNode *rangeBarImageNode = [[ASImageNode alloc] init]; + rangeBarImageNode.image = [UIImage as_resizableRoundedImageWithCornerRadius:RANGE_BAR_CORNER_RADIUS + cornerColor:[UIColor clearColor] + fillColor:[color colorWithAlphaComponent:0.5] + borderColor:[[UIColor blackColor] colorWithAlphaComponent:0.9] + borderWidth:RANGE_BAR_BORDER_WIDTH + roundedCorners:UIRectCornerAllCorners + scale:[[UIScreen mainScreen] scale]]; + [self addSubnode:rangeBarImageNode]; + + return rangeBarImageNode; +} + ++ (NSAttributedString *)whiteAttributedStringFromString:(NSString *)string withSize:(CGFloat)size +{ + NSDictionary *attributes = @{NSForegroundColorAttributeName : [UIColor whiteColor], + NSFontAttributeName : [UIFont systemFontOfSize:size]}; + return [[NSAttributedString alloc] initWithString:string attributes:attributes]; +} + +@end diff --git a/AsyncDisplayKit/Details/ASRangeController.h b/AsyncDisplayKit/Details/ASRangeController.h index d682e07b9d..c46b1d99b5 100644 --- a/AsyncDisplayKit/Details/ASRangeController.h +++ b/AsyncDisplayKit/Details/ASRangeController.h @@ -134,6 +134,8 @@ NS_ASSUME_NONNULL_BEGIN - (NSArray *> *)completedNodes; +- (NSString *)nameForRangeControllerDataSource; + @end /** diff --git a/AsyncDisplayKit/Details/ASRangeController.mm b/AsyncDisplayKit/Details/ASRangeController.mm index d82073779d..e71e990123 100644 --- a/AsyncDisplayKit/Details/ASRangeController.mm +++ b/AsyncDisplayKit/Details/ASRangeController.mm @@ -11,13 +11,16 @@ #import "ASRangeController.h" #import "ASAssert.h" -#import "ASWeakSet.h" +#import "ASCellNode.h" #import "ASDisplayNodeExtras.h" #import "ASDisplayNodeInternal.h" #import "ASMultidimensionalArrayUtils.h" #import "ASInternalHelpers.h" +#import "ASMultiDimensionalArrayUtils.h" +#import "ASWeakSet.h" + #import "ASDisplayNode+FrameworkPrivate.h" -#import "ASCellNode.h" +#import "AsyncDisplayKit+Debug.h" #define AS_RANGECONTROLLER_LOG_UPDATE_FREQ 0 @@ -64,6 +67,10 @@ static UIApplicationState __ApplicationState = UIApplicationStateActive; [_displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes]; #endif + if ([ASRangeController shouldShowRangeDebugOverlay]) { + [self addRangeControllerToRangeDebugOverlay]; + } + return self; } @@ -335,6 +342,25 @@ static UIApplicationState __ApplicationState = UIApplicationStateActive; } } + // TODO: This code is for debugging only, but would be great to clean up with a delegate method implementation. + if ([ASRangeController shouldShowRangeDebugOverlay]) { + ASScrollDirection scrollableDirections = ASScrollDirectionUp | ASScrollDirectionDown; + if ([_dataSource isKindOfClass:NSClassFromString(@"ASCollectionView")]) { +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wundeclared-selector" + scrollableDirections = (ASScrollDirection)[_dataSource performSelector:@selector(scrollableDirections)]; +#pragma clang diagnostic pop + } + + [self updateRangeController:self + withScrollableDirections:scrollableDirections + scrollDirection:scrollDirection + rangeMode:rangeMode + displayTuningParameters:parametersDisplay + fetchDataTuningParameters:parametersFetchData + interfaceState:selfInterfaceState]; + } + _rangeIsValid = YES; #if ASRangeControllerLoggingEnabled diff --git a/examples/VerticalWithinHorizontalScrolling/Sample/ViewController.m b/examples/VerticalWithinHorizontalScrolling/Sample/ViewController.m index 74a5f53cc8..c506a3faa9 100644 --- a/examples/VerticalWithinHorizontalScrolling/Sample/ViewController.m +++ b/examples/VerticalWithinHorizontalScrolling/Sample/ViewController.m @@ -38,6 +38,7 @@ _pagerNode = [[ASPagerNode alloc] init]; _pagerNode.dataSource = self; + [ASRangeController setShouldShowRangeDebugOverlay:YES]; // Could implement ASCollectionDelegate if we wanted extra callbacks, like from UIScrollView. //_pagerNode.delegate = self;