[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
This commit is contained in:
Michael Schneider 2016-08-31 16:45:02 -07:00 committed by Adlai Holler
parent ea4d88e053
commit 027358fc6b
2 changed files with 71 additions and 12 deletions

View File

@ -1321,22 +1321,21 @@ static ASDisplayNodeMethodOverrides GetASDisplayNodeMethodOverrides(Class c)
- (void)measureNodeWithBoundsIfNecessary:(CGRect)bounds - (void)measureNodeWithBoundsIfNecessary:(CGRect)bounds
{ {
BOOL supportsRangedManagedInterfaceState = NO;
BOOL hasDirtyLayout = NO; BOOL hasDirtyLayout = NO;
BOOL hasSupernode = NO; CGSize calculatedLayoutSize = CGSizeZero;
{ {
ASDN::MutexLocker l(__instanceLock__); ASDN::MutexLocker l(__instanceLock__);
supportsRangedManagedInterfaceState = [self supportsRangeManagedInterfaceState];
hasDirtyLayout = [self _hasDirtyLayout]; 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. // If no measure pass happened or the bounds changed between layout passes we manually trigger a measurement pass
// We simply call measureWithSizeRange: using a size range equal to whatever bounds were provided to that element // for the node using a size range equal to whatever bounds were provided to the node
if (!hasSupernode && !supportsRangedManagedInterfaceState && hasDirtyLayout) { if (hasDirtyLayout || CGSizeEqualToSize(calculatedLayoutSize, bounds.size) == NO) {
if (CGRectEqualToRect(bounds, CGRectZero)) { 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); LOG(@"Warning: No size given for node before node was trying to layout itself: %@. Please provide a frame for the node.", self);
} else { } 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)]; [self measureWithSizeRange:ASSizeRangeMake(bounds.size, bounds.size)];
} }
} }

View File

@ -13,25 +13,59 @@
#import "ASLayoutSpecSnapshotTestsHelper.h" #import "ASLayoutSpecSnapshotTestsHelper.h"
#import "ASDisplayNode+FrameworkPrivate.h" #import "ASDisplayNode+FrameworkPrivate.h"
@interface ASDisplayNodeLayoutTests : XCTestCase @interface ASDisplayNodeLayoutTests : XCTestCase
@end @end
@implementation ASDisplayNodeLayoutTests @implementation ASDisplayNodeLayoutTests
- (void)testMeasurePassOnLayoutIfNotHappenedBefore - (void)testMeasureOnLayoutIfNotHappenedBefore
{ {
CGSize nodeSize = CGSizeMake(100, 100);
ASStaticSizeDisplayNode *displayNode = [ASStaticSizeDisplayNode new]; ASStaticSizeDisplayNode *displayNode = [ASStaticSizeDisplayNode new];
displayNode.staticSize = CGSizeMake(100, 100); displayNode.staticSize = nodeSize;
displayNode.frame = CGRectMake(0, 0, 100, 100);
// 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(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 // Trigger view creation and layout pass without a manual measure: call before so the automatic measurement
// pass will trigger in the layout pass // pass will trigger in the layout pass
[displayNode.view layoutIfNeeded]; [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 #if DEBUG
@ -65,4 +99,30 @@
} }
#endif #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 @end