diff --git a/AsyncDisplayKit/ASDisplayNode.mm b/AsyncDisplayKit/ASDisplayNode.mm index ba7a5b402a..cfe350f82a 100644 --- a/AsyncDisplayKit/ASDisplayNode.mm +++ b/AsyncDisplayKit/ASDisplayNode.mm @@ -1321,22 +1321,21 @@ static ASDisplayNodeMethodOverrides GetASDisplayNodeMethodOverrides(Class c) - (void)measureNodeWithBoundsIfNecessary:(CGRect)bounds { - BOOL supportsRangedManagedInterfaceState = NO; BOOL hasDirtyLayout = NO; - BOOL hasSupernode = NO; + CGSize calculatedLayoutSize = CGSizeZero; { ASDN::MutexLocker l(__instanceLock__); - supportsRangedManagedInterfaceState = [self supportsRangeManagedInterfaceState]; hasDirtyLayout = [self _hasDirtyLayout]; - hasSupernode = (self.supernode != nil); + calculatedLayoutSize = _calculatedLayout.size; } - // Normally measure will be called before layout occurs. If this doesn't happen, nothing is going to call it at all. - // We simply call measureWithSizeRange: using a size range equal to whatever bounds were provided to that element - if (!hasSupernode && !supportsRangedManagedInterfaceState && hasDirtyLayout) { + // 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 (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 { + // This is a no op if the bounds size is the same as the cosntrained size we used to create the layout previously [self measureWithSizeRange:ASSizeRangeMake(bounds.size, bounds.size)]; } } diff --git a/AsyncDisplayKitTests/ASDisplayNodeLayoutTests.mm b/AsyncDisplayKitTests/ASDisplayNodeLayoutTests.mm index 0c27c4c992..a2156291c9 100644 --- a/AsyncDisplayKitTests/ASDisplayNodeLayoutTests.mm +++ b/AsyncDisplayKitTests/ASDisplayNodeLayoutTests.mm @@ -13,25 +13,59 @@ #import "ASLayoutSpecSnapshotTestsHelper.h" #import "ASDisplayNode+FrameworkPrivate.h" - @interface ASDisplayNodeLayoutTests : XCTestCase @end @implementation ASDisplayNodeLayoutTests -- (void)testMeasurePassOnLayoutIfNotHappenedBefore +- (void)testMeasureOnLayoutIfNotHappenedBefore { + CGSize nodeSize = CGSizeMake(100, 100); + ASStaticSizeDisplayNode *displayNode = [ASStaticSizeDisplayNode new]; - displayNode.staticSize = CGSizeMake(100, 100); - displayNode.frame = CGRectMake(0, 0, 100, 100); + displayNode.staticSize = nodeSize; + + // Use a button node in here as ASButtonNode uses layoutSpecThatFits: + ASButtonNode *buttonNode = [ASButtonNode new]; + [displayNode addSubnode:buttonNode]; + + displayNode.frame = {.size = nodeSize}; + buttonNode.frame = {.size = nodeSize}; ASXCTAssertEqualSizes(displayNode.calculatedSize, CGSizeZero, @"Calculated size before measurement and layout should be 0"); + ASXCTAssertEqualSizes(buttonNode.calculatedSize, CGSizeZero, @"Calculated size before measurement and layout should be 0"); // Trigger view creation and layout pass without a manual measure: call before so the automatic measurement // pass will trigger in the layout pass [displayNode.view layoutIfNeeded]; - ASXCTAssertEqualSizes(displayNode.calculatedSize, CGSizeMake(100, 100), @"Automatic measurement pass should be happened in layout"); + ASXCTAssertEqualSizes(displayNode.calculatedSize, nodeSize, @"Automatic measurement pass should have happened in layout pass"); + ASXCTAssertEqualSizes(buttonNode.calculatedSize, nodeSize, @"Automatic measurement pass should have happened in layout pass"); +} + +- (void)testMeasureOnLayoutIfNotHappenedBeforeForRangeManagedNodes +{ + CGSize nodeSize = CGSizeMake(100, 100); + + ASStaticSizeDisplayNode *displayNode = [ASStaticSizeDisplayNode new]; + displayNode.staticSize = nodeSize; + + ASButtonNode *buttonNode = [ASButtonNode new]; + [displayNode addSubnode:buttonNode]; + + [displayNode enterHierarchyState:ASHierarchyStateRangeManaged]; + + displayNode.frame = {.size = nodeSize}; + buttonNode.frame = {.size = nodeSize}; + + ASXCTAssertEqualSizes(displayNode.calculatedSize, CGSizeZero, @"Calculated size before measurement and layout should be 0"); + ASXCTAssertEqualSizes(buttonNode.calculatedSize, CGSizeZero, @"Calculated size before measurement and layout should be 0"); + + // Trigger layout pass without a maeasurment pass before + [displayNode.view layoutIfNeeded]; + + ASXCTAssertEqualSizes(displayNode.calculatedSize, nodeSize, @"Automatic measurement pass should have happened in layout pass"); + ASXCTAssertEqualSizes(buttonNode.calculatedSize, nodeSize, @"Automatic measurement pass should have happened in layout pass"); } #if DEBUG @@ -65,4 +99,30 @@ } #endif +- (void)testMeasureOnLayoutIfNotHappenedBeforeNoRemeasureForSameBounds +{ + CGSize nodeSize = CGSizeMake(100, 100); + + ASStaticSizeDisplayNode *displayNode = [ASStaticSizeDisplayNode new]; + displayNode.staticSize = nodeSize; + + ASButtonNode *buttonNode = [ASButtonNode new]; + [displayNode addSubnode:buttonNode]; + + __block size_t numberOfLayoutSpecThatFitsCalls = 0; + displayNode.layoutSpecBlock = ^(ASDisplayNode * _Nonnull node, ASSizeRange constrainedSize) { + __sync_fetch_and_add(&numberOfLayoutSpecThatFitsCalls, 1); + return [ASInsetLayoutSpec insetLayoutSpecWithInsets:UIEdgeInsetsZero child:buttonNode]; + }; + + displayNode.frame = {.size = nodeSize}; + + // Trigger initial layout pass without a measurement pass before + [displayNode.view layoutIfNeeded]; + XCTAssertEqual(numberOfLayoutSpecThatFitsCalls, 1, @"Should measure during layout if not measured"); + + [displayNode measureWithSizeRange:ASSizeRangeMake(nodeSize, nodeSize)]; + XCTAssertEqual(numberOfLayoutSpecThatFitsCalls, 1, @"Should not remeasure with same bounds"); +} + @end