mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2026-04-04 20:52:41 +00:00
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:
@@ -527,6 +527,7 @@
|
||||
- (void)layout
|
||||
{
|
||||
[super layout];
|
||||
|
||||
_backgroundImageNode.hidden = (_backgroundImageNode.image == nil);
|
||||
_imageNode.hidden = (_imageNode.image == nil);
|
||||
_titleNode.hidden = (_titleNode.attributedText.length == 0);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:.
|
||||
*
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -204,7 +204,7 @@ struct ASImageNodeDrawParameters {
|
||||
if (!ASObjectIsEqual(_image, image)) {
|
||||
_image = image;
|
||||
|
||||
[self invalidateCalculatedLayout];
|
||||
[self setNeedsLayout];
|
||||
if (image) {
|
||||
[self setNeedsDisplay];
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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];
|
||||
}
|
||||
|
||||
|
||||
@@ -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];
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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};
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -39,7 +39,7 @@
|
||||
|
||||
node.layoutSpecUnderTest = layoutSpec;
|
||||
|
||||
[node layoutThatFits:sizeRange];
|
||||
ASDisplayNodeSizeToFitSizeRange(node, sizeRange);
|
||||
ASSnapshotVerifyNode(node, identifier);
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
//
|
||||
|
||||
#import <FBSnapshotTestCase/FBSnapshotTestCase.h>
|
||||
#import "ASDisplayNodeTestsHelper.h"
|
||||
|
||||
@class ASDisplayNode;
|
||||
|
||||
|
||||
@@ -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];
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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];
|
||||
}
|
||||
|
||||
|
||||
@@ -30,4 +30,5 @@
|
||||
[self.node.collectionNode.view.collectionViewLayout invalidateLayout];
|
||||
}
|
||||
|
||||
|
||||
@end
|
||||
|
||||
Reference in New Issue
Block a user