diff --git a/AsyncDisplayKit.xcodeproj/project.pbxproj b/AsyncDisplayKit.xcodeproj/project.pbxproj index 51f5e6d31..ae59c106b 100644 --- a/AsyncDisplayKit.xcodeproj/project.pbxproj +++ b/AsyncDisplayKit.xcodeproj/project.pbxproj @@ -421,6 +421,7 @@ DEFAD8131CC48914000527C4 /* ASVideoNode.mm in Sources */ = {isa = PBXBuildFile; fileRef = AEEC47E01C20C2DD00EC1693 /* ASVideoNode.mm */; }; E516FC7F1E9FE24200714FF4 /* ASHashing.h in Headers */ = {isa = PBXBuildFile; fileRef = E516FC7D1E9FE24200714FF4 /* ASHashing.h */; }; E516FC801E9FE24200714FF4 /* ASHashing.m in Sources */ = {isa = PBXBuildFile; fileRef = E516FC7E1E9FE24200714FF4 /* ASHashing.m */; }; + E51B78BF1F028ABF00E32604 /* ASLayoutFlatteningTests.m in Sources */ = {isa = PBXBuildFile; fileRef = E51B78BD1F01A0EE00E32604 /* ASLayoutFlatteningTests.m */; }; E54E81FC1EB357BD00FFE8E1 /* ASPageTable.h in Headers */ = {isa = PBXBuildFile; fileRef = E54E81FA1EB357BD00FFE8E1 /* ASPageTable.h */; }; E54E81FD1EB357BD00FFE8E1 /* ASPageTable.m in Sources */ = {isa = PBXBuildFile; fileRef = E54E81FB1EB357BD00FFE8E1 /* ASPageTable.m */; }; E55D86331CA8A14000A0C26F /* ASLayoutElement.mm in Sources */ = {isa = PBXBuildFile; fileRef = E55D86311CA8A14000A0C26F /* ASLayoutElement.mm */; }; @@ -890,6 +891,7 @@ DECBD6E61BE56E1900CF4905 /* ASButtonNode.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASButtonNode.mm; sourceTree = ""; }; E516FC7D1E9FE24200714FF4 /* ASHashing.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASHashing.h; sourceTree = ""; }; E516FC7E1E9FE24200714FF4 /* ASHashing.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASHashing.m; sourceTree = ""; }; + E51B78BD1F01A0EE00E32604 /* ASLayoutFlatteningTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASLayoutFlatteningTests.m; sourceTree = ""; }; E52405B21C8FEF03004DC8E7 /* ASLayoutTransition.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASLayoutTransition.mm; sourceTree = ""; }; E52405B41C8FEF16004DC8E7 /* ASLayoutTransition.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASLayoutTransition.h; sourceTree = ""; }; E54E81FA1EB357BD00FFE8E1 /* ASPageTable.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASPageTable.h; sourceTree = ""; }; @@ -1166,6 +1168,7 @@ 058D0A30195D057000B7D73C /* ASDisplayNodeTestsHelper.h */, 058D0A31195D057000B7D73C /* ASDisplayNodeTestsHelper.m */, 697B31591CFE4B410049936F /* ASEditableTextNodeTests.m */, + E51B78BD1F01A0EE00E32604 /* ASLayoutFlatteningTests.m */, 052EE0651A159FEF002C6279 /* ASMultiplexImageNodeTests.m */, 058D0A32195D057000B7D73C /* ASMutableAttributedStringBuilderTests.m */, 3C9C128419E616EF00E942A0 /* ASTableViewTests.mm */, @@ -2052,6 +2055,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + E51B78BF1F028ABF00E32604 /* ASLayoutFlatteningTests.m in Sources */, 29CDC2E21AAE70D000833CA4 /* ASBasicImageDownloaderContextTests.m in Sources */, CC583AD71EF9BDC100134156 /* NSInvocation+ASTestHelpers.m in Sources */, CC051F1F1D7A286A006434CB /* ASCALayerTests.m in Sources */, diff --git a/CHANGELOG.md b/CHANGELOG.md index fb5adc85a..ed063e22f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ - Fixed an issue where calls to setNeedsDisplay and setNeedsLayout would stop working on loaded nodes. [Garrett Moon](https://github.com/garrettmoon) - Migrated unit tests to OCMock 3.4 (from 2.2) and improved the multiplex image node tests. [Adlai Holler](https://github.com/Adlai-Holler) - Fix CollectionNode double-load issue. This should significantly improve performance in cases where a collection node has content immediately available on first layout i.e. not fetched from the network. [Adlai Holler](https://github.com/Adlai-Holler) +- Overhaul layout flattening algorithm [Huy Nguyen](https://github.com/nguyenhuy) [#395](https://github.com/TextureGroup/Texture/pull/395). ## 2.3.3 - [ASTextKitFontSizeAdjuster] Replace use of NSAttributedString's boundingRectWithSize:options:context: with NSLayoutManager's boundingRectForGlyphRange:inTextContainer: [Ricky Cancro](https://github.com/rcancro) diff --git a/Source/ASDisplayNode+Yoga.mm b/Source/ASDisplayNode+Yoga.mm index dcfadaf51..c47b64fe7 100644 --- a/Source/ASDisplayNode+Yoga.mm +++ b/Source/ASDisplayNode+Yoga.mm @@ -36,10 +36,6 @@ @interface ASDisplayNode (YogaInternal) - (ASSizeRange)_locked_constrainedSizeForLayoutPass; @end -@interface ASLayout (YogaInternal) -@property (nonatomic, getter=isFlattened) BOOL flattened; -@end - @implementation ASDisplayNode (Yoga) - (void)setYogaChildren:(NSArray *)yogaChildren @@ -174,9 +170,7 @@ - (void)setupYogaCalculatedLayout NSMutableArray *sublayouts = [NSMutableArray arrayWithCapacity:childCount]; for (ASDisplayNode *subnode in self.yogaChildren) { - ASLayout *sublayout = [subnode layoutForYogaNode]; - sublayout.flattened = YES; - [sublayouts addObject:sublayout]; + [sublayouts addObject:[subnode layoutForYogaNode]]; } // The layout for self should have position CGPointNull, but include the calculated size. diff --git a/Source/Layout/ASLayout.h b/Source/Layout/ASLayout.h index 82cd9d605..039032512 100644 --- a/Source/Layout/ASLayout.h +++ b/Source/Layout/ASLayout.h @@ -134,14 +134,6 @@ ASDISPLAYNODE_EXTERN_C_END */ + (instancetype)layoutWithLayoutElement:(id)layoutElement size:(CGSize)size AS_WARN_UNUSED_RESULT; -/** - * Convenience initializer that creates a layout based on the values of the given layout, with a new position - * - * @param layout The layout to use to create the new layout - * @param position The position of the new layout - */ -+ (instancetype)layoutWithLayout:(ASLayout *)layout position:(CGPoint)position AS_WARN_UNUSED_RESULT; - /** * Traverses the existing layout tree and generates a new tree that represents only ASDisplayNode layouts */ diff --git a/Source/Layout/ASLayout.mm b/Source/Layout/ASLayout.mm index ddc0fb279..9a2712798 100644 --- a/Source/Layout/ASLayout.mm +++ b/Source/Layout/ASLayout.mm @@ -37,7 +37,7 @@ extern BOOL ASPointIsNull(CGPoint point) /** * Creates an defined number of " |" indent blocks for the recursive description. */ -static inline NSString * descriptionIndents(NSUInteger indents) +ASDISPLAYNODE_INLINE AS_WARN_UNUSED_RESULT NSString * descriptionIndents(NSUInteger indents) { NSMutableString *description = [NSMutableString string]; for (NSUInteger i = 0; i < indents; i++) { @@ -49,14 +49,31 @@ extern BOOL ASPointIsNull(CGPoint point) return description; } +ASDISPLAYNODE_INLINE AS_WARN_UNUSED_RESULT BOOL ASLayoutIsDisplayNodeType(ASLayout *layout) +{ + return layout.type == ASLayoutElementTypeDisplayNode; +} + +ASDISPLAYNODE_INLINE AS_WARN_UNUSED_RESULT BOOL ASLayoutIsFlattened(ASLayout *layout) +{ + // A layout is flattened if its position is null, and all of its sublayouts are of type displaynode with no sublayouts. + if (! ASPointIsNull(layout.position)) { + return NO; + } + + for (ASLayout *sublayout in layout.sublayouts) { + if (ASLayoutIsDisplayNodeType(sublayout) == NO || sublayout.sublayouts.count > 0) { + return NO; + } + } + + return YES; +} + @interface ASLayout () { ASLayoutElementType _layoutElementType; } -/** - * A boolean describing if the current layout has been flattened. - */ -@property (nonatomic, getter=isFlattened) BOOL flattened; /* * Caches all sublayouts if set to YES or destroys the sublayout cache if set to NO. Defaults to YES @@ -117,7 +134,7 @@ - (instancetype)initWithLayoutElement:(id)layoutElement _size = size; if (ASPointIsNull(position) == NO) { - _position = CGPointMake(ASCeilPixelValue(position.x), ASCeilPixelValue(position.y)); + _position = ASCeilPointValues(position); } else { _position = position; } @@ -129,7 +146,6 @@ - (instancetype)initWithLayoutElement:(id)layoutElement [_elementToRectTable setRect:layout.frame forKey:layout.layoutElement]; } - _flattened = NO; self.retainSublayoutLayoutElements = [ASLayout shouldRetainSublayoutLayoutElements]; } @@ -173,14 +189,6 @@ + (instancetype)layoutWithLayoutElement:(id)layoutElement size: sublayouts:nil]; } -+ (instancetype)layoutWithLayout:(ASLayout *)layout position:(CGPoint)position -{ - return [self layoutWithLayoutElement:layout.layoutElement - size:layout.size - position:position - sublayouts:layout.sublayouts]; -} - #pragma mark - Sublayout Elements Caching - (void)setRetainSublayoutLayoutElements:(BOOL)retainSublayoutLayoutElements @@ -207,7 +215,12 @@ - (void)setRetainSublayoutLayoutElements:(BOOL)retainSublayoutLayoutElements - (ASLayout *)filteredNodeLayoutTree { - NSMutableArray *flattenedSublayouts = [NSMutableArray array]; + if (ASLayoutIsFlattened(self)) { + // All flattened layouts must have this flag enabled + // to ensure sublayout elements are retained until the layouts are applied. + self.retainSublayoutLayoutElements = YES; + return self; + } struct Context { ASLayout *layout; @@ -216,28 +229,42 @@ - (ASLayout *)filteredNodeLayoutTree // Queue used to keep track of sublayouts while traversing this layout in a DFS fashion. std::deque queue; - queue.push_front({self, CGPointZero}); + for (ASLayout *sublayout in self.sublayouts) { + queue.push_back({sublayout, sublayout.position}); + } + + NSMutableArray *flattenedSublayouts = [NSMutableArray array]; while (!queue.empty()) { - Context context = queue.front(); + const Context context = queue.front(); queue.pop_front(); - - if (self != context.layout && context.layout.type == ASLayoutElementTypeDisplayNode) { - ASLayout *layout = [ASLayout layoutWithLayout:context.layout position:context.absolutePosition]; - layout.flattened = YES; - [flattenedSublayouts addObject:layout]; - } - std::vector sublayoutContexts; - for (ASLayout *sublayout in context.layout.sublayouts) { - if (sublayout.isFlattened == NO) { + ASLayout *layout = context.layout; + const NSArray *sublayouts = layout.sublayouts; + const NSUInteger sublayoutsCount = sublayouts.count; + const CGPoint absolutePosition = context.absolutePosition; + + if (ASLayoutIsDisplayNodeType(layout)) { + if (sublayoutsCount > 0 || CGPointEqualToPoint(ASCeilPointValues(absolutePosition), layout.position) == NO) { + // Only create a new layout if the existing one can't be reused, which means it has either some sublayouts or an invalid absolute position. + layout = [ASLayout layoutWithLayoutElement:layout.layoutElement + size:layout.size + position:absolutePosition + sublayouts:@[]]; + } + [flattenedSublayouts addObject:layout]; + } else if (sublayoutsCount > 0){ + std::vector sublayoutContexts; + for (ASLayout *sublayout in sublayouts) { sublayoutContexts.push_back({sublayout, context.absolutePosition + sublayout.position}); } + queue.insert(queue.cbegin(), sublayoutContexts.begin(), sublayoutContexts.end()); } - queue.insert(queue.cbegin(), sublayoutContexts.begin(), sublayoutContexts.end()); } - ASLayout *layout = [ASLayout layoutWithLayoutElement:_layoutElement size:_size position:CGPointZero sublayouts:flattenedSublayouts]; + ASLayout *layout = [ASLayout layoutWithLayoutElement:_layoutElement size:_size sublayouts:flattenedSublayouts]; + // All flattened layouts must have this flag enabled + // to ensure sublayout elements are retained until the layouts are applied. layout.retainSublayoutLayoutElements = YES; return layout; } diff --git a/Source/Layout/ASYogaLayoutSpec.mm b/Source/Layout/ASYogaLayoutSpec.mm index 2da6ed92d..e0cc80e51 100644 --- a/Source/Layout/ASYogaLayoutSpec.mm +++ b/Source/Layout/ASYogaLayoutSpec.mm @@ -42,7 +42,6 @@ - (ASLayout *)layoutForYogaNode:(YGNodeRef)yogaNode return [ASLayout layoutWithLayoutElement:layoutElement size:size sublayouts:sublayouts]; } else { CGPoint position = CGPointMake(YGNodeLayoutGetLeft(yogaNode), YGNodeLayoutGetTop(yogaNode)); - // TODO: If it were possible to set .flattened = YES, it would be valid to do so here. return [ASLayout layoutWithLayoutElement:layoutElement size:size position:position sublayouts:nil]; } } diff --git a/Source/Private/ASInternalHelpers.h b/Source/Private/ASInternalHelpers.h index 8ec7c3718..31766b366 100644 --- a/Source/Private/ASInternalHelpers.h +++ b/Source/Private/ASInternalHelpers.h @@ -46,6 +46,8 @@ CGSize ASFloorSizeValues(CGSize s); CGFloat ASFloorPixelValue(CGFloat f); +CGPoint ASCeilPointValues(CGPoint p); + CGSize ASCeilSizeValues(CGSize s); CGFloat ASCeilPixelValue(CGFloat f); diff --git a/Source/Private/ASInternalHelpers.m b/Source/Private/ASInternalHelpers.m index f75c97921..088ddccea 100644 --- a/Source/Private/ASInternalHelpers.m +++ b/Source/Private/ASInternalHelpers.m @@ -153,6 +153,11 @@ CGFloat ASFloorPixelValue(CGFloat f) return floor(f * scale) / scale; } +CGPoint ASCeilPointValues(CGPoint p) +{ + return CGPointMake(ASCeilPixelValue(p.x), ASCeilPixelValue(p.y)); +} + CGSize ASCeilSizeValues(CGSize s) { return CGSizeMake(ASCeilPixelValue(s.width), ASCeilPixelValue(s.height)); diff --git a/Tests/ASLayoutFlatteningTests.m b/Tests/ASLayoutFlatteningTests.m new file mode 100644 index 000000000..56f76c5d4 --- /dev/null +++ b/Tests/ASLayoutFlatteningTests.m @@ -0,0 +1,210 @@ +// +// ASLayoutFlatteningTests.m +// Texture +// +// Copyright (c) 2017-present, Pinterest, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// + +#import +#import +#import +#import + +@interface ASLayoutFlatteningTests : XCTestCase +@end + +@implementation ASLayoutFlatteningTests + +static ASLayout *layoutWithCustomPosition(CGPoint position, id element, NSArray *sublayouts) +{ + return [ASLayout layoutWithLayoutElement:element + size:CGSizeMake(100, 100) + position:position + sublayouts:sublayouts]; +} + +static ASLayout *layout(id element, NSArray *sublayouts) +{ + return layoutWithCustomPosition(CGPointZero, element, sublayouts); +} + +- (void)testThatFlattenedLayoutContainsOnlyDirectSubnodesInValidOrder +{ + ASLayout *flattenedLayout; + + @autoreleasepool { + NSMutableArray *subnodes = [NSMutableArray array]; + NSMutableArray *layoutSpecs = [NSMutableArray array]; + NSMutableArray *indirectSubnodes = [NSMutableArray array]; + + ASDisplayNode *(^subnode)() = ^ASDisplayNode *() { [subnodes addObject:[[ASDisplayNode alloc] init]]; return [subnodes lastObject]; }; + ASLayoutSpec *(^layoutSpec)() = ^ASLayoutSpec *() { [layoutSpecs addObject:[[ASLayoutSpec alloc] init]]; return [layoutSpecs lastObject]; }; + ASDisplayNode *(^indirectSubnode)() = ^ASDisplayNode *() { [indirectSubnodes addObject:[[ASDisplayNode alloc] init]]; return [indirectSubnodes lastObject]; }; + + NSArray *sublayouts = @[ + layout(subnode(), @[ + layout(indirectSubnode(), @[]), + ]), + layout(layoutSpec(), @[ + layout(subnode(), @[]), + layout(layoutSpec(), @[ + layout(layoutSpec(), @[]), + layout(subnode(), @[]), + ]), + layout(layoutSpec(), @[]), + ]), + layout(layoutSpec(), @[ + layout(subnode(), @[ + layout(indirectSubnode(), @[]), + layout(indirectSubnode(), @[ + layout(indirectSubnode(), @[]) + ]), + ]) + ]), + layout(subnode(), @[]), + ]; + + ASDisplayNode *rootNode = [[ASDisplayNode alloc] init]; + ASLayout *originalLayout = [ASLayout layoutWithLayoutElement:rootNode + size:CGSizeMake(1000, 1000) + sublayouts:sublayouts]; + flattenedLayout = [originalLayout filteredNodeLayoutTree]; + NSArray *flattenedSublayouts = flattenedLayout.sublayouts; + NSUInteger sublayoutsCount = flattenedSublayouts.count; + + XCTAssertEqualObjects(originalLayout.layoutElement, flattenedLayout.layoutElement, @"The root node should be reserved"); + XCTAssertTrue(ASPointIsNull(flattenedLayout.position), @"Position of the root layout should be null"); + XCTAssertEqual(subnodes.count, sublayoutsCount, @"Flattened layout should only contain direct subnodes"); + for (int i = 0; i < sublayoutsCount; i++) { + XCTAssertEqualObjects(subnodes[i], flattenedSublayouts[i].layoutElement, @"Sublayouts should be in correct order (flattened in DFS fashion)"); + } + } + + for (ASLayout *sublayout in flattenedLayout.sublayouts) { + XCTAssertNotNil(sublayout.layoutElement, @"Sublayout elements should be retained"); + XCTAssertEqual(0, sublayout.sublayouts.count, @"Sublayouts should not have their own sublayouts"); + } +} + +#pragma mark - Test reusing ASLayouts while flattening + +- (void)testThatLayoutWithNonNullPositionIsNotReused +{ + ASDisplayNode *rootNode = [[ASDisplayNode alloc] init]; + ASLayout *originalLayout = layoutWithCustomPosition(CGPointMake(10, 10), rootNode, @[]); + ASLayout *flattenedLayout = [originalLayout filteredNodeLayoutTree]; + XCTAssertNotEqualObjects(originalLayout, flattenedLayout, "@Layout should be reused"); + XCTAssertTrue(ASPointIsNull(flattenedLayout.position), @"Position of a root layout should be null"); +} + +- (void)testThatLayoutWithNullPositionAndNoSublayoutIsReused +{ + ASDisplayNode *rootNode = [[ASDisplayNode alloc] init]; + ASLayout *originalLayout = layoutWithCustomPosition(ASPointNull, rootNode, @[]); + ASLayout *flattenedLayout = [originalLayout filteredNodeLayoutTree]; + XCTAssertEqualObjects(originalLayout, flattenedLayout, "@Layout should be reused"); + XCTAssertTrue(ASPointIsNull(flattenedLayout.position), @"Position of a root layout should be null"); +} + +- (void)testThatLayoutWithNullPositionAndFlattenedNodeSublayoutsIsReused +{ + ASLayout *flattenedLayout; + + @autoreleasepool { + ASDisplayNode *rootNode = [[ASDisplayNode alloc] init]; + NSMutableArray *subnodes = [NSMutableArray array]; + ASDisplayNode *(^subnode)() = ^ASDisplayNode *() { [subnodes addObject:[[ASDisplayNode alloc] init]]; return [subnodes lastObject]; }; + ASLayout *originalLayout = layoutWithCustomPosition(ASPointNull, + rootNode, + @[ + layoutWithCustomPosition(CGPointMake(10, 10), subnode(), @[]), + layoutWithCustomPosition(CGPointMake(20, 20), subnode(), @[]), + layoutWithCustomPosition(CGPointMake(30, 30), subnode(), @[]), + ]); + flattenedLayout = [originalLayout filteredNodeLayoutTree]; + XCTAssertEqualObjects(originalLayout, flattenedLayout, "@Layout should be reused"); + XCTAssertTrue(ASPointIsNull(flattenedLayout.position), @"Position of the root layout should be null"); + } + + for (ASLayout *sublayout in flattenedLayout.sublayouts) { + XCTAssertNotNil(sublayout.layoutElement, @"Sublayout elements should be retained"); + XCTAssertEqual(0, sublayout.sublayouts.count, @"Sublayouts should not have their own sublayouts"); + } +} + +- (void)testThatLayoutWithNullPositionAndUnflattenedSublayoutsIsNotReused +{ + ASLayout *flattenedLayout; + + @autoreleasepool { + ASDisplayNode *rootNode = [[ASDisplayNode alloc] init]; + NSMutableArray *subnodes = [NSMutableArray array]; + NSMutableArray *layoutSpecs = [NSMutableArray array]; + NSMutableArray *indirectSubnodes = [NSMutableArray array]; + NSMutableArray *reusedLayouts = [NSMutableArray array]; + + ASDisplayNode *(^subnode)() = ^ASDisplayNode *() { [subnodes addObject:[[ASDisplayNode alloc] init]]; return [subnodes lastObject]; }; + ASLayoutSpec *(^layoutSpec)() = ^ASLayoutSpec *() { [layoutSpecs addObject:[[ASLayoutSpec alloc] init]]; return [layoutSpecs lastObject]; }; + ASDisplayNode *(^indirectSubnode)() = ^ASDisplayNode *() { [indirectSubnodes addObject:[[ASDisplayNode alloc] init]]; return [indirectSubnodes lastObject]; }; + ASLayout *(^reusedLayout)(ASDisplayNode *) = ^ASLayout *(ASDisplayNode *subnode) { [reusedLayouts addObject:layout(subnode, @[])]; return [reusedLayouts lastObject]; }; + + /* + * Layouts with sublayouts of both nodes and layout specs should not be reused. + * However, all flattened node sublayouts with valid position should be reused. + */ + ASLayout *originalLayout = layoutWithCustomPosition(ASPointNull, + rootNode, + @[ + reusedLayout(subnode()), + // The 2 node sublayouts below should be reused although they are in a layout spec sublayout. + // That is because each of them have an absolute position of zero. + // This case can happen, for example, as the result of a background/overlay layout spec. + layout(layoutSpec(), @[ + reusedLayout(subnode()), + reusedLayout(subnode()) + ]), + layout(subnode(), @[ + layout(layoutSpec(), @[]) + ]), + layout(subnode(), @[ + layout(indirectSubnode(), @[]) + ]), + layoutWithCustomPosition(CGPointMake(10, 10), subnode(), @[]), + // The 2 node sublayouts below shouldn't be reused because they have non-zero absolute positions. + layoutWithCustomPosition(CGPointMake(20, 20), layoutSpec(), @[ + layout(subnode(), @[]), + layout(subnode(), @[]) + ]), + ]); + flattenedLayout = [originalLayout filteredNodeLayoutTree]; + NSArray *flattenedSublayouts = flattenedLayout.sublayouts; + NSUInteger sublayoutsCount = flattenedSublayouts.count; + + XCTAssertNotEqualObjects(originalLayout, flattenedLayout, @"Original layout should not be reused"); + XCTAssertEqualObjects(originalLayout.layoutElement, flattenedLayout.layoutElement, @"The root node should be reserved"); + XCTAssertTrue(ASPointIsNull(flattenedLayout.position), @"Position of the root layout should be null"); + XCTAssertTrue(reusedLayouts.count <= sublayoutsCount, @"Some sublayouts can't be reused"); + XCTAssertEqual(subnodes.count, sublayoutsCount, @"Flattened layout should only contain direct subnodes"); + int numOfActualReusedLayouts = 0; + for (int i = 0; i < sublayoutsCount; i++) { + ASLayout *sublayout = flattenedSublayouts[i]; + XCTAssertEqualObjects(subnodes[i], sublayout.layoutElement, @"Sublayouts should be in correct order (flattened in DFS fashion)"); + if ([reusedLayouts containsObject:sublayout]) { + numOfActualReusedLayouts++; + } + } + XCTAssertEqual(numOfActualReusedLayouts, reusedLayouts.count, @"Should reuse all layouts that can be reused"); + } + + for (ASLayout *sublayout in flattenedLayout.sublayouts) { + XCTAssertNotNil(sublayout.layoutElement, @"Sublayout elements should be retained"); + XCTAssertEqual(0, sublayout.sublayouts.count, @"Sublayouts should not have their own sublayouts"); + } +} + +@end