From 027358fc6bea0ed0a38d686b4b6449df1a4dcf96 Mon Sep 17 00:00:00 2001 From: Michael Schneider Date: Wed, 31 Aug 2016 16:45:02 -0700 Subject: [PATCH] [Layout] Automatic measure pass in layout pass if not happened before (#2163) * Automatic measure pass in layout pass if not happened before 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 * Add test method that ensures that on a second layout pass with same bounds, layoutSpecThatFits: / layoutSpecBlock is not called --- AsyncDisplayKit/ASDisplayNode.mm | 13 ++-- .../ASDisplayNodeLayoutTests.mm | 70 +++++++++++++++++-- 2 files changed, 71 insertions(+), 12 deletions(-) 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