diff --git a/AsyncDisplayKit/ASCollectionView.mm b/AsyncDisplayKit/ASCollectionView.mm index a9c79acf1c..f8929f170d 100644 --- a/AsyncDisplayKit/ASCollectionView.mm +++ b/AsyncDisplayKit/ASCollectionView.mm @@ -15,6 +15,7 @@ #import "ASCollectionViewLayoutController.h" #import "ASCollectionViewFlowLayoutInspector.h" #import "ASCollectionViewLayoutFacilitatorProtocol.h" +#import "ASDisplayNodeExtras.h" #import "ASDisplayNode+FrameworkPrivate.h" #import "ASDisplayNode+Beta.h" #import "ASInternalHelpers.h" @@ -57,6 +58,36 @@ static NSString * const kCellReuseIdentifier = @"_ASCollectionViewCell"; @end +#pragma mark - +#pragma mark _ASCollectionViewNodeSizeUpdateContext + +/** + * This class contains all the nodes that have a new size and UICollectionView should requery them all at once. + * It is intended to be used strictly on main thread and is not thread safe. + */ +@interface _ASCollectionViewNodeSizeInvalidationContext : NSObject +/** + * It's possible that a node triggered multiple size changes before main thread has a chance to execute `requeryNodeSizes`. + * Therefore, a set is preferred here, to avoid asking ASDataController to search for index path of the same node multiple times. + */ +@property (nonatomic, strong) NSMutableSet *invalidatedNodes; +@property (nonatomic, assign) BOOL shouldAnimate; +@end + +@implementation _ASCollectionViewNodeSizeInvalidationContext + +- (instancetype)init +{ + self = [super init]; + if (self) { + _invalidatedNodes = [NSMutableSet set]; + _shouldAnimate = YES; + } + return self; +} + +@end + #pragma mark - #pragma mark ASCollectionView. @@ -78,7 +109,7 @@ static NSString * const kCellReuseIdentifier = @"_ASCollectionViewCell"; BOOL _asyncDelegateImplementsScrollviewDidScroll; BOOL _asyncDataSourceImplementsConstrainedSizeForNode; BOOL _asyncDataSourceImplementsNodeBlockForItemAtIndexPath; - BOOL _queuedNodeSizeUpdate; + _ASCollectionViewNodeSizeInvalidationContext *_queuedNodeSizeInvalidationContext; // Main thread only BOOL _isDeallocating; ASBatchContext *_batchContext; @@ -1018,24 +1049,61 @@ static NSString * const kCellReuseIdentifier = @"_ASCollectionViewCell"; { ASDisplayNodeAssertMainThread(); - if (!sizeChanged || _queuedNodeSizeUpdate) { + if (!sizeChanged) { return; } - _queuedNodeSizeUpdate = YES; - [_layoutFacilitator collectionViewWillEditCellsAtIndexPaths:@[[self indexPathForNode:node]] batched:NO]; - [self performSelector:@selector(requeryNodeSizes) - withObject:nil - afterDelay:0 - inModes:@[ NSRunLoopCommonModes ]]; + BOOL queued = (_queuedNodeSizeInvalidationContext != nil); + if (!queued) { + _queuedNodeSizeInvalidationContext = [[_ASCollectionViewNodeSizeInvalidationContext alloc] init]; + + __weak __typeof__(self) weakSelf = self; + dispatch_async(dispatch_get_main_queue(), ^{ + __typeof__(self) strongSelf = weakSelf; + if (strongSelf) { + [strongSelf requeryNodeSizes]; + } + }); + } + + [_queuedNodeSizeInvalidationContext.invalidatedNodes addObject:node]; + + // Check if this node or one of its subnodes can be animated. + // If the context is already non-animated, don't bother checking this node. + if (_queuedNodeSizeInvalidationContext.shouldAnimate) { + BOOL (^shouldNotAnimateBlock)(ASDisplayNode *) = ^BOOL(ASDisplayNode * _Nonnull node) { + return node.shouldAnimateSizeChanges == NO; + }; + if (ASDisplayNodeFindFirstNode(node, shouldNotAnimateBlock) != nil) { + // One single non-animated cell node causes the whole context to be non-animated + _queuedNodeSizeInvalidationContext.shouldAnimate = NO; + } + } } // Cause UICollectionView to requery for the new size of all nodes - (void)requeryNodeSizes { - _queuedNodeSizeUpdate = NO; + ASDisplayNodeAssertMainThread(); + NSSet *nodes = _queuedNodeSizeInvalidationContext.invalidatedNodes; + NSMutableArray *indexPaths = [NSMutableArray arrayWithCapacity:nodes.count]; + for (ASCellNode *node in nodes) { + NSIndexPath *indexPath = [self indexPathForNode:node]; + if (indexPath != nil) { + [indexPaths addObject:indexPath]; + } + } - [super performBatchUpdates:^{} completion:nil]; + if (indexPaths.count > 0) { + [_layoutFacilitator collectionViewWillEditCellsAtIndexPaths:indexPaths batched:NO]; + + ASPerformBlockWithoutAnimation(!_queuedNodeSizeInvalidationContext.shouldAnimate, ^{ + // Perform an empty update transaction here to trigger UICollectionView to requery row sizes and layout its subviews again + [super performBatchUpdates:^{} completion:nil]; + }); + } + + _queuedNodeSizeInvalidationContext = nil; } #pragma mark - Memory Management diff --git a/AsyncDisplayKit/ASDisplayNode.h b/AsyncDisplayKit/ASDisplayNode.h index 2f67dc170c..7a8d6ddffe 100644 --- a/AsyncDisplayKit/ASDisplayNode.h +++ b/AsyncDisplayKit/ASDisplayNode.h @@ -428,6 +428,11 @@ NS_ASSUME_NONNULL_BEGIN */ @property (nonatomic, assign) BOOL displaySuspended; +/** + * @abstract Whether size changes should be animated. Default to YES. + */ +@property (nonatomic, assign) BOOL shouldAnimateSizeChanges; + /** * @abstract Prevent the node and its descendants' layer from displaying. * diff --git a/AsyncDisplayKit/ASDisplayNode.mm b/AsyncDisplayKit/ASDisplayNode.mm index d16479a5d5..cacd44e51b 100644 --- a/AsyncDisplayKit/ASDisplayNode.mm +++ b/AsyncDisplayKit/ASDisplayNode.mm @@ -114,6 +114,7 @@ static struct ASDisplayNodeFlags GetASDisplayNodeFlags(Class c, ASDisplayNode *i flags.isInHierarchy = NO; flags.displaysAsynchronously = YES; + flags.shouldAnimateSizeChanges = YES; flags.implementsDrawRect = ([c respondsToSelector:@selector(drawRect:withParameters:isCancelled:isRasterizing:)] ? 1 : 0); flags.implementsImageDisplay = ([c respondsToSelector:@selector(displayWithParameters:isCancelled:)] ? 1 : 0); if (instance) { @@ -2502,6 +2503,20 @@ static void _recursivelySetDisplaySuspended(ASDisplayNode *node, CALayer *layer, } } +- (BOOL)shouldAnimateSizeChanges +{ + ASDisplayNodeAssertThreadAffinity(self); + ASDN::MutexLocker l(_propertyLock); + return _flags.shouldAnimateSizeChanges; +} + +-(void)setShouldAnimateSizeChanges:(BOOL)shouldAnimateSizeChanges +{ + ASDisplayNodeAssertThreadAffinity(self); + ASDN::MutexLocker l(_propertyLock); + _flags.shouldAnimateSizeChanges = shouldAnimateSizeChanges; +} + static const char *ASDisplayNodeDrawingPriorityKey = "ASDrawingPriority"; - (void)setDrawingPriority:(NSInteger)drawingPriority diff --git a/AsyncDisplayKit/ASDisplayNodeExtras.h b/AsyncDisplayKit/ASDisplayNodeExtras.h index 319b5ff62d..6b5bf3f84d 100644 --- a/AsyncDisplayKit/ASDisplayNodeExtras.h +++ b/AsyncDisplayKit/ASDisplayNodeExtras.h @@ -91,12 +91,12 @@ extern void ASDisplayNodePerformBlockOnEverySubnode(ASDisplayNode *node, void(^b /** Given a display node, traverses up the layer tree hierarchy, returning the first display node that passes block. */ -extern id _Nullable ASDisplayNodeFind(ASDisplayNode * _Nullable node, BOOL (^block)(ASDisplayNode *node)); +extern id _Nullable ASDisplayNodeFindFirstSupernode(ASDisplayNode * _Nullable node, BOOL (^block)(ASDisplayNode *node)); /** Given a display node, traverses up the layer tree hierarchy, returning the first display node of kind class. */ -extern id _Nullable ASDisplayNodeFindClass(ASDisplayNode *start, Class c); +extern id _Nullable ASDisplayNodeFindFirstSupernodeOfClass(ASDisplayNode *start, Class c); /** * Given two nodes, finds their most immediate common parent. Used for geometry conversion methods. @@ -124,7 +124,12 @@ extern NSArray *ASDisplayNodeFindAllSubnodes(ASDisplayNode *sta extern NSArray *ASDisplayNodeFindAllSubnodesOfClass(ASDisplayNode *start, Class c); /** - Given a display node, traverses down the node hierarchy, returning the depth-first display node that pass the block. + Given a display node, traverses down the node hierarchy, returning the depth-first display node, including the start node that pass the block. + */ +extern __kindof ASDisplayNode * ASDisplayNodeFindFirstNode(ASDisplayNode *start, BOOL (^block)(ASDisplayNode *node)); + +/** + Given a display node, traverses down the node hierarchy, returning the depth-first display node, excluding the start node, that pass the block */ extern __kindof ASDisplayNode * ASDisplayNodeFindFirstSubnode(ASDisplayNode *start, BOOL (^block)(ASDisplayNode *node)); diff --git a/AsyncDisplayKit/ASDisplayNodeExtras.mm b/AsyncDisplayKit/ASDisplayNodeExtras.mm index 67e9185d4e..75b9ddd575 100644 --- a/AsyncDisplayKit/ASDisplayNodeExtras.mm +++ b/AsyncDisplayKit/ASDisplayNodeExtras.mm @@ -53,7 +53,7 @@ extern void ASDisplayNodePerformBlockOnEverySubnode(ASDisplayNode *node, void(^b } } -id ASDisplayNodeFind(ASDisplayNode *node, BOOL (^block)(ASDisplayNode *node)) +id ASDisplayNodeFindFirstSupernode(ASDisplayNode *node, BOOL (^block)(ASDisplayNode *node)) { CALayer *layer = node.layer; @@ -68,9 +68,9 @@ id ASDisplayNodeFind(ASDisplayNode *node, BOOL (^block)(ASDisplayNode *node)) return nil; } -id ASDisplayNodeFindClass(ASDisplayNode *start, Class c) +id ASDisplayNodeFindFirstSupernodeOfClass(ASDisplayNode *start, Class c) { - return ASDisplayNodeFind(start, ^(ASDisplayNode *n) { + return ASDisplayNodeFindFirstSupernode(start, ^(ASDisplayNode *n) { return [n isKindOfClass:c]; }); } @@ -128,10 +128,10 @@ extern NSArray *ASDisplayNodeFindAllSubnodesOfClass(ASDisplayNo #pragma mark - Find first subnode -static ASDisplayNode *_ASDisplayNodeFindFirstSubnode(ASDisplayNode *startNode, BOOL includeStartNode, BOOL (^block)(ASDisplayNode *node)) +static ASDisplayNode *_ASDisplayNodeFindFirstNode(ASDisplayNode *startNode, BOOL includeStartNode, BOOL (^block)(ASDisplayNode *node)) { for (ASDisplayNode *subnode in startNode.subnodes) { - ASDisplayNode *foundNode = _ASDisplayNodeFindFirstSubnode(subnode, YES, block); + ASDisplayNode *foundNode = _ASDisplayNodeFindFirstNode(subnode, YES, block); if (foundNode) { return foundNode; } @@ -143,9 +143,14 @@ static ASDisplayNode *_ASDisplayNodeFindFirstSubnode(ASDisplayNode *startNode, B return nil; } +extern __kindof ASDisplayNode * ASDisplayNodeFindFirstNode(ASDisplayNode *startNode, BOOL (^block)(ASDisplayNode *node)) +{ + return _ASDisplayNodeFindFirstNode(startNode, YES, block); +} + extern __kindof ASDisplayNode * ASDisplayNodeFindFirstSubnode(ASDisplayNode *startNode, BOOL (^block)(ASDisplayNode *node)) { - return _ASDisplayNodeFindFirstSubnode(startNode, NO, block); + return _ASDisplayNodeFindFirstNode(startNode, NO, block); } extern __kindof ASDisplayNode * ASDisplayNodeFindFirstSubnodeOfClass(ASDisplayNode *start, Class c) diff --git a/AsyncDisplayKit/Private/ASDisplayNodeInternal.h b/AsyncDisplayKit/Private/ASDisplayNodeInternal.h index e491aac0c6..89c5678a96 100644 --- a/AsyncDisplayKit/Private/ASDisplayNodeInternal.h +++ b/AsyncDisplayKit/Private/ASDisplayNodeInternal.h @@ -70,6 +70,7 @@ FOUNDATION_EXPORT NSString * const ASRenderingEngineDidDisplayNodesScheduledBefo unsigned shouldRasterizeDescendants:1; unsigned shouldBypassEnsureDisplay:1; unsigned displaySuspended:1; + unsigned shouldAnimateSizeChanges:1; unsigned hasCustomDrawingPriority:1; // whether custom drawing is enabled