Implement async transition

This commit is contained in:
Huy Nguyen
2016-03-08 23:30:03 -08:00
parent cdd1bd1e39
commit fa8f2f4429
12 changed files with 544 additions and 233 deletions

View File

@@ -9,6 +9,7 @@
#import "ASDisplayNodeInternal.h"
#import "ASDisplayNode+Subclasses.h"
#import "ASDisplayNode+FrameworkPrivate.h"
#import "ASDisplayNode+Beta.h"
#import "ASLayoutOptionsPrivate.h"
#import <objc/runtime.h>
@@ -20,10 +21,10 @@
#import "_ASDisplayView.h"
#import "_ASScopeTimer.h"
#import "_ASCoreAnimationExtras.h"
#import "ASDisplayNodeLayoutContext.h"
#import "ASDisplayNodeExtras.h"
#import "ASEqualityHelpers.h"
#import "ASRunLoopQueue.h"
#import "NSArray+Diffing.h"
#import "ASInternalHelpers.h"
#import "ASLayout.h"
@@ -34,7 +35,7 @@ NSInteger const ASDefaultDrawingPriority = ASDefaultTransactionPriority;
NSString * const ASRenderingEngineDidDisplayScheduledNodesNotification = @"ASRenderingEngineDidDisplayScheduledNodes";
NSString * const ASRenderingEngineDidDisplayNodesScheduledBeforeTimestamp = @"ASRenderingEngineDidDisplayNodesScheduledBeforeTimestamp";
@interface ASDisplayNode () <UIGestureRecognizerDelegate, _ASDisplayLayerDelegate, _ASTransitionContextDelegate>
@interface ASDisplayNode () <UIGestureRecognizerDelegate, _ASDisplayLayerDelegate, _ASTransitionContextCompletionDelegate>
/**
*
@@ -350,9 +351,9 @@ static ASDisplayNodeMethodOverrides GetASDisplayNodeMethodOverrides(Class c)
[self __setSupernode:nil];
_pendingViewState = nil;
_replaceAsyncSentinel = nil;
_displaySentinel = nil;
_transitionSentinel = nil;
_pendingDisplayNodes = nil;
}
@@ -583,111 +584,162 @@ static ASDisplayNodeMethodOverrides GetASDisplayNodeMethodOverrides(Class c)
- (ASLayout *)measureWithSizeRange:(ASSizeRange)constrainedSize
{
void (^manageSubnodesBlock)() = ^void() {
ASDN::MutexLocker l(_propertyLock);
if (self.usesImplicitHierarchyManagement) {
[self __implicitlyInsertSubnodes];
[self __implicitlyRemoveSubnodes];
}
[self __completeLayoutCalculation];
};
ASDN::MutexLocker l(_propertyLock);
if (! [self shouldMeasureWithSizeRange:constrainedSize]) {
return _layout;
}
if ([self _hasTransitionsInProgress]) {
// Invalidate transition sentinel to cancel transitions in progress
[self _invalidateTransitionSentinel];
// Tell subnodes to exit layout pending state and clear related properties
ASDisplayNodePerformBlockOnEverySubnode(self, ^(ASDisplayNode * _Nonnull node) {
node.hierarchyState &= (~ASHierarchyStateLayoutPending);
});
}
ASLayout *previousLayout = _layout;
ASSizeRange previousConstrainedSize = _constrainedSize;
ASLayout *newLayout = [self calculateLayoutThatFits:constrainedSize];
return [self measureWithSizeRange:constrainedSize completion:^{
if (!self.isNodeLoaded) {
manageSubnodesBlock();
} else {
ASPerformBlockOnMainThread(manageSubnodesBlock);
if (ASHierarchyStateIncludesLayoutPending(_hierarchyState)) {
_pendingLayoutContext = [[ASDisplayNodeLayoutContext alloc] initWithNode:self
pendingLayout:newLayout
pendingConstrainedSize:constrainedSize
previousLayout:previousLayout
previousConstrainedSize:previousConstrainedSize];
} else {
ASDisplayNodeLayoutContext *layoutContext;
if (self.usesImplicitHierarchyManagement) {
layoutContext = [[ASDisplayNodeLayoutContext alloc] initWithNode:self
pendingLayout:newLayout
pendingConstrainedSize:constrainedSize
previousLayout:previousLayout
previousConstrainedSize:previousConstrainedSize];
}
}];
[self applyLayout:newLayout constrainedSize:constrainedSize layoutContext:layoutContext];
[self _completeLayoutCalculation];
}
return newLayout;
}
- (ASLayout *)measureWithSizeRange:(ASSizeRange)constrainedSize completion:(void(^)())completion
- (BOOL)shouldMeasureWithSizeRange:(ASSizeRange)constrainedSize
{
ASDN::MutexLocker l(_propertyLock);
if (![self __shouldSize])
return nil;
if (![self __shouldSize]) {
return NO;
}
if (ASHierarchyStateIncludesLayoutPending(_hierarchyState) && constrainedSize.transitionID != _pendingTransitionID) {
return NO;
}
// only calculate the size if
// - we haven't already
// - the constrained size range is different
if (!_flags.isMeasured || !ASSizeRangeEqualToSizeRange(constrainedSize, _constrainedSize)) {
_previousLayout = _layout;
_layout = [self calculateLayoutThatFits:constrainedSize];
return (!_flags.isMeasured || !ASSizeRangeEqualToSizeRange(constrainedSize, _constrainedSize));
}
ASDisplayNodeAssertTrue(_layout.layoutableObject == self);
ASDisplayNodeAssertTrue(_layout.size.width >= 0.0);
ASDisplayNodeAssertTrue(_layout.size.height >= 0.0);
_previousConstrainedSize = _constrainedSize;
_constrainedSize = constrainedSize;
if (self.usesImplicitHierarchyManagement) {
[self __calculateSubnodeOperations];
}
_flags.isMeasured = YES;
- (void)transitionLayoutWithAnimation:(BOOL)animated
shouldMeasureAsync:(BOOL)shouldMeasureAsync
measurementCompletion:(void(^)())completion
{
ASSizeRange currentConstrainedSize = _constrainedSize;
[self invalidateCalculatedLayout];
[self transitionLayoutWithSizeRange:currentConstrainedSize
animated:animated
shouldMeasureAsync:shouldMeasureAsync
measurementCompletion:completion];
}
completion();
- (void)transitionLayoutWithSizeRange:(ASSizeRange)constrainedSize
animated:(BOOL)animated
shouldMeasureAsync:(BOOL)shouldMeasureAsync
measurementCompletion:(void(^)())completion
{
ASDisplayNodeAssertMainThread();
if (! [self shouldMeasureWithSizeRange:constrainedSize]) {
return;
}
{
ASDN::MutexLocker l(_propertyLock);
ASDisplayNodeAssert(ASHierarchyStateIncludesLayoutPending(_hierarchyState) == NO, @"Can't start a transition when one of the supernodes is performing one.");
}
return _layout;
}
int32_t transitionID = [self _newTransitionID];
constrainedSize.transitionID = transitionID;
- (ASLayout *)transitionLayoutWithAnimation:(BOOL)animated
{
[self invalidateCalculatedLayout];
return [self transitionLayoutWithSizeRange:_constrainedSize animated:animated];
}
- (ASLayout *)transitionLayoutWithSizeRange:(ASSizeRange)constrainedSize animated:(BOOL)animated
{
BOOL disableImplicitHierarchyManagement = self.usesImplicitHierarchyManagement == NO;
self.usesImplicitHierarchyManagement = YES; // Temporary flag for 1.9.x
ASDisplayNodePerformBlockOnEverySubnode(self, ^(ASDisplayNode * _Nonnull node) {
ASDisplayNodeAssert([node _hasTransitionsInProgress] == NO, @"Can't start a transition when one of the subnodes is performing one.");
node.hierarchyState |= ASHierarchyStateLayoutPending;
node.pendingTransitionID = transitionID;
});
return [self measureWithSizeRange:constrainedSize completion:^{
if (disableImplicitHierarchyManagement) {
self.usesImplicitHierarchyManagement = NO; // Temporary flag for 1.9.x
}
ASPerformBlockOnMainThread(^{
void (^transitionBlock)() = ^{
ASLayout *newLayout;
{
ASDN::MutexLocker l(_propertyLock);
_transitionContext = [[_ASTransitionContext alloc] initWithAnimation:animated delegate:self];
[self __implicitlyInsertSubnodes];
BOOL disableImplicitHierarchyManagement = self.usesImplicitHierarchyManagement == NO;
self.usesImplicitHierarchyManagement = YES; // Temporary flag for 1.9.x
newLayout = [self calculateLayoutThatFits:constrainedSize];
if (disableImplicitHierarchyManagement) {
self.usesImplicitHierarchyManagement = NO; // Temporary flag for 1.9.x
}
}
if ([self _shouldAbortTransitionWithID:transitionID]) {
return;
}
ASPerformBlockOnMainThread(^{
if ([self _shouldAbortTransitionWithID:transitionID]) {
return;
}
ASDN::MutexLocker l(_propertyLock);
ASLayout *previousLayout = _layout;
ASSizeRange previousConstrainedSize = _constrainedSize;
[self applyLayout:newLayout constrainedSize:constrainedSize layoutContext:nil];
[self _invalidateTransitionSentinel];
ASDisplayNodePerformBlockOnEverySubnode(self, ^(ASDisplayNode * _Nonnull node) {
[node applyPendingLayoutContext];
[node _completeLayoutCalculation];
node.hierarchyState &= (~ASHierarchyStateLayoutPending);
});
if (completion) {
completion();
}
_pendingLayoutContext = [[ASDisplayNodeLayoutContext alloc] initWithNode:self
pendingLayout:newLayout
pendingConstrainedSize:constrainedSize
previousLayout:previousLayout
previousConstrainedSize:previousConstrainedSize];
[_pendingLayoutContext applySubnodeInsertions];
_transitionContext = [[_ASTransitionContext alloc] initWithAnimation:animated
layoutDelegate:_pendingLayoutContext
completionDelegate:self];
[self animateLayoutTransition:_transitionContext];
});
}];
}
};
- (void)__calculateSubnodeOperations
{
ASDN::MutexLocker l(_propertyLock);
if (_previousLayout) {
NSIndexSet *insertions, *deletions;
[_previousLayout.immediateSublayouts asdk_diffWithArray:_layout.immediateSublayouts
insertions:&insertions
deletions:&deletions
compareBlock:^BOOL(ASLayout *lhs, ASLayout *rhs) {
return ASObjectIsEqual(lhs.layoutableObject, rhs.layoutableObject);
}];
filterNodesInLayoutAtIndexes(_layout, insertions, &_insertedSubnodes, &_insertedSubnodePositions);
filterNodesInLayoutAtIndexesWithIntersectingNodes(_previousLayout,
deletions,
_insertedSubnodes,
&_removedSubnodes,
&_removedSubnodePositions);
if (shouldMeasureAsync) {
ASPerformBlockOnBackgroundThread(transitionBlock);
} else {
NSIndexSet *indexes = [NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, [_layout.immediateSublayouts count])];
filterNodesInLayoutAtIndexes(_layout, indexes, &_insertedSubnodes, &_insertedSubnodePositions);
_removedSubnodes = nil;
transitionBlock();
}
}
- (void)__completeLayoutCalculation
- (void)_completeLayoutCalculation
{
ASDN::MutexLocker l(_propertyLock);
_insertedSubnodes = nil;
_removedSubnodes = nil;
_previousLayout = nil;
[self calculatedLayoutDidChange];
// we generate placeholders at measureWithSizeRange: time so that a node is guaranteed
@@ -704,53 +756,6 @@ static ASDisplayNodeMethodOverrides GetASDisplayNodeMethodOverrides(Class c)
}
}
/**
* @abstract Stores the nodes at the given indexes in the `storedNodes` array, storing indexes in a `storedPositions` c++ vector.
*/
static inline void filterNodesInLayoutAtIndexes(
ASLayout *layout,
NSIndexSet *indexes,
NSArray<ASDisplayNode *> * __strong *storedNodes,
std::vector<NSInteger> *storedPositions
)
{
filterNodesInLayoutAtIndexesWithIntersectingNodes(layout, indexes, nil, storedNodes, storedPositions);
}
/**
* @abstract Stores the nodes at the given indexes in the `storedNodes` array, storing indexes in a `storedPositions` c++ vector.
* @discussion If the node exists in the `intersectingNodes` array, the node is not added to `storedNodes`.
*/
static inline void filterNodesInLayoutAtIndexesWithIntersectingNodes(
ASLayout *layout,
NSIndexSet *indexes,
NSArray<ASDisplayNode *> *intersectingNodes,
NSArray<ASDisplayNode *> * __strong *storedNodes,
std::vector<NSInteger> *storedPositions
)
{
NSMutableArray<ASDisplayNode *> *nodes = [NSMutableArray array];
std::vector<NSInteger> positions = std::vector<NSInteger>();
NSInteger idx = [indexes firstIndex];
while (idx != NSNotFound) {
BOOL skip = NO;
ASDisplayNode *node = (ASDisplayNode *)layout.immediateSublayouts[idx].layoutableObject;
ASDisplayNodeCAssert(node, @"A flattened layout must consist exclusively of node sublayouts");
for (ASDisplayNode *i in intersectingNodes) {
if (node == i) {
skip = YES;
break;
}
}
if (!skip) {
[nodes addObject:node];
positions.push_back(idx);
}
idx = [indexes indexGreaterThanIndex:idx];
}
*storedNodes = nodes;
*storedPositions = positions;
}
- (void)calculatedLayoutDidChange
{
@@ -779,66 +784,12 @@ static inline void filterNodesInLayoutAtIndexesWithIntersectingNodes(
- (void)didCompleteLayoutTransition:(id<ASContextTransitioning>)context
{
[self __implicitlyRemoveSubnodes];
[self __completeLayoutCalculation];
[_pendingLayoutContext applySubnodeRemovals];
[self _completeLayoutCalculation];
_pendingLayoutContext = nil;
}
#pragma mark - Implicit node hierarchy managagment
- (void)__implicitlyInsertSubnodes
{
ASDN::MutexLocker l(_propertyLock);
for (NSInteger i = 0; i < [_insertedSubnodes count]; i++) {
NSInteger p = _insertedSubnodePositions[i];
[self insertSubnode:_insertedSubnodes[i] atIndex:p];
}
}
- (void)__implicitlyRemoveSubnodes
{
ASDN::MutexLocker l(_propertyLock);
for (NSInteger i = 0; i < [_removedSubnodes count]; i++) {
[_removedSubnodes[i] removeFromSupernode];
}
}
#pragma mark - _ASTransitionContextDelegate
- (NSArray<ASDisplayNode *> *)currentSubnodesWithTransitionContext:(_ASTransitionContext *)context
{
return _subnodes;
}
- (NSArray<ASDisplayNode *> *)insertedSubnodesWithTransitionContext:(_ASTransitionContext *)context
{
return _insertedSubnodes;
}
- (NSArray<ASDisplayNode *> *)removedSubnodesWithTransitionContext:(_ASTransitionContext *)context
{
return _removedSubnodes;
}
- (ASLayout *)transitionContext:(_ASTransitionContext *)context layoutForKey:(NSString *)key
{
if ([key isEqualToString:ASTransitionContextFromLayoutKey]) {
return _previousLayout;
} else if ([key isEqualToString:ASTransitionContextToLayoutKey]) {
return _layout;
} else {
return nil;
}
}
- (ASSizeRange)transitionContext:(_ASTransitionContext *)context constrainedSizeForKey:(NSString *)key
{
if ([key isEqualToString:ASTransitionContextFromLayoutKey]) {
return _previousConstrainedSize;
} else if ([key isEqualToString:ASTransitionContextToLayoutKey]) {
return _constrainedSize;
} else {
return ASSizeRangeMake(CGSizeZero, CGSizeZero);
}
}
#pragma mark - _ASTransitionContextCompletionDelegate
- (void)transitionContext:(_ASTransitionContext *)context didComplete:(BOOL)didComplete
{
@@ -1896,6 +1847,13 @@ void recursivelyTriggerDisplayForLayer(CALayer *layer, BOOL shouldBlock)
return _constrainedSize;
}
- (void)setPendingTransitionID:(int32_t)pendingTransitionID
{
ASDN::MutexLocker l(_propertyLock);
ASDisplayNodeAssertTrue(_pendingTransitionID < pendingTransitionID);
_pendingTransitionID = pendingTransitionID;
}
- (void)setPreferredFrameSize:(CGSize)preferredFrameSize
{
ASDN::MutexLocker l(_propertyLock);
@@ -2211,6 +2169,19 @@ void recursivelyTriggerDisplayForLayer(CALayer *layer, BOOL shouldBlock)
}
}
if ((newState & ASHierarchyStateLayoutPending) != (oldState & ASHierarchyStateLayoutPending)) {
if (newState & ASHierarchyStateLayoutPending) {
// Entering layout pending state
} else {
// Leaving layout pending state, reset related properties
{
ASDN::MutexLocker l(_propertyLock);
_pendingTransitionID = 0;
_pendingLayoutContext = nil;
}
}
}
if (newState != oldState) {
LOG(@"setHierarchyState: oldState = %lu, newState = %lu", (unsigned long)oldState, (unsigned long)newState);
}
@@ -2236,6 +2207,37 @@ void recursivelyTriggerDisplayForLayer(CALayer *layer, BOOL shouldBlock)
});
}
- (void)applyPendingLayoutContext
{
ASDN::MutexLocker l(_propertyLock);
if (_pendingLayoutContext) {
[self applyLayout:_pendingLayoutContext.pendingLayout
constrainedSize:_pendingLayoutContext.pendingConstrainedSize
layoutContext:_pendingLayoutContext];
_pendingLayoutContext = nil;
}
}
- (void)applyLayout:(ASLayout *)layout
constrainedSize:(ASSizeRange)constrainedSize
layoutContext:(ASDisplayNodeLayoutContext *)layoutContext
{
ASDN::MutexLocker l(_propertyLock);
_layout = layout;
ASDisplayNodeAssertTrue(layout.layoutableObject == self);
ASDisplayNodeAssertTrue(layout.size.width >= 0.0);
ASDisplayNodeAssertTrue(layout.size.height >= 0.0);
_constrainedSize = constrainedSize;
_flags.isMeasured = YES;
if (self.usesImplicitHierarchyManagement && layoutContext != nil) {
[layoutContext applySubnodeInsertions];
[layoutContext applySubnodeRemovals];
}
}
- (void)layout
{
ASDisplayNodeAssertMainThread();
@@ -2552,24 +2554,31 @@ static const char *ASDisplayNodeDrawingPriorityKey = "ASDrawingPriority";
return asyncSizingQueue;
}
- (BOOL)_isMarkedForReplacement
- (BOOL)_hasTransitionsInProgress
{
ASDN::MutexLocker l(_propertyLock);
return _replaceAsyncSentinel != nil;
return _transitionSentinel != nil;
}
// FIXME: This method doesn't appear to be called, and could be removed.
// However, it may be useful for an API similar to what Paper used to create a new node hierarchy,
// trigger asynchronous measurement and display on it, and have it swap out and replace an old hierarchy.
- (ASSentinel *)_asyncReplaceSentinel
- (void)_invalidateTransitionSentinel
{
ASDN::MutexLocker l(_propertyLock);
_transitionSentinel = nil;
}
if (!_replaceAsyncSentinel) {
_replaceAsyncSentinel = [[ASSentinel alloc] init];
- (BOOL)_shouldAbortTransitionWithID:(int32_t)transitionID
{
ASDN::MutexLocker l(_propertyLock);
return _transitionSentinel == nil || transitionID != _transitionSentinel.value;
}
- (int32_t)_newTransitionID
{
ASDN::MutexLocker l(_propertyLock);
if (!_transitionSentinel) {
_transitionSentinel = [[ASSentinel alloc] init];
}
return _replaceAsyncSentinel;
return [_transitionSentinel increment];
}
// Calls completion with nil to indicated cancellation