diff --git a/AsyncDisplayKit/Layout/ASStackLayoutDefines.h b/AsyncDisplayKit/Layout/ASStackLayoutDefines.h index 0ca7bb3c49..47d72b34d6 100644 --- a/AsyncDisplayKit/Layout/ASStackLayoutDefines.h +++ b/AsyncDisplayKit/Layout/ASStackLayoutDefines.h @@ -67,3 +67,27 @@ typedef NS_ENUM(NSUInteger, ASStackLayoutAlignSelf) { /** Expand to fill cross axis */ ASStackLayoutAlignSelfStretch, }; + +/** Orientation of children along horizontal axis */ +typedef NS_ENUM(NSUInteger, ASHorizontalAlignment) { + /** No alignment specified. Default value */ + ASHorizontalAlignmentNone, + /** Left aligned */ + ASAlignmentLeft, + /** Center aligned */ + ASAlignmentMiddle, + /** Right aligned */ + ASAlignmentRight, +}; + +/** Orientation of children along vertical axis */ +typedef NS_ENUM(NSUInteger, ASVerticalAlignment) { + /** No alignment specified. Default value */ + ASVerticalAlignmentNone, + /** Top aligned */ + ASAlignmentTop, + /** Center aligned */ + ASAlignmentCenter, + /** Bottom aligned */ + ASAlignmentBottom, +}; diff --git a/AsyncDisplayKit/Layout/ASStackLayoutSpec.h b/AsyncDisplayKit/Layout/ASStackLayoutSpec.h index ebf7130bfe..a2afc29609 100644 --- a/AsyncDisplayKit/Layout/ASStackLayoutSpec.h +++ b/AsyncDisplayKit/Layout/ASStackLayoutSpec.h @@ -35,10 +35,25 @@ */ @interface ASStackLayoutSpec : ASLayoutSpec -/** Specifies the direction children are stacked in. */ +/** + Specifies the direction children are stacked in. If horizontalAlignment and verticalAlignment were set, + they will be resolved again, causing justifyContent and alignItems to be updated accordingly + */ @property (nonatomic, assign) ASStackLayoutDirection direction; /** The amount of space between each child. */ @property (nonatomic, assign) CGFloat spacing; +/** + Specifies how children are aligned horizontally. Depends on the stack direction, setting the alignment causes either + justifyContent or alignItems to be updated. The alignment will remain valid after future direction changes. + Thus, it is preferred to those properties + */ +@property (nonatomic, assign) ASHorizontalAlignment horizontalAlignment; +/** + Specifies how children are aligned vertically. Depends on the stack direction, setting the alignment causes either + justifyContent or alignItems to be updated. The alignment will remain valid after future direction changes. + Thus, it is preferred to those properties + */ +@property (nonatomic, assign) ASVerticalAlignment verticalAlignment; /** The amount of space between each child. */ @property (nonatomic, assign) ASStackLayoutJustifyContent justifyContent; /** Orientation of children along cross axis */ diff --git a/AsyncDisplayKit/Layout/ASStackLayoutSpec.mm b/AsyncDisplayKit/Layout/ASStackLayoutSpec.mm index 0763bf2615..ed07b7d6e0 100644 --- a/AsyncDisplayKit/Layout/ASStackLayoutSpec.mm +++ b/AsyncDisplayKit/Layout/ASStackLayoutSpec.mm @@ -25,7 +25,7 @@ @implementation ASStackLayoutSpec { - ASDN::RecursiveMutex _propertyLock; + ASDN::RecursiveMutex _propertyLock; } - (instancetype)init @@ -58,8 +58,10 @@ return nil; } _direction = direction; - _alignItems = alignItems; _spacing = spacing; + _horizontalAlignment = ASHorizontalAlignmentNone; + _verticalAlignment = ASVerticalAlignmentNone; + _alignItems = alignItems; _justifyContent = justifyContent; [self setChildren:children]; @@ -69,18 +71,44 @@ - (void)setDirection:(ASStackLayoutDirection)direction { ASDisplayNodeAssert(self.isMutable, @"Cannot set properties when layout spec is not mutable"); - _direction = direction; + if (_direction != direction) { + _direction = direction; + [self resolveHorizontalAlignment]; + [self resolveVerticalAlignment]; + } +} + +- (void)setHorizontalAlignment:(ASHorizontalAlignment)horizontalAlignment +{ + ASDisplayNodeAssert(self.isMutable, @"Cannot set properties when layout spec is not mutable"); + if (_horizontalAlignment != horizontalAlignment) { + _horizontalAlignment = horizontalAlignment; + [self resolveHorizontalAlignment]; + } +} + +- (void)setVerticalAlignment:(ASVerticalAlignment)verticalAlignment +{ + ASDisplayNodeAssert(self.isMutable, @"Cannot set properties when layout spec is not mutable"); + if (_verticalAlignment != verticalAlignment) { + _verticalAlignment = verticalAlignment; + [self resolveVerticalAlignment]; + } } - (void)setAlignItems:(ASStackLayoutAlignItems)alignItems { ASDisplayNodeAssert(self.isMutable, @"Cannot set properties when layout spec is not mutable"); + ASDisplayNodeAssert(_horizontalAlignment == ASHorizontalAlignmentNone, @"Cannot set this property directly because horizontalAlignment is being used"); + ASDisplayNodeAssert(_verticalAlignment == ASVerticalAlignmentNone, @"Cannot set this property directly because verticalAlignment is being used"); _alignItems = alignItems; } - (void)setJustifyContent:(ASStackLayoutJustifyContent)justifyContent { ASDisplayNodeAssert(self.isMutable, @"Cannot set properties when layout spec is not mutable"); + ASDisplayNodeAssert(_horizontalAlignment == ASHorizontalAlignmentNone, @"Cannot set this property directly because horizontalAlignment is being used"); + ASDisplayNodeAssert(_verticalAlignment == ASVerticalAlignmentNone, @"Cannot set this property directly because verticalAlignment is being used"); _justifyContent = justifyContent; } @@ -149,6 +177,24 @@ sublayouts:sublayouts]; } +- (void)resolveHorizontalAlignment +{ + if (_direction == ASStackLayoutDirectionHorizontal) { + _justifyContent = justifyContent(_horizontalAlignment, _justifyContent); + } else { + _alignItems = alignment(_horizontalAlignment, _alignItems); + } +} + +- (void)resolveVerticalAlignment +{ + if (_direction == ASStackLayoutDirectionHorizontal) { + _alignItems = alignment(_verticalAlignment, _alignItems); + } else { + _justifyContent = justifyContent(_verticalAlignment, _justifyContent); + } +} + @end @implementation ASStackLayoutSpec (Debugging) diff --git a/AsyncDisplayKit/Private/ASStackLayoutSpecUtilities.h b/AsyncDisplayKit/Private/ASStackLayoutSpecUtilities.h index a61218cfe4..ae018a1f87 100644 --- a/AsyncDisplayKit/Private/ASStackLayoutSpecUtilities.h +++ b/AsyncDisplayKit/Private/ASStackLayoutSpecUtilities.h @@ -68,3 +68,63 @@ inline ASStackLayoutAlignItems alignment(ASStackLayoutAlignSelf childAlignment, return stackAlignment; } } + +inline ASStackLayoutAlignItems alignment(ASHorizontalAlignment alignment, ASStackLayoutAlignItems defaultAlignment) +{ + switch (alignment) { + case ASAlignmentLeft: + return ASStackLayoutAlignItemsStart; + case ASAlignmentMiddle: + return ASStackLayoutAlignItemsCenter; + case ASAlignmentRight: + return ASStackLayoutAlignItemsEnd; + case ASHorizontalAlignmentNone: + default: + return defaultAlignment; + } +} + +inline ASStackLayoutAlignItems alignment(ASVerticalAlignment alignment, ASStackLayoutAlignItems defaultAlignment) +{ + switch (alignment) { + case ASAlignmentTop: + return ASStackLayoutAlignItemsStart; + case ASAlignmentCenter: + return ASStackLayoutAlignItemsCenter; + case ASAlignmentBottom: + return ASStackLayoutAlignItemsEnd; + case ASVerticalAlignmentNone: + default: + return defaultAlignment; + } +} + +inline ASStackLayoutJustifyContent justifyContent(ASHorizontalAlignment alignment, ASStackLayoutJustifyContent defaultJustifyContent) +{ + switch (alignment) { + case ASAlignmentLeft: + return ASStackLayoutJustifyContentStart; + case ASAlignmentMiddle: + return ASStackLayoutJustifyContentCenter; + case ASAlignmentRight: + return ASStackLayoutJustifyContentEnd; + case ASHorizontalAlignmentNone: + default: + return defaultJustifyContent; + } +} + +inline ASStackLayoutJustifyContent justifyContent(ASVerticalAlignment alignment, ASStackLayoutJustifyContent defaultJustifyContent) +{ + switch (alignment) { + case ASAlignmentTop: + return ASStackLayoutJustifyContentStart; + case ASAlignmentCenter: + return ASStackLayoutJustifyContentCenter; + case ASAlignmentBottom: + return ASStackLayoutJustifyContentEnd; + case ASVerticalAlignmentNone: + default: + return defaultJustifyContent; + } +} diff --git a/AsyncDisplayKitTests/ASStackLayoutSpecSnapshotTests.mm b/AsyncDisplayKitTests/ASStackLayoutSpecSnapshotTests.mm index 2252078da6..5c3733e707 100644 --- a/AsyncDisplayKitTests/ASStackLayoutSpecSnapshotTests.mm +++ b/AsyncDisplayKitTests/ASStackLayoutSpecSnapshotTests.mm @@ -28,6 +28,8 @@ self.recordMode = NO; } +#pragma mark - Utility methods + static NSArray *defaultSubnodes() { return defaultSubnodesWithSameSize(CGSizeZero, NO); @@ -63,6 +65,24 @@ static NSArray *defaultSubnodesWithSameSize(CGSize subnodeSize, BOOL flex) [self testStackLayoutSpecWithStyle:style sizeRange:sizeRange subnodes:subnodes identifier:identifier]; } +- (void)testStackLayoutSpecWithDirection:(ASStackLayoutDirection)direction + itemsHorizontalAlignment:(ASHorizontalAlignment)horizontalAlignment + itemsVerticalAlignment:(ASVerticalAlignment)verticalAlignment + identifier:(NSString *)identifier +{ + NSArray *subnodes = defaultSubnodesWithSameSize({50, 50}, NO); + + ASStackLayoutSpec *stackLayoutSpec = [[ASStackLayoutSpec alloc] init]; + stackLayoutSpec.direction = direction; + stackLayoutSpec.children = subnodes; + [stackLayoutSpec setHorizontalAlignment:horizontalAlignment]; + [stackLayoutSpec setVerticalAlignment:verticalAlignment]; + + CGSize exactSize = CGSizeMake(200, 200); + static ASSizeRange kSize = ASSizeRangeMake(exactSize, exactSize); + [self testStackLayoutSpec:stackLayoutSpec sizeRange:kSize subnodes:subnodes identifier:identifier]; +} + - (void)testStackLayoutSpecWithStyle:(ASStackLayoutSpecStyle)style sizeRange:(ASSizeRange)sizeRange subnodes:(NSArray *)subnodes @@ -76,13 +96,23 @@ static NSArray *defaultSubnodesWithSameSize(CGSize subnodeSize, BOOL flex) sizeRange:(ASSizeRange)sizeRange subnodes:(NSArray *)subnodes identifier:(NSString *)identifier +{ + ASStackLayoutSpec *stackLayoutSpec = [ASStackLayoutSpec stackLayoutSpecWithDirection:style.direction + spacing:style.spacing + justifyContent:style.justifyContent + alignItems:style.alignItems + children:children]; + [self testStackLayoutSpec:stackLayoutSpec sizeRange:sizeRange subnodes:subnodes identifier:identifier]; +} + +- (void)testStackLayoutSpec:(ASStackLayoutSpec *)stackLayoutSpec + sizeRange:(ASSizeRange)sizeRange + subnodes:(NSArray *)subnodes + identifier:(NSString *)identifier { ASDisplayNode *backgroundNode = ASDisplayNodeWithBackgroundColor([UIColor whiteColor]); - ASLayoutSpec *layoutSpec = - [ASBackgroundLayoutSpec - backgroundLayoutSpecWithChild:[ASStackLayoutSpec stackLayoutSpecWithDirection:style.direction spacing:style.spacing justifyContent:style.justifyContent alignItems:style.alignItems children:children] - background:backgroundNode]; + ASLayoutSpec *layoutSpec = [ASBackgroundLayoutSpec backgroundLayoutSpecWithChild:stackLayoutSpec background:backgroundNode]; NSMutableArray *newSubnodes = [NSMutableArray arrayWithObject:backgroundNode]; [newSubnodes addObjectsFromArray:subnodes]; @@ -90,6 +120,8 @@ static NSArray *defaultSubnodesWithSameSize(CGSize subnodeSize, BOOL flex) [self testLayoutSpec:layoutSpec sizeRange:sizeRange subnodes:newSubnodes identifier:identifier]; } +#pragma mark - + - (void)testUnderflowBehaviors { // width 300px; height 0-300px @@ -522,4 +554,54 @@ static NSArray *defaultSubnodesWithSameSize(CGSize subnodeSize, BOOL flex) [self testStackLayoutSpecWithStyle:style sizeRange:kSize subnodes:subnodes identifier:nil]; } +- (void)testHorizontalAndVerticalAlignments +{ + [self testStackLayoutSpecWithDirection:ASStackLayoutDirectionHorizontal itemsHorizontalAlignment:ASAlignmentLeft itemsVerticalAlignment:ASAlignmentTop identifier:@"horizontalTopLeft"]; + [self testStackLayoutSpecWithDirection:ASStackLayoutDirectionHorizontal itemsHorizontalAlignment:ASAlignmentMiddle itemsVerticalAlignment:ASAlignmentCenter identifier:@"horizontalCenter"]; + [self testStackLayoutSpecWithDirection:ASStackLayoutDirectionHorizontal itemsHorizontalAlignment:ASAlignmentRight itemsVerticalAlignment:ASAlignmentBottom identifier:@"horizontalBottomRight"]; + [self testStackLayoutSpecWithDirection:ASStackLayoutDirectionVertical itemsHorizontalAlignment:ASAlignmentLeft itemsVerticalAlignment:ASAlignmentTop identifier:@"verticalTopLeft"]; + [self testStackLayoutSpecWithDirection:ASStackLayoutDirectionVertical itemsHorizontalAlignment:ASAlignmentMiddle itemsVerticalAlignment:ASAlignmentCenter identifier:@"verticalCenter"]; + [self testStackLayoutSpecWithDirection:ASStackLayoutDirectionVertical itemsHorizontalAlignment:ASAlignmentRight itemsVerticalAlignment:ASAlignmentBottom identifier:@"verticalBottomRight"]; +} + +- (void)testDirectionChangeAfterSettingHorizontalAndVerticalAlignments +{ + ASStackLayoutSpec *stackLayoutSpec = [[ASStackLayoutSpec alloc] init]; // Default direction is horizontal + stackLayoutSpec.horizontalAlignment = ASAlignmentRight; + stackLayoutSpec.verticalAlignment = ASAlignmentCenter; + XCTAssertEqual(stackLayoutSpec.alignItems, ASStackLayoutAlignItemsCenter); + XCTAssertEqual(stackLayoutSpec.justifyContent, ASStackLayoutJustifyContentEnd); + + stackLayoutSpec.direction = ASStackLayoutDirectionVertical; + XCTAssertEqual(stackLayoutSpec.alignItems, ASStackLayoutAlignItemsEnd); + XCTAssertEqual(stackLayoutSpec.justifyContent, ASStackLayoutJustifyContentCenter); +} + +- (void)testAlignItemsAndJustifyContentRestrictionsIfHorizontalAndVerticalAlignmentsAreUsed +{ + ASStackLayoutSpec *stackLayoutSpec = [[ASStackLayoutSpec alloc] init]; + + // No assertions should be thrown here because alignments are not used + stackLayoutSpec.alignItems = ASStackLayoutAlignItemsEnd; + stackLayoutSpec.justifyContent = ASStackLayoutJustifyContentEnd; + + // Set alignments and assert that assertions are thrown + stackLayoutSpec.horizontalAlignment = ASAlignmentMiddle; + stackLayoutSpec.verticalAlignment = ASAlignmentCenter; + XCTAssertThrows(stackLayoutSpec.alignItems = ASStackLayoutAlignItemsEnd); + XCTAssertThrows(stackLayoutSpec.justifyContent = ASStackLayoutJustifyContentEnd); + + // Unset alignments. alignItems and justifyContent should not be changed + stackLayoutSpec.horizontalAlignment = ASHorizontalAlignmentNone; + stackLayoutSpec.verticalAlignment = ASVerticalAlignmentNone; + XCTAssertEqual(stackLayoutSpec.alignItems, ASStackLayoutAlignItemsCenter); + XCTAssertEqual(stackLayoutSpec.justifyContent, ASStackLayoutJustifyContentCenter); + + // Now that alignments are none, setting alignItems and justifyContent should be allowed again + stackLayoutSpec.alignItems = ASStackLayoutAlignItemsEnd; + stackLayoutSpec.justifyContent = ASStackLayoutJustifyContentEnd; + XCTAssertEqual(stackLayoutSpec.alignItems, ASStackLayoutAlignItemsEnd); + XCTAssertEqual(stackLayoutSpec.justifyContent, ASStackLayoutJustifyContentEnd); +} + @end diff --git a/AsyncDisplayKitTests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testHorizontalAndVerticalAlignments_horizontalBottomRight@2x.png b/AsyncDisplayKitTests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testHorizontalAndVerticalAlignments_horizontalBottomRight@2x.png new file mode 100644 index 0000000000..489764fe6e Binary files /dev/null and b/AsyncDisplayKitTests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testHorizontalAndVerticalAlignments_horizontalBottomRight@2x.png differ diff --git a/AsyncDisplayKitTests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testHorizontalAndVerticalAlignments_horizontalCenter@2x.png b/AsyncDisplayKitTests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testHorizontalAndVerticalAlignments_horizontalCenter@2x.png new file mode 100644 index 0000000000..eb0ea8c3da Binary files /dev/null and b/AsyncDisplayKitTests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testHorizontalAndVerticalAlignments_horizontalCenter@2x.png differ diff --git a/AsyncDisplayKitTests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testHorizontalAndVerticalAlignments_horizontalTopLeft@2x.png b/AsyncDisplayKitTests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testHorizontalAndVerticalAlignments_horizontalTopLeft@2x.png new file mode 100644 index 0000000000..8864402768 Binary files /dev/null and b/AsyncDisplayKitTests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testHorizontalAndVerticalAlignments_horizontalTopLeft@2x.png differ diff --git a/AsyncDisplayKitTests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testHorizontalAndVerticalAlignments_verticalBottomRight@2x.png b/AsyncDisplayKitTests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testHorizontalAndVerticalAlignments_verticalBottomRight@2x.png new file mode 100644 index 0000000000..4e46d0f9dd Binary files /dev/null and b/AsyncDisplayKitTests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testHorizontalAndVerticalAlignments_verticalBottomRight@2x.png differ diff --git a/AsyncDisplayKitTests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testHorizontalAndVerticalAlignments_verticalCenter@2x.png b/AsyncDisplayKitTests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testHorizontalAndVerticalAlignments_verticalCenter@2x.png new file mode 100644 index 0000000000..a0b412886f Binary files /dev/null and b/AsyncDisplayKitTests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testHorizontalAndVerticalAlignments_verticalCenter@2x.png differ diff --git a/AsyncDisplayKitTests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testHorizontalAndVerticalAlignments_verticalTopLeft@2x.png b/AsyncDisplayKitTests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testHorizontalAndVerticalAlignments_verticalTopLeft@2x.png new file mode 100644 index 0000000000..8277b97c00 Binary files /dev/null and b/AsyncDisplayKitTests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testHorizontalAndVerticalAlignments_verticalTopLeft@2x.png differ