diff --git a/AsyncDisplayKit.xcodeproj/project.pbxproj b/AsyncDisplayKit.xcodeproj/project.pbxproj index 06e5fbe4f0..33b7608fac 100644 --- a/AsyncDisplayKit.xcodeproj/project.pbxproj +++ b/AsyncDisplayKit.xcodeproj/project.pbxproj @@ -157,6 +157,7 @@ 698DFF441E36B6C9002891F1 /* ASStackLayoutSpecUtilities.h in Headers */ = {isa = PBXBuildFile; fileRef = 698DFF431E36B6C9002891F1 /* ASStackLayoutSpecUtilities.h */; settings = {ATTRIBUTES = (Private, ); }; }; 698DFF471E36B7E9002891F1 /* ASLayoutSpecUtilities.h in Headers */ = {isa = PBXBuildFile; fileRef = 698DFF461E36B7E9002891F1 /* ASLayoutSpecUtilities.h */; settings = {ATTRIBUTES = (Private, ); }; }; 69B225671D72535E00B25B22 /* ASDisplayNodeLayoutTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 69B225661D72535E00B25B22 /* ASDisplayNodeLayoutTests.mm */; }; + 69BCE3D91EC6513B007DCCAD /* ASDisplayNode+Layout.mm in Sources */ = {isa = PBXBuildFile; fileRef = 69BCE3D71EC6513B007DCCAD /* ASDisplayNode+Layout.mm */; }; 69CB62AC1CB8165900024920 /* _ASDisplayViewAccessiblity.h in Headers */ = {isa = PBXBuildFile; fileRef = 69CB62A91CB8165900024920 /* _ASDisplayViewAccessiblity.h */; settings = {ATTRIBUTES = (Private, ); }; }; 69CB62AE1CB8165900024920 /* _ASDisplayViewAccessiblity.mm in Sources */ = {isa = PBXBuildFile; fileRef = 69CB62AA1CB8165900024920 /* _ASDisplayViewAccessiblity.mm */; }; 69E0E8A71D356C9400627613 /* ASEqualityHelpers.h in Headers */ = {isa = PBXBuildFile; fileRef = 1950C4481A3BB5C1005C8279 /* ASEqualityHelpers.h */; settings = {ATTRIBUTES = (Public, ); }; }; @@ -649,6 +650,7 @@ 699B83501E3C1BA500433FA4 /* ASLayoutSpecTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASLayoutSpecTests.m; sourceTree = ""; }; 69B225661D72535E00B25B22 /* ASDisplayNodeLayoutTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASDisplayNodeLayoutTests.mm; sourceTree = ""; }; 69B225681D7265DA00B25B22 /* ASXCTExtensions.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASXCTExtensions.h; sourceTree = ""; }; + 69BCE3D71EC6513B007DCCAD /* ASDisplayNode+Layout.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = "ASDisplayNode+Layout.mm"; sourceTree = ""; }; 69CB62A91CB8165900024920 /* _ASDisplayViewAccessiblity.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = _ASDisplayViewAccessiblity.h; sourceTree = ""; }; 69CB62AA1CB8165900024920 /* _ASDisplayViewAccessiblity.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = _ASDisplayViewAccessiblity.mm; sourceTree = ""; }; 69F10C851C84C35D0026140C /* ASRangeControllerUpdateRangeProtocol+Beta.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "ASRangeControllerUpdateRangeProtocol+Beta.h"; sourceTree = ""; }; @@ -1017,6 +1019,7 @@ 058D09DA195D050800B7D73C /* ASDisplayNode+Subclasses.h */, CC034A071E60BEB400626263 /* ASDisplayNode+Convenience.h */, CC034A081E60BEB400626263 /* ASDisplayNode+Convenience.m */, + 69BCE3D71EC6513B007DCCAD /* ASDisplayNode+Layout.mm */, 058D09DB195D050800B7D73C /* ASDisplayNodeExtras.h */, 058D09DC195D050800B7D73C /* ASDisplayNodeExtras.mm */, 0587F9BB1A7309ED00AFF0BA /* ASEditableTextNode.h */, @@ -2171,6 +2174,7 @@ CCA282C91E9EB64B0037E8B7 /* ASDisplayNodeTipState.m in Sources */, 509E68601B3AED8E009B9150 /* ASScrollDirection.m in Sources */, B35062091B010EFD0018CF92 /* ASScrollNode.mm in Sources */, + 69BCE3D91EC6513B007DCCAD /* ASDisplayNode+Layout.mm in Sources */, 8BDA5FC81CDBDF95007D13B2 /* ASVideoPlayerNode.mm in Sources */, E54E81FD1EB357BD00FFE8E1 /* ASPageTable.m in Sources */, 34EFC7721B701D0300AD841F /* ASStackLayoutSpec.mm in Sources */, diff --git a/CHANGELOG.md b/CHANGELOG.md index 0e467856fe..1da6431511 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,3 +21,4 @@ - [ASTextNode] Add an experimental new implementation. See `+[ASTextNode setExperimentOptions:]`. [Adlai Holler](https://github.com/Adlai-Holler)[#259](https://github.com/TextureGroup/Texture/pull/259) - [ASVideoNode] Added error reporing to ASVideoNode and it's delegate [#260](https://github.com/TextureGroup/Texture/pull/260) - [ASCollectionNode] Fixed conversion of item index paths between node & view. [Adlai Holler](https://github.com/Adlai-Holler) [#262](https://github.com/TextureGroup/Texture/pull/262) +- [Layout] Extract layout implementation code into it's own subcategories [Michael Schneider] (https://github.com/maicki)[#272](https://github.com/TextureGroup/Texture/pull/272) diff --git a/Source/ASDisplayNode+Layout.mm b/Source/ASDisplayNode+Layout.mm new file mode 100644 index 0000000000..add82bab83 --- /dev/null +++ b/Source/ASDisplayNode+Layout.mm @@ -0,0 +1,908 @@ +// +// ASDisplayNode+Layout.mm +// Texture +// +// Copyright (c) 2017-present, Pinterest, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// + +#import +#import +#import +#import +#import + +#import + +#pragma mark - +#pragma mark - ASDisplayNode (ASLayoutElement) + +@implementation ASDisplayNode (ASLayoutElement) + +#pragma mark + +- (ASLayoutElementStyle *)style +{ + ASDN::MutexLocker l(__instanceLock__); + if (_style == nil) { + _style = [[ASLayoutElementStyle alloc] init]; + } + return _style; +} + +- (ASLayoutElementType)layoutElementType +{ + return ASLayoutElementTypeDisplayNode; +} + +- (NSArray> *)sublayoutElements +{ + return self.subnodes; +} + +#pragma mark Measurement Pass + +- (ASLayout *)layoutThatFits:(ASSizeRange)constrainedSize +{ +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + // For now we just call the deprecated measureWithSizeRange: method to not break old API + return [self measureWithSizeRange:constrainedSize]; +#pragma clang diagnostic pop +} + +- (ASLayout *)measureWithSizeRange:(ASSizeRange)constrainedSize +{ + return [self layoutThatFits:constrainedSize parentSize:constrainedSize.max]; +} + +- (ASLayout *)layoutThatFits:(ASSizeRange)constrainedSize parentSize:(CGSize)parentSize +{ + ASDN::MutexLocker l(__instanceLock__); + + // If one or multiple layout transitions are in flight it still can happen that layout information is requested + // on other threads. As the pending and calculated layout to be updated in the layout transition in here just a + // layout calculation wil be performed without side effect + if ([self _isLayoutTransitionInvalid]) { + return [self calculateLayoutThatFits:constrainedSize restrictedToSize:self.style.size relativeToParentSize:parentSize]; + } + + if (_calculatedDisplayNodeLayout->isValidForConstrainedSizeParentSize(constrainedSize, parentSize)) { + ASDisplayNodeAssertNotNil(_calculatedDisplayNodeLayout->layout, @"-[ASDisplayNode layoutThatFits:parentSize:] _calculatedDisplayNodeLayout->layout should not be nil! %@", self); + // Our calculated layout is suitable for this constrainedSize, so keep using it and + // invalidate any pending layout that has been generated in the past. + _pendingDisplayNodeLayout = nullptr; + return _calculatedDisplayNodeLayout->layout ?: [ASLayout layoutWithLayoutElement:self size:{0, 0}]; + } + + // Create a pending display node layout for the layout pass + _pendingDisplayNodeLayout = std::make_shared( + [self calculateLayoutThatFits:constrainedSize restrictedToSize:self.style.size relativeToParentSize:parentSize], + constrainedSize, + parentSize + ); + + ASDisplayNodeAssertNotNil(_pendingDisplayNodeLayout->layout, @"-[ASDisplayNode layoutThatFits:parentSize:] _pendingDisplayNodeLayout->layout should not be nil! %@", self); + return _pendingDisplayNodeLayout->layout ?: [ASLayout layoutWithLayoutElement:self size:{0, 0}]; +} + +#pragma mark ASLayoutElementStyleExtensibility + +ASLayoutElementStyleExtensibilityForwarding + +#pragma mark ASPrimitiveTraitCollection + +- (ASPrimitiveTraitCollection)primitiveTraitCollection +{ + ASDN::MutexLocker l(__instanceLock__); + return _primitiveTraitCollection; +} + +- (void)setPrimitiveTraitCollection:(ASPrimitiveTraitCollection)traitCollection +{ + __instanceLock__.lock(); + if (ASPrimitiveTraitCollectionIsEqualToASPrimitiveTraitCollection(traitCollection, _primitiveTraitCollection) == NO) { + _primitiveTraitCollection = traitCollection; + ASDisplayNodeLogEvent(self, @"asyncTraitCollectionDidChange: %@", NSStringFromASPrimitiveTraitCollection(traitCollection)); + __instanceLock__.unlock(); + + [self asyncTraitCollectionDidChange]; + return; + } + + __instanceLock__.unlock(); +} + +- (ASTraitCollection *)asyncTraitCollection +{ + ASDN::MutexLocker l(__instanceLock__); + return [ASTraitCollection traitCollectionWithASPrimitiveTraitCollection:self.primitiveTraitCollection]; +} + +ASPrimitiveTraitCollectionDeprecatedImplementation + +@end + +#pragma mark - +#pragma mark - ASLayoutElementAsciiArtProtocol + +@implementation ASDisplayNode (ASLayoutElementAsciiArtProtocol) + +- (NSString *)asciiArtString +{ + return [ASLayoutSpec asciiArtStringForChildren:@[] parentName:[self asciiArtName]]; +} + +- (NSString *)asciiArtName +{ + NSString *string = NSStringFromClass([self class]); + if (_debugName) { + string = [string stringByAppendingString:[NSString stringWithFormat:@"\"%@\"",_debugName]]; + } + return string; +} + +@end + +#pragma mark - +#pragma mark - ASDisplayNode (ASLayout) + +@implementation ASDisplayNode (ASLayout) + +- (void)setLayoutSpecBlock:(ASLayoutSpecBlock)layoutSpecBlock +{ + // For now there should never be an override of layoutSpecThatFits: / layoutElementThatFits: and a layoutSpecBlock + ASDisplayNodeAssert(!(_methodOverrides & ASDisplayNodeMethodOverrideLayoutSpecThatFits), @"Overwriting layoutSpecThatFits: and providing a layoutSpecBlock block is currently not supported"); + + ASDN::MutexLocker l(__instanceLock__); + _layoutSpecBlock = layoutSpecBlock; +} + +- (ASLayoutSpecBlock)layoutSpecBlock +{ + ASDN::MutexLocker l(__instanceLock__); + return _layoutSpecBlock; +} + +- (ASLayout *)calculatedLayout +{ + ASDN::MutexLocker l(__instanceLock__); + return _calculatedDisplayNodeLayout->layout; +} + +- (CGSize)calculatedSize +{ + ASDN::MutexLocker l(__instanceLock__); + if (_pendingDisplayNodeLayout != nullptr) { + return _pendingDisplayNodeLayout->layout.size; + } + return _calculatedDisplayNodeLayout->layout.size; +} + +- (ASSizeRange)constrainedSizeForCalculatedLayout +{ + ASDN::MutexLocker l(__instanceLock__); + if (_pendingDisplayNodeLayout != nullptr) { + return _pendingDisplayNodeLayout->constrainedSize; + } + return _calculatedDisplayNodeLayout->constrainedSize; +} + +@end + +#pragma mark - +#pragma mark - ASDisplayNode (ASLayoutElementStylability) + +@implementation ASDisplayNode (ASLayoutElementStylability) + +- (instancetype)styledWithBlock:(AS_NOESCAPE void (^)(__kindof ASLayoutElementStyle *style))styleBlock +{ + styleBlock(self.style); + return self; +} + +@end + +#pragma mark - +#pragma mark - ASDisplayNode (ASLayoutInternal) + +@implementation ASDisplayNode (ASLayoutInternal) + +/** + * @abstract Informs the root node that the intrinsic size of the receiver is no longer valid. + * + * @discussion The size of a root node is determined by each subnode. Calling invalidateSize will let the root node know + * that the intrinsic size of the receiver node is no longer valid and a resizing of the root node needs to happen. + */ +- (void)_setNeedsLayoutFromAbove +{ + ASDisplayNodeAssertThreadAffinity(self); + + // Mark the node for layout in the next layout pass + [self setNeedsLayout]; + + __instanceLock__.lock(); + // Escalate to the root; entire tree must allow adjustments so the layout fits the new child. + // Much of the layout will be re-used as cached (e.g. other items in an unconstrained stack) + ASDisplayNode *supernode = _supernode; + __instanceLock__.unlock(); + + if (supernode) { + // Threading model requires that we unlock before calling a method on our parent. + [supernode _setNeedsLayoutFromAbove]; + } else { + // Let the root node method know that the size was invalidated + [self _rootNodeDidInvalidateSize]; + } +} + +- (void)_rootNodeDidInvalidateSize +{ + ASDisplayNodeAssertThreadAffinity(self); + ASDisplayNodeAssertLockUnownedByCurrentThread(__instanceLock__); + + __instanceLock__.lock(); + + // We are the root node and need to re-flow the layout; at least one child needs a new size. + CGSize boundsSizeForLayout = ASCeilSizeValues(self.bounds.size); + + // Figure out constrainedSize to use + ASSizeRange constrainedSize = ASSizeRangeMake(boundsSizeForLayout); + if (_pendingDisplayNodeLayout != nullptr) { + constrainedSize = _pendingDisplayNodeLayout->constrainedSize; + } else if (_calculatedDisplayNodeLayout->layout != nil) { + constrainedSize = _calculatedDisplayNodeLayout->constrainedSize; + } + + __instanceLock__.unlock(); + + // Perform a measurement pass to get the full tree layout, adapting to the child's new size. + ASLayout *layout = [self layoutThatFits:constrainedSize]; + + // Check if the returned layout has a different size than our current bounds. + if (CGSizeEqualToSize(boundsSizeForLayout, layout.size) == NO) { + // If so, inform our container we need an update (e.g Table, Collection, ViewController, etc). + [self displayNodeDidInvalidateSizeNewSize:layout.size]; + } +} + +- (void)displayNodeDidInvalidateSizeNewSize:(CGSize)size +{ + ASDisplayNodeAssertThreadAffinity(self); + ASDisplayNodeAssertLockUnownedByCurrentThread(__instanceLock__); + + // The default implementation of display node changes the size of itself to the new size + CGRect oldBounds = self.bounds; + CGSize oldSize = oldBounds.size; + CGSize newSize = size; + + if (! CGSizeEqualToSize(oldSize, newSize)) { + self.bounds = (CGRect){ oldBounds.origin, newSize }; + + // Frame's origin must be preserved. Since it is computed from bounds size, anchorPoint + // and position (see frame setter in ASDisplayNode+UIViewBridge), position needs to be adjusted. + CGPoint anchorPoint = self.anchorPoint; + CGPoint oldPosition = self.position; + CGFloat xDelta = (newSize.width - oldSize.width) * anchorPoint.x; + CGFloat yDelta = (newSize.height - oldSize.height) * anchorPoint.y; + self.position = CGPointMake(oldPosition.x + xDelta, oldPosition.y + yDelta); + } +} + +/// Needs to be called with lock held +- (void)_locked_measureNodeWithBoundsIfNecessary:(CGRect)bounds +{ + // Check if we are a subnode in a layout transition. + // In this case no measurement is needed as it's part of the layout transition + if ([self _isLayoutTransitionInvalid]) { + return; + } + + CGSize boundsSizeForLayout = ASCeilSizeValues(bounds.size); + + // Prefer _pendingDisplayNodeLayout over _calculatedDisplayNodeLayout (if exists, it's the newest) + // If there is no _pending, check if _calculated is valid to reuse (avoiding recalculation below). + if (_pendingDisplayNodeLayout == nullptr) { + if (_calculatedDisplayNodeLayout->isDirty() == NO + && (_calculatedDisplayNodeLayout->requestedLayoutFromAbove == YES + || CGSizeEqualToSize(_calculatedDisplayNodeLayout->layout.size, boundsSizeForLayout))) { + return; + } + } + + // _calculatedDisplayNodeLayout is not reusable we need to transition to a new one + [self cancelLayoutTransition]; + + BOOL didCreateNewContext = NO; + ASLayoutElementContext context = ASLayoutElementGetCurrentContext(); + if (ASLayoutElementContextIsNull(context)) { + context = ASLayoutElementContextMake(ASLayoutElementContextDefaultTransitionID); + ASLayoutElementSetCurrentContext(context); + didCreateNewContext = YES; + } + + // Figure out previous and pending layouts for layout transition + std::shared_ptr nextLayout = _pendingDisplayNodeLayout; + #define layoutSizeDifferentFromBounds !CGSizeEqualToSize(nextLayout->layout.size, boundsSizeForLayout) + + // nextLayout was likely created by a call to layoutThatFits:, check if it is valid and can be applied. + // If our bounds size is different than it, or invalid, recalculate. Use #define to avoid nullptr-> + if (nextLayout == nullptr || nextLayout->isDirty() == YES || layoutSizeDifferentFromBounds) { + // Use the last known constrainedSize passed from a parent during layout (if never, use bounds). + ASSizeRange constrainedSize = [self _locked_constrainedSizeForLayoutPass]; + ASLayout *layout = [self calculateLayoutThatFits:constrainedSize + restrictedToSize:self.style.size + relativeToParentSize:boundsSizeForLayout]; + + nextLayout = std::make_shared(layout, constrainedSize, boundsSizeForLayout); + } + + if (didCreateNewContext) { + ASLayoutElementClearCurrentContext(); + } + + // If our new layout's desired size for self doesn't match current size, ask our parent to update it. + // This can occur for either pre-calculated or newly-calculated layouts. + if (nextLayout->requestedLayoutFromAbove == NO + && CGSizeEqualToSize(boundsSizeForLayout, nextLayout->layout.size) == NO) { + // The layout that we have specifies that this node (self) would like to be a different size + // than it currently is. Because that size has been computed within the constrainedSize, we + // expect that calling setNeedsLayoutFromAbove will result in our parent resizing us to this. + // However, in some cases apps may manually interfere with this (setting a different bounds). + // In this case, we need to detect that we've already asked to be resized to match this + // particular ASLayout object, and shouldn't loop asking again unless we have a different ASLayout. + nextLayout->requestedLayoutFromAbove = YES; + [self _setNeedsLayoutFromAbove]; + } + + // Prepare to transition to nextLayout + ASDisplayNodeAssertNotNil(nextLayout->layout, @"nextLayout->layout should not be nil! %@", self); + _pendingLayoutTransition = [[ASLayoutTransition alloc] initWithNode:self + pendingLayout:nextLayout + previousLayout:_calculatedDisplayNodeLayout]; + + // If a parent is currently executing a layout transition, perform our layout application after it. + if (ASHierarchyStateIncludesLayoutPending(_hierarchyState) == NO) { + // If no transition, apply our new layout immediately (common case). + [self _completePendingLayoutTransition]; + } +} + +- (ASSizeRange)_locked_constrainedSizeForLayoutPass +{ + // TODO: The logic in -_setNeedsLayoutFromAbove seems correct and doesn't use this method. + // logic seems correct. For what case does -this method need to do the CGSizeEqual checks? + // IF WE CAN REMOVE BOUNDS CHECKS HERE, THEN WE CAN ALSO REMOVE "REQUESTED FROM ABOVE" CHECK + + CGSize boundsSizeForLayout = ASCeilSizeValues(self.threadSafeBounds.size); + + // Checkout if constrained size of pending or calculated display node layout can be used + if (_pendingDisplayNodeLayout != nullptr + && (_pendingDisplayNodeLayout->requestedLayoutFromAbove + || CGSizeEqualToSize(_pendingDisplayNodeLayout->layout.size, boundsSizeForLayout))) { + // We assume the size from the last returned layoutThatFits: layout was applied so use the pending display node + // layout constrained size + return _pendingDisplayNodeLayout->constrainedSize; + } else if (_calculatedDisplayNodeLayout->layout != nil + && (_calculatedDisplayNodeLayout->requestedLayoutFromAbove + || CGSizeEqualToSize(_calculatedDisplayNodeLayout->layout.size, boundsSizeForLayout))) { + // We assume the _calculatedDisplayNodeLayout is still valid and the frame is not different + return _calculatedDisplayNodeLayout->constrainedSize; + } else { + // In this case neither the _pendingDisplayNodeLayout or the _calculatedDisplayNodeLayout constrained size can + // be reused, so the current bounds is used. This is usual the case if a frame was set manually that differs to + // the one returned from layoutThatFits: or layoutThatFits: was never called + return ASSizeRangeMake(boundsSizeForLayout); + } +} + +- (void)_layoutSublayouts +{ + ASDisplayNodeAssertThreadAffinity(self); + ASDisplayNodeAssertLockUnownedByCurrentThread(__instanceLock__); + + ASLayout *layout; + { + ASDN::MutexLocker l(__instanceLock__); + if (_calculatedDisplayNodeLayout->isDirty()) { + return; + } + layout = _calculatedDisplayNodeLayout->layout; + } + + for (ASDisplayNode *node in self.subnodes) { + CGRect frame = [layout frameForElement:node]; + if (CGRectIsNull(frame)) { + // There is no frame for this node in our layout. + // This currently can happen if we get a CA layout pass + // while waiting for the client to run animateLayoutTransition: + } else { + node.frame = frame; + } + } +} + +@end + +#pragma mark - +#pragma mark - ASDisplayNode (ASAutomatic Subnode Management) + +@implementation ASDisplayNode (ASAutomaticSubnodeManagement) + +#pragma mark Automatically Manages Subnodes + +- (BOOL)automaticallyManagesSubnodes +{ + ASDN::MutexLocker l(__instanceLock__); + return _automaticallyManagesSubnodes; +} + +- (void)setAutomaticallyManagesSubnodes:(BOOL)automaticallyManagesSubnodes +{ + ASDN::MutexLocker l(__instanceLock__); + _automaticallyManagesSubnodes = automaticallyManagesSubnodes; +} + +@end + +#pragma mark - +#pragma mark - ASDisplayNode (ASLayoutTransition) + +@implementation ASDisplayNode (ASLayoutTransition) + +- (BOOL)_isTransitionInProgress +{ + ASDN::MutexLocker l(__instanceLock__); + return _transitionInProgress; +} + +- (BOOL)_isLayoutTransitionInvalid +{ + ASDN::MutexLocker l(__instanceLock__); + if (ASHierarchyStateIncludesLayoutPending(_hierarchyState)) { + ASLayoutElementContext context = ASLayoutElementGetCurrentContext(); + if (ASLayoutElementContextIsNull(context) || _pendingTransitionID != context.transitionID) { + return YES; + } + } + return NO; +} + +/// Starts a new transition and returns the transition id +- (int32_t)_startNewTransition +{ + ASDN::MutexLocker l(__instanceLock__); + _transitionInProgress = YES; + _transitionID = OSAtomicAdd32(1, &_transitionID); + return _transitionID; +} + +- (void)_finishOrCancelTransition +{ + ASDN::MutexLocker l(__instanceLock__); + _transitionInProgress = NO; +} + +- (void)setPendingTransitionID:(int32_t)pendingTransitionID +{ + ASDN::MutexLocker l(__instanceLock__); + ASDisplayNodeAssertTrue(_pendingTransitionID < pendingTransitionID); + _pendingTransitionID = pendingTransitionID; +} + +- (int32_t)pendingTransitionID +{ + ASDN::MutexLocker l(__instanceLock__); + return _pendingTransitionID; +} + +- (BOOL)_shouldAbortTransitionWithID:(int32_t)transitionID +{ + ASDN::MutexLocker l(__instanceLock__); + return [self _locked_shouldAbortTransitionWithID:transitionID]; +} + +- (BOOL)_locked_shouldAbortTransitionWithID:(int32_t)transitionID +{ + return (!_transitionInProgress || _transitionID != transitionID); +} + +#pragma mark Layout Transition + +- (void)transitionLayoutWithAnimation:(BOOL)animated + shouldMeasureAsync:(BOOL)shouldMeasureAsync + measurementCompletion:(void(^)())completion +{ + ASDisplayNodeAssertMainThread(); + + [self setNeedsLayout]; + + [self transitionLayoutWithSizeRange:[self _locked_constrainedSizeForLayoutPass] + animated:animated + shouldMeasureAsync:shouldMeasureAsync + measurementCompletion:completion]; + +} + +- (void)transitionLayoutWithSizeRange:(ASSizeRange)constrainedSize + animated:(BOOL)animated + shouldMeasureAsync:(BOOL)shouldMeasureAsync + measurementCompletion:(void(^)())completion +{ + ASDisplayNodeAssertMainThread(); + + if (constrainedSize.max.width <= 0.0 || constrainedSize.max.height <= 0.0) { + // Using CGSizeZero for the sizeRange can cause negative values in client layout code. + // Most likely called transitionLayout: without providing a size, before first layout pass. + return; + } + + // Check if we are a subnode in a layout transition. + // In this case no measurement is needed as we're part of the layout transition. + if ([self _isLayoutTransitionInvalid]) { + return; + } + + { + ASDN::MutexLocker l(__instanceLock__); + ASDisplayNodeAssert(ASHierarchyStateIncludesLayoutPending(_hierarchyState) == NO, @"Can't start a transition when one of the supernodes is performing one."); + } + + // Every new layout transition has a transition id associated to check in subsequent transitions for cancelling + int32_t transitionID = [self _startNewTransition]; + + // Move all subnodes in layout pending state for this transition + ASDisplayNodePerformBlockOnEverySubnode(self, NO, ^(ASDisplayNode * _Nonnull node) { + ASDisplayNodeAssert([node _isTransitionInProgress] == NO, @"Can't start a transition when one of the subnodes is performing one."); + node.hierarchyState |= ASHierarchyStateLayoutPending; + node.pendingTransitionID = transitionID; + }); + + // Transition block that executes the layout transition + void (^transitionBlock)(void) = ^{ + if ([self _shouldAbortTransitionWithID:transitionID]) { + return; + } + + // Perform a full layout creation pass with passed in constrained size to create the new layout for the transition + ASLayout *newLayout; + { + ASDN::MutexLocker l(__instanceLock__); + + ASLayoutElementSetCurrentContext(ASLayoutElementContextMake(transitionID)); + + BOOL automaticallyManagesSubnodesDisabled = (self.automaticallyManagesSubnodes == NO); + self.automaticallyManagesSubnodes = YES; // Temporary flag for 1.9.x + newLayout = [self calculateLayoutThatFits:constrainedSize + restrictedToSize:self.style.size + relativeToParentSize:constrainedSize.max]; + if (automaticallyManagesSubnodesDisabled) { + self.automaticallyManagesSubnodes = NO; // Temporary flag for 1.9.x + } + + ASLayoutElementClearCurrentContext(); + } + + if ([self _shouldAbortTransitionWithID:transitionID]) { + return; + } + + ASPerformBlockOnMainThread(^{ + ASLayoutTransition *pendingLayoutTransition; + _ASTransitionContext *pendingLayoutTransitionContext; + { + // Grab __instanceLock__ here to make sure this transition isn't invalidated + // right after it passed the validation test and before it proceeds + ASDN::MutexLocker l(__instanceLock__); + + if ([self _locked_shouldAbortTransitionWithID:transitionID]) { + return; + } + + // Update calculated layout + auto previousLayout = _calculatedDisplayNodeLayout; + auto pendingLayout = std::make_shared(newLayout, + constrainedSize, + constrainedSize.max); + [self _locked_setCalculatedDisplayNodeLayout:pendingLayout]; + + // Setup pending layout transition for animation + _pendingLayoutTransition = pendingLayoutTransition = [[ASLayoutTransition alloc] initWithNode:self + pendingLayout:pendingLayout + previousLayout:previousLayout]; + // Setup context for pending layout transition. we need to hold a strong reference to the context + _pendingLayoutTransitionContext = pendingLayoutTransitionContext = [[_ASTransitionContext alloc] initWithAnimation:animated + layoutDelegate:_pendingLayoutTransition + completionDelegate:self]; + } + + // Apply complete layout transitions for all subnodes + ASDisplayNodePerformBlockOnEverySubnode(self, NO, ^(ASDisplayNode * _Nonnull node) { + [node _completePendingLayoutTransition]; + node.hierarchyState &= (~ASHierarchyStateLayoutPending); + }); + + // Measurement pass completion + // Give the subclass a change to hook into before calling the completion block + [self _layoutTransitionMeasurementDidFinish]; + if (completion) { + completion(); + } + + // Apply the subnode insertion immediately to be able to animate the nodes + [pendingLayoutTransition applySubnodeInsertions]; + + // Kick off animating the layout transition + [self animateLayoutTransition:pendingLayoutTransitionContext]; + + // Mark transaction as finished + [self _finishOrCancelTransition]; + }); + }; + + // Start transition based on flag on current or background thread + if (shouldMeasureAsync) { + ASPerformBlockOnBackgroundThread(transitionBlock); + } else { + transitionBlock(); + } +} + +- (void)cancelLayoutTransition +{ + __instanceLock__.lock(); + BOOL transitionInProgress = _transitionInProgress; + __instanceLock__.unlock(); + + if (transitionInProgress) { + // Cancel transition in progress + [self _finishOrCancelTransition]; + + // Tell subnodes to exit layout pending state and clear related properties + ASDisplayNodePerformBlockOnEverySubnode(self, NO, ^(ASDisplayNode * _Nonnull node) { + node.hierarchyState &= (~ASHierarchyStateLayoutPending); + }); + } +} + +- (void)setDefaultLayoutTransitionDuration:(NSTimeInterval)defaultLayoutTransitionDuration +{ + ASDN::MutexLocker l(__instanceLock__); + _defaultLayoutTransitionDuration = defaultLayoutTransitionDuration; +} + +- (NSTimeInterval)defaultLayoutTransitionDuration +{ + ASDN::MutexLocker l(__instanceLock__); + return _defaultLayoutTransitionDuration; +} + +- (void)setDefaultLayoutTransitionDelay:(NSTimeInterval)defaultLayoutTransitionDelay +{ + ASDN::MutexLocker l(__instanceLock__); + _defaultLayoutTransitionDelay = defaultLayoutTransitionDelay; +} + +- (NSTimeInterval)defaultLayoutTransitionDelay +{ + ASDN::MutexLocker l(__instanceLock__); + return _defaultLayoutTransitionDelay; +} + +- (void)setDefaultLayoutTransitionOptions:(UIViewAnimationOptions)defaultLayoutTransitionOptions +{ + ASDN::MutexLocker l(__instanceLock__); + _defaultLayoutTransitionOptions = defaultLayoutTransitionOptions; +} + +- (UIViewAnimationOptions)defaultLayoutTransitionOptions +{ + ASDN::MutexLocker l(__instanceLock__); + return _defaultLayoutTransitionOptions; +} + +#pragma mark + +/* + * Hook for subclasses to perform an animation based on the given ASContextTransitioning. By default a fade in and out + * animation is provided. + */ +- (void)animateLayoutTransition:(id)context +{ + if ([context isAnimated] == NO) { + [self _layoutSublayouts]; + [context completeTransition:YES]; + return; + } + + ASDisplayNode *node = self; + + NSAssert(node.isNodeLoaded == YES, @"Invalid node state"); + + NSArray *removedSubnodes = [context removedSubnodes]; + NSMutableArray *insertedSubnodes = [[context insertedSubnodes] mutableCopy]; + NSMutableArray *movedSubnodes = [NSMutableArray array]; + + NSMutableArray<_ASAnimatedTransitionContext *> *insertedSubnodeContexts = [NSMutableArray array]; + NSMutableArray<_ASAnimatedTransitionContext *> *removedSubnodeContexts = [NSMutableArray array]; + + for (ASDisplayNode *subnode in [context subnodesForKey:ASTransitionContextToLayoutKey]) { + if ([insertedSubnodes containsObject:subnode] == NO) { + // This is an existing subnode, check if it is resized, moved or both + CGRect fromFrame = [context initialFrameForNode:subnode]; + CGRect toFrame = [context finalFrameForNode:subnode]; + if (CGSizeEqualToSize(fromFrame.size, toFrame.size) == NO) { + [insertedSubnodes addObject:subnode]; + } + if (CGPointEqualToPoint(fromFrame.origin, toFrame.origin) == NO) { + [movedSubnodes addObject:subnode]; + } + } + } + + // Create contexts for inserted and removed subnodes + for (ASDisplayNode *insertedSubnode in insertedSubnodes) { + [insertedSubnodeContexts addObject:[_ASAnimatedTransitionContext contextForNode:insertedSubnode alpha:insertedSubnode.alpha]]; + } + for (ASDisplayNode *removedSubnode in removedSubnodes) { + [removedSubnodeContexts addObject:[_ASAnimatedTransitionContext contextForNode:removedSubnode alpha:removedSubnode.alpha]]; + } + + // Fade out inserted subnodes + for (ASDisplayNode *insertedSubnode in insertedSubnodes) { + insertedSubnode.frame = [context finalFrameForNode:insertedSubnode]; + insertedSubnode.alpha = 0; + } + + // Adjust groupOpacity for animation + BOOL originAllowsGroupOpacity = node.allowsGroupOpacity; + node.allowsGroupOpacity = YES; + + [UIView animateWithDuration:self.defaultLayoutTransitionDuration delay:self.defaultLayoutTransitionDelay options:self.defaultLayoutTransitionOptions animations:^{ + // Fade removed subnodes and views out + for (ASDisplayNode *removedSubnode in removedSubnodes) { + removedSubnode.alpha = 0; + } + + // Fade inserted subnodes in + for (_ASAnimatedTransitionContext *insertedSubnodeContext in insertedSubnodeContexts) { + insertedSubnodeContext.node.alpha = insertedSubnodeContext.alpha; + } + + // Update frame of self and moved subnodes + CGSize fromSize = [context layoutForKey:ASTransitionContextFromLayoutKey].size; + CGSize toSize = [context layoutForKey:ASTransitionContextToLayoutKey].size; + BOOL isResized = (CGSizeEqualToSize(fromSize, toSize) == NO); + if (isResized == YES) { + CGPoint position = node.frame.origin; + node.frame = CGRectMake(position.x, position.y, toSize.width, toSize.height); + } + for (ASDisplayNode *movedSubnode in movedSubnodes) { + movedSubnode.frame = [context finalFrameForNode:movedSubnode]; + } + } completion:^(BOOL finished) { + // Restore all removed subnode alpha values + for (_ASAnimatedTransitionContext *removedSubnodeContext in removedSubnodeContexts) { + removedSubnodeContext.node.alpha = removedSubnodeContext.alpha; + } + + // Restore group opacity + node.allowsGroupOpacity = originAllowsGroupOpacity; + + // Subnode removals are automatically performed + [context completeTransition:finished]; + }]; +} + +/** + * 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 +{ + ASDisplayNodeAssertMainThread(); + + __instanceLock__.lock(); + ASLayoutTransition *pendingLayoutTransition = _pendingLayoutTransition; + __instanceLock__.unlock(); + + [pendingLayoutTransition applySubnodeRemovals]; +} + +/** + * Completes the pending layout transition immediately without going through the the Layout Transition Animation API + */ +- (void)_completePendingLayoutTransition +{ + __instanceLock__.lock(); + ASLayoutTransition *pendingLayoutTransition = _pendingLayoutTransition; + __instanceLock__.unlock(); + + if (pendingLayoutTransition != nil) { + [self _setCalculatedDisplayNodeLayout: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 nodes that are not have automatic subnode management enabled + if (layoutTransition == nil || self.automaticallyManagesSubnodes == 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 +{ + // Subclass hook + [self calculatedLayoutDidChange]; + + // Grab lock after calling out to subclass + ASDN::MutexLocker l(__instanceLock__); + + // 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 && !_placeholderImage && [self _locked_displaysAsynchronously]) { + + // Zero-sized nodes do not require a placeholder. + ASLayout *layout = _calculatedDisplayNodeLayout->layout; + CGSize layoutSize = (layout ? layout.size : CGSizeZero); + if (layoutSize.width * layoutSize.height <= 0.0) { + return; + } + + // If we've displayed our contents, we don't need a placeholder. + // Contents is a thread-affined property and can't be read off main after loading. + if (self.isNodeLoaded) { + ASPerformBlockOnMainThread(^{ + if (self.contents == nil) { + _placeholderImage = [self placeholderImage]; + } + }); + } else { + if (self.contents == nil) { + _placeholderImage = [self placeholderImage]; + } + } + } + + // Cleanup pending layout transition + _pendingLayoutTransition = nil; +} + +- (void)_setCalculatedDisplayNodeLayout:(std::shared_ptr)displayNodeLayout +{ + ASDN::MutexLocker l(__instanceLock__); + [self _locked_setCalculatedDisplayNodeLayout:displayNodeLayout]; +} + +- (void)_locked_setCalculatedDisplayNodeLayout:(std::shared_ptr)displayNodeLayout +{ + ASDisplayNodeAssertTrue(displayNodeLayout->layout.layoutElement == self); + ASDisplayNodeAssertTrue(displayNodeLayout->layout.size.width >= 0.0); + ASDisplayNodeAssertTrue(displayNodeLayout->layout.size.height >= 0.0); + + _calculatedDisplayNodeLayout = displayNodeLayout; +} + +@end diff --git a/Source/ASDisplayNode.h b/Source/ASDisplayNode.h index 207b284fb6..6b63d44282 100644 --- a/Source/ASDisplayNode.h +++ b/Source/ASDisplayNode.h @@ -295,44 +295,6 @@ extern NSInteger const ASDefaultDrawingPriority; */ @property (nonatomic, class, copy) ASDisplayNodeNonFatalErrorBlock nonFatalErrorBlock; - -/** @name Managing dimensions */ - -/** - * @abstract Provides a way to declare a block to provide an ASLayoutSpec without having to subclass ASDisplayNode and - * implement layoutSpecThatFits: - * - * @return A block that takes a constrainedSize ASSizeRange argument, and must return an ASLayoutSpec that includes all - * of the subnodes to position in the layout. This input-output relationship is identical to the subclass override - * method -layoutSpecThatFits: - * - * @warning Subclasses that implement -layoutSpecThatFits: must not also use .layoutSpecBlock. Doing so will trigger - * an exception. A future version of the framework may support using both, calling them serially, with the - * .layoutSpecBlock superseding any values set by the method override. - * - * @code ^ASLayoutSpec *(__kindof ASDisplayNode * _Nonnull node, ASSizeRange constrainedSize) {}; - */ -@property (nonatomic, readwrite, copy, nullable) ASLayoutSpecBlock layoutSpecBlock; - -/** - * @abstract Return the calculated size. - * - * @discussion Ideal for use by subclasses in -layout, having already prompted their subnodes to calculate their size by - * calling -measure: on them in -calculateLayoutThatFits. - * - * @return Size already calculated by -calculateLayoutThatFits:. - * - * @warning Subclasses must not override this; it returns the last cached measurement and is never expensive. - */ -@property (nonatomic, readonly, assign) CGSize calculatedSize; - -/** - * @abstract Return the constrained size range used for calculating layout. - * - * @return The minimum and maximum constrained sizes used by calculateLayoutThatFits:. - */ -@property (nonatomic, readonly, assign) ASSizeRange constrainedSizeForCalculatedLayout; - /** @name Managing the nodes hierarchy */ @@ -589,7 +551,7 @@ extern NSInteger const ASDefaultDrawingPriority; /** * Convenience methods for debugging. */ -@interface ASDisplayNode (Debugging) +@interface ASDisplayNode (Debugging) /** * @abstract Return a description of the node hierarchy. @@ -600,7 +562,6 @@ extern NSInteger const ASDefaultDrawingPriority; @end - /** * ## UIView bridge * @@ -763,7 +724,52 @@ extern NSInteger const ASDefaultDrawingPriority; @end -@interface ASDisplayNode (LayoutTransitioning) +@interface ASDisplayNode (ASLayoutElementAsciiArtProtocol) +@end + +@interface ASDisplayNode (ASLayout) + +/** @name Managing dimensions */ + +/** + * @abstract Provides a way to declare a block to provide an ASLayoutSpec without having to subclass ASDisplayNode and + * implement layoutSpecThatFits: + * + * @return A block that takes a constrainedSize ASSizeRange argument, and must return an ASLayoutSpec that includes all + * of the subnodes to position in the layout. This input-output relationship is identical to the subclass override + * method -layoutSpecThatFits: + * + * @warning Subclasses that implement -layoutSpecThatFits: must not also use .layoutSpecBlock. Doing so will trigger + * an exception. A future version of the framework may support using both, calling them serially, with the + * .layoutSpecBlock superseding any values set by the method override. + * + * @code ^ASLayoutSpec *(__kindof ASDisplayNode * _Nonnull node, ASSizeRange constrainedSize) {}; + */ +@property (nonatomic, readwrite, copy, nullable) ASLayoutSpecBlock layoutSpecBlock; + +/** + * @abstract Return the calculated size. + * + * @discussion Ideal for use by subclasses in -layout, having already prompted their subnodes to calculate their size by + * calling -measure: on them in -calculateLayoutThatFits. + * + * @return Size already calculated by -calculateLayoutThatFits:. + * + * @warning Subclasses must not override this; it returns the last cached measurement and is never expensive. + */ +@property (nonatomic, readonly, assign) CGSize calculatedSize; + +/** + * @abstract Return the constrained size range used for calculating layout. + * + * @return The minimum and maximum constrained sizes used by calculateLayoutThatFits:. + */ +@property (nonatomic, readonly, assign) ASSizeRange constrainedSizeForCalculatedLayout; + + +@end + +@interface ASDisplayNode (ASLayoutTransitioning) /** * @abstract The amount of time it takes to complete the default transition animation. Default is 0.2. @@ -837,7 +843,7 @@ extern NSInteger const ASDefaultDrawingPriority; /* * ASDisplayNode support for automatic subnode management. */ -@interface ASDisplayNode (AutomaticSubnodeManagement) +@interface ASDisplayNode (ASAutomaticSubnodeManagement) /** * @abstract A boolean that shows whether the node automatically inserts and removes nodes based on the presence or diff --git a/Source/ASDisplayNode.mm b/Source/ASDisplayNode.mm index 522fc070da..f491939bf0 100644 --- a/Source/ASDisplayNode.mm +++ b/Source/ASDisplayNode.mm @@ -67,7 +67,7 @@ NSInteger const ASDefaultDrawingPriority = ASDefaultTransactionPriority; // We have to forward declare the protocol as this place otherwise it will not compile compiling with an Base SDK < iOS 10 @protocol CALayerDelegate; -@interface ASDisplayNode () +@interface ASDisplayNode () /** * See ASDisplayNodeInternal.h for ivars @@ -79,9 +79,7 @@ NSInteger const ASDefaultDrawingPriority = ASDefaultTransactionPriority; @dynamic layoutElementType; -@synthesize debugName = _debugName; @synthesize threadSafeBounds = _threadSafeBounds; -@synthesize layoutSpecBlock = _layoutSpecBlock; static BOOL suppressesInvalidCollectionUpdateExceptions = NO; @@ -838,27 +836,6 @@ static ASDisplayNodeMethodOverrides GetASDisplayNodeMethodOverrides(Class c) _flags.viewEverHadAGestureRecognizerAttached = YES; } -#pragma mark - Layout - -#if DEBUG - #define AS_DEDUPE_LAYOUT_SPEC_TREE 1 -#endif - -// At most a layoutSpecBlock or one of the three layout methods is overridden -#define __ASDisplayNodeCheckForLayoutMethodOverrides \ - ASDisplayNodeAssert(_layoutSpecBlock != NULL || \ - ((ASDisplayNodeSubclassOverridesSelector(self.class, @selector(calculateSizeThatFits:)) ? 1 : 0) \ - + (ASDisplayNodeSubclassOverridesSelector(self.class, @selector(layoutSpecThatFits:)) ? 1 : 0) \ - + (ASDisplayNodeSubclassOverridesSelector(self.class, @selector(calculateLayoutThatFits:)) ? 1 : 0)) <= 1, \ - @"Subclass %@ must at least provide a layoutSpecBlock or override at most one of the three layout methods: calculateLayoutThatFits:, layoutSpecThatFits:, or calculateSizeThatFits:", NSStringFromClass(self.class)) - -#pragma mark - -- (BOOL)canLayoutAsynchronous -{ - return !self.isNodeLoaded; -} - #pragma mark - (NSString *)debugName @@ -875,6 +852,28 @@ static ASDisplayNodeMethodOverrides GetASDisplayNodeMethodOverrides(Class c) } } +#pragma mark - Layout + +#if DEBUG + #define AS_DEDUPE_LAYOUT_SPEC_TREE 1 +#endif + +// At most a layoutSpecBlock or one of the three layout methods is overridden +#define __ASDisplayNodeCheckForLayoutMethodOverrides \ + ASDisplayNodeAssert(_layoutSpecBlock != NULL || \ + ((ASDisplayNodeSubclassOverridesSelector(self.class, @selector(calculateSizeThatFits:)) ? 1 : 0) \ + + (ASDisplayNodeSubclassOverridesSelector(self.class, @selector(layoutSpecThatFits:)) ? 1 : 0) \ + + (ASDisplayNodeSubclassOverridesSelector(self.class, @selector(calculateLayoutThatFits:)) ? 1 : 0)) <= 1, \ + @"Subclass %@ must at least provide a layoutSpecBlock or override at most one of the three layout methods: calculateLayoutThatFits:, layoutSpecThatFits:, or calculateSizeThatFits:", NSStringFromClass(self.class)) + + +#pragma mark + +- (BOOL)canLayoutAsynchronous +{ + return !self.isNodeLoaded; +} + #pragma mark Layout Pass - (void)__setNeedsLayout @@ -937,113 +936,6 @@ static ASDisplayNodeMethodOverrides GetASDisplayNodeMethodOverrides(Class c) }); } -/// Needs to be called with lock held -- (void)_locked_measureNodeWithBoundsIfNecessary:(CGRect)bounds -{ - // Check if we are a subnode in a layout transition. - // In this case no measurement is needed as it's part of the layout transition - if ([self _isLayoutTransitionInvalid]) { - return; - } - - CGSize boundsSizeForLayout = ASCeilSizeValues(bounds.size); - - // Prefer _pendingDisplayNodeLayout over _calculatedDisplayNodeLayout (if exists, it's the newest) - // If there is no _pending, check if _calculated is valid to reuse (avoiding recalculation below). - if (_pendingDisplayNodeLayout == nullptr) { - if (_calculatedDisplayNodeLayout->isDirty() == NO - && (_calculatedDisplayNodeLayout->requestedLayoutFromAbove == YES - || CGSizeEqualToSize(_calculatedDisplayNodeLayout->layout.size, boundsSizeForLayout))) { - return; - } - } - - // _calculatedDisplayNodeLayout is not reusable we need to transition to a new one - [self cancelLayoutTransition]; - - BOOL didCreateNewContext = NO; - ASLayoutElementContext context = ASLayoutElementGetCurrentContext(); - if (ASLayoutElementContextIsNull(context)) { - context = ASLayoutElementContextMake(ASLayoutElementContextDefaultTransitionID); - ASLayoutElementSetCurrentContext(context); - didCreateNewContext = YES; - } - - // Figure out previous and pending layouts for layout transition - std::shared_ptr nextLayout = _pendingDisplayNodeLayout; - #define layoutSizeDifferentFromBounds !CGSizeEqualToSize(nextLayout->layout.size, boundsSizeForLayout) - - // nextLayout was likely created by a call to layoutThatFits:, check if it is valid and can be applied. - // If our bounds size is different than it, or invalid, recalculate. Use #define to avoid nullptr-> - if (nextLayout == nullptr || nextLayout->isDirty() == YES || layoutSizeDifferentFromBounds) { - // Use the last known constrainedSize passed from a parent during layout (if never, use bounds). - ASSizeRange constrainedSize = [self _locked_constrainedSizeForLayoutPass]; - ASLayout *layout = [self calculateLayoutThatFits:constrainedSize - restrictedToSize:self.style.size - relativeToParentSize:boundsSizeForLayout]; - - nextLayout = std::make_shared(layout, constrainedSize, boundsSizeForLayout); - } - - if (didCreateNewContext) { - ASLayoutElementClearCurrentContext(); - } - - // If our new layout's desired size for self doesn't match current size, ask our parent to update it. - // This can occur for either pre-calculated or newly-calculated layouts. - if (nextLayout->requestedLayoutFromAbove == NO - && CGSizeEqualToSize(boundsSizeForLayout, nextLayout->layout.size) == NO) { - // The layout that we have specifies that this node (self) would like to be a different size - // than it currently is. Because that size has been computed within the constrainedSize, we - // expect that calling setNeedsLayoutFromAbove will result in our parent resizing us to this. - // However, in some cases apps may manually interfere with this (setting a different bounds). - // In this case, we need to detect that we've already asked to be resized to match this - // particular ASLayout object, and shouldn't loop asking again unless we have a different ASLayout. - nextLayout->requestedLayoutFromAbove = YES; - [self _setNeedsLayoutFromAbove]; - } - - // Prepare to transition to nextLayout - ASDisplayNodeAssertNotNil(nextLayout->layout, @"nextLayout->layout should not be nil! %@", self); - _pendingLayoutTransition = [[ASLayoutTransition alloc] initWithNode:self - pendingLayout:nextLayout - previousLayout:_calculatedDisplayNodeLayout]; - - // If a parent is currently executing a layout transition, perform our layout application after it. - if (ASHierarchyStateIncludesLayoutPending(_hierarchyState) == NO) { - // If no transition, apply our new layout immediately (common case). - [self _completePendingLayoutTransition]; - } -} - -- (ASSizeRange)_locked_constrainedSizeForLayoutPass -{ - // TODO: The logic in -_setNeedsLayoutFromAbove seems correct and doesn't use this method. - // logic seems correct. For what case does -this method need to do the CGSizeEqual checks? - // IF WE CAN REMOVE BOUNDS CHECKS HERE, THEN WE CAN ALSO REMOVE "REQUESTED FROM ABOVE" CHECK - - CGSize boundsSizeForLayout = ASCeilSizeValues(self.threadSafeBounds.size); - - // Checkout if constrained size of pending or calculated display node layout can be used - if (_pendingDisplayNodeLayout != nullptr - && (_pendingDisplayNodeLayout->requestedLayoutFromAbove - || CGSizeEqualToSize(_pendingDisplayNodeLayout->layout.size, boundsSizeForLayout))) { - // We assume the size from the last returned layoutThatFits: layout was applied so use the pending display node - // layout constrained size - return _pendingDisplayNodeLayout->constrainedSize; - } else if (_calculatedDisplayNodeLayout->layout != nil - && (_calculatedDisplayNodeLayout->requestedLayoutFromAbove - || CGSizeEqualToSize(_calculatedDisplayNodeLayout->layout.size, boundsSizeForLayout))) { - // We assume the _calculatedDisplayNodeLayout is still valid and the frame is not different - return _calculatedDisplayNodeLayout->constrainedSize; - } else { - // In this case neither the _pendingDisplayNodeLayout or the _calculatedDisplayNodeLayout constrained size can - // be reused, so the current bounds is used. This is usual the case if a frame was set manually that differs to - // the one returned from layoutThatFits: or layoutThatFits: was never called - return ASSizeRangeMake(boundsSizeForLayout); - } -} - - (void)layoutDidFinish { // Hook for subclasses @@ -1188,549 +1080,17 @@ static ASDisplayNodeMethodOverrides GetASDisplayNodeMethodOverrides(Class c) return [[ASLayoutSpec alloc] init]; } -- (void)setLayoutSpecBlock:(ASLayoutSpecBlock)layoutSpecBlock -{ - // For now there should never be an override of layoutSpecThatFits: / layoutElementThatFits: and a layoutSpecBlock - ASDisplayNodeAssert(!(_methodOverrides & ASDisplayNodeMethodOverrideLayoutSpecThatFits), @"Overwriting layoutSpecThatFits: and providing a layoutSpecBlock block is currently not supported"); - - ASDN::MutexLocker l(__instanceLock__); - _layoutSpecBlock = layoutSpecBlock; -} - -- (ASLayoutSpecBlock)layoutSpecBlock -{ - ASDN::MutexLocker l(__instanceLock__); - return _layoutSpecBlock; -} - -- (ASLayout *)calculatedLayout -{ - ASDN::MutexLocker l(__instanceLock__); - return _calculatedDisplayNodeLayout->layout; -} - -- (void)_setCalculatedDisplayNodeLayout:(std::shared_ptr)displayNodeLayout -{ - ASDN::MutexLocker l(__instanceLock__); - [self _locked_setCalculatedDisplayNodeLayout:displayNodeLayout]; -} - -- (void)_locked_setCalculatedDisplayNodeLayout:(std::shared_ptr)displayNodeLayout -{ - ASDisplayNodeAssertTrue(displayNodeLayout->layout.layoutElement == self); - ASDisplayNodeAssertTrue(displayNodeLayout->layout.size.width >= 0.0); - ASDisplayNodeAssertTrue(displayNodeLayout->layout.size.height >= 0.0); - - _calculatedDisplayNodeLayout = displayNodeLayout; -} - -- (CGSize)calculatedSize -{ - ASDN::MutexLocker l(__instanceLock__); - if (_pendingDisplayNodeLayout != nullptr) { - return _pendingDisplayNodeLayout->layout.size; - } - return _calculatedDisplayNodeLayout->layout.size; -} - -- (ASSizeRange)constrainedSizeForCalculatedLayout -{ - ASDN::MutexLocker l(__instanceLock__); - if (_pendingDisplayNodeLayout != nullptr) { - return _pendingDisplayNodeLayout->constrainedSize; - } - return _calculatedDisplayNodeLayout->constrainedSize; -} - -/** - * @abstract Informs the root node that the intrinsic size of the receiver is no longer valid. - * - * @discussion The size of a root node is determined by each subnode. Calling invalidateSize will let the root node know - * that the intrinsic size of the receiver node is no longer valid and a resizing of the root node needs to happen. - */ -- (void)_setNeedsLayoutFromAbove -{ - ASDisplayNodeAssertThreadAffinity(self); - - // Mark the node for layout in the next layout pass - [self setNeedsLayout]; - - __instanceLock__.lock(); - // Escalate to the root; entire tree must allow adjustments so the layout fits the new child. - // Much of the layout will be re-used as cached (e.g. other items in an unconstrained stack) - ASDisplayNode *supernode = _supernode; - __instanceLock__.unlock(); - - if (supernode) { - // Threading model requires that we unlock before calling a method on our parent. - [supernode _setNeedsLayoutFromAbove]; - } else { - // Let the root node method know that the size was invalidated - [self _rootNodeDidInvalidateSize]; - } -} - -- (void)_rootNodeDidInvalidateSize -{ - ASDisplayNodeAssertThreadAffinity(self); - ASDisplayNodeAssertLockUnownedByCurrentThread(__instanceLock__); - - __instanceLock__.lock(); - - // We are the root node and need to re-flow the layout; at least one child needs a new size. - CGSize boundsSizeForLayout = ASCeilSizeValues(self.bounds.size); - - // Figure out constrainedSize to use - ASSizeRange constrainedSize = ASSizeRangeMake(boundsSizeForLayout); - if (_pendingDisplayNodeLayout != nullptr) { - constrainedSize = _pendingDisplayNodeLayout->constrainedSize; - } else if (_calculatedDisplayNodeLayout->layout != nil) { - constrainedSize = _calculatedDisplayNodeLayout->constrainedSize; - } - - __instanceLock__.unlock(); - - // Perform a measurement pass to get the full tree layout, adapting to the child's new size. - ASLayout *layout = [self layoutThatFits:constrainedSize]; - - // Check if the returned layout has a different size than our current bounds. - if (CGSizeEqualToSize(boundsSizeForLayout, layout.size) == NO) { - // If so, inform our container we need an update (e.g Table, Collection, ViewController, etc). - [self displayNodeDidInvalidateSizeNewSize:layout.size]; - } -} - -- (void)displayNodeDidInvalidateSizeNewSize:(CGSize)size -{ - ASDisplayNodeAssertThreadAffinity(self); - ASDisplayNodeAssertLockUnownedByCurrentThread(__instanceLock__); - - // The default implementation of display node changes the size of itself to the new size - CGRect oldBounds = self.bounds; - CGSize oldSize = oldBounds.size; - CGSize newSize = size; - - if (! CGSizeEqualToSize(oldSize, newSize)) { - self.bounds = (CGRect){ oldBounds.origin, newSize }; - - // Frame's origin must be preserved. Since it is computed from bounds size, anchorPoint - // and position (see frame setter in ASDisplayNode+UIViewBridge), position needs to be adjusted. - CGPoint anchorPoint = self.anchorPoint; - CGPoint oldPosition = self.position; - CGFloat xDelta = (newSize.width - oldSize.width) * anchorPoint.x; - CGFloat yDelta = (newSize.height - oldSize.height) * anchorPoint.y; - self.position = CGPointMake(oldPosition.x + xDelta, oldPosition.y + yDelta); - } -} - - (void)layout { ASDisplayNodeAssertMainThread(); // Subclass hook } -- (void)_layoutSublayouts -{ - ASDisplayNodeAssertThreadAffinity(self); - ASDisplayNodeAssertLockUnownedByCurrentThread(__instanceLock__); - - ASLayout *layout; - { - ASDN::MutexLocker l(__instanceLock__); - if (_calculatedDisplayNodeLayout->isDirty()) { - return; - } - layout = _calculatedDisplayNodeLayout->layout; - } - - for (ASDisplayNode *node in self.subnodes) { - CGRect frame = [layout frameForElement:node]; - if (CGRectIsNull(frame)) { - // There is no frame for this node in our layout. - // This currently can happen if we get a CA layout pass - // while waiting for the client to run animateLayoutTransition: - } else { - node.frame = frame; - } - } -} - -#pragma mark Automatically Manages Subnodes - -- (BOOL)automaticallyManagesSubnodes -{ - ASDN::MutexLocker l(__instanceLock__); - return _automaticallyManagesSubnodes; -} - -- (void)setAutomaticallyManagesSubnodes:(BOOL)automaticallyManagesSubnodes -{ - ASDN::MutexLocker l(__instanceLock__); - _automaticallyManagesSubnodes = automaticallyManagesSubnodes; -} - #pragma mark Layout Transition -- (void)transitionLayoutWithAnimation:(BOOL)animated - shouldMeasureAsync:(BOOL)shouldMeasureAsync - measurementCompletion:(void(^)())completion -{ - ASDisplayNodeAssertMainThread(); - - [self setNeedsLayout]; - - [self transitionLayoutWithSizeRange:[self _locked_constrainedSizeForLayoutPass] - animated:animated - shouldMeasureAsync:shouldMeasureAsync - measurementCompletion:completion]; - -} - -- (void)transitionLayoutWithSizeRange:(ASSizeRange)constrainedSize - animated:(BOOL)animated - shouldMeasureAsync:(BOOL)shouldMeasureAsync - measurementCompletion:(void(^)())completion -{ - ASDisplayNodeAssertMainThread(); - - if (constrainedSize.max.width <= 0.0 || constrainedSize.max.height <= 0.0) { - // Using CGSizeZero for the sizeRange can cause negative values in client layout code. - // Most likely called transitionLayout: without providing a size, before first layout pass. - return; - } - - // Check if we are a subnode in a layout transition. - // In this case no measurement is needed as we're part of the layout transition. - if ([self _isLayoutTransitionInvalid]) { - return; - } - - { - ASDN::MutexLocker l(__instanceLock__); - ASDisplayNodeAssert(ASHierarchyStateIncludesLayoutPending(_hierarchyState) == NO, @"Can't start a transition when one of the supernodes is performing one."); - } - - // Every new layout transition has a transition id associated to check in subsequent transitions for cancelling - int32_t transitionID = [self _startNewTransition]; - - // Move all subnodes in layout pending state for this transition - ASDisplayNodePerformBlockOnEverySubnode(self, NO, ^(ASDisplayNode * _Nonnull node) { - ASDisplayNodeAssert([node _isTransitionInProgress] == NO, @"Can't start a transition when one of the subnodes is performing one."); - node.hierarchyState |= ASHierarchyStateLayoutPending; - node.pendingTransitionID = transitionID; - }); - - // Transition block that executes the layout transition - void (^transitionBlock)(void) = ^{ - if ([self _shouldAbortTransitionWithID:transitionID]) { - return; - } - - // Perform a full layout creation pass with passed in constrained size to create the new layout for the transition - ASLayout *newLayout; - { - ASDN::MutexLocker l(__instanceLock__); - - ASLayoutElementSetCurrentContext(ASLayoutElementContextMake(transitionID)); - - BOOL automaticallyManagesSubnodesDisabled = (self.automaticallyManagesSubnodes == NO); - self.automaticallyManagesSubnodes = YES; // Temporary flag for 1.9.x - newLayout = [self calculateLayoutThatFits:constrainedSize - restrictedToSize:self.style.size - relativeToParentSize:constrainedSize.max]; - if (automaticallyManagesSubnodesDisabled) { - self.automaticallyManagesSubnodes = NO; // Temporary flag for 1.9.x - } - - ASLayoutElementClearCurrentContext(); - } - - if ([self _shouldAbortTransitionWithID:transitionID]) { - return; - } - - ASPerformBlockOnMainThread(^{ - ASLayoutTransition *pendingLayoutTransition; - _ASTransitionContext *pendingLayoutTransitionContext; - { - // Grab __instanceLock__ here to make sure this transition isn't invalidated - // right after it passed the validation test and before it proceeds - ASDN::MutexLocker l(__instanceLock__); - - if ([self _locked_shouldAbortTransitionWithID:transitionID]) { - return; - } - - // Update calculated layout - auto previousLayout = _calculatedDisplayNodeLayout; - auto pendingLayout = std::make_shared( - newLayout, - constrainedSize, - constrainedSize.max - ); - [self _locked_setCalculatedDisplayNodeLayout:pendingLayout]; - - // Setup pending layout transition for animation - _pendingLayoutTransition = pendingLayoutTransition = [[ASLayoutTransition alloc] initWithNode:self - pendingLayout:pendingLayout - previousLayout:previousLayout]; - // Setup context for pending layout transition. we need to hold a strong reference to the context - _pendingLayoutTransitionContext = pendingLayoutTransitionContext = [[_ASTransitionContext alloc] initWithAnimation:animated - layoutDelegate:_pendingLayoutTransition - completionDelegate:self]; - } - - // Apply complete layout transitions for all subnodes - ASDisplayNodePerformBlockOnEverySubnode(self, NO, ^(ASDisplayNode * _Nonnull node) { - [node _completePendingLayoutTransition]; - node.hierarchyState &= (~ASHierarchyStateLayoutPending); - }); - - // Measurement pass completion - // Give the subclass a change to hook into before calling the completion block - [self _layoutTransitionMeasurementDidFinish]; - if (completion) { - completion(); - } - - // Apply the subnode insertion immediately to be able to animate the nodes - [pendingLayoutTransition applySubnodeInsertions]; - - // Kick off animating the layout transition - [self animateLayoutTransition:pendingLayoutTransitionContext]; - - // Mark transaction as finished - [self _finishOrCancelTransition]; - }); - }; - - // Start transition based on flag on current or background thread - if (shouldMeasureAsync) { - ASPerformBlockOnBackgroundThread(transitionBlock); - } else { - transitionBlock(); - } -} - -- (void)cancelLayoutTransition -{ - __instanceLock__.lock(); - BOOL transitionInProgress = _transitionInProgress; - __instanceLock__.unlock(); - - if (transitionInProgress) { - // Cancel transition in progress - [self _finishOrCancelTransition]; - - // Tell subnodes to exit layout pending state and clear related properties - ASDisplayNodePerformBlockOnEverySubnode(self, NO, ^(ASDisplayNode * _Nonnull node) { - node.hierarchyState &= (~ASHierarchyStateLayoutPending); - }); - } -} - -- (BOOL)_isTransitionInProgress -{ - ASDN::MutexLocker l(__instanceLock__); - return _transitionInProgress; -} - -- (BOOL)_isLayoutTransitionInvalid -{ - ASDN::MutexLocker l(__instanceLock__); - if (ASHierarchyStateIncludesLayoutPending(_hierarchyState)) { - ASLayoutElementContext context = ASLayoutElementGetCurrentContext(); - if (ASLayoutElementContextIsNull(context) || _pendingTransitionID != context.transitionID) { - return YES; - } - } - return NO; -} - -/// Starts a new transition and returns the transition id -- (int32_t)_startNewTransition -{ - ASDN::MutexLocker l(__instanceLock__); - _transitionInProgress = YES; - _transitionID = OSAtomicAdd32(1, &_transitionID); - return _transitionID; -} - - (void)_layoutTransitionMeasurementDidFinish { - // No-Op in ASDisplayNode -} - -- (void)_finishOrCancelTransition -{ - ASDN::MutexLocker l(__instanceLock__); - _transitionInProgress = NO; -} - -- (void)setPendingTransitionID:(int32_t)pendingTransitionID -{ - ASDN::MutexLocker l(__instanceLock__); - ASDisplayNodeAssertTrue(_pendingTransitionID < pendingTransitionID); - _pendingTransitionID = pendingTransitionID; -} - -- (int32_t)pendingTransitionID -{ - ASDN::MutexLocker l(__instanceLock__); - return _pendingTransitionID; -} - -- (BOOL)_shouldAbortTransitionWithID:(int32_t)transitionID -{ - ASDN::MutexLocker l(__instanceLock__); - return [self _locked_shouldAbortTransitionWithID:transitionID]; -} - -- (BOOL)_locked_shouldAbortTransitionWithID:(int32_t)transitionID -{ - return (!_transitionInProgress || _transitionID != transitionID); -} - -- (void)setDefaultLayoutTransitionDuration:(NSTimeInterval)defaultLayoutTransitionDuration -{ - ASDN::MutexLocker l(__instanceLock__); - _defaultLayoutTransitionDuration = defaultLayoutTransitionDuration; -} - -- (NSTimeInterval)defaultLayoutTransitionDuration -{ - ASDN::MutexLocker l(__instanceLock__); - return _defaultLayoutTransitionDuration; -} - -- (void)setDefaultLayoutTransitionDelay:(NSTimeInterval)defaultLayoutTransitionDelay -{ - ASDN::MutexLocker l(__instanceLock__); - _defaultLayoutTransitionDelay = defaultLayoutTransitionDelay; -} - -- (NSTimeInterval)defaultLayoutTransitionDelay -{ - ASDN::MutexLocker l(__instanceLock__); - return _defaultLayoutTransitionDelay; -} - -- (void)setDefaultLayoutTransitionOptions:(UIViewAnimationOptions)defaultLayoutTransitionOptions -{ - ASDN::MutexLocker l(__instanceLock__); - _defaultLayoutTransitionOptions = defaultLayoutTransitionOptions; -} - -- (UIViewAnimationOptions)defaultLayoutTransitionOptions -{ - ASDN::MutexLocker l(__instanceLock__); - return _defaultLayoutTransitionOptions; -} - -#pragma mark - -/* - * Hook for subclasses to perform an animation based on the given ASContextTransitioning. By default a fade in and out - * animation is provided. - */ -- (void)animateLayoutTransition:(id)context -{ - if ([context isAnimated] == NO) { - [self _layoutSublayouts]; - [context completeTransition:YES]; - return; - } - - ASDisplayNode *node = self; - - NSAssert(node.isNodeLoaded == YES, @"Invalid node state"); - - NSArray *removedSubnodes = [context removedSubnodes]; - NSMutableArray *insertedSubnodes = [[context insertedSubnodes] mutableCopy]; - NSMutableArray *movedSubnodes = [NSMutableArray array]; - - NSMutableArray<_ASAnimatedTransitionContext *> *insertedSubnodeContexts = [NSMutableArray array]; - NSMutableArray<_ASAnimatedTransitionContext *> *removedSubnodeContexts = [NSMutableArray array]; - - for (ASDisplayNode *subnode in [context subnodesForKey:ASTransitionContextToLayoutKey]) { - if ([insertedSubnodes containsObject:subnode] == NO) { - // This is an existing subnode, check if it is resized, moved or both - CGRect fromFrame = [context initialFrameForNode:subnode]; - CGRect toFrame = [context finalFrameForNode:subnode]; - if (CGSizeEqualToSize(fromFrame.size, toFrame.size) == NO) { - [insertedSubnodes addObject:subnode]; - } - if (CGPointEqualToPoint(fromFrame.origin, toFrame.origin) == NO) { - [movedSubnodes addObject:subnode]; - } - } - } - - // Create contexts for inserted and removed subnodes - for (ASDisplayNode *insertedSubnode in insertedSubnodes) { - [insertedSubnodeContexts addObject:[_ASAnimatedTransitionContext contextForNode:insertedSubnode alpha:insertedSubnode.alpha]]; - } - for (ASDisplayNode *removedSubnode in removedSubnodes) { - [removedSubnodeContexts addObject:[_ASAnimatedTransitionContext contextForNode:removedSubnode alpha:removedSubnode.alpha]]; - } - - // Fade out inserted subnodes - for (ASDisplayNode *insertedSubnode in insertedSubnodes) { - insertedSubnode.frame = [context finalFrameForNode:insertedSubnode]; - insertedSubnode.alpha = 0; - } - - // Adjust groupOpacity for animation - BOOL originAllowsGroupOpacity = node.allowsGroupOpacity; - node.allowsGroupOpacity = YES; - - [UIView animateWithDuration:self.defaultLayoutTransitionDuration delay:self.defaultLayoutTransitionDelay options:self.defaultLayoutTransitionOptions animations:^{ - // Fade removed subnodes and views out - for (ASDisplayNode *removedSubnode in removedSubnodes) { - removedSubnode.alpha = 0; - } - - // Fade inserted subnodes in - for (_ASAnimatedTransitionContext *insertedSubnodeContext in insertedSubnodeContexts) { - insertedSubnodeContext.node.alpha = insertedSubnodeContext.alpha; - } - - // Update frame of self and moved subnodes - CGSize fromSize = [context layoutForKey:ASTransitionContextFromLayoutKey].size; - CGSize toSize = [context layoutForKey:ASTransitionContextToLayoutKey].size; - BOOL isResized = (CGSizeEqualToSize(fromSize, toSize) == NO); - if (isResized == YES) { - CGPoint position = node.frame.origin; - node.frame = CGRectMake(position.x, position.y, toSize.width, toSize.height); - } - for (ASDisplayNode *movedSubnode in movedSubnodes) { - movedSubnode.frame = [context finalFrameForNode:movedSubnode]; - } - } completion:^(BOOL finished) { - // Restore all removed subnode alpha values - for (_ASAnimatedTransitionContext *removedSubnodeContext in removedSubnodeContexts) { - removedSubnodeContext.node.alpha = removedSubnodeContext.alpha; - } - - // Restore group opacity - node.allowsGroupOpacity = originAllowsGroupOpacity; - - // Subnode removals are automatically performed - [context completeTransition:finished]; - }]; -} - -/** - * 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 -{ - __instanceLock__.lock(); - ASLayoutTransition *pendingLayoutTransition = _pendingLayoutTransition; - __instanceLock__.unlock(); - - [pendingLayoutTransition applySubnodeRemovals]; + // Hook for subclasses - No-Op in ASDisplayNode } #pragma mark <_ASTransitionContextCompletionDelegate> @@ -1750,86 +1110,9 @@ static ASDisplayNodeMethodOverrides GetASDisplayNodeMethodOverrides(Class c) [self _pendingLayoutTransitionDidComplete]; } -/** - * Completes the pending layout transition immediately without going through the the Layout Transition Animation API - */ -- (void)_completePendingLayoutTransition -{ - __instanceLock__.lock(); - ASLayoutTransition *pendingLayoutTransition = _pendingLayoutTransition; - __instanceLock__.unlock(); - - if (pendingLayoutTransition != nil) { - [self _setCalculatedDisplayNodeLayout: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 nodes that are not have automatic subnode management enabled - if (layoutTransition == nil || self.automaticallyManagesSubnodes == 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 -{ - // Subclass hook - [self calculatedLayoutDidChange]; - - // Grab lock after calling out to subclass - ASDN::MutexLocker l(__instanceLock__); - - // 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 && !_placeholderImage && [self _locked_displaysAsynchronously]) { - - // Zero-sized nodes do not require a placeholder. - ASLayout *layout = _calculatedDisplayNodeLayout->layout; - CGSize layoutSize = (layout ? layout.size : CGSizeZero); - if (layoutSize.width * layoutSize.height <= 0.0) { - return; - } - - // If we've displayed our contents, we don't need a placeholder. - // Contents is a thread-affined property and can't be read off main after loading. - if (self.isNodeLoaded) { - ASPerformBlockOnMainThread(^{ - if (self.contents == nil) { - _placeholderImage = [self placeholderImage]; - } - }); - } else { - if (self.contents == nil) { - _placeholderImage = [self placeholderImage]; - } - } - } - - // Cleanup pending layout transition - _pendingLayoutTransition = nil; -} - - (void)calculatedLayoutDidChange { - // subclass override + // Subclass override } #pragma mark - Display @@ -4000,127 +3283,6 @@ ASDISPLAYNODE_INLINE BOOL subtreeIsRasterized(ASDisplayNode *node) { @end -#pragma mark - ASDisplayNode (ASLayoutElement) - -@implementation ASDisplayNode (ASLayoutElement) - -#pragma mark - -- (ASLayoutElementStyle *)style -{ - ASDN::MutexLocker l(__instanceLock__); - if (_style == nil) { - _style = [[ASLayoutElementStyle alloc] init]; - } - return _style; -} - -- (ASLayoutElementType)layoutElementType -{ - return ASLayoutElementTypeDisplayNode; -} - -- (NSArray> *)sublayoutElements -{ - return self.subnodes; -} - -#pragma mark Measurement Pass - -- (ASLayout *)layoutThatFits:(ASSizeRange)constrainedSize -{ -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Wdeprecated-declarations" - // For now we just call the deprecated measureWithSizeRange: method to not break old API - return [self measureWithSizeRange:constrainedSize]; -#pragma clang diagnostic pop -} - -- (ASLayout *)measureWithSizeRange:(ASSizeRange)constrainedSize -{ - return [self layoutThatFits:constrainedSize parentSize:constrainedSize.max]; -} - -- (ASLayout *)layoutThatFits:(ASSizeRange)constrainedSize parentSize:(CGSize)parentSize -{ - ASDN::MutexLocker l(__instanceLock__); - - // If one or multiple layout transitions are in flight it still can happen that layout information is requested - // on other threads. As the pending and calculated layout to be updated in the layout transition in here just a - // layout calculation wil be performed without side effect - if ([self _isLayoutTransitionInvalid]) { - return [self calculateLayoutThatFits:constrainedSize restrictedToSize:self.style.size relativeToParentSize:parentSize]; - } - - if (_calculatedDisplayNodeLayout->isValidForConstrainedSizeParentSize(constrainedSize, parentSize)) { - ASDisplayNodeAssertNotNil(_calculatedDisplayNodeLayout->layout, @"-[ASDisplayNode layoutThatFits:parentSize:] _calculatedDisplayNodeLayout->layout should not be nil! %@", self); - // Our calculated layout is suitable for this constrainedSize, so keep using it and - // invalidate any pending layout that has been generated in the past. - _pendingDisplayNodeLayout = nullptr; - return _calculatedDisplayNodeLayout->layout ?: [ASLayout layoutWithLayoutElement:self size:{0, 0}]; - } - - // Create a pending display node layout for the layout pass - _pendingDisplayNodeLayout = std::make_shared( - [self calculateLayoutThatFits:constrainedSize restrictedToSize:self.style.size relativeToParentSize:parentSize], - constrainedSize, - parentSize - ); - - ASDisplayNodeAssertNotNil(_pendingDisplayNodeLayout->layout, @"-[ASDisplayNode layoutThatFits:parentSize:] _pendingDisplayNodeLayout->layout should not be nil! %@", self); - return _pendingDisplayNodeLayout->layout ?: [ASLayout layoutWithLayoutElement:self size:{0, 0}]; -} - -#pragma mark ASLayoutElementStyleExtensibility - -ASLayoutElementStyleExtensibilityForwarding - -#pragma mark ASPrimitiveTraitCollection - -- (ASPrimitiveTraitCollection)primitiveTraitCollection -{ - ASDN::MutexLocker l(__instanceLock__); - return _primitiveTraitCollection; -} - -- (void)setPrimitiveTraitCollection:(ASPrimitiveTraitCollection)traitCollection -{ - __instanceLock__.lock(); - if (ASPrimitiveTraitCollectionIsEqualToASPrimitiveTraitCollection(traitCollection, _primitiveTraitCollection) == NO) { - _primitiveTraitCollection = traitCollection; - ASDisplayNodeLogEvent(self, @"asyncTraitCollectionDidChange: %@", NSStringFromASPrimitiveTraitCollection(traitCollection)); - __instanceLock__.unlock(); - - [self asyncTraitCollectionDidChange]; - return; - } - - __instanceLock__.unlock(); -} - -- (ASTraitCollection *)asyncTraitCollection -{ - ASDN::MutexLocker l(__instanceLock__); - return [ASTraitCollection traitCollectionWithASPrimitiveTraitCollection:self.primitiveTraitCollection]; -} - -ASPrimitiveTraitCollectionDeprecatedImplementation - -@end - - -#pragma mark - ASDisplayNode (ASLayoutElementStylability) - -@implementation ASDisplayNode (ASLayoutElementStylability) - -- (instancetype)styledWithBlock:(AS_NOESCAPE void (^)(__kindof ASLayoutElementStyle *style))styleBlock -{ - styleBlock(self.style); - return self; -} - -@end - #pragma mark - ASDisplayNode (Debugging) @implementation ASDisplayNode (Debugging) @@ -4139,22 +3301,6 @@ ASPrimitiveTraitCollectionDeprecatedImplementation return subtree; } -#pragma mark - ASLayoutElementAsciiArtProtocol - -- (NSString *)asciiArtString -{ - return [ASLayoutSpec asciiArtStringForChildren:@[] parentName:[self asciiArtName]]; -} - -- (NSString *)asciiArtName -{ - NSString *string = NSStringFromClass([self class]); - if (_debugName) { - string = [string stringByAppendingString:[NSString stringWithFormat:@"\"%@\"",_debugName]]; - } - return string; -} - @end #pragma mark - ASDisplayNode UIKit / CA Categories diff --git a/Source/Private/ASDisplayNode+FrameworkPrivate.h b/Source/Private/ASDisplayNode+FrameworkPrivate.h index 11b6888415..080557534b 100644 --- a/Source/Private/ASDisplayNode+FrameworkPrivate.h +++ b/Source/Private/ASDisplayNode+FrameworkPrivate.h @@ -163,6 +163,8 @@ __unused static NSString * _Nonnull NSStringFromASHierarchyState(ASHierarchyStat */ - (BOOL)supportsRangeManagedInterfaceState; +- (BOOL)_locked_displaysAsynchronously; + // The two methods below will eventually be exposed, but their names are subject to change. /** * @abstract Ensure that all rendering is complete for this node and its descendants. @@ -224,6 +226,11 @@ __unused static NSString * _Nonnull NSStringFromASHierarchyState(ASHierarchyStat */ - (BOOL)shouldScheduleDisplayWithNewInterfaceState:(ASInterfaceState)newInterfaceState; +@end + + +@interface ASDisplayNode (ASsLayoutInternal) + /** * @abstract Informs the root node that the intrinsic size of the receiver is no longer valid. * @@ -239,11 +246,47 @@ __unused static NSString * _Nonnull NSStringFromASHierarchyState(ASHierarchyStat - (void)_rootNodeDidInvalidateSize; /** - * @abstract Subclass hook for nodes that are acting as root nodes. This method is called after measurement - * finished in a layout transition but before the measurement completion handler is called + * This method will confirm that the layout is up to date (and update if needed). + * Importantly, it will also APPLY the layout to all of our subnodes if (unless parent is transitioning). + */ +- (void)_locked_measureNodeWithBoundsIfNecessary:(CGRect)bounds; + +/** + * Layout all of the subnodes based on the sublayouts + */ +- (void)_layoutSublayouts; + +@end + +@interface ASDisplayNode (ASLayoutTransitionInternal) + +/** + * Sentinel of the current layout transition + */ +@property (atomic, assign) int32_t pendingTransitionID; + +/** + * If one or multiple layout transitions are in flight this methods returns if the current layout transition that + * happens in in this particular thread was invalidated through another thread is starting a transition for this node + */ +- (BOOL)_isLayoutTransitionInvalid; + +/** + * Internal method that can be overriden by subclasses to add specific behavior after the measurement of a layout + * transition did finish. */ - (void)_layoutTransitionMeasurementDidFinish; +/** + * Informs the node that hte pending layout transition did complete + */ +- (void)_completePendingLayoutTransition; + +/** + * Called if the pending layout transition did complete + */ +- (void)_pendingLayoutTransitionDidComplete; + @end @interface UIView (ASDisplayNodeInternal) diff --git a/Source/Private/ASDisplayNodeInternal.h b/Source/Private/ASDisplayNodeInternal.h index dfa5b48c09..488f2513c1 100644 --- a/Source/Private/ASDisplayNodeInternal.h +++ b/Source/Private/ASDisplayNodeInternal.h @@ -73,7 +73,7 @@ FOUNDATION_EXPORT NSString * const ASRenderingEngineDidDisplayNodesScheduledBefo #define TIME_DISPLAYNODE_OPS 0 // If you're using this information frequently, try: (DEBUG || PROFILE) -@interface ASDisplayNode () +@interface ASDisplayNode () <_ASTransitionContextCompletionDelegate> { @package _ASPendingState *_pendingViewState; @@ -133,6 +133,7 @@ FOUNDATION_EXPORT NSString * const ASRenderingEngineDidDisplayNodesScheduledBefo // This is the desired contentsScale, not the scale at which the layer's contents should be displayed CGFloat _contentsScaleForDisplay; + ASDisplayNodeMethodOverrides _methodOverrides; UIEdgeInsets _hitTestSlop; @@ -146,6 +147,8 @@ FOUNDATION_EXPORT NSString * const ASRenderingEngineDidDisplayNodesScheduledBefo NSTimeInterval _defaultLayoutTransitionDuration; NSTimeInterval _defaultLayoutTransitionDelay; UIViewAnimationOptions _defaultLayoutTransitionOptions; + + ASLayoutSpecBlock _layoutSpecBlock; int32_t _transitionID; BOOL _transitionInProgress; @@ -162,6 +165,7 @@ FOUNDATION_EXPORT NSString * const ASRenderingEngineDidDisplayNodesScheduledBefo Class _layerClass; // nil -> _ASDisplayLayer UIImage *_placeholderImage; + BOOL _placeholderEnabled; CALayer *_placeholderLayer; // keeps track of nodes/subnodes that have not finished display, used with placeholders @@ -200,6 +204,8 @@ FOUNDATION_EXPORT NSString * const ASRenderingEngineDidDisplayNodesScheduledBefo NSMutableArray *_yogaChildren; ASLayout *_yogaCalculatedLayout; #endif + + NSString *_debugName; #if TIME_DISPLAYNODE_OPS @public