diff --git a/AsyncDisplayKit/Layout/ASStackLayoutDefines.h b/AsyncDisplayKit/Layout/ASStackLayoutDefines.h index 47d72b34d6..2ed646ab67 100644 --- a/AsyncDisplayKit/Layout/ASStackLayoutDefines.h +++ b/AsyncDisplayKit/Layout/ASStackLayoutDefines.h @@ -33,6 +33,21 @@ typedef NS_ENUM(NSUInteger, ASStackLayoutJustifyContent) { On underflow, children are right/bottom-aligned within this spec's bounds. */ ASStackLayoutJustifyContentEnd, + /** + On overflow or if the stack has only 1 child, this value is identical to ASStackLayoutJustifyContentStart. + Otherwise, the starting edge of the first child is at the starting edge of the stack, + the ending edge of the last child is at the ending edge of the stack, and the remaining children + are distributed so that the spacing between any two adjacent ones is the same. + If there is a remaining space after spacing division, it is combined with the last spacing (i.e the one between the last 2 children). + */ + ASStackLayoutJustifyContentSpaceBetween, + /** + On overflow or if the stack has only 1 child, this value is identical to ASStackLayoutJustifyContentCenter. + Otherwise, children are distributed such that the spacing between any two adjacent ones is the same, + and the spacing between the first/last child and the stack edges is half the size of the spacing between children. + If there is a remaining space after spacing division, it is combined with the last spacing (i.e the one between the last child and the stack ending edge). + */ + ASStackLayoutJustifyContentSpaceAround }; /** Orientation of children along cross axis */ diff --git a/AsyncDisplayKit/Layout/ASStackLayoutSpec.mm b/AsyncDisplayKit/Layout/ASStackLayoutSpec.mm index ed07b7d6e0..facbbb1e1c 100644 --- a/AsyncDisplayKit/Layout/ASStackLayoutSpec.mm +++ b/AsyncDisplayKit/Layout/ASStackLayoutSpec.mm @@ -137,6 +137,10 @@ - (ASLayout *)measureWithSizeRange:(ASSizeRange)constrainedSize { + if (self.children.count == 0) { + return [ASLayout layoutWithLayoutableObject:self size:constrainedSize.min]; + } + ASStackLayoutSpecStyle style = {.direction = _direction, .spacing = _spacing, .justifyContent = _justifyContent, .alignItems = _alignItems, .baselineRelativeArrangement = _baselineRelativeArrangement}; BOOL needsBaselinePass = _baselineRelativeArrangement || _alignItems == ASStackLayoutAlignItemsBaselineFirst || _alignItems == ASStackLayoutAlignItemsBaselineLast; diff --git a/AsyncDisplayKit/Private/ASStackPositionedLayout.mm b/AsyncDisplayKit/Private/ASStackPositionedLayout.mm index 4d23cf6786..428319d317 100644 --- a/AsyncDisplayKit/Private/ASStackPositionedLayout.mm +++ b/AsyncDisplayKit/Private/ASStackPositionedLayout.mm @@ -15,6 +15,7 @@ #import "ASStackLayoutSpecUtilities.h" #import "ASLayoutable.h" #import "ASLayoutOptions.h" +#import "ASAssert.h" static CGFloat crossOffset(const ASStackLayoutSpecStyle &style, const ASStackUnpositionedItem &l, @@ -33,8 +34,20 @@ static CGFloat crossOffset(const ASStackLayoutSpecStyle &style, } } +/** + * Positions children according to the stack style and positioning properties. + * + * @param style The layout style of the overall stack layout + * @param firstChildOffset Offset of the first child + * @param extraSpacing Spacing between children, in addition to spacing set to the stack's layout style + * @param lastChildOffset Offset of the last child + * @param unpositionedLayout Unpositioned children of the stack + * @param constrainedSize Constrained size of the stack + */ static ASStackPositionedLayout stackedLayout(const ASStackLayoutSpecStyle &style, - const CGFloat offset, + const CGFloat firstChildOffset, + const CGFloat extraSpacing, + const CGFloat lastChildOffset, const ASStackUnpositionedLayout &unpositionedLayout, const ASSizeRange &constrainedSize) { @@ -48,12 +61,16 @@ static ASStackPositionedLayout stackedLayout(const ASStackLayoutSpecStyle &style const auto maxCrossSize = crossDimension(style.direction, constrainedSize.max); const CGFloat crossSize = MIN(MAX(minCrossSize, largestChildCrossSize), maxCrossSize); - CGPoint p = directionPoint(style.direction, offset, 0); + CGPoint p = directionPoint(style.direction, firstChildOffset, 0); BOOL first = YES; + const auto lastChild = unpositionedLayout.items.back().child; + CGFloat offset = 0; + auto stackedChildren = AS::map(unpositionedLayout.items, [&](const ASStackUnpositionedItem &l) -> ASLayout *{ - p = p + directionPoint(style.direction, l.child.spacingBefore, 0); + offset = (l.child == lastChild) ? lastChildOffset : 0; + p = p + directionPoint(style.direction, l.child.spacingBefore + offset, 0); if (!first) { - p = p + directionPoint(style.direction, style.spacing, 0); + p = p + directionPoint(style.direction, style.spacing + extraSpacing, 0); } first = NO; l.layout.position = p + directionPoint(style.direction, 0, crossOffset(style, l, crossSize)); @@ -64,16 +81,45 @@ static ASStackPositionedLayout stackedLayout(const ASStackLayoutSpecStyle &style return {stackedChildren, crossSize}; } +static ASStackPositionedLayout stackedLayout(const ASStackLayoutSpecStyle &style, + const CGFloat firstChildOffset, + const ASStackUnpositionedLayout &unpositionedLayout, + const ASSizeRange &constrainedSize) +{ + return stackedLayout(style, firstChildOffset, 0, 0, unpositionedLayout, constrainedSize); +} + ASStackPositionedLayout ASStackPositionedLayout::compute(const ASStackUnpositionedLayout &unpositionedLayout, const ASStackLayoutSpecStyle &style, const ASSizeRange &constrainedSize) { - switch (style.justifyContent) { + const auto numOfItems = unpositionedLayout.items.size(); + ASDisplayNodeCAssertTrue(numOfItems > 0); + const CGFloat violation = unpositionedLayout.violation; + ASStackLayoutJustifyContent justifyContent = style.justifyContent; + + // Handle edge cases of "space between" and "space around" + if (justifyContent == ASStackLayoutJustifyContentSpaceBetween && (violation < 0 || numOfItems == 1)) { + justifyContent = ASStackLayoutJustifyContentStart; + } else if (justifyContent == ASStackLayoutJustifyContentSpaceAround && (violation < 0 || numOfItems == 1)) { + justifyContent = ASStackLayoutJustifyContentCenter; + } + + switch (justifyContent) { case ASStackLayoutJustifyContentStart: return stackedLayout(style, 0, unpositionedLayout, constrainedSize); case ASStackLayoutJustifyContentCenter: - return stackedLayout(style, floorf(unpositionedLayout.violation / 2), unpositionedLayout, constrainedSize); + return stackedLayout(style, floorf(violation / 2), unpositionedLayout, constrainedSize); case ASStackLayoutJustifyContentEnd: - return stackedLayout(style, unpositionedLayout.violation, unpositionedLayout, constrainedSize); + return stackedLayout(style, violation, unpositionedLayout, constrainedSize); + case ASStackLayoutJustifyContentSpaceBetween: { + const auto numOfSpacings = numOfItems - 1; + return stackedLayout(style, 0, floorf(violation / numOfSpacings), fmodf(violation, numOfSpacings), unpositionedLayout, constrainedSize); + } + case ASStackLayoutJustifyContentSpaceAround: { + // Spacing between items are twice the spacing on the edges + CGFloat spacingUnit = floorf(violation / (numOfItems * 2)); + return stackedLayout(style, spacingUnit, spacingUnit * 2, 0, unpositionedLayout, constrainedSize); + } } } diff --git a/AsyncDisplayKitTests/ASStackLayoutSpecSnapshotTests.mm b/AsyncDisplayKitTests/ASStackLayoutSpecSnapshotTests.mm index 5c3733e707..ba98a1f76a 100644 --- a/AsyncDisplayKitTests/ASStackLayoutSpecSnapshotTests.mm +++ b/AsyncDisplayKitTests/ASStackLayoutSpecSnapshotTests.mm @@ -130,6 +130,8 @@ static NSArray *defaultSubnodesWithSameSize(CGSize subnodeSize, BOOL flex) [self testStackLayoutSpecWithJustify:ASStackLayoutJustifyContentCenter flex:NO sizeRange:kSize identifier:@"justifyCenter"]; [self testStackLayoutSpecWithJustify:ASStackLayoutJustifyContentEnd flex:NO sizeRange:kSize identifier:@"justifyEnd"]; [self testStackLayoutSpecWithJustify:ASStackLayoutJustifyContentStart flex:YES sizeRange:kSize identifier:@"flex"]; + [self testStackLayoutSpecWithJustify:ASStackLayoutJustifyContentSpaceBetween flex:NO sizeRange:kSize identifier:@"justifySpaceBetween"]; + [self testStackLayoutSpecWithJustify:ASStackLayoutJustifyContentSpaceAround flex:NO sizeRange:kSize identifier:@"justifySpaceAround"]; } - (void)testOverflowBehaviors @@ -140,6 +142,10 @@ static NSArray *defaultSubnodesWithSameSize(CGSize subnodeSize, BOOL flex) [self testStackLayoutSpecWithJustify:ASStackLayoutJustifyContentCenter flex:NO sizeRange:kSize identifier:@"justifyCenter"]; [self testStackLayoutSpecWithJustify:ASStackLayoutJustifyContentEnd flex:NO sizeRange:kSize identifier:@"justifyEnd"]; [self testStackLayoutSpecWithJustify:ASStackLayoutJustifyContentStart flex:YES sizeRange:kSize identifier:@"flex"]; + // On overflow, "space between" is identical to "content start" + [self testStackLayoutSpecWithJustify:ASStackLayoutJustifyContentSpaceBetween flex:NO sizeRange:kSize identifier:@"justifyStart"]; + // On overflow, "space around" is identical to "content center" + [self testStackLayoutSpecWithJustify:ASStackLayoutJustifyContentSpaceAround flex:NO sizeRange:kSize identifier:@"justifyCenter"]; } - (void)testOverflowBehaviorsWhenAllFlexShrinkChildrenHaveBeenClampedToZeroButViolationStillExists @@ -278,6 +284,50 @@ static NSArray *defaultSubnodesWithSameSize(CGSize subnodeSize, BOOL flex) [self testStackLayoutSpecWithStyle:style sizeRange:kVariableHeight subnodes:subnodes identifier:@"variableHeight"]; } +- (void)testJustifiedSpaceBetweenWithOneChild +{ + ASStackLayoutSpecStyle style = { + .direction = ASStackLayoutDirectionHorizontal, + .justifyContent = ASStackLayoutJustifyContentSpaceBetween + }; + + ASStaticSizeDisplayNode *child = ASDisplayNodeWithBackgroundColor([UIColor redColor]); + child.staticSize = {50, 50}; + + // width 300px; height 0-INF + static ASSizeRange kVariableHeight = {{300, 0}, {300, INFINITY}}; + [self testStackLayoutSpecWithStyle:style sizeRange:kVariableHeight subnodes:@[child] identifier:nil]; +} + +- (void)testJustifiedSpaceAroundWithOneChild +{ + ASStackLayoutSpecStyle style = { + .direction = ASStackLayoutDirectionHorizontal, + .justifyContent = ASStackLayoutJustifyContentSpaceAround + }; + + ASStaticSizeDisplayNode *child = ASDisplayNodeWithBackgroundColor([UIColor redColor]); + child.staticSize = {50, 50}; + + // width 300px; height 0-INF + static ASSizeRange kVariableHeight = {{300, 0}, {300, INFINITY}}; + [self testStackLayoutSpecWithStyle:style sizeRange:kVariableHeight subnodes:@[child] identifier:nil]; +} + +- (void)testJustifiedSpaceBetweenWithRemainingSpace +{ + // width 301px; height 0-300px; 1px remaining + static ASSizeRange kSize = {{301, 0}, {301, 300}}; + [self testStackLayoutSpecWithJustify:ASStackLayoutJustifyContentSpaceBetween flex:NO sizeRange:kSize identifier:nil]; +} + +- (void)testJustifiedSpaceAroundWithRemainingSpace +{ + // width 305px; height 0-300px; 5px remaining + static ASSizeRange kSize = {{305, 0}, {305, 300}}; + [self testStackLayoutSpecWithJustify:ASStackLayoutJustifyContentSpaceAround flex:NO sizeRange:kSize identifier:nil]; +} + - (void)testChildThatChangesCrossSizeWhenMainSizeIsFlexed { ASStackLayoutSpecStyle style = {.direction = ASStackLayoutDirectionHorizontal}; diff --git a/AsyncDisplayKitTests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testJustifiedSpaceAroundWithOneChild@2x.png b/AsyncDisplayKitTests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testJustifiedSpaceAroundWithOneChild@2x.png new file mode 100644 index 0000000000..da37b62262 Binary files /dev/null and b/AsyncDisplayKitTests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testJustifiedSpaceAroundWithOneChild@2x.png differ diff --git a/AsyncDisplayKitTests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testJustifiedSpaceAroundWithRemainingSpace@2x.png b/AsyncDisplayKitTests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testJustifiedSpaceAroundWithRemainingSpace@2x.png new file mode 100644 index 0000000000..088b49a293 Binary files /dev/null and b/AsyncDisplayKitTests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testJustifiedSpaceAroundWithRemainingSpace@2x.png differ diff --git a/AsyncDisplayKitTests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testJustifiedSpaceBetweenWithOneChild@2x.png b/AsyncDisplayKitTests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testJustifiedSpaceBetweenWithOneChild@2x.png new file mode 100644 index 0000000000..88d0aaf203 Binary files /dev/null and b/AsyncDisplayKitTests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testJustifiedSpaceBetweenWithOneChild@2x.png differ diff --git a/AsyncDisplayKitTests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testJustifiedSpaceBetweenWithRemainingSpace@2x.png b/AsyncDisplayKitTests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testJustifiedSpaceBetweenWithRemainingSpace@2x.png new file mode 100644 index 0000000000..4f85986130 Binary files /dev/null and b/AsyncDisplayKitTests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testJustifiedSpaceBetweenWithRemainingSpace@2x.png differ diff --git a/AsyncDisplayKitTests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testUnderflowBehaviors_justifySpaceAround@2x.png b/AsyncDisplayKitTests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testUnderflowBehaviors_justifySpaceAround@2x.png new file mode 100644 index 0000000000..1d71834f70 Binary files /dev/null and b/AsyncDisplayKitTests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testUnderflowBehaviors_justifySpaceAround@2x.png differ diff --git a/AsyncDisplayKitTests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testUnderflowBehaviors_justifySpaceBetween@2x.png b/AsyncDisplayKitTests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testUnderflowBehaviors_justifySpaceBetween@2x.png new file mode 100644 index 0000000000..49943caaa8 Binary files /dev/null and b/AsyncDisplayKitTests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testUnderflowBehaviors_justifySpaceBetween@2x.png differ