Merge pull request #2597 from maicki/CoalAnotherApproach

[Layout] Upgrade and optimize the behavior of the layout system; coalesce -setNeedsLayout calls.
This commit is contained in:
appleguy
2016-11-19 15:27:42 -08:00
committed by GitHub
27 changed files with 419 additions and 235 deletions

View File

@@ -527,6 +527,7 @@
- (void)layout
{
[super layout];
_backgroundImageNode.hidden = (_backgroundImageNode.image == nil);
_imageNode.hidden = (_imageNode.image == nil);
_titleNode.hidden = (_titleNode.attributedText.length == 0);

View File

@@ -58,6 +58,7 @@ static NSMutableSet *__cellClassesForVisibilityNotifications = nil; // See +init
// Use UITableViewCell defaults
_selectionStyle = UITableViewCellSelectionStyleDefault;
self.clipsToBounds = YES;
return self;
}
@@ -117,15 +118,13 @@ static NSMutableSet *__cellClassesForVisibilityNotifications = nil; // See +init
_viewControllerNode.frame = self.bounds;
}
- (void)__setNeedsLayout
- (void)_locked_displayNodeDidInvalidateSizeNewSize:(CGSize)newSize
{
CGSize oldSize = self.calculatedSize;
[super __setNeedsLayout];
//Adding this lock because lock used to be held when this method was called. Not sure if it's necessary for
//didRelayoutFromOldSize:toNewSize:
ASDN::MutexLocker l(__instanceLock__);
[self didRelayoutFromOldSize:oldSize toNewSize:self.calculatedSize];
CGSize oldSize = self.bounds.size;
if (CGSizeEqualToSize(oldSize, newSize) == NO) {
self.frame = {self.frame.origin, newSize};
[self didRelayoutFromOldSize:oldSize toNewSize:newSize];
}
}
- (void)transitionLayoutWithAnimation:(BOOL)animated

View File

@@ -637,11 +637,11 @@ extern NSInteger const ASDefaultDrawingPriority;
/**
* Marks the node as needing layout. Convenience for use whether the view / layer is loaded or not. Safe to call from a background thread.
*
* If this node was measured, calling this method triggers an internal relayout: the calculated layout is invalidated,
* and the supernode is notified or (if this node is the root one) a full measurement pass is executed using the old constrained size.
*
* Note: ASCellNode has special behavior in that calling this method will automatically notify
* If the node determines its own desired layout size will change in the next layout pass, it will propagate this
* information up the tree so its parents can have a chance to consider and apply if necessary the new size onto the node.
*
* Note: ASCellNode has special behavior in that calling this method will automatically notify
* the containing ASTableView / ASCollectionView that the cell should be resized, if necessary.
*/
- (void)setNeedsLayout;
@@ -796,7 +796,7 @@ extern NSInteger const ASDefaultDrawingPriority;
/**
* @abstract Invalidates the current layout and begins a relayout of the node with the current `constrainedSize`. Must be called on main thread.
* @abstract Invalidates the layout and begins a relayout of the node with the current `constrainedSize`. Must be called on main thread.
*
* @discussion It is called right after the measurement and before -animateLayoutTransition:.
*

View File

@@ -315,6 +315,7 @@ static ASDisplayNodeMethodOverrides GetASDisplayNodeMethodOverrides(Class c)
_environmentState = ASEnvironmentStateMakeDefault();
_calculatedDisplayNodeLayout = std::make_shared<ASDisplayNodeLayout>();
_pendingDisplayNodeLayout = nullptr;
_defaultLayoutTransitionDuration = 0.2;
_defaultLayoutTransitionDelay = 0.0;
@@ -703,6 +704,70 @@ static ASDisplayNodeMethodOverrides GetASDisplayNodeMethodOverrides(Class c)
#pragma mark - Layout
- (void)setNeedsLayoutFromAbove
{
ASDisplayNodeAssertThreadAffinity(self);
__instanceLock__.lock();
// Mark the node for layout in the next layout pass
[self setNeedsLayout];
// 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;
if (supernode) {
// Threading model requires that we unlock before calling a method on our parent.
__instanceLock__.unlock();
[supernode setNeedsLayoutFromAbove];
return;
}
// 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;
}
// 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 _locked_displayNodeDidInvalidateSizeNewSize:layout.size];
}
__instanceLock__.unlock();
}
- (void)_locked_displayNodeDidInvalidateSizeNewSize:(CGSize)size
{
ASDisplayNodeAssertThreadAffinity(self);
// 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);
}
}
- (ASLayout *)layoutThatFits:(ASSizeRange)constrainedSize
{
#pragma clang diagnostic push
@@ -714,75 +779,27 @@ static ASDisplayNodeMethodOverrides GetASDisplayNodeMethodOverrides(Class c)
- (ASLayout *)layoutThatFits:(ASSizeRange)constrainedSize parentSize:(CGSize)parentSize
{
ASDN::MutexLocker l(__instanceLock__);
if ([self shouldCalculateLayoutWithConstrainedSize:constrainedSize parentSize:parentSize] == NO) {
ASDisplayNodeAssertNotNil(_calculatedDisplayNodeLayout->layout, @"-[ASDisplayNode layoutThatFits:parentSize:] _layout should not be nil! %@", self);
return _calculatedDisplayNodeLayout->layout ? : [ASLayout layoutWithLayoutElement:self size:{0, 0}];
// If multiple layout transitions are in progress it can happen that an invalid one is still trying to do a measurement
// before it get's cancelled. In this case we should not touch any layout and return a no op layout
if ([self _isLayoutTransitionInvalid]) {
return [ASLayout layoutWithLayoutElement:self size:{0, 0}];
}
[self cancelLayoutTransition];
BOOL didCreateNewContext = NO;
BOOL didOverrideExistingContext = NO;
BOOL shouldVisualizeLayout = ASHierarchyStateIncludesVisualizeLayout(_hierarchyState);
ASLayoutElementContext context;
if (ASLayoutElementContextIsNull(ASLayoutElementGetCurrentContext())) {
context = ASLayoutElementContextMake(ASLayoutElementContextDefaultTransitionID, shouldVisualizeLayout);
ASLayoutElementSetCurrentContext(context);
didCreateNewContext = YES;
} else {
context = ASLayoutElementGetCurrentContext();
if (context.needsVisualizeNode != shouldVisualizeLayout) {
context.needsVisualizeNode = shouldVisualizeLayout;
ASLayoutElementSetCurrentContext(context);
didOverrideExistingContext = YES;
}
if (_calculatedDisplayNodeLayout->isValidForConstrainedSizeParentSize(constrainedSize, parentSize)) {
ASDisplayNodeAssertNotNil(_calculatedDisplayNodeLayout->layout, @"-[ASDisplayNode layoutThatFits:parentSize:] _calculatedDisplayNodeLayout->layout should not be nil! %@", self);
return _calculatedDisplayNodeLayout->layout ?: [ASLayout layoutWithLayoutElement:self size:{0, 0}];
}
// Prepare for layout transition
auto previousLayout = _calculatedDisplayNodeLayout;
auto pendingLayout = std::make_shared<ASDisplayNodeLayout>(
// Creat a pending display node layout for the layout pass
_pendingDisplayNodeLayout = std::make_shared<ASDisplayNodeLayout>(
[self calculateLayoutThatFits:constrainedSize restrictedToSize:self.style.size relativeToParentSize:parentSize],
constrainedSize,
parentSize
);
if (didCreateNewContext) {
ASLayoutElementClearCurrentContext();
} else if (didOverrideExistingContext) {
context.needsVisualizeNode = !context.needsVisualizeNode;
ASLayoutElementSetCurrentContext(context);
}
_pendingLayoutTransition = [[ASLayoutTransition alloc] initWithNode:self
pendingLayout:pendingLayout
previousLayout:previousLayout];
// Only complete the pending layout transition if the node is not a subnode of a node that is currently
// in a layout transition
if (ASHierarchyStateIncludesLayoutPending(_hierarchyState) == NO) {
// Complete the pending layout transition immediately
[self _completePendingLayoutTransition];
}
ASDisplayNodeAssertNotNil(pendingLayout->layout, @"-[ASDisplayNode layoutThatFits:parentSize:] newLayout should not be nil! %@", self);
return pendingLayout->layout;
}
- (BOOL)shouldCalculateLayoutWithConstrainedSize:(ASSizeRange)constrainedSize parentSize:(CGSize)parentSize
{
ASDN::MutexLocker l(__instanceLock__);
// Don't remeasure if in layout pending state and a new transition already started
if (ASHierarchyStateIncludesLayoutPending(_hierarchyState)) {
ASLayoutElementContext context = ASLayoutElementGetCurrentContext();
if (ASLayoutElementContextIsNull(context) || _pendingTransitionID != context.transitionID) {
return NO;
}
}
// Check if display node layout is still valid
return _calculatedDisplayNodeLayout->isValidForConstrainedSizeParentSize(constrainedSize, parentSize) == NO;
ASDisplayNodeAssertNotNil(_pendingDisplayNodeLayout->layout, @"-[ASDisplayNode layoutThatFits:parentSize:] _pendingDisplayNodeLayout->layout should not be nil! %@", self);
return _pendingDisplayNodeLayout->layout ?: [ASLayout layoutWithLayoutElement:self size:{0, 0}];
}
- (ASLayoutElementType)layoutElementType
@@ -815,14 +832,7 @@ static ASDisplayNodeMethodOverrides GetASDisplayNodeMethodOverrides(Class c)
shouldMeasureAsync:(BOOL)shouldMeasureAsync
measurementCompletion:(void(^)())completion
{
if (_calculatedDisplayNodeLayout->layout == nil) {
// No measure pass happened before, it's not possible to reuse the constrained size for the transition
// Using CGSizeZero for the sizeRange can cause negative values in client layout code.
return;
}
[self invalidateCalculatedLayout];
[self transitionLayoutWithSizeRange:_calculatedDisplayNodeLayout->constrainedSize
[self transitionLayoutWithSizeRange:[self _locked_constrainedSizeForLayoutPass]
animated:animated
shouldMeasureAsync:shouldMeasureAsync
measurementCompletion:completion];
@@ -834,9 +844,11 @@ static ASDisplayNodeMethodOverrides GetASDisplayNodeMethodOverrides(Class c)
shouldMeasureAsync:(BOOL)shouldMeasureAsync
measurementCompletion:(void(^)())completion
{
// Passed constrainedSize is the the same as the node's current constrained size it's a noop
ASDisplayNodeAssertMainThread();
if ([self shouldCalculateLayoutWithConstrainedSize:constrainedSize parentSize:constrainedSize.max] == NO) {
// 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;
}
@@ -845,20 +857,26 @@ static ASDisplayNodeMethodOverrides GetASDisplayNodeMethodOverrides(Class c)
ASDisplayNodeAssert(ASHierarchyStateIncludesLayoutPending(_hierarchyState) == NO, @"Can't start a transition when one of the supernodes is performing one.");
}
// Invalidate the current layout to be able to measure a new layout based onthe given constrained size
[self setNeedsLayout];
// 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 a pending state
// 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__);
@@ -891,7 +909,7 @@ static ASDisplayNodeMethodOverrides GetASDisplayNodeMethodOverrides(Class c)
return;
}
// Update display node layout
// Update calculated layout
auto previousLayout = _calculatedDisplayNodeLayout;
auto pendingLayout = std::make_shared<ASDisplayNodeLayout>(
newLayout,
@@ -930,6 +948,7 @@ static ASDisplayNodeMethodOverrides GetASDisplayNodeMethodOverrides(Class c)
});
};
// Start transition based on flag on current or background thread
if (shouldMeasureAsync) {
ASPerformBlockOnBackgroundThread(transitionBlock);
} else {
@@ -957,6 +976,18 @@ static ASDisplayNodeMethodOverrides GetASDisplayNodeMethodOverrides(Class c)
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
{
@@ -1387,51 +1418,23 @@ static ASDisplayNodeMethodOverrides GetASDisplayNodeMethodOverrides(Class c)
[self displayImmediately];
}
//Calling this with the lock held can lead to deadlocks. Always call *unlocked*
- (void)invalidateCalculatedLayout
{
ASDN::MutexLocker l(__instanceLock__);
// This will cause the next layout pass to compute a new layout instead of returning
// the cached layout in case the constrained or parent size did not change
_calculatedDisplayNodeLayout->invalidate();
if (_pendingDisplayNodeLayout != nullptr) {
_pendingDisplayNodeLayout->invalidate();
}
}
- (void)__setNeedsLayout
{
ASDisplayNodeAssertThreadAffinity(self);
__instanceLock__.lock();
if (_calculatedDisplayNodeLayout->layout == nil) {
// Can't proceed without a layout as no constrained size would be available. If not layout exists at this moment
// no measurement pass did happen just bail out for now
__instanceLock__.unlock();
return;
}
ASDN::MutexLocker l(__instanceLock__);
[self invalidateCalculatedLayout];
if (_supernode) {
ASDisplayNode *supernode = _supernode;
__instanceLock__.unlock();
// Cause supernode's layout to be invalidated
// We need to release the lock to prevent a deadlock
[supernode setNeedsLayout];
return;
}
// This is the root node. Trigger a full measurement pass on *current* thread. Old constrained size is re-used.
[self layoutThatFits:_calculatedDisplayNodeLayout->constrainedSize];
CGRect oldBounds = self.bounds;
CGSize oldSize = oldBounds.size;
CGSize newSize = _calculatedDisplayNodeLayout->layout.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);
}
__instanceLock__.unlock();
}
- (void)__setNeedsDisplay
@@ -1450,58 +1453,152 @@ static ASDisplayNodeMethodOverrides GetASDisplayNodeMethodOverrides(Class c)
{
ASDisplayNodeAssertMainThread();
ASDN::MutexLocker l(__instanceLock__);
CGRect bounds = self.bounds;
[self measureNodeWithBoundsIfNecessary:bounds];
CGRect bounds = _threadSafeBounds;
if (CGRectEqualToRect(bounds, CGRectZero)) {
// Performing layout on a zero-bounds view often results in frame calculations
// with negative sizes after applying margins, which will cause
// measureWithSizeRange: on subnodes to assert.
LOG(@"Warning: No size given for node before node was trying to layout itself: %@. Please provide a frame for the node.", self);
return;
}
// 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).
[self _locked_measureNodeWithBoundsIfNecessary:bounds];
_pendingDisplayNodeLayout = nullptr;
// Handle placeholder layer creation in case the size of the node changed after the initial placeholder layer
// was created
if ([self _shouldHavePlaceholderLayer]) {
[self _setupPlaceholderLayerIfNeeded];
}
_placeholderLayer.frame = bounds;
[self _locked_layoutPlaceholderIfNecessary];
[self layout];
[self layoutDidFinish];
}
- (void)measureNodeWithBoundsIfNecessary:(CGRect)bounds
/// Needs to be called with lock held
- (void)_locked_measureNodeWithBoundsIfNecessary:(CGRect)bounds
{
BOOL supportsRangeManagedInterfaceState = NO;
BOOL hasDirtyLayout = NO;
CGSize calculatedLayoutSize = CGSizeZero;
{
ASDN::MutexLocker l(__instanceLock__);
supportsRangeManagedInterfaceState = [self supportsRangeManagedInterfaceState];
hasDirtyLayout = _calculatedDisplayNodeLayout->isDirty();
calculatedLayoutSize = _calculatedDisplayNodeLayout->layout.size;
// 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;
}
// Check if it's a subnode in a layout transition. In this case no measurement is needed as it's part of
// the layout transition
if (ASHierarchyStateIncludesLayoutPending(_hierarchyState)) {
ASLayoutElementContext context = ASLayoutElementGetCurrentContext();
if (ASLayoutElementContextIsNull(context) || _pendingTransitionID != context.transitionID) {
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;
}
}
// If no measure pass happened or the bounds changed between layout passes we manually trigger a measurement pass
// for the node using a size range equal to whatever bounds were provided to the node
if (supportsRangeManagedInterfaceState == NO && (hasDirtyLayout || CGSizeEqualToSize(calculatedLayoutSize, bounds.size) == NO)) {
if (CGRectEqualToRect(bounds, CGRectZero)) {
LOG(@"Warning: No size given for node before node was trying to layout itself: %@. Please provide a frame for the node.", self);
} else {
[self layoutThatFits:ASSizeRangeMake(bounds.size)];
// _calculatedDisplayNodeLayout is not reusable we need to transition to a new one
[self cancelLayoutTransition];
BOOL didCreateNewContext = NO;
BOOL didOverrideExistingContext = NO;
BOOL shouldVisualizeLayout = ASHierarchyStateIncludesVisualizeLayout(_hierarchyState);
ASLayoutElementContext context = ASLayoutElementGetCurrentContext();
if (ASLayoutElementContextIsNull(context)) {
context = ASLayoutElementContextMake(ASLayoutElementContextDefaultTransitionID, shouldVisualizeLayout);
ASLayoutElementSetCurrentContext(context);
didCreateNewContext = YES;
} else {
if (context.needsVisualizeNode != shouldVisualizeLayout) {
context.needsVisualizeNode = shouldVisualizeLayout;
ASLayoutElementSetCurrentContext(context);
didOverrideExistingContext = YES;
}
}
// Figure out previous and pending layouts for layout transition
std::shared_ptr<ASDisplayNodeLayout> nextLayout = _pendingDisplayNodeLayout;
#define layoutSizeDifferentFromBounds !CGSizeEqualToSize(nextLayout->layout.size, boundsSizeForLayout)
// nextLayout was likely created by a call to layoutThatFits:, check if 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<ASDisplayNodeLayout>(layout, constrainedSize, boundsSizeForLayout);
}
if (didCreateNewContext) {
ASLayoutElementClearCurrentContext();
} else if (didOverrideExistingContext) {
context.needsVisualizeNode = !context.needsVisualizeNode;
ASLayoutElementSetCurrentContext(context);
}
// 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)_locked_layoutPlaceholderIfNecessary
{
if ([self _shouldHavePlaceholderLayer]) {
[self _setupPlaceholderLayerIfNeeded];
}
// Update the placeholderLayer size in case the node size has changed since the placeholder was added.
_placeholderLayer.frame = self.threadSafeBounds;
}
- (void)layoutDidFinish
@@ -2591,12 +2688,18 @@ void recursivelyTriggerDisplayForLayer(CALayer *layer, BOOL shouldBlock)
- (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;
}
@@ -2630,15 +2733,6 @@ void recursivelyTriggerDisplayForLayer(CALayer *layer, BOOL shouldBlock)
return nil;
}
- (void)invalidateCalculatedLayout
{
ASDN::MutexLocker l(__instanceLock__);
// This will cause the next call to -layoutThatFits:parentSize: to compute a new layout instead of returning
// the cached layout in case the constrained or parent size did not change
_calculatedDisplayNodeLayout->invalidate();
}
- (void)__didLoad
{
ASDN::MutexLocker l(__instanceLock__);
@@ -3680,7 +3774,7 @@ static const char *ASDisplayNodeAssociatedNodeKey = "ASAssociatedNode";
{
// Deprecated preferredFrameSize just calls through to set width and height
self.style.preferredSize = preferredFrameSize;
[self invalidateCalculatedLayout];
[self setNeedsLayout];
}
- (CGSize)preferredFrameSize

View File

@@ -415,7 +415,7 @@
[_textKitComponents.textStorage setAttributedString:attributedStringToDisplay];
// Calculated size depends on the seeded text.
[self invalidateCalculatedLayout];
[self setNeedsLayout];
// Update if placeholder is shown.
[self _updateDisplayingPlaceholder];

View File

@@ -204,7 +204,7 @@ struct ASImageNodeDrawParameters {
if (!ASObjectIsEqual(_image, image)) {
_image = image;
[self invalidateCalculatedLayout];
[self setNeedsLayout];
if (image) {
[self setNeedsDisplay];

View File

@@ -1523,7 +1523,7 @@ static NSString * const kCellReuseIdentifier = @"_ASTableViewCell";
// Normally the content view width equals to the constrained size width (which equals to the table view width).
// If there is a mismatch between these values, for example after the table view entered or left editing mode,
// content view width is preferred and used to re-measure the cell node.
if (contentViewWidth != constrainedSize.max.width) {
if (CGSizeEqualToSize(node.calculatedSize, CGSizeZero) == NO && contentViewWidth != constrainedSize.max.width) {
constrainedSize.min.width = contentViewWidth;
constrainedSize.max.width = contentViewWidth;

View File

@@ -317,7 +317,6 @@ static NSArray *DefaultLinkAttributeNames = @[ NSLinkAttributeName ];
BOOL needsUpdate = !UIEdgeInsetsEqualToEdgeInsets(textContainerInset, _textContainerInset);
if (needsUpdate) {
_textContainerInset = textContainerInset;
[self invalidateCalculatedLayout];
[self setNeedsLayout];
}
}
@@ -474,7 +473,7 @@ static NSArray *DefaultLinkAttributeNames = @[ NSLinkAttributeName ];
}
// Tell the display node superclasses that the cached layout is incorrect now
[self invalidateCalculatedLayout];
[self setNeedsLayout];
// Force display to create renderer with new size and redisplay with new string
[self setNeedsDisplay];
@@ -497,7 +496,7 @@ static NSArray *DefaultLinkAttributeNames = @[ NSLinkAttributeName ];
_exclusionPaths = [exclusionPaths copy];
[self _invalidateRenderer];
[self invalidateCalculatedLayout];
[self setNeedsLayout];
[self setNeedsDisplay];
}

View File

@@ -135,10 +135,11 @@ ASVisibilityDidMoveToParentViewController;
[super viewWillAppear:animated];
_ensureDisplayed = YES;
// We do this early layout because we need to get any ASCollectionNodes etc. into the
// hierarchy before UIKit applies the scroll view inset adjustments, if you are using
// automatic subnode management.
// A measure as well as layout pass is forced this early to get nodes like ASCollectionNode, ASTableNode etc.
// into the hierarchy before UIKit applies the scroll view inset adjustments, if automatic subnode management
// is enabled. Otherwise the insets would not be applied.
[_node layoutThatFits:[self nodeConstrainedSize]];
[_node.view layoutIfNeeded];
[_node recursivelyFetchData];

View File

@@ -16,6 +16,7 @@
#import "ASDisplayNode+FrameworkPrivate.h"
#import "ASDisplayNode+Subclasses.h"
#import "ASObjectDescriptionHelpers.h"
#import "ASLayout.h"
@interface _ASDisplayView ()
@property (nullable, atomic, weak, readwrite) ASDisplayNode *asyncdisplaykit_node;
@@ -202,6 +203,12 @@
#endif
}
- (CGSize)sizeThatFits:(CGSize)size
{
ASDisplayNode *node = _asyncdisplaykit_node; // Create strong reference to weak ivar.
return node ? [node layoutThatFits:ASSizeRangeMake(size)].size : [super sizeThatFits:size];
}
- (void)setNeedsDisplay
{
ASDisplayNodeAssertMainThread();

View File

@@ -175,6 +175,20 @@ __unused static NSString * _Nonnull NSStringFromASHierarchyState(ASHierarchyStat
*/
- (BOOL)shouldScheduleDisplayWithNewInterfaceState:(ASInterfaceState)newInterfaceState;
/**
* @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;
/**
* @abstract Subclass hook for nodes that are acting as root nodes. This method is called if one of the subnodes
* size is invalidated and may need to result in a different size as the current calculated size.
*/
- (void)_locked_displayNodeDidInvalidateSizeNewSize:(CGSize)newSize;
@end
@interface UIView (ASDisplayNodeInternal)

View File

@@ -137,6 +137,7 @@ FOUNDATION_EXPORT NSString * const ASRenderingEngineDidDisplayNodesScheduledBefo
int32_t _pendingTransitionID;
ASLayoutTransition *_pendingLayoutTransition;
std::shared_ptr<ASDisplayNodeLayout> _calculatedDisplayNodeLayout;
std::shared_ptr<ASDisplayNodeLayout> _pendingDisplayNodeLayout;
ASDisplayNodeViewBlock _viewBlock;
ASDisplayNodeLayerBlock _layerBlock;
@@ -188,12 +189,13 @@ FOUNDATION_EXPORT NSString * const ASRenderingEngineDidDisplayNodesScheduledBefo
+ (void)scheduleNodeForRecursiveDisplay:(ASDisplayNode *)node;
// The _ASDisplayLayer backing the node, if any.
/// The _ASDisplayLayer backing the node, if any.
@property (nonatomic, readonly, strong) _ASDisplayLayer *asyncLayer;
// Bitmask to check which methods an object overrides.
/// Bitmask to check which methods an object overrides.
@property (nonatomic, assign, readonly) ASDisplayNodeMethodOverrides methodOverrides;
/// Thread safe way to access the bounds of the node
@property (nonatomic, assign) CGRect threadSafeBounds;
@@ -201,21 +203,28 @@ FOUNDATION_EXPORT NSString * const ASRenderingEngineDidDisplayNodesScheduledBefo
- (BOOL)__shouldLoadViewOrLayer;
/**
Invoked before a call to setNeedsLayout to the underlying view
* Invoked before a call to setNeedsLayout to the underlying view
*/
- (void)__setNeedsLayout;
/**
Invoked after a call to setNeedsDisplay to the underlying view
* Invoked after a call to setNeedsDisplay to the underlying view
*/
- (void)__setNeedsDisplay;
/**
* Called from [CALayer layoutSublayers:]. Executes the layout pass for the node
*/
- (void)__layout;
/*
* Internal method to set the supernode
*/
- (void)__setSupernode:(ASDisplayNode *)supernode;
/**
Internal method to add / replace / insert subnode and remove from supernode without checking if
node has automaticallyManagesSubnodes set to YES.
* Internal method to add / replace / insert subnode and remove from supernode without checking if
* node has automaticallyManagesSubnodes set to YES.
*/
- (void)_addSubnode:(ASDisplayNode *)subnode;
- (void)_replaceSubnode:(ASDisplayNode *)oldSubnode withSubnode:(ASDisplayNode *)replacementSubnode;
@@ -230,16 +239,16 @@ FOUNDATION_EXPORT NSString * const ASRenderingEngineDidDisplayNodesScheduledBefo
- (void)__incrementVisibilityNotificationsDisabled;
- (void)__decrementVisibilityNotificationsDisabled;
// Helper method to summarize whether or not the node run through the display process
/// Helper method to summarize whether or not the node run through the display process
- (BOOL)__implementsDisplay;
// Display the node's view/layer immediately on the current thread, bypassing the background thread rendering. Will be deprecated.
/// Display the node's view/layer immediately on the current thread, bypassing the background thread rendering. Will be deprecated.
- (void)displayImmediately;
// Alternative initialiser for backing with a custom view class. Supports asynchronous display with _ASDisplayView subclasses.
/// Alternative initialiser for backing with a custom view class. Supports asynchronous display with _ASDisplayView subclasses.
- (instancetype)initWithViewClass:(Class)viewClass;
// Alternative initialiser for backing with a custom layer class. Supports asynchronous display with _ASDisplayLayer subclasses.
/// Alternative initialiser for backing with a custom layer class. Supports asynchronous display with _ASDisplayLayer subclasses.
- (instancetype)initWithLayerClass:(Class)layerClass;
@property (nonatomic, assign) CGFloat contentsScaleForDisplay;

View File

@@ -24,6 +24,7 @@ struct ASDisplayNodeLayout {
ASLayout *layout;
ASSizeRange constrainedSize;
CGSize parentSize;
BOOL requestedLayoutFromAbove;
BOOL _dirty;
/*
@@ -33,13 +34,13 @@ struct ASDisplayNodeLayout {
* @param parentSize Parent size used to create the layout
*/
ASDisplayNodeLayout(ASLayout *layout, ASSizeRange constrainedSize, CGSize parentSize)
: layout(layout), constrainedSize(constrainedSize), parentSize(parentSize), _dirty(NO) {};
: layout(layout), constrainedSize(constrainedSize), parentSize(parentSize), requestedLayoutFromAbove(NO), _dirty(NO) {};
/*
* Creates a layout without any layout associated. By default this display node layout is dirty.
*/
ASDisplayNodeLayout()
: layout(nil), constrainedSize({{0, 0}, {0, 0}}), parentSize({0, 0}), _dirty(YES) {};
: layout(nil), constrainedSize({{0, 0}, {0, 0}}), parentSize({0, 0}), requestedLayoutFromAbove(NO), _dirty(YES) {};
/**
* Returns if the display node layout is dirty as it was invalidated or it was created without a layout.

View File

@@ -31,8 +31,12 @@ void ASPerformBackgroundDeallocation(id object);
CGFloat ASScreenScale();
CGSize ASFloorSizeValues(CGSize s);
CGFloat ASFloorPixelValue(CGFloat f);
CGSize ASCeilSizeValues(CGSize s);
CGFloat ASCeilPixelValue(CGFloat f);
CGFloat ASRoundPixelValue(CGFloat f);

View File

@@ -95,12 +95,22 @@ CGFloat ASScreenScale()
return __scale;
}
CGSize ASFloorSizeValues(CGSize s)
{
return CGSizeMake(ASFloorPixelValue(s.width), ASFloorPixelValue(s.height));
}
CGFloat ASFloorPixelValue(CGFloat f)
{
CGFloat scale = ASScreenScale();
return floor(f * scale) / scale;
}
CGSize ASCeilSizeValues(CGSize s)
{
return CGSizeMake(ASCeilPixelValue(s.width), ASCeilPixelValue(s.height));
}
CGFloat ASCeilPixelValue(CGFloat f)
{
CGFloat scale = ASScreenScale();

View File

@@ -12,6 +12,7 @@
#import <XCTest/XCTest.h>
#import "ASDisplayNodeTestsHelper.h"
#import "ASDisplayNode.h"
#import "ASDisplayNode+Beta.h"
#import "ASDisplayNode+Subclasses.h"
@@ -88,7 +89,10 @@
return [ASAbsoluteLayoutSpec absoluteLayoutSpecWithChildren:@[stack1, stack2, node5]];
};
[node layoutThatFits:ASSizeRangeMake(CGSizeZero)];
ASDisplayNodeSizeToFitSizeRange(node, ASSizeRangeMake(CGSizeZero, CGSizeMake(CGFLOAT_MAX, CGFLOAT_MAX)));
[node.view layoutIfNeeded];
XCTAssertEqual(node.subnodes[0], node1);
XCTAssertEqual(node.subnodes[1], node2);
XCTAssertEqual(node.subnodes[2], node3);
@@ -122,13 +126,14 @@
}
};
[node layoutThatFits:ASSizeRangeMake(CGSizeZero)];
ASDisplayNodeSizeToFitSizeRange(node, ASSizeRangeMake(CGSizeZero, CGSizeMake(CGFLOAT_MAX, CGFLOAT_MAX)));
[node.view layoutIfNeeded];
XCTAssertEqual(node.subnodes[0], node1);
XCTAssertEqual(node.subnodes[1], node2);
node.layoutState = @2;
[node invalidateCalculatedLayout];
[node layoutThatFits:ASSizeRangeMake(CGSizeZero)];
[node setNeedsLayout]; // After a state change the layout needs to be invalidated
[node.view layoutIfNeeded]; // A new layout pass will trigger the hiearchy transition
XCTAssertEqual(node.subnodes[0], node1);
XCTAssertEqual(node.subnodes[1], node3);
@@ -170,10 +175,12 @@
- (void)testMeasurementInBackgroundThreadWithLoadedNode
{
const CGSize kNodeSize = CGSizeMake(100, 100);
ASDisplayNode *node1 = [[ASDisplayNode alloc] init];
ASDisplayNode *node2 = [[ASDisplayNode alloc] init];
ASSpecTestDisplayNode *node = [[ASSpecTestDisplayNode alloc] init];
node.style.preferredSize = kNodeSize;
node.automaticallyManagesSubnodes = YES;
node.layoutSpecBlock = ^(ASDisplayNode *weakNode, ASSizeRange constrainedSize) {
ASSpecTestDisplayNode *strongNode = (ASSpecTestDisplayNode *)weakNode;
@@ -185,23 +192,42 @@
};
// Intentionally trigger view creation
[node view];
[node2 view];
XCTestExpectation *expectation = [self expectationWithDescription:@"Fix IHM layout also if one node is already loaded"];
dispatch_async(dispatch_get_global_queue( DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[node layoutThatFits:ASSizeRangeMake(CGSizeZero)];
XCTAssertEqual(node.subnodes[0], node1);
node.layoutState = @2;
[node invalidateCalculatedLayout];
[node layoutThatFits:ASSizeRangeMake(CGSizeZero)];
// Measurement happens in the background
ASDisplayNodeSizeToFitSizeRange(node, ASSizeRangeMake(CGSizeZero, CGSizeMake(INFINITY, INFINITY)));
// Dispatch back to the main thread to let the insertion / deletion of subnodes happening
dispatch_async(dispatch_get_main_queue(), ^{
XCTAssertEqual(node.subnodes[0], node2);
[expectation fulfill];
// Layout on main
[node setNeedsLayout];
[node.view layoutIfNeeded];
XCTAssertEqual(node.subnodes[0], node1);
dispatch_async(dispatch_get_global_queue( DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
// Change state and measure in the background
node.layoutState = @2;
[node setNeedsLayout];
ASDisplayNodeSizeToFitSizeRange(node, ASSizeRangeMake(CGSizeZero, CGSizeMake(INFINITY, INFINITY)));
// Dispatch back to the main thread to let the insertion / deletion of subnodes happening
dispatch_async(dispatch_get_main_queue(), ^{
// Layout on main again
[node.view layoutIfNeeded];
XCTAssertEqual(node.subnodes[0], node2);
[expectation fulfill];
});
});
});
});
@@ -214,12 +240,13 @@
- (void)testTransitionLayoutWithAnimationWithLoadedNodes
{
const CGSize kNodeSize = CGSizeMake(100, 100);
ASDisplayNode *node1 = [[ASDisplayNode alloc] init];
ASDisplayNode *node2 = [[ASDisplayNode alloc] init];
ASSpecTestDisplayNode *node = [[ASSpecTestDisplayNode alloc] init];
node.automaticallyManagesSubnodes = YES;
node.style.preferredSize = kNodeSize;
node.layoutSpecBlock = ^(ASDisplayNode *weakNode, ASSizeRange constrainedSize) {
ASSpecTestDisplayNode *strongNode = (ASSpecTestDisplayNode *)weakNode;
if ([strongNode.layoutState isEqualToNumber:@1]) {
@@ -230,13 +257,13 @@
};
// Intentionally trigger view creation
[node view];
[node1 view];
[node2 view];
XCTestExpectation *expectation = [self expectationWithDescription:@"Fix IHM layout transition also if one node is already loaded"];
[node layoutThatFits:ASSizeRangeMake(CGSizeZero)];
ASDisplayNodeSizeToFitSizeRange(node, ASSizeRangeMake(CGSizeZero, CGSizeMake(INFINITY, INFINITY)));
[node.view layoutIfNeeded];
XCTAssertEqual(node.subnodes[0], node1);
node.layoutState = @2;

View File

@@ -28,8 +28,8 @@
node.layoutSpecBlock = ^(ASDisplayNode * _Nonnull node, ASSizeRange constrainedSize) {
return [ASInsetLayoutSpec insetLayoutSpecWithInsets:UIEdgeInsetsMake(5, 5, 5, 5) child:subnode];
};
[node layoutThatFits:ASSizeRangeMake(CGSizeZero, CGSizeMake(100, 100))];
ASDisplayNodeSizeToFitSizeRange(node, ASSizeRangeMake(CGSizeZero, CGSizeMake(INFINITY, INFINITY)));
ASSnapshotVerifyNode(node, nil);
}

View File

@@ -2166,8 +2166,10 @@ static bool stringContainsPointer(NSString *description, id p) {
// The inset spec here is crucial. If the nodes themselves are children, it passed before the fix.
return [ASOverlayLayoutSpec overlayLayoutSpecWithChild:[ASInsetLayoutSpec insetLayoutSpecWithInsets:UIEdgeInsetsZero child:underlay] overlay:overlay];
};
[node layoutThatFits:ASSizeRangeMake(CGSizeMake(100, 100))];
node.frame = (CGRect){ .size = node.calculatedSize };
ASDisplayNodeSizeToFitSize(node, CGSizeMake(100, 100));
[node.view layoutIfNeeded];
NSInteger underlayIndex = [node.subnodes indexOfObjectIdenticalTo:underlay];
NSInteger overlayIndex = [node.subnodes indexOfObjectIdenticalTo:overlay];
XCTAssertLessThan(underlayIndex, overlayIndex);
@@ -2185,8 +2187,10 @@ static bool stringContainsPointer(NSString *description, id p) {
// The inset spec here is crucial. If the nodes themselves are children, it passed before the fix.
return [ASBackgroundLayoutSpec backgroundLayoutSpecWithChild:overlay background:[ASInsetLayoutSpec insetLayoutSpecWithInsets:UIEdgeInsetsZero child:underlay]];
};
[node layoutThatFits:ASSizeRangeMake(CGSizeMake(100, 100))];
node.frame = (CGRect){ .size = node.calculatedSize };
ASDisplayNodeSizeToFitSize(node, CGSizeMake(100, 100));
[node.view layoutIfNeeded];
NSInteger underlayIndex = [node.subnodes indexOfObjectIdenticalTo:underlay];
NSInteger overlayIndex = [node.subnodes indexOfObjectIdenticalTo:overlay];
XCTAssertLessThan(underlayIndex, overlayIndex);

View File

@@ -9,7 +9,13 @@
//
#import <Foundation/Foundation.h>
#import "ASDimension.h"
@class ASDisplayNode;
typedef BOOL (^as_condition_block_t)(void);
BOOL ASDisplayNodeRunRunLoopUntilBlockIsTrue(as_condition_block_t block);
void ASDisplayNodeSizeToFitSize(ASDisplayNode *node, CGSize size);
void ASDisplayNodeSizeToFitSizeRange(ASDisplayNode *node, ASSizeRange sizeRange);

View File

@@ -9,6 +9,8 @@
//
#import "ASDisplayNodeTestsHelper.h"
#import "ASDisplayNode.h"
#import "ASLayout.h"
#import <QuartzCore/QuartzCore.h>
@@ -41,3 +43,15 @@ BOOL ASDisplayNodeRunRunLoopUntilBlockIsTrue(as_condition_block_t block)
}
return passed;
}
void ASDisplayNodeSizeToFitSize(ASDisplayNode *node, CGSize size)
{
CGSize sizeThatFits = [node layoutThatFits:ASSizeRangeMake(size)].size;
node.bounds = (CGRect){.origin = CGPointZero, .size = sizeThatFits};
}
void ASDisplayNodeSizeToFitSizeRange(ASDisplayNode *node, ASSizeRange sizeRange)
{
CGSize sizeThatFits = [node layoutThatFits:sizeRange].size;
node.bounds = (CGRect){.origin = CGPointZero, .size = sizeThatFits};
}

View File

@@ -30,7 +30,7 @@
// trivial test case to ensure ASSnapshotTestCase works
ASImageNode *imageNode = [[ASImageNode alloc] init];
imageNode.image = [self testImage];
[imageNode layoutThatFits:ASSizeRangeMake(CGSizeZero, CGSizeMake(100, 100))];
ASDisplayNodeSizeToFitSize(imageNode, CGSizeMake(100, 100));
ASSnapshotVerifyNode(imageNode, nil);
}
@@ -46,13 +46,12 @@
// Snapshot testing requires that node is formally laid out.
imageNode.style.width = ASDimensionMake(forcedImageSize.width);
imageNode.style.height = ASDimensionMake(forcedImageSize.height);
[imageNode layoutThatFits:ASSizeRangeMake(CGSizeZero, forcedImageSize)];
ASDisplayNodeSizeToFitSize(imageNode, forcedImageSize);
ASSnapshotVerifyNode(imageNode, @"first");
imageNode.style.width = ASDimensionMake(200);
imageNode.style.height = ASDimensionMake(200);
[imageNode layoutThatFits:ASSizeRangeMake(CGSizeZero, CGSizeMake(200, 200))];
ASDisplayNodeSizeToFitSize(imageNode, CGSizeMake(200, 200));
ASSnapshotVerifyNode(imageNode, @"second");
XCTAssert(CGImageGetWidth((CGImageRef)imageNode.contents) == forcedImageSize.width * imageNode.contentsScale &&
@@ -66,7 +65,7 @@
UIImage *tinted = ASImageNodeTintColorModificationBlock([UIColor redColor])(test);
ASImageNode *node = [[ASImageNode alloc] init];
node.image = tinted;
[node layoutThatFits:ASSizeRangeMake(test.size)];
ASDisplayNodeSizeToFitSize(node, test.size);
ASSnapshotVerifyNode(node, nil);
}
@@ -81,7 +80,7 @@
UIImage *rounded = ASImageNodeRoundBorderModificationBlock(2, [UIColor redColor])(result);
ASImageNode *node = [[ASImageNode alloc] init];
node.image = rounded;
[node layoutThatFits:ASSizeRangeMake(rounded.size)];
ASDisplayNodeSizeToFitSize(node, rounded.size);
ASSnapshotVerifyNode(node, nil);
}

View File

@@ -39,7 +39,7 @@
node.layoutSpecUnderTest = layoutSpec;
[node layoutThatFits:sizeRange];
ASDisplayNodeSizeToFitSizeRange(node, sizeRange);
ASSnapshotVerifyNode(node, identifier);
}

View File

@@ -9,6 +9,7 @@
//
#import <FBSnapshotTestCase/FBSnapshotTestCase.h>
#import "ASDisplayNodeTestsHelper.h"
@class ASDisplayNode;

View File

@@ -37,8 +37,6 @@ NSOrderedSet *ASSnapshotTestCaseDefaultSuffixes(void)
+ (void)hackilySynchronouslyRecursivelyRenderNode:(ASDisplayNode *)node
{
ASDisplayNodeAssertNotNil(node.calculatedLayout, @"Node %@ must be measured before it is rendered.", node);
node.bounds = (CGRect) { .size = node.calculatedSize };
ASDisplayNodePerformBlockOnEveryNode(nil, node, YES, ^(ASDisplayNode * _Nonnull node) {
[node.layer setNeedsDisplay];
});

View File

@@ -11,7 +11,6 @@
#import "ASSnapshotTestCase.h"
#import <AsyncDisplayKit/AsyncDisplayKit.h>
#import "ASLayout.h"
@interface ASTextNodeSnapshotTests : ASSnapshotTestCase
@@ -25,8 +24,8 @@
ASTextNode *textNode = [[ASTextNode alloc] init];
textNode.attributedText = [[NSAttributedString alloc] initWithString:@"judar"
attributes:@{NSFontAttributeName : [UIFont italicSystemFontOfSize:24]}];
[textNode layoutThatFits:ASSizeRangeMake(CGSizeZero, CGSizeMake(CGFLOAT_MAX, CGFLOAT_MAX))];
textNode.textContainerInset = UIEdgeInsetsMake(0, 2, 0, 2);
ASDisplayNodeSizeToFitSizeRange(textNode, ASSizeRangeMake(CGSizeZero, CGSizeMake(CGFLOAT_MAX, CGFLOAT_MAX)));
ASSnapshotVerifyNode(textNode, nil);
}
@@ -40,13 +39,14 @@
textNode.attributedText = [[NSAttributedString alloc] initWithString:@"judar judar judar judar judar judar"
attributes:@{ NSFontAttributeName : [UIFont systemFontOfSize:30] }];
[textNode layoutThatFits:ASSizeRangeMake(CGSizeZero, CGSizeMake(100, 80))];
textNode.frame = CGRectMake(50, 50, textNode.calculatedSize.width, textNode.calculatedSize.height);
textNode.textContainerInset = UIEdgeInsetsMake(10, 10, 10, 10);
ASLayout *layout = [textNode layoutThatFits:ASSizeRangeMake(CGSizeZero, CGSizeMake(100, 80))];
textNode.frame = CGRectMake(50, 50, layout.size.width, layout.size.height);
[backgroundView addSubview:textNode.view];
backgroundView.frame = UIEdgeInsetsInsetRect(textNode.bounds, UIEdgeInsetsMake(-50, -50, -50, -50));
textNode.highlightRange = NSMakeRange(0, textNode.attributedText.length);
[ASSnapshotTestCase hackilySynchronouslyRecursivelyRenderNode:textNode];
@@ -62,9 +62,9 @@
textNode.attributedText = [[NSAttributedString alloc] initWithString:@"yolo"
attributes:@{ NSFontAttributeName : [UIFont systemFontOfSize:30] }];
[textNode layoutThatFits:ASSizeRangeMake(CGSizeZero, CGSizeMake(CGFLOAT_MAX, CGFLOAT_MAX))];
textNode.frame = CGRectMake(50, 50, textNode.calculatedSize.width, textNode.calculatedSize.height);
textNode.textContainerInset = UIEdgeInsetsMake(5, 10, 10, 5);
ASLayout *layout = [textNode layoutThatFits:ASSizeRangeMake(CGSizeZero, CGSizeMake(INFINITY, INFINITY))];
textNode.frame = CGRectMake(50, 50, layout.size.width, layout.size.height);
[backgroundView addSubview:textNode.view];
backgroundView.frame = UIEdgeInsetsInsetRect(textNode.bounds, UIEdgeInsetsMake(-50, -50, -50, -50));
@@ -90,7 +90,7 @@
textNode.attributedText = [[NSAttributedString alloc] initWithString:@"Quality is Important" attributes:@{ NSForegroundColorAttributeName: [UIColor blueColor], NSFontAttributeName: [UIFont italicSystemFontOfSize:24] }];
// Set exclusion paths to trigger slow path
textNode.exclusionPaths = @[ [UIBezierPath bezierPath] ];
[textNode layoutThatFits:ASSizeRangeMake(CGSizeZero, CGSizeMake(100, 50))];
ASDisplayNodeSizeToFitSizeRange(textNode, ASSizeRangeMake(CGSizeZero, CGSizeMake(100, 50)));
ASSnapshotVerifyNode(textNode, nil);
}
@@ -102,7 +102,7 @@
textNode.shadowOpacity = 0.3;
textNode.shadowRadius = 3;
textNode.shadowOffset = CGSizeMake(0, 1);
[textNode layoutThatFits:ASSizeRangeMake(CGSizeZero, CGSizeMake(CGFLOAT_MAX, CGFLOAT_MAX))];
ASDisplayNodeSizeToFitSizeRange(textNode, ASSizeRangeMake(CGSizeZero, CGSizeMake(INFINITY, INFINITY)));
ASSnapshotVerifyNode(textNode, nil);
}

View File

@@ -61,11 +61,7 @@
_buttonNode = [[ASButtonNode alloc] init];
[_buttonNode setTitle:buttonTitle withFont:buttonFont withColor:buttonColor forState:ASControlStateNormal];
// Note: Currently we have to set all the button properties to the same one as for ASControlStateNormal. Otherwise
// if the button is involved in the layout transition it would break the transition as it does a layout pass
// while changing the title. This needs and will be fixed in the future!
[_buttonNode setTitle:buttonTitle withFont:buttonFont withColor:buttonColor forState:ASControlStateHighlighted];
[_buttonNode setTitle:buttonTitle withFont:buttonFont withColor:[buttonColor colorWithAlphaComponent:0.5] forState:ASControlStateHighlighted];
// Some debug colors
@@ -80,7 +76,7 @@
{
[super didLoad];
[self.buttonNode addTarget:self action:@selector(buttonPressed:) forControlEvents:ASControlNodeEventTouchDown];
[self.buttonNode addTarget:self action:@selector(buttonPressed:) forControlEvents:ASControlNodeEventTouchUpInside];
}
#pragma mark - Actions
@@ -88,7 +84,6 @@
- (void)buttonPressed:(id)sender
{
self.enabled = !self.enabled;
[self transitionLayoutWithAnimation:YES shouldMeasureAsync:NO measurementCompletion:nil];
}

View File

@@ -30,4 +30,5 @@
[self.node.collectionNode.view.collectionViewLayout invalidateLayout];
}
@end