diff --git a/AsyncDisplayKit/ASDisplayNode.mm b/AsyncDisplayKit/ASDisplayNode.mm index 0b56764e..3589207f 100644 --- a/AsyncDisplayKit/ASDisplayNode.mm +++ b/AsyncDisplayKit/ASDisplayNode.mm @@ -642,24 +642,31 @@ static ASDisplayNodeMethodOverrides GetASDisplayNodeMethodOverrides(Class c) { ASDN::MutexLocker l(__instanceLock__); if (! [self shouldMeasureWithSizeRange:constrainedSize]) { - ASDisplayNodeAssertNotNil(_calculatedLayout, @"-[ASDisplayNode measureWithSizeRange:] _layout should not be nil! %@", self); - return _calculatedLayout ? : [ASLayout layoutWithLayoutableObject:self constrainedSizeRange:constrainedSize size:CGSizeZero]; + ASDisplayNodeAssertNotNil(_layout, @"-[ASDisplayNode measureWithSizeRange:] _layout should not be nil! %@", self); + return _layout ? : [ASLayout layoutWithLayoutableObject:self constrainedSizeRange:constrainedSize size:CGSizeZero]; } [self cancelLayoutTransitionsInProgress]; - ASLayout *previousLayout = _calculatedLayout; + ASLayout *previousLayout = _layout; ASLayout *newLayout = [self calculateLayoutThatFits:constrainedSize]; - _pendingLayoutTransition = [[ASLayoutTransition alloc] initWithNode:self - pendingLayout:newLayout - previousLayout:previousLayout]; - - if (ASHierarchyStateIncludesLayoutPending(_hierarchyState) == NO) { - // Complete the pending layout transition immediately - [self _completePendingLayoutTransition]; + if (ASHierarchyStateIncludesLayoutPending(_hierarchyState)) { + _pendingLayoutTransition = [[ASLayoutTransition alloc] initWithNode:self + pendingLayout:newLayout + previousLayout:previousLayout]; + } else { + ASLayoutTransition *layoutTransition = nil; + if (self.usesImplicitHierarchyManagement) { + layoutTransition = [[ASLayoutTransition alloc] initWithNode:self + pendingLayout:newLayout + previousLayout:previousLayout]; + } + + [self _applyLayout:newLayout layoutTransition:layoutTransition]; + [self _completeLayoutCalculation]; } - + ASDisplayNodeAssertNotNil(newLayout, @"-[ASDisplayNode measureWithSizeRange:] newLayout should not be nil! %@", self); return newLayout; } @@ -681,12 +688,12 @@ static ASDisplayNodeMethodOverrides GetASDisplayNodeMethodOverrides(Class c) // Only generate a new layout if: // - The current layout is dirty // - The passed constrained size is different than the layout's constrained size - return ([self _hasDirtyLayout] || !ASSizeRangeEqualToSizeRange(constrainedSize, _calculatedLayout.constrainedSizeRange)); + return ([self _hasDirtyLayout] || !ASSizeRangeEqualToSizeRange(constrainedSize, _layout.constrainedSizeRange)); } - (BOOL)_hasDirtyLayout { - return _calculatedLayout == nil || _calculatedLayout.isDirty; + return _layout == nil || _layout.isDirty; } - (ASLayoutableType)layoutableType @@ -705,14 +712,13 @@ static ASDisplayNodeMethodOverrides GetASDisplayNodeMethodOverrides(Class c) shouldMeasureAsync:(BOOL)shouldMeasureAsync measurementCompletion:(void(^)())completion { - if (_calculatedLayout == nil) { + if (_layout == nil) { // constrainedSizeRange returns a struct and is invalid to call on nil. // Defaulting to CGSizeZero can cause negative values in client layout code. return; } - [self invalidateCalculatedLayout]; - [self transitionLayoutWithSizeRange:_calculatedLayout.constrainedSizeRange + [self transitionLayoutWithSizeRange:_layout.constrainedSizeRange animated:animated shouldMeasureAsync:shouldMeasureAsync measurementCompletion:completion]; @@ -774,11 +780,12 @@ static ASDisplayNodeMethodOverrides GetASDisplayNodeMethodOverrides(Class c) return; } - ASLayout *previousLayout = _calculatedLayout; - [self setCalculatedLayout:newLayout]; + ASLayout *previousLayout = _layout; + [self _applyLayout:newLayout layoutTransition:nil]; ASDisplayNodePerformBlockOnEverySubnode(self, ^(ASDisplayNode * _Nonnull node) { - [node _completePendingLayoutTransition]; + [node _applyPendingLayoutContext]; + [node _completeLayoutCalculation]; node.hierarchyState &= (~ASHierarchyStateLayoutPending); }); @@ -788,26 +795,48 @@ static ASDisplayNodeMethodOverrides GetASDisplayNodeMethodOverrides(Class c) completion(); } - // Setup pending layout transition for animation _pendingLayoutTransition = [[ASLayoutTransition alloc] initWithNode:self pendingLayout:newLayout - previousLayout:previousLayout]; - // Setup context for pending layout transition. we need to hold a strong reference to the context - _pendingLayoutTransitionContext = [[_ASTransitionContext alloc] initWithAnimation:animated - layoutDelegate:_pendingLayoutTransition - completionDelegate:self]; - - // Apply the subnode insertion immediately to be able to animate the nodes + previousLayout:previousLayout]; [_pendingLayoutTransition applySubnodeInsertions]; - - // Kick off animating the layout transition - [self animateLayoutTransition:_pendingLayoutTransitionContext]; + + _transitionContext = [[_ASTransitionContext alloc] initWithAnimation:animated + layoutDelegate:_pendingLayoutTransition + completionDelegate:self]; + [self animateLayoutTransition:_transitionContext]; }); }; ASPerformBlockOnBackgroundThread(transitionBlock); } +- (void)_completeLayoutCalculation +{ + ASDN::MutexLocker l(__instanceLock__); + [self calculatedLayoutDidChange]; + + // We generate placeholders at measureWithSizeRange: time so that a node is guaranteed to have a placeholder ready to go. + // This is also because measurement is usually asynchronous, but placeholders need to be set up synchronously. + // First measurement is guaranteed to be before the node is onscreen, so we can create the image async. but still have it appear sync. + if (_placeholderEnabled && [self _displaysAsynchronously] && self.contents == nil) { + + // Zero-sized nodes do not require a placeholder. + CGSize layoutSize = (_layout ? _layout.size : CGSizeZero); + if (CGSizeEqualToSize(layoutSize, CGSizeZero)) { + return; + } + + if (!_placeholderImage) { + _placeholderImage = [self placeholderImage]; + } + } +} + +- (void)calculatedLayoutDidChange +{ + // subclass override +} + - (void)cancelLayoutTransitionsInProgress { ASDN::MutexLocker l(__instanceLock__); @@ -861,108 +890,25 @@ static ASDisplayNodeMethodOverrides GetASDisplayNodeMethodOverrides(Class c) return (!_transitionInProgress || _transitionID != transitionID); } -#pragma mark - Layout Transition API / ASDisplayNode (Beta) - -/* - * Hook for subclasse to perform an animation based on the given ASContextTransitioning. By default this just layouts - * applies all subnodes without animation and calls completes the transition on the context. - */ - (void)animateLayoutTransition:(id)context { - [self __layoutSublayouts]; + [self __layoutSubnodes]; [context completeTransition:YES]; } -/* - * Hook for subclasses to clean up nodes after the transition happened. Furthermore this can be used from subclasses - * to manually perform deletions. - */ - (void)didCompleteLayoutTransition:(id)context { [_pendingLayoutTransition applySubnodeRemovals]; + [self _completeLayoutCalculation]; + _pendingLayoutTransition = nil; } #pragma mark - _ASTransitionContextCompletionDelegate -/* - * After completeTransition: is called on the ASContextTransitioning object in animateLayoutTransition: this - * delegate method will be called that start the completion process of the - */ - (void)transitionContext:(_ASTransitionContext *)context didComplete:(BOOL)didComplete { [self didCompleteLayoutTransition:context]; - _pendingLayoutTransitionContext = nil; - - [self _pendingLayoutTransitionDidComplete]; -} - -#pragma mark - Layout - -/* - * Completes the pending layout transition immediately without going through the the Layout Transition Animation API - */ -- (void)_completePendingLayoutTransition -{ - ASDN::MutexLocker l(__instanceLock__); - if (_pendingLayoutTransition) { - [self setCalculatedLayout:_pendingLayoutTransition.pendingLayout]; - [self _completeLayoutTransition:_pendingLayoutTransition]; - } - [self _pendingLayoutTransitionDidComplete]; -} - -/* - * Can be directly called to commit the given layout transition immediately to complete without calling through to the - * Layout Transition Animation API - */ -- (void)_completeLayoutTransition:(ASLayoutTransition *)layoutTransition -{ - // Layout transition is not supported for non implicit hierarchy managed nodes yet - if (layoutTransition == nil || self.usesImplicitHierarchyManagement == NO) { - return; - } - - // Trampoline to the main thread if necessary - if (ASDisplayNodeThreadIsMain() || layoutTransition.isSynchronous == NO) { - [layoutTransition commitTransition]; - } else { - // Subnode insertions and removals need to happen always on the main thread if at least one subnode is already loaded - ASPerformBlockOnMainThread(^{ - [layoutTransition commitTransition]; - }); - } -} - -- (void)_pendingLayoutTransitionDidComplete -{ - ASDN::MutexLocker l(__instanceLock__); - - // Subclass hook - [self calculatedLayoutDidChange]; - - // We generate placeholders at measureWithSizeRange: time so that a node is guaranteed to have a placeholder ready to go. - // This is also because measurement is usually asynchronous, but placeholders need to be set up synchronously. - // First measurement is guaranteed to be before the node is onscreen, so we can create the image async. but still have it appear sync. - if (_placeholderEnabled && [self _displaysAsynchronously] && self.contents == nil) { - - // Zero-sized nodes do not require a placeholder. - CGSize layoutSize = (_calculatedLayout ? _calculatedLayout.size : CGSizeZero); - if (CGSizeEqualToSize(layoutSize, CGSizeZero)) { - return; - } - - if (!_placeholderImage) { - _placeholderImage = [self placeholderImage]; - } - } - - // Cleanup pending layout transition - _pendingLayoutTransition = nil; -} - -- (void)calculatedLayoutDidChange -{ - // subclass override + _transitionContext = nil; } #pragma mark - Asynchronous display @@ -1124,7 +1070,7 @@ static ASDisplayNodeMethodOverrides GetASDisplayNodeMethodOverrides(Class c) __instanceLock__.lock(); - if (_calculatedLayout == nil) { + if (_layout == nil) { // Can't proceed without a layout as no constrained size would be available __instanceLock__.unlock(); return; @@ -1142,11 +1088,11 @@ static ASDisplayNodeMethodOverrides GetASDisplayNodeMethodOverrides(Class c) } // This is the root node. Trigger a full measurement pass on *current* thread. Old constrained size is re-used. - [self measureWithSizeRange:_calculatedLayout.constrainedSizeRange]; + [self measureWithSizeRange:_layout.constrainedSizeRange]; CGRect oldBounds = self.bounds; CGSize oldSize = oldBounds.size; - CGSize newSize = _calculatedLayout.size; + CGSize newSize = _layout.size; if (! CGSizeEqualToSize(oldSize, newSize)) { self.bounds = (CGRect){ oldBounds.origin, newSize }; @@ -1224,6 +1170,24 @@ static ASDisplayNodeMethodOverrides GetASDisplayNodeMethodOverrides(Class c) } } +- (void)layout +{ + ASDisplayNodeAssertMainThread(); + + if ([self _hasDirtyLayout]) { + return; + } + + [self __layoutSubnodes]; +} + +- (void)__layoutSubnodes +{ + for (ASLayout *subnodeLayout in _layout.sublayouts) { + ((ASDisplayNode *)subnodeLayout.layoutableObject).frame = [subnodeLayout frame]; + } +} + - (void)layoutDidFinish { // Hook for subclasses @@ -2137,30 +2101,19 @@ void recursivelyTriggerDisplayForLayer(CALayer *layer, BOOL shouldBlock) - (ASLayout *)calculatedLayout { ASDN::MutexLocker l(__instanceLock__); - return _calculatedLayout; -} - -- (void)setCalculatedLayout:(ASLayout *)calculatedLayout -{ - ASDN::MutexLocker l(__instanceLock__); - - ASDisplayNodeAssertTrue(calculatedLayout.layoutableObject == self); - ASDisplayNodeAssertTrue(calculatedLayout.size.width >= 0.0); - ASDisplayNodeAssertTrue(calculatedLayout.size.height >= 0.0); - - _calculatedLayout = calculatedLayout; + return _layout; } - (CGSize)calculatedSize { ASDN::MutexLocker l(__instanceLock__); - return _calculatedLayout.size; + return _layout.size; } - (ASSizeRange)constrainedSizeForCalculatedLayout { ASDN::MutexLocker l(__instanceLock__); - return _calculatedLayout.constrainedSizeRange; + return _layout.constrainedSizeRange; } - (void)setLayoutSpecBlock:(ASLayoutSpecBlock)layoutSpecBlock @@ -2223,7 +2176,7 @@ void recursivelyTriggerDisplayForLayer(CALayer *layer, BOOL shouldBlock) // This will cause the next call to -measureWithSizeRange: to actually compute a new layout // instead of returning the current layout - _calculatedLayout.dirty = YES; + _layout.dirty = YES; } - (void)__didLoad @@ -2590,7 +2543,7 @@ void recursivelyTriggerDisplayForLayer(CALayer *layer, BOOL shouldBlock) - (void)_applyLayout:(ASLayout *)layout layoutTransition:(ASLayoutTransition *)layoutTransition { ASDN::MutexLocker l(__instanceLock__); - _calculatedLayout = layout; + _layout = layout; ASDisplayNodeAssertTrue(layout.layoutableObject == self); ASDisplayNodeAssertTrue(layout.size.width >= 0.0); @@ -2605,35 +2558,15 @@ void recursivelyTriggerDisplayForLayer(CALayer *layer, BOOL shouldBlock) // Subnode insertions and removals need to happen always on the main thread if at least one subnode is already loaded ASPerformBlockOnMainThread(^{ - [layoutTransition commitTransition]; + [layoutTransition startTransition]; }); return; } - [layoutTransition commitTransition]; + [layoutTransition startTransition]; } -- (void)layout -{ - ASDisplayNodeAssertMainThread(); - - if ([self _hasDirtyLayout]) { - return; - } - - [self __layoutSublayouts]; -} - -- (void)__layoutSublayouts -{ - for (ASLayout *subnodeLayout in _calculatedLayout.sublayouts) { - ((ASDisplayNode *)subnodeLayout.layoutableObject).frame = [subnodeLayout frame]; - } -} - -#pragma mark - Display - - (void)displayWillStart { ASDisplayNodeAssertMainThread(); diff --git a/AsyncDisplayKit/Private/ASDisplayNodeInternal.h b/AsyncDisplayKit/Private/ASDisplayNodeInternal.h index f6337ba0..c894bf76 100644 --- a/AsyncDisplayKit/Private/ASDisplayNodeInternal.h +++ b/AsyncDisplayKit/Private/ASDisplayNodeInternal.h @@ -116,14 +116,14 @@ FOUNDATION_EXPORT NSString * const ASRenderingEngineDidDisplayNodesScheduledBefo CGFloat _contentsScaleForDisplay; ASEnvironmentState _environmentState; - ASLayout *_calculatedLayout; + ASLayout *_layout; UIEdgeInsets _hitTestSlop; NSMutableArray *_subnodes; // Main thread only - _ASTransitionContext *_pendingLayoutTransitionContext; + _ASTransitionContext *_transitionContext; BOOL _usesImplicitHierarchyManagement; int32_t _pendingTransitionID; diff --git a/AsyncDisplayKit/Private/ASLayoutTransition.h b/AsyncDisplayKit/Private/ASLayoutTransition.h index 64869dc0..3c8e5781 100644 --- a/AsyncDisplayKit/Private/ASLayoutTransition.h +++ b/AsyncDisplayKit/Private/ASLayoutTransition.h @@ -34,7 +34,7 @@ @property (nonatomic, readonly, strong) ASLayout *pendingLayout; /** - * Returns if the layout transition needs to happen synchronously + * Returns if the layout transition can happen asynchronously */ @property (nonatomic, readonly, assign) BOOL isSynchronous; @@ -47,7 +47,7 @@ /** * Insert and remove subnodes that where added or removed between the previousLayout and the pendingLayout */ -- (void)commitTransition; +- (void)startTransition; /** * Insert all new subnodes that where added between the previous layout and the pending layout diff --git a/AsyncDisplayKit/Private/ASLayoutTransition.mm b/AsyncDisplayKit/Private/ASLayoutTransition.mm index e169b6e1..e7176077 100644 --- a/AsyncDisplayKit/Private/ASLayoutTransition.mm +++ b/AsyncDisplayKit/Private/ASLayoutTransition.mm @@ -71,10 +71,10 @@ static inline BOOL ASLayoutCanTransitionAsynchronous(ASLayout *layout) { - (BOOL)isSynchronous { ASDN::MutexLocker l(__instanceLock__); - return !ASLayoutCanTransitionAsynchronous(_pendingLayout); + return ASLayoutCanTransitionAsynchronous(_pendingLayout); } -- (void)commitTransition +- (void)startTransition { [self applySubnodeInsertions]; [self applySubnodeRemovals];