ASCollectionView doesn't animate size changes if some of the updated cell nodes don't want to

This commit is contained in:
Huy Nguyen
2016-03-14 22:30:31 -07:00
parent f97a509541
commit 22b105bfdc
6 changed files with 118 additions and 19 deletions

View File

@@ -15,6 +15,7 @@
#import "ASCollectionViewLayoutController.h" #import "ASCollectionViewLayoutController.h"
#import "ASCollectionViewFlowLayoutInspector.h" #import "ASCollectionViewFlowLayoutInspector.h"
#import "ASCollectionViewLayoutFacilitatorProtocol.h" #import "ASCollectionViewLayoutFacilitatorProtocol.h"
#import "ASDisplayNodeExtras.h"
#import "ASDisplayNode+FrameworkPrivate.h" #import "ASDisplayNode+FrameworkPrivate.h"
#import "ASDisplayNode+Beta.h" #import "ASDisplayNode+Beta.h"
#import "ASInternalHelpers.h" #import "ASInternalHelpers.h"
@@ -57,6 +58,36 @@ static NSString * const kCellReuseIdentifier = @"_ASCollectionViewCell";
@end @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<ASCellNode *> *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 -
#pragma mark ASCollectionView. #pragma mark ASCollectionView.
@@ -78,7 +109,7 @@ static NSString * const kCellReuseIdentifier = @"_ASCollectionViewCell";
BOOL _asyncDelegateImplementsScrollviewDidScroll; BOOL _asyncDelegateImplementsScrollviewDidScroll;
BOOL _asyncDataSourceImplementsConstrainedSizeForNode; BOOL _asyncDataSourceImplementsConstrainedSizeForNode;
BOOL _asyncDataSourceImplementsNodeBlockForItemAtIndexPath; BOOL _asyncDataSourceImplementsNodeBlockForItemAtIndexPath;
BOOL _queuedNodeSizeUpdate; _ASCollectionViewNodeSizeInvalidationContext *_queuedNodeSizeInvalidationContext; // Main thread only
BOOL _isDeallocating; BOOL _isDeallocating;
ASBatchContext *_batchContext; ASBatchContext *_batchContext;
@@ -1018,24 +1049,61 @@ static NSString * const kCellReuseIdentifier = @"_ASCollectionViewCell";
{ {
ASDisplayNodeAssertMainThread(); ASDisplayNodeAssertMainThread();
if (!sizeChanged || _queuedNodeSizeUpdate) { if (!sizeChanged) {
return; return;
} }
_queuedNodeSizeUpdate = YES; BOOL queued = (_queuedNodeSizeInvalidationContext != nil);
[_layoutFacilitator collectionViewWillEditCellsAtIndexPaths:@[[self indexPathForNode:node]] batched:NO]; if (!queued) {
[self performSelector:@selector(requeryNodeSizes) _queuedNodeSizeInvalidationContext = [[_ASCollectionViewNodeSizeInvalidationContext alloc] init];
withObject:nil
afterDelay:0 __weak __typeof__(self) weakSelf = self;
inModes:@[ NSRunLoopCommonModes ]]; 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 // Cause UICollectionView to requery for the new size of all nodes
- (void)requeryNodeSizes - (void)requeryNodeSizes
{ {
_queuedNodeSizeUpdate = NO; ASDisplayNodeAssertMainThread();
NSSet<ASCellNode *> *nodes = _queuedNodeSizeInvalidationContext.invalidatedNodes;
NSMutableArray<NSIndexPath *> *indexPaths = [NSMutableArray arrayWithCapacity:nodes.count];
for (ASCellNode *node in nodes) {
NSIndexPath *indexPath = [self indexPathForNode:node];
if (indexPath != nil) {
[indexPaths addObject:indexPath];
}
}
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]; [super performBatchUpdates:^{} completion:nil];
});
}
_queuedNodeSizeInvalidationContext = nil;
} }
#pragma mark - Memory Management #pragma mark - Memory Management

View File

@@ -428,6 +428,11 @@ NS_ASSUME_NONNULL_BEGIN
*/ */
@property (nonatomic, assign) BOOL displaySuspended; @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. * @abstract Prevent the node and its descendants' layer from displaying.
* *

View File

@@ -114,6 +114,7 @@ static struct ASDisplayNodeFlags GetASDisplayNodeFlags(Class c, ASDisplayNode *i
flags.isInHierarchy = NO; flags.isInHierarchy = NO;
flags.displaysAsynchronously = YES; flags.displaysAsynchronously = YES;
flags.shouldAnimateSizeChanges = YES;
flags.implementsDrawRect = ([c respondsToSelector:@selector(drawRect:withParameters:isCancelled:isRasterizing:)] ? 1 : 0); flags.implementsDrawRect = ([c respondsToSelector:@selector(drawRect:withParameters:isCancelled:isRasterizing:)] ? 1 : 0);
flags.implementsImageDisplay = ([c respondsToSelector:@selector(displayWithParameters:isCancelled:)] ? 1 : 0); flags.implementsImageDisplay = ([c respondsToSelector:@selector(displayWithParameters:isCancelled:)] ? 1 : 0);
if (instance) { 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"; static const char *ASDisplayNodeDrawingPriorityKey = "ASDrawingPriority";
- (void)setDrawingPriority:(NSInteger)drawingPriority - (void)setDrawingPriority:(NSInteger)drawingPriority

View File

@@ -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. 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. 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. * Given two nodes, finds their most immediate common parent. Used for geometry conversion methods.
@@ -124,7 +124,12 @@ extern NSArray<ASDisplayNode *> *ASDisplayNodeFindAllSubnodes(ASDisplayNode *sta
extern NSArray<ASDisplayNode *> *ASDisplayNodeFindAllSubnodesOfClass(ASDisplayNode *start, Class c); extern NSArray<ASDisplayNode *> *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)); extern __kindof ASDisplayNode * ASDisplayNodeFindFirstSubnode(ASDisplayNode *start, BOOL (^block)(ASDisplayNode *node));

View File

@@ -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; CALayer *layer = node.layer;
@@ -68,9 +68,9 @@ id ASDisplayNodeFind(ASDisplayNode *node, BOOL (^block)(ASDisplayNode *node))
return nil; 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]; return [n isKindOfClass:c];
}); });
} }
@@ -128,10 +128,10 @@ extern NSArray<ASDisplayNode *> *ASDisplayNodeFindAllSubnodesOfClass(ASDisplayNo
#pragma mark - Find first subnode #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) { for (ASDisplayNode *subnode in startNode.subnodes) {
ASDisplayNode *foundNode = _ASDisplayNodeFindFirstSubnode(subnode, YES, block); ASDisplayNode *foundNode = _ASDisplayNodeFindFirstNode(subnode, YES, block);
if (foundNode) { if (foundNode) {
return foundNode; return foundNode;
} }
@@ -143,9 +143,14 @@ static ASDisplayNode *_ASDisplayNodeFindFirstSubnode(ASDisplayNode *startNode, B
return nil; 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)) 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) extern __kindof ASDisplayNode * ASDisplayNodeFindFirstSubnodeOfClass(ASDisplayNode *start, Class c)

View File

@@ -70,6 +70,7 @@ FOUNDATION_EXPORT NSString * const ASRenderingEngineDidDisplayNodesScheduledBefo
unsigned shouldRasterizeDescendants:1; unsigned shouldRasterizeDescendants:1;
unsigned shouldBypassEnsureDisplay:1; unsigned shouldBypassEnsureDisplay:1;
unsigned displaySuspended:1; unsigned displaySuspended:1;
unsigned shouldAnimateSizeChanges:1;
unsigned hasCustomDrawingPriority:1; unsigned hasCustomDrawingPriority:1;
// whether custom drawing is enabled // whether custom drawing is enabled