From e48edef4e77a4cd532df5f3bf2162100147f7a87 Mon Sep 17 00:00:00 2001 From: Huy Nguyen Date: Sun, 26 Feb 2017 19:48:32 +0000 Subject: [PATCH] [ASStackLayoutSpec] Implement flex wrap (#2914) * Implement flex wrap * Add tests for content alignments * Revert unnecessary changes * More flex wrap tests * Define FB_REFERENCE_IMAGE_DIR in scheme * Clean up ASStackPositionedLayout --- AsyncDisplayKit.xcodeproj/project.pbxproj | 7 +- .../xcschemes/AsyncDisplayKit.xcscheme | 7 + AsyncDisplayKit/Layout/ASStackLayoutDefines.h | 16 + AsyncDisplayKit/Layout/ASStackLayoutSpec.h | 26 +- AsyncDisplayKit/Layout/ASStackLayoutSpec.mm | 25 +- .../Layout/ASStackLayoutSpecUtilities.h | 6 + .../Private/Layout/ASStackPositionedLayout.h | 4 +- .../Private/Layout/ASStackPositionedLayout.mm | 200 ++++++--- .../Layout/ASStackUnpositionedLayout.h | 37 +- .../Layout/ASStackUnpositionedLayout.mm | 385 +++++++++++++----- .../ASStackLayoutSpecSnapshotTests.mm | 106 ++++- ...nContentOverflow_alignContentCenter@2x.png | Bin 0 -> 1021 bytes ...lignContentOverflow_alignContentEnd@2x.png | Bin 0 -> 890 bytes ...entOverflow_alignContentSpaceAround@2x.png | Bin 0 -> 1021 bytes ...ntOverflow_alignContentSpaceBetween@2x.png | Bin 0 -> 916 bytes ...gnContentOverflow_alignContentStart@2x.png | Bin 0 -> 916 bytes ...ContentUnderflow_alignContentCenter@2x.png | Bin 0 -> 3619 bytes ...ignContentUnderflow_alignContentEnd@2x.png | Bin 0 -> 3239 bytes ...ntUnderflow_alignContentSpaceAround@2x.png | Bin 0 -> 3678 bytes ...tUnderflow_alignContentSpaceBetween@2x.png | Bin 0 -> 3600 bytes ...nContentUnderflow_alignContentStart@2x.png | Bin 0 -> 3239 bytes ...nContentOverflow_alignContentCenter@2x.png | Bin 0 -> 1627 bytes ...lignContentOverflow_alignContentEnd@2x.png | Bin 0 -> 1650 bytes ...entOverflow_alignContentSpaceAround@2x.png | Bin 0 -> 1627 bytes ...ntOverflow_alignContentSpaceBetween@2x.png | Bin 0 -> 1649 bytes ...gnContentOverflow_alignContentStart@2x.png | Bin 0 -> 1649 bytes ...ignContentStretchAndOtherAlignments@2x.png | Bin 0 -> 4916 bytes ...ContentUnderflow_alignContentCenter@2x.png | Bin 0 -> 5122 bytes ...ignContentUnderflow_alignContentEnd@2x.png | Bin 0 -> 4971 bytes ...ntUnderflow_alignContentSpaceAround@2x.png | Bin 0 -> 5023 bytes ...tUnderflow_alignContentSpaceBetween@2x.png | Bin 0 -> 5130 bytes ...nContentUnderflow_alignContentStart@2x.png | Bin 0 -> 4971 bytes ...ontentUnderflow_alignContentStretch@2x.png | Bin 0 -> 5089 bytes ...gnContentWithUnconstrainedCrossSize@2x.png | Bin 0 -> 3300 bytes 34 files changed, 616 insertions(+), 203 deletions(-) create mode 100644 AsyncDisplayKitTests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testAlignContentOverflow_alignContentCenter@2x.png create mode 100644 AsyncDisplayKitTests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testAlignContentOverflow_alignContentEnd@2x.png create mode 100644 AsyncDisplayKitTests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testAlignContentOverflow_alignContentSpaceAround@2x.png create mode 100644 AsyncDisplayKitTests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testAlignContentOverflow_alignContentSpaceBetween@2x.png create mode 100644 AsyncDisplayKitTests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testAlignContentOverflow_alignContentStart@2x.png create mode 100644 AsyncDisplayKitTests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testAlignContentUnderflow_alignContentCenter@2x.png create mode 100644 AsyncDisplayKitTests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testAlignContentUnderflow_alignContentEnd@2x.png create mode 100644 AsyncDisplayKitTests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testAlignContentUnderflow_alignContentSpaceAround@2x.png create mode 100644 AsyncDisplayKitTests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testAlignContentUnderflow_alignContentSpaceBetween@2x.png create mode 100644 AsyncDisplayKitTests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testAlignContentUnderflow_alignContentStart@2x.png create mode 100644 AsyncDisplayKitTests/ReferenceImages_iOS_10/ASStackLayoutSpecSnapshotTests/testAlignContentOverflow_alignContentCenter@2x.png create mode 100644 AsyncDisplayKitTests/ReferenceImages_iOS_10/ASStackLayoutSpecSnapshotTests/testAlignContentOverflow_alignContentEnd@2x.png create mode 100644 AsyncDisplayKitTests/ReferenceImages_iOS_10/ASStackLayoutSpecSnapshotTests/testAlignContentOverflow_alignContentSpaceAround@2x.png create mode 100644 AsyncDisplayKitTests/ReferenceImages_iOS_10/ASStackLayoutSpecSnapshotTests/testAlignContentOverflow_alignContentSpaceBetween@2x.png create mode 100644 AsyncDisplayKitTests/ReferenceImages_iOS_10/ASStackLayoutSpecSnapshotTests/testAlignContentOverflow_alignContentStart@2x.png create mode 100644 AsyncDisplayKitTests/ReferenceImages_iOS_10/ASStackLayoutSpecSnapshotTests/testAlignContentStretchAndOtherAlignments@2x.png create mode 100644 AsyncDisplayKitTests/ReferenceImages_iOS_10/ASStackLayoutSpecSnapshotTests/testAlignContentUnderflow_alignContentCenter@2x.png create mode 100644 AsyncDisplayKitTests/ReferenceImages_iOS_10/ASStackLayoutSpecSnapshotTests/testAlignContentUnderflow_alignContentEnd@2x.png create mode 100644 AsyncDisplayKitTests/ReferenceImages_iOS_10/ASStackLayoutSpecSnapshotTests/testAlignContentUnderflow_alignContentSpaceAround@2x.png create mode 100644 AsyncDisplayKitTests/ReferenceImages_iOS_10/ASStackLayoutSpecSnapshotTests/testAlignContentUnderflow_alignContentSpaceBetween@2x.png create mode 100644 AsyncDisplayKitTests/ReferenceImages_iOS_10/ASStackLayoutSpecSnapshotTests/testAlignContentUnderflow_alignContentStart@2x.png create mode 100644 AsyncDisplayKitTests/ReferenceImages_iOS_10/ASStackLayoutSpecSnapshotTests/testAlignContentUnderflow_alignContentStretch@2x.png create mode 100644 AsyncDisplayKitTests/ReferenceImages_iOS_10/ASStackLayoutSpecSnapshotTests/testAlignContentWithUnconstrainedCrossSize@2x.png diff --git a/AsyncDisplayKit.xcodeproj/project.pbxproj b/AsyncDisplayKit.xcodeproj/project.pbxproj index fcf1c500b0..bea602effe 100644 --- a/AsyncDisplayKit.xcodeproj/project.pbxproj +++ b/AsyncDisplayKit.xcodeproj/project.pbxproj @@ -1996,6 +1996,7 @@ COPY_PHASE_STRIP = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; GCC_INSTRUMENT_PROGRAM_FLOW_ARCS = NO; + GCC_PREPROCESSOR_DEFINITIONS = ""; INFOPLIST_FILE = AsyncDisplayKitTestHost/Info.plist; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; MTL_ENABLE_DEBUG_INFO = NO; @@ -2106,7 +2107,6 @@ "DEBUG=1", "$(inherited)", "COCOAPODS=1", - "FB_REFERENCE_IMAGE_DIR=\"\\\"$(SOURCE_ROOT)/$(PROJECT_NAME)Tests/ReferenceImages\\\"\"", ); GCC_TREAT_WARNINGS_AS_ERRORS = YES; GCC_WARN_INITIALIZER_NOT_FULLY_BRACKETED = YES; @@ -2132,7 +2132,6 @@ GCC_PREPROCESSOR_DEFINITIONS = ( "$(inherited)", "COCOAPODS=1", - "FB_REFERENCE_IMAGE_DIR=\"\\\"$(SOURCE_ROOT)/$(PROJECT_NAME)Tests/ReferenceImages\\\"\"", ); GCC_TREAT_WARNINGS_AS_ERRORS = YES; GCC_WARN_INITIALIZER_NOT_FULLY_BRACKETED = YES; @@ -2193,6 +2192,7 @@ GCC_NO_COMMON_BLOCKS = YES; GCC_PRECOMPILE_PREFIX_HEADER = YES; GCC_PREFIX_HEADER = "AsyncDisplayKit/AsyncDisplayKit-Prefix.pch"; + GCC_PREPROCESSOR_DEFINITIONS = ""; INFOPLIST_FILE = "$(SRCROOT)/AsyncDisplayKit/Info.plist"; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; @@ -2260,7 +2260,6 @@ GCC_PREPROCESSOR_DEFINITIONS = ( "$(inherited)", "COCOAPODS=1", - "FB_REFERENCE_IMAGE_DIR=\"\\\"$(SOURCE_ROOT)/$(PROJECT_NAME)Tests/ReferenceImages\\\"\"", ); GCC_TREAT_WARNINGS_AS_ERRORS = YES; GCC_WARN_INITIALIZER_NOT_FULLY_BRACKETED = YES; @@ -2281,6 +2280,7 @@ COPY_PHASE_STRIP = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; GCC_INSTRUMENT_PROGRAM_FLOW_ARCS = NO; + GCC_PREPROCESSOR_DEFINITIONS = ""; INFOPLIST_FILE = AsyncDisplayKitTestHost/Info.plist; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; MTL_ENABLE_DEBUG_INFO = NO; @@ -2307,6 +2307,7 @@ GCC_NO_COMMON_BLOCKS = YES; GCC_PRECOMPILE_PREFIX_HEADER = YES; GCC_PREFIX_HEADER = "AsyncDisplayKit/AsyncDisplayKit-Prefix.pch"; + GCC_PREPROCESSOR_DEFINITIONS = ""; INFOPLIST_FILE = "$(SRCROOT)/AsyncDisplayKit/Info.plist"; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; diff --git a/AsyncDisplayKit.xcodeproj/xcshareddata/xcschemes/AsyncDisplayKit.xcscheme b/AsyncDisplayKit.xcodeproj/xcshareddata/xcschemes/AsyncDisplayKit.xcscheme index 5421b65a77..f37a5b1a83 100644 --- a/AsyncDisplayKit.xcodeproj/xcshareddata/xcschemes/AsyncDisplayKit.xcscheme +++ b/AsyncDisplayKit.xcodeproj/xcshareddata/xcschemes/AsyncDisplayKit.xcscheme @@ -75,6 +75,13 @@ ReferencedContainer = "container:AsyncDisplayKit.xcodeproj"> + + + + diff --git a/AsyncDisplayKit/Layout/ASStackLayoutDefines.h b/AsyncDisplayKit/Layout/ASStackLayoutDefines.h index 772c1022e4..79281d73dc 100644 --- a/AsyncDisplayKit/Layout/ASStackLayoutDefines.h +++ b/AsyncDisplayKit/Layout/ASStackLayoutDefines.h @@ -86,6 +86,22 @@ typedef NS_ENUM(NSUInteger, ASStackLayoutAlignSelf) { ASStackLayoutAlignSelfStretch, }; +// TODO documentation +typedef NS_ENUM(NSUInteger, ASStackLayoutFlexWrap) { + ASStackLayoutFlexWrapNoWrap, + ASStackLayoutFlexWrapWrap, +}; + +// TODO documentation +typedef NS_ENUM(NSUInteger, ASStackLayoutAlignContent) { + ASStackLayoutAlignContentStart, + ASStackLayoutAlignContentCenter, + ASStackLayoutAlignContentEnd, + ASStackLayoutAlignContentSpaceBetween, + ASStackLayoutAlignContentSpaceAround, + ASStackLayoutAlignContentStretch, +}; + /** Orientation of children along horizontal axis */ typedef NS_ENUM(NSUInteger, ASHorizontalAlignment) { /** No alignment specified. Default value */ diff --git a/AsyncDisplayKit/Layout/ASStackLayoutSpec.h b/AsyncDisplayKit/Layout/ASStackLayoutSpec.h index d805f1e9dc..d218db7b42 100644 --- a/AsyncDisplayKit/Layout/ASStackLayoutSpec.h +++ b/AsyncDisplayKit/Layout/ASStackLayoutSpec.h @@ -59,6 +59,10 @@ NS_ASSUME_NONNULL_BEGIN @property (nonatomic, assign) ASStackLayoutJustifyContent justifyContent; /** Orientation of children along cross axis. Defaults to ASStackLayoutAlignItemsStretch */ @property (nonatomic, assign) ASStackLayoutAlignItems alignItems; +//TODO documentation. Defaults to ASStackLayoutFlexWrapNoWrap +@property (nonatomic, assign) ASStackLayoutFlexWrap flexWrap; +//TODO documentation. Defaults to ASStackLayoutAlignContentStart +@property (nonatomic, assign) ASStackLayoutAlignContent alignContent; - (instancetype)init; @@ -69,7 +73,27 @@ NS_ASSUME_NONNULL_BEGIN @param alignItems Orientation of the children along the cross axis @param children ASLayoutElement children to be positioned. */ -+ (instancetype)stackLayoutSpecWithDirection:(ASStackLayoutDirection)direction spacing:(CGFloat)spacing justifyContent:(ASStackLayoutJustifyContent)justifyContent alignItems:(ASStackLayoutAlignItems)alignItems children:(NSArray> *)children AS_WARN_UNUSED_RESULT; ++ (instancetype)stackLayoutSpecWithDirection:(ASStackLayoutDirection)direction + spacing:(CGFloat)spacing + justifyContent:(ASStackLayoutJustifyContent)justifyContent + alignItems:(ASStackLayoutAlignItems)alignItems + children:(NSArray> *)children AS_WARN_UNUSED_RESULT; + +/** + @param direction The direction of the stack view (horizontal or vertical) + @param spacing The spacing between the children + @param justifyContent If no children are flexible, this describes how to fill any extra space + @param alignItems Orientation of the children along the cross axis + @param children ASLayoutElement children to be positioned. + TODO documentation flex wrap and align content + */ ++ (instancetype)stackLayoutSpecWithDirection:(ASStackLayoutDirection)direction + spacing:(CGFloat)spacing + justifyContent:(ASStackLayoutJustifyContent)justifyContent + alignItems:(ASStackLayoutAlignItems)alignItems + flexWrap:(ASStackLayoutFlexWrap)flexWrap + alignContent:(ASStackLayoutAlignContent)alignContent + children:(NSArray> *)children AS_WARN_UNUSED_RESULT; /** * @return A stack layout spec with direction of ASStackLayoutDirectionVertical diff --git a/AsyncDisplayKit/Layout/ASStackLayoutSpec.mm b/AsyncDisplayKit/Layout/ASStackLayoutSpec.mm index 1089592f91..1a78a5428c 100644 --- a/AsyncDisplayKit/Layout/ASStackLayoutSpec.mm +++ b/AsyncDisplayKit/Layout/ASStackLayoutSpec.mm @@ -25,12 +25,17 @@ - (instancetype)init { - return [self initWithDirection:ASStackLayoutDirectionHorizontal spacing:0.0 justifyContent:ASStackLayoutJustifyContentStart alignItems:ASStackLayoutAlignItemsStretch children:nil]; + return [self initWithDirection:ASStackLayoutDirectionHorizontal spacing:0.0 justifyContent:ASStackLayoutJustifyContentStart alignItems:ASStackLayoutAlignItemsStretch flexWrap:ASStackLayoutFlexWrapNoWrap alignContent:ASStackLayoutAlignContentStart children:nil]; } + (instancetype)stackLayoutSpecWithDirection:(ASStackLayoutDirection)direction spacing:(CGFloat)spacing justifyContent:(ASStackLayoutJustifyContent)justifyContent alignItems:(ASStackLayoutAlignItems)alignItems children:(NSArray *)children { - return [[self alloc] initWithDirection:direction spacing:spacing justifyContent:justifyContent alignItems:alignItems children:children]; + return [[self alloc] initWithDirection:direction spacing:spacing justifyContent:justifyContent alignItems:alignItems flexWrap:ASStackLayoutFlexWrapNoWrap alignContent:ASStackLayoutAlignContentStart children:children]; +} + ++ (instancetype)stackLayoutSpecWithDirection:(ASStackLayoutDirection)direction spacing:(CGFloat)spacing justifyContent:(ASStackLayoutJustifyContent)justifyContent alignItems:(ASStackLayoutAlignItems)alignItems flexWrap:(ASStackLayoutFlexWrap)flexWrap alignContent:(ASStackLayoutAlignContent)alignContent children:(NSArray> *)children +{ + return [[self alloc] initWithDirection:direction spacing:spacing justifyContent:justifyContent alignItems:alignItems flexWrap:flexWrap alignContent:alignContent children:children]; } + (instancetype)verticalStackLayoutSpec @@ -47,7 +52,7 @@ return stackLayoutSpec; } -- (instancetype)initWithDirection:(ASStackLayoutDirection)direction spacing:(CGFloat)spacing justifyContent:(ASStackLayoutJustifyContent)justifyContent alignItems:(ASStackLayoutAlignItems)alignItems children:(NSArray *)children +- (instancetype)initWithDirection:(ASStackLayoutDirection)direction spacing:(CGFloat)spacing justifyContent:(ASStackLayoutJustifyContent)justifyContent alignItems:(ASStackLayoutAlignItems)alignItems flexWrap:(ASStackLayoutFlexWrap)flexWrap alignContent:(ASStackLayoutAlignContent)alignContent children:(NSArray *)children { if (!(self = [super init])) { return nil; @@ -58,6 +63,8 @@ _verticalAlignment = ASVerticalAlignmentNone; _alignItems = alignItems; _justifyContent = justifyContent; + _flexWrap = flexWrap; + _alignContent = alignContent; [self setChildren:children]; return self; @@ -127,7 +134,7 @@ return {child, style, style.size}; }); - const ASStackLayoutSpecStyle style = {.direction = _direction, .spacing = _spacing, .justifyContent = _justifyContent, .alignItems = _alignItems}; + const ASStackLayoutSpecStyle style = {.direction = _direction, .spacing = _spacing, .justifyContent = _justifyContent, .alignItems = _alignItems, .flexWrap = _flexWrap, .alignContent = _alignContent}; const auto unpositionedLayout = ASStackUnpositionedLayout::compute(stackChildren, style, constrainedSize); const auto positionedLayout = ASStackPositionedLayout::compute(unpositionedLayout, style, constrainedSize); @@ -137,14 +144,12 @@ self.style.descender = stackChildren.back().style.descender; } - const CGSize finalSize = directionSize(style.direction, unpositionedLayout.stackDimensionSum, unpositionedLayout.crossSize); - NSMutableArray *sublayouts = [NSMutableArray array]; - for (const auto &l : positionedLayout.items) { - [sublayouts addObject:l.layout]; + for (const auto &item : positionedLayout.items) { + [sublayouts addObject:item.layout]; } - - return [ASLayout layoutWithLayoutElement:self size:ASSizeRangeClamp(constrainedSize, finalSize) sublayouts:sublayouts]; + + return [ASLayout layoutWithLayoutElement:self size:positionedLayout.size sublayouts:sublayouts]; } - (void)resolveHorizontalAlignment diff --git a/AsyncDisplayKit/Private/Layout/ASStackLayoutSpecUtilities.h b/AsyncDisplayKit/Private/Layout/ASStackLayoutSpecUtilities.h index 6bee0a6866..fbcafdff41 100644 --- a/AsyncDisplayKit/Private/Layout/ASStackLayoutSpecUtilities.h +++ b/AsyncDisplayKit/Private/Layout/ASStackLayoutSpecUtilities.h @@ -15,6 +15,8 @@ typedef struct { CGFloat spacing; ASStackLayoutJustifyContent justifyContent; ASStackLayoutAlignItems alignItems; + ASStackLayoutFlexWrap flexWrap; + ASStackLayoutAlignContent alignContent; } ASStackLayoutSpecStyle; inline CGFloat stackDimension(const ASStackLayoutDirection direction, const CGSize size) @@ -42,6 +44,10 @@ inline CGSize directionSize(const ASStackLayoutDirection direction, const CGFloa return (direction == ASStackLayoutDirectionVertical) ? CGSizeMake(cross, stack) : CGSizeMake(stack, cross); } +inline void setStackValueToPoint(const ASStackLayoutDirection direction, const CGFloat stack, CGPoint &point) { + (direction == ASStackLayoutDirectionVertical) ? (point.y = stack) : (point.x = stack); +} + inline ASSizeRange directionSizeRange(const ASStackLayoutDirection direction, const CGFloat stackMin, const CGFloat stackMax, diff --git a/AsyncDisplayKit/Private/Layout/ASStackPositionedLayout.h b/AsyncDisplayKit/Private/Layout/ASStackPositionedLayout.h index d4c7b3628a..dd0b697555 100644 --- a/AsyncDisplayKit/Private/Layout/ASStackPositionedLayout.h +++ b/AsyncDisplayKit/Private/Layout/ASStackPositionedLayout.h @@ -15,7 +15,9 @@ /** Represents a set of laid out and positioned stack layout children. */ struct ASStackPositionedLayout { const std::vector items; - + /** Final size of the stack */ + const CGSize size; + /** Given an unpositioned layout, computes the positions each child should be placed at. */ static ASStackPositionedLayout compute(const ASStackUnpositionedLayout &unpositionedLayout, const ASStackLayoutSpecStyle &style, diff --git a/AsyncDisplayKit/Private/Layout/ASStackPositionedLayout.mm b/AsyncDisplayKit/Private/Layout/ASStackPositionedLayout.mm index 870bfec09b..c39bdcdab6 100644 --- a/AsyncDisplayKit/Private/Layout/ASStackPositionedLayout.mm +++ b/AsyncDisplayKit/Private/Layout/ASStackPositionedLayout.mm @@ -11,24 +11,26 @@ #import #import +#import +#import #import #import #import -static CGFloat crossOffset(const ASStackLayoutSpecStyle &style, - const ASStackLayoutSpecItem &l, - const CGFloat crossSize, - const CGFloat baseline) +static CGFloat crossOffsetForItem(const ASStackLayoutSpecItem &item, + const ASStackLayoutSpecStyle &style, + const CGFloat crossSize, + const CGFloat baseline) { - switch (alignment(l.child.style.alignSelf, style.alignItems)) { + switch (alignment(item.child.style.alignSelf, style.alignItems)) { case ASStackLayoutAlignItemsEnd: - return crossSize - crossDimension(style.direction, l.layout.size); + return crossSize - crossDimension(style.direction, item.layout.size); case ASStackLayoutAlignItemsCenter: - return ASFloorPixelValue((crossSize - crossDimension(style.direction, l.layout.size)) / 2); + return ASFloorPixelValue((crossSize - crossDimension(style.direction, item.layout.size)) / 2); case ASStackLayoutAlignItemsBaselineFirst: case ASStackLayoutAlignItemsBaselineLast: - return baseline - ASStackUnpositionedLayout::baselineForItem(style, l); + return baseline - ASStackUnpositionedLayout::baselineForItem(style, item); case ASStackLayoutAlignItemsStart: case ASStackLayoutAlignItemsStretch: case ASStackLayoutAlignItemsNotSet: @@ -36,77 +38,149 @@ 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 unpositionedLayout Unpositioned children of the stack - * @param constrainedSize Constrained size of the stack - */ -static ASStackPositionedLayout stackedLayout(const ASStackLayoutSpecStyle &style, - const CGFloat firstChildOffset, - const CGFloat extraSpacing, - const ASStackUnpositionedLayout &unpositionedLayout, - const ASSizeRange &constrainedSize) +static void crossOffsetAndSpacingForEachLine(const std::size_t numOfLines, + const CGFloat crossViolation, + ASStackLayoutAlignContent alignContent, + CGFloat &offset, + CGFloat &spacing) { - CGFloat crossSize = unpositionedLayout.crossSize; - CGFloat baseline = unpositionedLayout.baseline; + ASDisplayNodeCAssertTrue(numOfLines > 0); - // Adjust the position of the unpositioned layouts to be positioned - CGPoint p = directionPoint(style.direction, firstChildOffset, 0); - BOOL first = YES; - const auto stackedChildren = unpositionedLayout.items; - for (const auto &l : stackedChildren) { - p = p + directionPoint(style.direction, l.child.style.spacingBefore, 0); - if (!first) { - p = p + directionPoint(style.direction, style.spacing + extraSpacing, 0); - } - first = NO; - l.layout.position = p + directionPoint(style.direction, 0, crossOffset(style, l, crossSize, baseline)); - - p = p + directionPoint(style.direction, stackDimension(style.direction, l.layout.size) + l.child.style.spacingAfter, 0); + // Handle edge cases + if (alignContent == ASStackLayoutAlignContentSpaceBetween && (crossViolation < kViolationEpsilon || numOfLines == 1)) { + alignContent = ASStackLayoutAlignContentStart; + } else if (alignContent == ASStackLayoutAlignContentSpaceAround && (crossViolation < kViolationEpsilon || numOfLines == 1)) { + alignContent = ASStackLayoutAlignContentCenter; + } + + offset = 0; + spacing = 0; + + switch (alignContent) { + case ASStackLayoutAlignContentCenter: + offset = crossViolation / 2; + break; + case ASStackLayoutAlignContentEnd: + offset = crossViolation; + break; + case ASStackLayoutAlignContentSpaceBetween: + // Spacing between the items, no spaces at the edges, evenly distributed + spacing = crossViolation / (numOfLines - 1); + break; + case ASStackLayoutAlignContentSpaceAround: { + // Spacing between items are twice the spacing on the edges + CGFloat spacingUnit = crossViolation / (numOfLines * 2); + offset = spacingUnit; + spacing = spacingUnit * 2; + break; + } + case ASStackLayoutAlignContentStart: + case ASStackLayoutAlignContentStretch: + break; } - - return {std::move(stackedChildren)}; } -ASStackPositionedLayout ASStackPositionedLayout::compute(const ASStackUnpositionedLayout &unpositionedLayout, - const ASStackLayoutSpecStyle &style, - const ASSizeRange &constrainedSize) +static void stackOffsetAndSpacingForEachItem(const std::size_t numOfItems, + const CGFloat stackViolation, + ASStackLayoutJustifyContent justifyContent, + CGFloat &offset, + CGFloat &spacing) { - 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)) { + // Handle edge cases + if (justifyContent == ASStackLayoutJustifyContentSpaceBetween && (stackViolation < kViolationEpsilon || numOfItems == 1)) { justifyContent = ASStackLayoutJustifyContentStart; - } else if (justifyContent == ASStackLayoutJustifyContentSpaceAround && (violation < 0 || numOfItems == 1)) { + } else if (justifyContent == ASStackLayoutJustifyContentSpaceAround && (stackViolation < kViolationEpsilon || numOfItems == 1)) { justifyContent = ASStackLayoutJustifyContentCenter; } + offset = 0; + spacing = 0; + switch (justifyContent) { - case ASStackLayoutJustifyContentStart: { - return stackedLayout(style, 0, 0, unpositionedLayout, constrainedSize); - } - case ASStackLayoutJustifyContentCenter: { - return stackedLayout(style, std::floor(violation / 2), 0, unpositionedLayout, constrainedSize); - } - case ASStackLayoutJustifyContentEnd: { - return stackedLayout(style, violation, 0, unpositionedLayout, constrainedSize); - } - case ASStackLayoutJustifyContentSpaceBetween: { + case ASStackLayoutJustifyContentCenter: + offset = stackViolation / 2; + break; + case ASStackLayoutJustifyContentEnd: + offset = stackViolation; + break; + case ASStackLayoutJustifyContentSpaceBetween: // Spacing between the items, no spaces at the edges, evenly distributed - const auto numOfSpacings = numOfItems - 1; - return stackedLayout(style, 0, violation / numOfSpacings, unpositionedLayout, constrainedSize); - } + spacing = stackViolation / (numOfItems - 1); + break; case ASStackLayoutJustifyContentSpaceAround: { // Spacing between items are twice the spacing on the edges - CGFloat spacingUnit = violation / (numOfItems * 2); - return stackedLayout(style, spacingUnit, spacingUnit * 2, unpositionedLayout, constrainedSize); + CGFloat spacingUnit = stackViolation / (numOfItems * 2); + offset = spacingUnit; + spacing = spacingUnit * 2; + break; } + case ASStackLayoutJustifyContentStart: + break; } } + +static void positionItemsInLine(const ASStackUnpositionedLine &line, + const ASStackLayoutSpecStyle &style, + const CGPoint &startingPoint, + const CGFloat stackSpacing) +{ + CGPoint p = startingPoint; + BOOL first = YES; + + for (const auto &item : line.items) { + p = p + directionPoint(style.direction, item.child.style.spacingBefore, 0); + if (!first) { + p = p + directionPoint(style.direction, style.spacing + stackSpacing, 0); + } + first = NO; + item.layout.position = p + directionPoint(style.direction, 0, crossOffsetForItem(item, style, line.crossSize, line.baseline)); + + p = p + directionPoint(style.direction, stackDimension(style.direction, item.layout.size) + item.child.style.spacingAfter, 0); + } +} + +ASStackPositionedLayout ASStackPositionedLayout::compute(const ASStackUnpositionedLayout &layout, + const ASStackLayoutSpecStyle &style, + const ASSizeRange &sizeRange) +{ + const auto &lines = layout.lines; + if (lines.empty()) { + return {}; + } + + const auto numOfLines = lines.size(); + const auto direction = style.direction; + const auto alignContent = style.alignContent; + const auto justifyContent = style.justifyContent; + const auto crossViolation = ASStackUnpositionedLayout::computeCrossViolation(layout.crossDimensionSum, style, sizeRange); + CGFloat crossOffset; + CGFloat crossSpacing; + crossOffsetAndSpacingForEachLine(numOfLines, crossViolation, alignContent, crossOffset, crossSpacing); + + std::vector positionedItems; + CGPoint p = directionPoint(direction, 0, crossOffset); + BOOL first = YES; + for (const auto &line : lines) { + if (!first) { + p = p + directionPoint(direction, 0, crossSpacing); + } + first = NO; + + const auto &items = line.items; + const auto stackViolation = ASStackUnpositionedLayout::computeStackViolation(line.stackDimensionSum, style, sizeRange); + CGFloat stackOffset; + CGFloat stackSpacing; + stackOffsetAndSpacingForEachItem(items.size(), stackViolation, justifyContent, stackOffset, stackSpacing); + + setStackValueToPoint(direction, stackOffset, p); + positionItemsInLine(line, style, p, stackSpacing); + std::move(items.begin(), items.end(), std::back_inserter(positionedItems)); + + p = p + directionPoint(direction, -stackOffset, line.crossSize); + } + + const CGSize finalSize = directionSize(direction, layout.stackDimensionSum, layout.crossDimensionSum); + return {std::move(positionedItems), ASSizeRangeClamp(sizeRange, finalSize)}; +} diff --git a/AsyncDisplayKit/Private/Layout/ASStackUnpositionedLayout.h b/AsyncDisplayKit/Private/Layout/ASStackUnpositionedLayout.h index af7bb8d0cd..e98852b948 100644 --- a/AsyncDisplayKit/Private/Layout/ASStackUnpositionedLayout.h +++ b/AsyncDisplayKit/Private/Layout/ASStackUnpositionedLayout.h @@ -14,6 +14,9 @@ #import #import +/** The threshold that determines if a violation has actually occurred. */ +extern CGFloat const kViolationEpsilon; + struct ASStackLayoutSpecChild { /** The original source child. */ id element; @@ -30,19 +33,27 @@ struct ASStackLayoutSpecItem { ASLayout *layout; }; +struct ASStackUnpositionedLine { + /** The set of proposed children in this line, each contains child layout, not yet positioned. */ + std::vector items; + /** The total size of the children in the stack dimension, including all spacing. */ + CGFloat stackDimensionSum; + /** The size in the cross dimension */ + CGFloat crossSize; + /** The baseline of the stack which baseline aligned children should align to */ + CGFloat baseline; +}; /** Represents a set of stack layout children that have their final layout computed, but are not yet positioned. */ struct ASStackUnpositionedLayout { - /** A set of proposed child layouts, not yet positioned. */ - const std::vector items; - /** The total size of the children in the stack dimension, including all spacing. */ + /** The set of proposed lines, each contains child layouts, not yet positioned. */ + const std::vector lines; + /** + * In a single line stack (e.g no wrao), this is the total size of the children in the stack dimension, including all spacing. + * In a multi-line stack, this is the largest stack dimension among lines. + */ const CGFloat stackDimensionSum; - /** The amount by which stackDimensionSum violates constraints. If positive, less than min; negative, greater than max. */ - const CGFloat violation; - /** The size in the cross dimension */ - const CGFloat crossSize; - /** The baseline of the stack which baseline aligned children should align to */ - const CGFloat baseline; + const CGFloat crossDimensionSum; /** Given a set of children, computes the unpositioned layouts for those children. */ static ASStackUnpositionedLayout compute(const std::vector &children, @@ -51,4 +62,12 @@ struct ASStackUnpositionedLayout { static CGFloat baselineForItem(const ASStackLayoutSpecStyle &style, const ASStackLayoutSpecItem &l); + + static CGFloat computeStackViolation(const CGFloat stackDimensionSum, + const ASStackLayoutSpecStyle &style, + const ASSizeRange &sizeRange); + + static CGFloat computeCrossViolation(const CGFloat crossDimensionSum, + const ASStackLayoutSpecStyle &style, + const ASSizeRange &sizeRange); }; diff --git a/AsyncDisplayKit/Private/Layout/ASStackUnpositionedLayout.mm b/AsyncDisplayKit/Private/Layout/ASStackUnpositionedLayout.mm index 8b7e06d4df..4ec2568591 100644 --- a/AsyncDisplayKit/Private/Layout/ASStackUnpositionedLayout.mm +++ b/AsyncDisplayKit/Private/Layout/ASStackUnpositionedLayout.mm @@ -16,6 +16,8 @@ #import #import +CGFloat const kViolationEpsilon = 0.01; + static CGFloat resolveCrossDimensionMaxForStretchChild(const ASStackLayoutSpecStyle &style, const ASStackLayoutSpecChild &child, const CGFloat stackMax, @@ -65,8 +67,76 @@ static ASLayout *crossChildLayout(const ASStackLayoutSpecChild &child, return layout ? : [ASLayout layoutWithLayoutElement:child.element size:{0, 0}]; } -/** The threshold that determines if a violation has actually occurred. */ -static const CGFloat kViolationEpsilon = 0.01; +/** + Computes the consumed cross dimension length for the given vector of lines and stacking style. + + Cross Dimension + +---------------------> + +--------+ +--------+ +--------+ +---------+ + Vertical |Vertical| |Vertical| |Vertical| |Vertical | + Stack | Line 1 | | Line 2 | | Line 3 | | Line 4 | + | | | | | | | | + +--------+ +--------+ +--------+ +---------+ + crossDimensionSum + |------------------------------------------| + + @param lines unpositioned lines + */ +static CGFloat computeLinesCrossDimensionSum(const std::vector &lines) +{ + return std::accumulate(lines.begin(), lines.end(), 0.0, + [&](CGFloat x, const ASStackUnpositionedLine &l) { + return x + l.crossSize; + }); +} + + +/** + Computes the violation by comparing a cross dimension sum with the overall allowable size range for the stack. + + Violation is the distance you would have to add to the unbounded cross-direction length of the stack spec's + lines in order to bring the stack within its allowed sizeRange. The diagram below shows 3 vertical stacks, each contains 3-5 vertical lines, + with the different types of violation. + + Cross Dimension + +---------------------> + cross size range + |------------| + +--------+ +--------+ +--------+ +---------+ - - - - - - - - + Vertical |Vertical| |Vertical| |Vertical| |Vertical | | ^ + Stack 1 | Line 1 | | Line 2 | | Line 3 | | Line 4 | (zero violation) | stack size range + | | | | | | | | | | v + +--------+ +--------+ +--------+ +---------+ - - - - - - - - + | | + +--------+ +--------+ +--------+ - - - - - - - - - - - - + Vertical | | | | | | | | ^ + Stack 2 | | | | | |<--> (positive violation) | stack size range + | | | | | | | | v + +--------+ +--------+ +--------+ - - - - - - - - - - - - + | |<------> (negative violation) + +--------+ +--------+ +--------+ +---------+ +-----------+ - - - + Vertical | | | | | | | | | | | | ^ + Stack 3 | | | | | | | | | | | stack size range + | | | | | | | | | | | | v + +--------+ +--------+ +--------+ +---------+ +-----------+ - - - + + @param crossDimensionSum the consumed length of the lines in the stack along the cross dimension + @param style layout style to be applied to all children + @param sizeRange the range of allowable sizes for the stack layout spec + */ +CGFloat ASStackUnpositionedLayout::computeCrossViolation(const CGFloat crossDimensionSum, + const ASStackLayoutSpecStyle &style, + const ASSizeRange &sizeRange) +{ + const CGFloat minCrossDimension = crossDimension(style.direction, sizeRange.min); + const CGFloat maxCrossDimension = crossDimension(style.direction, sizeRange.max); + if (crossDimensionSum < minCrossDimension) { + return minCrossDimension - crossDimensionSum; + } else if (crossDimensionSum > maxCrossDimension) { + return maxCrossDimension - crossDimensionSum; + } + return 0; +} /** Stretches children to lay out along the cross axis according to the alignment stretch settings of the children @@ -97,13 +167,13 @@ static const CGFloat kViolationEpsilon = 0.01; | +--------------------------------------------------+ + crossMax - @param items pre-computed child layouts; modified in-place as needed + @param items pre-computed items; modified in-place as needed @param style the layout style of the overall stack layout */ -static void stretchChildrenAlongCrossDimension(std::vector &items, - const ASStackLayoutSpecStyle &style, - const CGSize parentSize, - const CGFloat crossSize) +static void stretchItemsAlongCrossDimension(std::vector &items, + const ASStackLayoutSpecStyle &style, + const CGSize parentSize, + const CGFloat crossSize) { for (auto &item : items) { const ASStackLayoutAlignItems alignItems = alignment(item.child.style.alignSelf, style.alignItems); @@ -122,6 +192,33 @@ static void stretchChildrenAlongCrossDimension(std::vector &lines, + const ASStackLayoutSpecStyle &style, + const ASSizeRange &sizeRange, + const CGSize parentSize) +{ + ASDisplayNodeCAssertFalse(lines.empty()); + const std::size_t numOfLines = lines.size(); + const CGFloat violation = ASStackUnpositionedLayout::computeCrossViolation(computeLinesCrossDimensionSum(lines), style, sizeRange); + // Don't stretch if the stack is single line, because the line's cross size was clamped against the stack's constrained size. + const BOOL shouldStretchLines = (numOfLines > 1 + && style.alignContent == ASStackLayoutAlignContentStretch + && violation > kViolationEpsilon); + + CGFloat extraCrossSizePerLine = violation / numOfLines; + for (auto &line : lines) { + if (shouldStretchLines) { + line.crossSize += extraCrossSizePerLine; + } + + stretchItemsAlongCrossDimension(line.items, style, parentSize, line.crossSize); + } +} static BOOL itemIsBaselineAligned(const ASStackLayoutSpecStyle &style, const ASStackLayoutSpecItem &l) @@ -131,20 +228,20 @@ static BOOL itemIsBaselineAligned(const ASStackLayoutSpecStyle &style, } CGFloat ASStackUnpositionedLayout::baselineForItem(const ASStackLayoutSpecStyle &style, - const ASStackLayoutSpecItem &l) + const ASStackLayoutSpecItem &item) { - switch (alignment(l.child.style.alignSelf, style.alignItems)) { + switch (alignment(item.child.style.alignSelf, style.alignItems)) { case ASStackLayoutAlignItemsBaselineFirst: - return l.child.style.ascender; + return item.child.style.ascender; case ASStackLayoutAlignItemsBaselineLast: - return crossDimension(style.direction, l.layout.size) + l.child.style.descender; + return crossDimension(style.direction, item.layout.size) + item.child.style.descender; default: return 0; } } /** - * Finds cross dimension size and baseline of the stack. + * Computes cross size and baseline of each line. * https://www.w3.org/TR/css-flexbox-1/#algo-cross-line * * @param items All items to lay out @@ -153,41 +250,64 @@ CGFloat ASStackUnpositionedLayout::baselineForItem(const ASStackLayoutSpecStyle * @param crossSize result of the cross size * @param baseline result of the stack baseline */ -static void computeCrossSizeAndBaseline(const std::vector &items, - const ASStackLayoutSpecStyle &style, - const ASSizeRange &sizeRange, - CGFloat &crossSize, - CGFloat &baseline) +static void computeLinesCrossSizeAndBaseline(std::vector &lines, + const ASStackLayoutSpecStyle &style, + const ASSizeRange &sizeRange) { + ASDisplayNodeCAssertFalse(lines.empty()); + const BOOL isSingleLine = (lines.size() == 1); + const auto minCrossSize = crossDimension(style.direction, sizeRange.min); const auto maxCrossSize = crossDimension(style.direction, sizeRange.max); + const BOOL definiteCrossSize = (minCrossSize == maxCrossSize); - // Step 1. Collect all the flex items whose align-self is baseline. Find the largest of the distances - // between each item’s baseline and its hypothetical outer cross-start edge (aka. its ascender value), - // and the largest of the distances between each item’s baseline and its hypothetical outer cross-end edge - // (aka. the opposite of its descender value, because a negative descender means the item extends below its baseline), - // and sum these two values. - // - // Step 2. Find the maximum cross dimension size among child layouts. - CGFloat maxStartToBaselineDistance = 0; - CGFloat maxBaselineToEndDistance = 0; - CGFloat maxItemCrossSize = 0; - for (const auto &item : items) { - if (itemIsBaselineAligned(style, item)) { - CGFloat baseline = ASStackUnpositionedLayout::baselineForItem(style, item); - maxStartToBaselineDistance = MAX(maxStartToBaselineDistance, baseline); - maxBaselineToEndDistance = MAX(maxBaselineToEndDistance, crossDimension(style.direction, item.layout.size) - baseline); - } else { - maxItemCrossSize = MAX(maxItemCrossSize, crossDimension(style.direction, item.layout.size)); + // If the stack is single-line and has a definite cross size, the cross size of the line is the stack's definite cross size. + if (isSingleLine && definiteCrossSize) { + auto &line = lines[0]; + line.crossSize = minCrossSize; + + // We still need to determine the line's baseline + //TODO unit test + for (const auto &item : line.items) { + if (itemIsBaselineAligned(style, item)) { + CGFloat baseline = ASStackUnpositionedLayout::baselineForItem(style, item); + line.baseline = MAX(line.baseline, baseline); + } } + + return; } - // Step 3. The used cross-size of the flex line is the largest of the numbers found in the previous two steps and zero. - crossSize = MAX(maxStartToBaselineDistance + maxBaselineToEndDistance, maxItemCrossSize); - // Clamp the cross-size to be within the stack's min and max cross-size properties. - crossSize = MIN(MAX(minCrossSize, crossSize), maxCrossSize); - - baseline = maxStartToBaselineDistance; + for (auto &line : lines) { + const auto &items = line.items; + CGFloat maxStartToBaselineDistance = 0; + CGFloat maxBaselineToEndDistance = 0; + CGFloat maxItemCrossSize = 0; + + for (const auto &item : items) { + if (itemIsBaselineAligned(style, item)) { + // Step 1. Collect all the items whose align-self is baseline. Find the largest of the distances + // between each item’s baseline and its hypothetical outer cross-start edge (aka. its baseline value), + // and the largest of the distances between each item’s baseline and its hypothetical outer cross-end edge, + // and sum these two values. + CGFloat baseline = ASStackUnpositionedLayout::baselineForItem(style, item); + maxStartToBaselineDistance = MAX(maxStartToBaselineDistance, baseline); + maxBaselineToEndDistance = MAX(maxBaselineToEndDistance, crossDimension(style.direction, item.layout.size) - baseline); + } else { + // Step 2. Among all the items not collected by the previous step, find the largest outer hypothetical cross size. + maxItemCrossSize = MAX(maxItemCrossSize, crossDimension(style.direction, item.layout.size)); + } + } + + // Step 3. The used cross-size of the flex line is the largest of the numbers found in the previous two steps and zero. + line.crossSize = MAX(maxStartToBaselineDistance + maxBaselineToEndDistance, maxItemCrossSize); + if (isSingleLine) { + // If the stack is single-line, then clamp the line’s cross-size to be within the stack's min and max cross-size properties. + line.crossSize = MIN(MAX(minCrossSize, line.crossSize), maxCrossSize); + } + + line.baseline = maxStartToBaselineDistance; + } } /** @@ -314,8 +434,8 @@ static void layoutFlexibleChildrenAtZeroSize(std::vector @param items unpositioned layouts for items @param style the layout style of the overall stack layout */ -static CGFloat computeStackDimensionSum(const std::vector &items, - const ASStackLayoutSpecStyle &style) +static CGFloat computeItemsStackDimensionSum(const std::vector &items, + const ASStackLayoutSpecStyle &style) { // Sum up the childrens' spacing const CGFloat childSpacingSum = std::accumulate(items.begin(), items.end(), @@ -333,6 +453,7 @@ static CGFloat computeStackDimensionSum(const std::vector return childStackDimensionSum; } +//TODO move this up near computeCrossViolation and make both methods share the same code path, to make sure they share the same concept of "negative" and "positive" violations. /** Computes the violation by comparing a stack dimension sum with the overall allowable size range for the stack. @@ -364,9 +485,9 @@ static CGFloat computeStackDimensionSum(const std::vector @param style layout style to be applied to all children @param sizeRange the range of allowable sizes for the stack layout spec */ -static CGFloat computeViolation(const CGFloat stackDimensionSum, - const ASStackLayoutSpecStyle &style, - const ASSizeRange &sizeRange) +CGFloat ASStackUnpositionedLayout::computeStackViolation(const CGFloat stackDimensionSum, + const ASStackLayoutSpecStyle &style, + const ASSizeRange &sizeRange) { const CGFloat minStackDimension = stackDimension(style.direction, sizeRange.min); const CGFloat maxStackDimension = stackDimension(style.direction, sizeRange.max); @@ -394,65 +515,106 @@ ASDISPLAYNODE_INLINE BOOL useOptimizedFlexing(const std::vector &items, - const ASStackLayoutSpecStyle &style, - const ASSizeRange &sizeRange, - const CGSize parentSize, - const BOOL useOptimizedFlexing) +static void flexLinesAlongStackDimension(std::vector &lines, + const ASStackLayoutSpecStyle &style, + const ASSizeRange &sizeRange, + const CGSize parentSize, + const BOOL useOptimizedFlexing) { - const CGFloat violation = computeViolation(computeStackDimensionSum(items, style), style, sizeRange); - std::function flexFactor = flexFactorInViolationDirection(violation); - // The flex factor sum is needed to determine if flexing is necessary. - // This value is also needed if the violation is positive and flexible children need to grow, so keep it around. - const CGFloat flexFactorSum = std::accumulate(items.begin(), items.end(), 0.0, [&](CGFloat x, const ASStackLayoutSpecItem &item) { - return x + flexFactor(item); - }); - // If no children are able to flex then there is nothing left to do. Bail. - if (flexFactorSum == 0) { - // If optimized flexing was used then we have to clean up the unsized children and lay them out at zero size. - if (useOptimizedFlexing) { - layoutFlexibleChildrenAtZeroSize(items, style, sizeRange, parentSize); + for (auto &line : lines) { + auto &items = line.items; + const CGFloat violation = ASStackUnpositionedLayout::computeStackViolation(computeItemsStackDimensionSum(items, style), style, sizeRange); + std::function flexFactor = flexFactorInViolationDirection(violation); + // The flex factor sum is needed to determine if flexing is necessary. + // This value is also needed if the violation is positive and flexible children need to grow, so keep it around. + const CGFloat flexFactorSum = std::accumulate(items.begin(), items.end(), 0.0, [&](CGFloat x, const ASStackLayoutSpecItem &item) { + return x + flexFactor(item); + }); + // If no children are able to flex then there is nothing left to do. Bail. + if (flexFactorSum == 0) { + // If optimized flexing was used then we have to clean up the unsized children and lay them out at zero size. + if (useOptimizedFlexing) { + layoutFlexibleChildrenAtZeroSize(items, style, sizeRange, parentSize); + } + return; + } + std::function flexAdjustment = flexAdjustmentInViolationDirection(items, + style, + violation, + flexFactorSum); + + // Compute any remaining violation to the first flexible child. + const CGFloat remainingViolation = std::accumulate(items.begin(), items.end(), violation, [&](CGFloat x, const ASStackLayoutSpecItem &item) { + return x - flexAdjustment(item); + }); + BOOL isFirstFlex = YES; + for (ASStackLayoutSpecItem &item : items) { + const CGFloat currentFlexAdjustment = flexAdjustment(item); + // Children are consider inflexible if they do not need to make a flex adjustment. + if (currentFlexAdjustment != 0) { + const CGFloat originalStackSize = stackDimension(style.direction, item.layout.size); + // Only apply the remaining violation for the first flexible child that has a flex grow factor. + const CGFloat flexedStackSize = originalStackSize + currentFlexAdjustment + (isFirstFlex && item.child.style.flexGrow > 0 ? remainingViolation : 0); + item.layout = crossChildLayout(item.child, + style, + MAX(flexedStackSize, 0), + MAX(flexedStackSize, 0), + crossDimension(style.direction, sizeRange.min), + crossDimension(style.direction, sizeRange.max), + parentSize); + isFirstFlex = NO; + } } - return; } - std::function flexAdjustment = flexAdjustmentInViolationDirection(items, - style, - violation, - flexFactorSum); +} - // Compute any remaining violation to the first flexible child. - const CGFloat remainingViolation = std::accumulate(items.begin(), items.end(), violation, [&](CGFloat x, const ASStackLayoutSpecItem &item) { - return x - flexAdjustment(item); - }); - BOOL isFirstFlex = YES; - for (ASStackLayoutSpecItem &item : items) { - const CGFloat currentFlexAdjustment = flexAdjustment(item); - // Children are consider inflexible if they do not need to make a flex adjustment. - if (currentFlexAdjustment != 0) { - const CGFloat originalStackSize = stackDimension(style.direction, item.layout.size); - // Only apply the remaining violation for the first flexible child that has a flex grow factor. - const CGFloat flexedStackSize = originalStackSize + currentFlexAdjustment + (isFirstFlex && item.child.style.flexGrow > 0 ? remainingViolation : 0); - item.layout = crossChildLayout(item.child, - style, - MAX(flexedStackSize, 0), - MAX(flexedStackSize, 0), - crossDimension(style.direction, sizeRange.min), - crossDimension(style.direction, sizeRange.max), - parentSize); - isFirstFlex = NO; - } +/** + https://www.w3.org/TR/css-flexbox-1/#algo-line-break + */ +static std::vector collectChildrenIntoLines(const std::vector &items, + const ASStackLayoutSpecStyle &style, + const ASSizeRange &sizeRange) +{ + //TODO if infinite max stack size, fast path + if (style.flexWrap == ASStackLayoutFlexWrapNoWrap) { + return std::vector (1, {.items = std::move(items)}); } + + std::vector lines; + std::vector lineItems; + CGFloat lineStackDimensionSum = 0; + + for(auto it = items.begin(); it != items.end(); ++it) { + const auto &item = *it; + const CGFloat itemStackDimension = stackDimension(style.direction, item.layout.size); + const BOOL negativeViolationIfAddItem = (ASStackUnpositionedLayout::computeStackViolation(lineStackDimensionSum + itemStackDimension, style, sizeRange) < 0); + const BOOL breakCurrentLine = negativeViolationIfAddItem && !lineItems.empty(); + + if (breakCurrentLine) { + lines.push_back({.items = std::vector (lineItems)}); + lineItems.clear(); + lineStackDimensionSum = 0; + } + + lineItems.push_back(std::move(item)); + lineStackDimensionSum += itemStackDimension; + } + + // Handle last line + lines.push_back({.items = std::vector (lineItems)}); + + return lines; } /** @@ -507,25 +669,34 @@ ASStackUnpositionedLayout ASStackUnpositionedLayout::compute(const std::vector items = layoutChildrenAlongUnconstrainedStackDimension(children, - style, - sizeRange, - parentSize, - optimizedFlexing); + style, + sizeRange, + parentSize, + optimizedFlexing); - // Resolve the flexible lengths (https://www.w3.org/TR/css-flexbox-1/#algo-flex) - // Determine the hypothetical cross size of each item (https://www.w3.org/TR/css-flexbox-1/#algo-cross-item) - flexChildrenAlongStackDimension(items, style, sizeRange, parentSize, optimizedFlexing); + // Collect items into lines (https://www.w3.org/TR/css-flexbox-1/#algo-line-break) + std::vector lines = collectChildrenIntoLines(items, style, sizeRange); - // Step 4. Cross Size Determination (https://www.w3.org/TR/css-flexbox-1/#cross-sizing) - // - // Calculate the cross size of the stack (https://www.w3.org/TR/css-flexbox-1/#algo-cross-line) - CGFloat crossSize; - CGFloat baseline; - computeCrossSizeAndBaseline(items, style, sizeRange, crossSize, baseline); + // Resolve the flexible lengths (https://www.w3.org/TR/css-flexbox-1/#resolve-flexible-lengths) + flexLinesAlongStackDimension(lines, style, sizeRange, parentSize, optimizedFlexing); + + // Calculate the cross size of each flex line (https://www.w3.org/TR/css-flexbox-1/#algo-cross-line) + computeLinesCrossSizeAndBaseline(lines, style, sizeRange); + + // Handle 'align-content: stretch' (https://www.w3.org/TR/css-flexbox-1/#algo-line-stretch) // Determine the used cross size of each item (https://www.w3.org/TR/css-flexbox-1/#algo-stretch) - // If the flex item has stretch alignment, redo layout - stretchChildrenAlongCrossDimension(items, style, parentSize, crossSize); + stretchLinesAlongCrossDimension(lines, style, sizeRange, parentSize); - const CGFloat stackDimensionSum = computeStackDimensionSum(items, style); - return {std::move(items), stackDimensionSum, computeViolation(stackDimensionSum, style, sizeRange), crossSize, baseline}; + // Compute stack dimension sum of each line and the whole stack + CGFloat layoutStackDimensionSum = 0; + for (auto &line : lines) { + line.stackDimensionSum = computeItemsStackDimensionSum(line.items, style); + // layoutStackDimensionSum is the max stackDimensionSum among all lines + layoutStackDimensionSum = MAX(line.stackDimensionSum, layoutStackDimensionSum); + } + // Compute cross dimension sum of the stack. + // This should be done before `lines` are moved to a new ASStackUnpositionedLayout struct (i.e `std::move(lines)`) + CGFloat layoutCrossDimensionSum = computeLinesCrossDimensionSum(lines); + + return {.lines = std::move(lines), .stackDimensionSum = layoutStackDimensionSum, .crossDimensionSum = layoutCrossDimensionSum}; } diff --git a/AsyncDisplayKitTests/ASStackLayoutSpecSnapshotTests.mm b/AsyncDisplayKitTests/ASStackLayoutSpecSnapshotTests.mm index f0438e6496..a21a37a83b 100644 --- a/AsyncDisplayKitTests/ASStackLayoutSpecSnapshotTests.mm +++ b/AsyncDisplayKitTests/ASStackLayoutSpecSnapshotTests.mm @@ -22,15 +22,6 @@ @implementation ASStackLayoutSpecSnapshotTests -#pragma mark - XCTestCase - -- (void)setUp -{ - [super setUp]; - - self.recordMode = NO; -} - #pragma mark - Utility methods static NSArray *defaultSubnodes() @@ -110,6 +101,8 @@ static NSArray *defaultTextNodes() spacing:style.spacing justifyContent:style.justifyContent alignItems:style.alignItems + flexWrap:style.flexWrap + alignContent:style.alignContent children:children]; [self testStackLayoutSpec:stackLayoutSpec sizeRange:sizeRange subnodes:subnodes identifier:identifier]; @@ -162,6 +155,28 @@ static NSArray *defaultTextNodes() [self testLayoutSpec:layoutSpec sizeRange:sizeRange subnodes:newSubnodes identifier:identifier]; } +- (void)testStackLayoutSpecWithAlignContent:(ASStackLayoutAlignContent)alignContent + sizeRange:(ASSizeRange)sizeRange + identifier:(NSString *)identifier +{ + ASStackLayoutSpecStyle style = { + .direction = ASStackLayoutDirectionHorizontal, + .flexWrap = ASStackLayoutFlexWrapWrap, + .alignContent = alignContent, + }; + + CGSize subnodeSize = {50, 50}; + NSArray *subnodes = @[ + ASDisplayNodeWithBackgroundColor([UIColor redColor], subnodeSize), + ASDisplayNodeWithBackgroundColor([UIColor yellowColor], subnodeSize), + ASDisplayNodeWithBackgroundColor([UIColor blueColor], subnodeSize), + ASDisplayNodeWithBackgroundColor([UIColor magentaColor], subnodeSize), + ASDisplayNodeWithBackgroundColor([UIColor greenColor], subnodeSize), + ASDisplayNodeWithBackgroundColor([UIColor cyanColor], subnodeSize), + ]; + + [self testStackLayoutSpecWithStyle:style sizeRange:sizeRange subnodes:subnodes identifier:identifier]; +} #pragma mark - @@ -1167,4 +1182,77 @@ static NSArray *defaultTextNodes() [self testStackLayoutSpec:stackLayoutSpec sizeRange:kSize subnodes:children identifier:nil]; } +#pragma mark - Content alignment tests + +- (void)testAlignContentUnderflow +{ + // 3 lines, each line has 2 items, each item has a size of {50, 50} + // width is 110px. It's 10px bigger than the required width of each line (110px vs 100px) to test that items are still correctly collected into lines + static ASSizeRange kSize = {{110, 300}, {110, 300}}; + [self testStackLayoutSpecWithAlignContent:ASStackLayoutAlignContentStart sizeRange:kSize identifier:@"alignContentStart"]; + [self testStackLayoutSpecWithAlignContent:ASStackLayoutAlignContentCenter sizeRange:kSize identifier:@"alignContentCenter"]; + [self testStackLayoutSpecWithAlignContent:ASStackLayoutAlignContentEnd sizeRange:kSize identifier:@"alignContentEnd"]; + [self testStackLayoutSpecWithAlignContent:ASStackLayoutAlignContentSpaceBetween sizeRange:kSize identifier:@"alignContentSpaceBetween"]; + [self testStackLayoutSpecWithAlignContent:ASStackLayoutAlignContentSpaceAround sizeRange:kSize identifier:@"alignContentSpaceAround"]; + [self testStackLayoutSpecWithAlignContent:ASStackLayoutAlignContentStretch sizeRange:kSize identifier:@"alignContentStretch"]; +} + +- (void)testAlignContentOverflow +{ + // 6 lines, each line has 1 item, each item has a size of {50, 50} + // width is 40px. It's 10px smaller than the width of each item (40px vs 50px) to test that items are still correctly collected into lines + static ASSizeRange kSize = {{40, 260}, {40, 260}}; + [self testStackLayoutSpecWithAlignContent:ASStackLayoutAlignContentStart sizeRange:kSize identifier:@"alignContentStart"]; + [self testStackLayoutSpecWithAlignContent:ASStackLayoutAlignContentCenter sizeRange:kSize identifier:@"alignContentCenter"]; + [self testStackLayoutSpecWithAlignContent:ASStackLayoutAlignContentEnd sizeRange:kSize identifier:@"alignContentEnd"]; + [self testStackLayoutSpecWithAlignContent:ASStackLayoutAlignContentSpaceBetween sizeRange:kSize identifier:@"alignContentSpaceBetween"]; + [self testStackLayoutSpecWithAlignContent:ASStackLayoutAlignContentSpaceAround sizeRange:kSize identifier:@"alignContentSpaceAround"]; +} + +- (void)testAlignContentWithUnconstrainedCrossSize +{ + // 3 lines, each line has 2 items, each item has a size of {50, 50} + // width is 110px. It's 10px bigger than the required width of each line (110px vs 100px) to test that items are still correctly collected into lines + // height is unconstrained. It causes no cross size violation and the end results are all similar to ASStackLayoutAlignContentStart. + static ASSizeRange kSize = {{110, 0}, {110, CGFLOAT_MAX}}; + [self testStackLayoutSpecWithAlignContent:ASStackLayoutAlignContentStart sizeRange:kSize identifier:nil]; + [self testStackLayoutSpecWithAlignContent:ASStackLayoutAlignContentCenter sizeRange:kSize identifier:nil]; + [self testStackLayoutSpecWithAlignContent:ASStackLayoutAlignContentEnd sizeRange:kSize identifier:nil]; + [self testStackLayoutSpecWithAlignContent:ASStackLayoutAlignContentSpaceBetween sizeRange:kSize identifier:nil]; + [self testStackLayoutSpecWithAlignContent:ASStackLayoutAlignContentSpaceAround sizeRange:kSize identifier:nil]; + [self testStackLayoutSpecWithAlignContent:ASStackLayoutAlignContentStretch sizeRange:kSize identifier:nil]; +} + +- (void)testAlignContentStretchAndOtherAlignments +{ + ASStackLayoutSpecStyle style = { + .direction = ASStackLayoutDirectionHorizontal, + .flexWrap = ASStackLayoutFlexWrapWrap, + .alignContent = ASStackLayoutAlignContentStretch, + .alignItems = ASStackLayoutAlignItemsStart, + }; + + CGSize subnodeSize = {50, 50}; + NSArray *subnodes = @[ + // 1st line + ASDisplayNodeWithBackgroundColor([UIColor redColor], subnodeSize), + ASDisplayNodeWithBackgroundColor([UIColor yellowColor], subnodeSize), + // 2nd line + ASDisplayNodeWithBackgroundColor([UIColor blueColor], subnodeSize), + ASDisplayNodeWithBackgroundColor([UIColor magentaColor], subnodeSize), + // 3rd line + ASDisplayNodeWithBackgroundColor([UIColor greenColor], subnodeSize), + ASDisplayNodeWithBackgroundColor([UIColor cyanColor], subnodeSize), + ]; + + subnodes[1].style.alignSelf = ASStackLayoutAlignSelfStart; + subnodes[3].style.alignSelf = ASStackLayoutAlignSelfCenter; + subnodes[5].style.alignSelf = ASStackLayoutAlignSelfEnd; + + // 3 lines, each line has 2 items, each item has a size of {50, 50} + // width is 110px. It's 10px bigger than the required width of each line (110px vs 100px) to test that items are still correctly collected into lines + static ASSizeRange kSize = {{110, 300}, {110, 300}}; + [self testStackLayoutSpecWithStyle:style sizeRange:kSize subnodes:subnodes identifier:nil]; +} + @end diff --git a/AsyncDisplayKitTests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testAlignContentOverflow_alignContentCenter@2x.png b/AsyncDisplayKitTests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testAlignContentOverflow_alignContentCenter@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..cee1ed8cf7f776b7955afb025d469a9413d12fa9 GIT binary patch literal 1021 zcmeAS@N?(olHy`uVBq!ia0vp^0YLnLgAGVd=Y8x9q!^2X+?^QKos)S9vL>4nJ zh^c}wqi2xH2cV!#ri*_FkY)m65U2oR4JZv{Gj5%k&{YYf8$DedLn;{GTyx|)Y{27k z@y`ECy0I^qLLF)?t=Kx|xyPx_kc>>7XkaNS#8fbz8Tb`^YMTQ+nW}eQ!z@}C|b$@Mx{@>2i z-zP9l=MQY;+EaQ5=<2%%zKAS29ACHZ%uOz!>n;aR;Irq8T?b>=XWpN)7U&)WTh^B! z9$$F63dWxP?JN~F9GKb|4~&!l|M%aVVhD_kx1h*)do3`F#gT{k;?Dm|&D$$@4=&>P z9oZZk)?IV|`|q#9?3_m!R1F&cM?0!+V3~H{^7;o~K74wAT-&U%G(ooFm*^b1f1&*+ z%Y`D?uQe`@Klt+D(%o?zIoB}1hA3TM<+E?{0jmvsHh;PN`1S98?ea~KjbNWw$GW_| z{%Z24xh&TjOCP)pFQ}?`xh_11>Gc7t4_~w`Z2zp5oBUp71K%2E|NTvu4||vIO%jG% z5&v~YjXK=^xLvPBX9Km>TYvceRh{ipJDb4t+qoM!U6?!amw0URP>^zn$=iNVO@LQn z-$bRGN{tx|g*)%E&2}(p_~F)-(#^7n@yOFM=^Unu43DHWC+P}x3_k-FNd92Xyiv5r U(Z${um;x9)UHx3vIVCg!0LIaz-2eap literal 0 HcmV?d00001 diff --git a/AsyncDisplayKitTests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testAlignContentOverflow_alignContentEnd@2x.png b/AsyncDisplayKitTests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testAlignContentOverflow_alignContentEnd@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..d6736f91537b7cc07ff7a3540d295ac1c4489e64 GIT binary patch literal 890 zcmeAS@N?(olHy`uVBq!ia0vp^0YLnLgAGVd=Y8x9q!^2X+?^QKos)S9vL>4nJ zh^c}wqi2xH2cV!#ri*_FkY)m65U2oR4JZv{GtRkvNk|k(XL!0ehEy=VxxTTN*-(V} z;{E!|**jWO9WGZKZ0#0iJ3O)G{N2azZJU|j+wRe~W;o2s#F1cj&?J#3MS*$w{6Mvw zLqPO0IN@hC?-d8$D+ivoEls};WH|8pSa15Xmn~}nTh@c3!W(FG9U<@>_gu7jvLEu21o5TqRHox@=zdp!a zN~{MuTe<+`?7W8=tn$F1|NsBMtbaDZFmDEh`P*w3IS)JVupWH9|E2M|L{_&7j-NFL zd9o)KpWD0dC_@4dhr)q;{|QW@D-K-EKlrL$zutb?Hio+n<^^A)H~f0I{9VLP*@nCY z(mQ_XM*RKow9Ge`aaUvBgIC)Vz8?0!yYY+l^It5D2CVvL>4nJ zh^c}wqi2xH2cV!#ri*_FkY)m65U2oR4JZv{Gj5%k&{YYf8$DedLn;{GTyx|)Y{27k z@y`ECy0I^qLLF)?t=Kx|xyPx_kc>>7XkaNS#8fbz8Tb`^YMTQ+nW}eQ!z@}C|b$@Mx{@>2i z-zP9l=MQY;+EaQ5=<2%%zKAS29ACHZ%uOz!>n;aR;Irq8T?b>=XWpN)7U&)WTh^B! z9$$F63dWxP?JN~F9GKb|4~&!l|M%aVVhD_kx1h*)do3`F#gT{k;?Dm|&D$$@4=&>P z9oZZk)?IV|`|q#9?3_m!R1F&cM?0!+V3~H{^7;o~K74wAT-&U%G(ooFm*^b1f1&*+ z%Y`D?uQe`@Klt+D(%o?zIoB}1hA3TM<+E?{0jmvsHh;PN`1S98?ea~KjbNWw$GW_| z{%Z24xh&TjOCP)pFQ}?`xh_11>Gc7t4_~w`Z2zp5oBUp71K%2E|NTvu4||vIO%jG% z5&v~YjXK=^xLvPBX9Km>TYvceRh{ipJDb4t+qoM!U6?!amw0URP>^zn$=iNVO@LQn z-$bRGN{tx|g*)%E&2}(p_~F)-(#^7n@yOFM=^Unu43DHWC+P}x3_k-FNd92Xyiv5r U(Z${um;x9)UHx3vIVCg!0LIaz-2eap literal 0 HcmV?d00001 diff --git a/AsyncDisplayKitTests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testAlignContentOverflow_alignContentSpaceBetween@2x.png b/AsyncDisplayKitTests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testAlignContentOverflow_alignContentSpaceBetween@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..1e32492bac6c7cacab7bd556359a2d270427b3c3 GIT binary patch literal 916 zcmeAS@N?(olHy`uVBq!ia0vp^0YLnLgAGVd=Y8x9q!^2X+?^QKos)S9vL>4nJ zh^c}wqi2xH2cV!#ri*_FkY)m65U2oR4JZv{GtOMSsCMO=#rNk=~Y;uD~pL;=t93pKO4%1KTv-=*`AJsgwt=JSyh_ zX$9ufZEJ6Wq>Ktmm)M*K(i0j}4_|u&l9JqE6*LcG!luORGO(dBvYO|?CM4Y4U}_CE zbY0(+=U@{yD@h6C%uV&YlAsx>}ihXVtqgoqGQ-=-cj=Jzs>(MFSrE z5ENZ;V0WK&qu&DI8TXn>e$2i;{rOEUpqnp8PvHA+QSvz+=#GRc?n@8zZ+*9k25~C5 z_W{HG|NqU_=AeLI01EK8*A8+r2MQd%@OJ-8-j$Bb2cHNQeMuMCcH?>Fz3cl87!1TX z92n|XEAU=%;GGk9@XN=ipC4;+E3g+m_`NwGCpzO0~;MKr~RG*rmTDS zx=jp43haLC6Mp^B?w?xD08@4}HawESasu=6aD&=EQ;(mrhAVrtZnaK>%!$U!+B59_ zol1Tx3sY88cs*1M=Fa-38$a>Ellsk;dTZ>gTe~DWM4fh)`pm literal 0 HcmV?d00001 diff --git a/AsyncDisplayKitTests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testAlignContentOverflow_alignContentStart@2x.png b/AsyncDisplayKitTests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testAlignContentOverflow_alignContentStart@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..1e32492bac6c7cacab7bd556359a2d270427b3c3 GIT binary patch literal 916 zcmeAS@N?(olHy`uVBq!ia0vp^0YLnLgAGVd=Y8x9q!^2X+?^QKos)S9vL>4nJ zh^c}wqi2xH2cV!#ri*_FkY)m65U2oR4JZv{GtOMSsCMO=#rNk=~Y;uD~pL;=t93pKO4%1KTv-=*`AJsgwt=JSyh_ zX$9ufZEJ6Wq>Ktmm)M*K(i0j}4_|u&l9JqE6*LcG!luORGO(dBvYO|?CM4Y4U}_CE zbY0(+=U@{yD@h6C%uV&YlAsx>}ihXVtqgoqGQ-=-cj=Jzs>(MFSrE z5ENZ;V0WK&qu&DI8TXn>e$2i;{rOEUpqnp8PvHA+QSvz+=#GRc?n@8zZ+*9k25~C5 z_W{HG|NqU_=AeLI01EK8*A8+r2MQd%@OJ-8-j$Bb2cHNQeMuMCcH?>Fz3cl87!1TX z92n|XEAU=%;GGk9@XN=ipC4;+E3g+m_`NwGCpzO0~;MKr~RG*rmTDS zx=jp43haLC6Mp^B?w?xD08@4}HawESasu=6aD&=EQ;(mrhAVrtZnaK>%!$U!+B59_ zol1Tx3sY88cs*1M=Fa-38$a>Ellsk;dTZ>gTe~DWM4fh)`pm literal 0 HcmV?d00001 diff --git a/AsyncDisplayKitTests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testAlignContentUnderflow_alignContentCenter@2x.png b/AsyncDisplayKitTests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testAlignContentUnderflow_alignContentCenter@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..c091f22768012f2dd3fefd319a6d7f9b38cf4b5e GIT binary patch literal 3619 zcmeAS@N?(olHy`uVBq!ia0vp^6$}ha5gcql*2-|Dvp|ZmILO_JVcj{Imp~3@fk$L9 z1A~|<2s3&HseAwm%4E9uhX83NAO-?P9U!3rp+RhRS@x5wG8h=xUV6GXhEy=Vxf>n(;+v0|06Fe@?P!M~TzUI{3GwGG~_Wzpq9bn%(_w-*5p8rA|ItG83K77sD zaNq;?f$N(OG9A`uh(FK9&9k4O##VY%a^wYqf!vSJ_ixzWFK+%Q{!aPb^xA{0|BNPl zT|fT-llgtd`bF1y_ngcA(zND4_=K;&8P;}k>|xtADli%zqsaklZm7BTzxLekpF8g? zIOx5JsXk9Z{LKA(Y8k$6361+-OaHsQecqRHU+c+hc()xi`=MX2I4R6bQaYzOcLVRg zO=5pkPyD)n_0J!*$ofV5-<+Pgz*Mr4_1pi0ujfC!zGvB7&ueVA6QuuS-TAe7$yDEC zAkLn=m)UWbdCOsF%>`x|-blv*-~6Sw_~>H5s{Nm~z^-C)}0CBGC)}GD2H8TaMecQqH$JW|=N8L=x0dZbW+xN`$ zt*<4E8$+fy5noQr}pd~ zkiRSLULwUipZOUW{{N2@Zw~`jdaFT|-rH%8T{j#Aj(qz+|DKAxfv$wp>Wgi2Vk}IT zPBgc-x8J}2@7>J@-#?x||G~S)-~C5l&u7s8-NafUIpKQz0S0!7yNz}iPwVk4GUs5o zII#QQ^q;@V?``|eyToN8&$$OY|K?9qwq<_^VxD`DD)z@;Vg3KL2gAi(f4S)Ip3`wp zLFNI&ck3Tt(|^3KW!4N-X*RZK-rrOGm*=G=M|uHU`iE@q{rAi#2)PPiKHry{XU0O0hUu{dF8Km{1r6r42i}!SdUPsD&N=vf?adOW zCC?S+9F*MivhMzSBZ!yZM{j=F;&I*)==qAvfA`%_RZ#6pDPX(5J?%2*L~uZSSoXVi z|7iy=k&_R2_T`^lCaeSrZr|^}>!vqO(U^2lvgZCQUv-x%zC%EJX21LXR}T`4}Jk44e0sykvMiVdEjjwCb0a7_TWNw=$pkc4;Z|8s}qN>@(gj z^=6M~?-AuQxa~ciZv&sCwoJmd>3XsWvPNMR2XgddtqxeFtSM-?8Mmus)WFd&9Ms9? ZKSK_8WR&wR-ix4KoTsaw%Q~loCIBy~;>!R4 literal 0 HcmV?d00001 diff --git a/AsyncDisplayKitTests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testAlignContentUnderflow_alignContentEnd@2x.png b/AsyncDisplayKitTests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testAlignContentUnderflow_alignContentEnd@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..888442daec10269a8ac9f1ebcee2dff2a70b66eb GIT binary patch literal 3239 zcmeAS@N?(olHy`uVBq!ia0vp^6$}ha5gcql*2-|Dvp|ZmILO_JVcj{Imp~3@fk$L9 z1A~|<2s3&HseAwm%4E9uhX83NAO-?P9U!3rp+RgG{U^pNv>6zfe|x$(hEy=Vxy&fY zz`)V$@O%0^+kYDvALPmAKFHYhAS~ezCs$0u9frQ^P24@w2R=p>G!`;j#Bu8xv(WNq-Tm*M|GU~Z$A3S+{ElnU z|D$357C*?{e^6x4njPPm12;6Cl>QK2virbGyCnStQ5(@q^Z1ttx*W`Pyj*)E?toT> z))HHJ59N-g+fB=UckFEl{Se|&Z8yP9fOR{o?{~p!#?=p2PWV=%(5t|e$35$v@;9ci zgTab#e>zAxirf)1x$FLhbxqT9r(2&JO`0@z=w|Hdzrh{B>fgHMIddjUU_tbS68Vkd z8@T3iWu9lh#IfkXwgsaO84Zy^oks3|u{rm@?(BTW2Rw>g|8_Mk+ca@6L)eC<^1UB2 z6>mRp)(}iOD3){Z|K_SG=?u{uSb<1?^@Dn@%>S_m^J80nXjk2Q?DCB%;vm%ofP~C^sJHDAi^%U&BG%5}ZlaU$@fBx?NlYiNt;U;$itA6!^M&lNZh=a51 z8t>Y3pE7RHz0veOeDljT$0-~e6S(hv+4p`v>r=@Cz*P5r_05vQko1&ab^rVMrcXQx z+}j@fzM8WuQ9#v6=SGwH-uJ)L9V^*D3GQ!}*>8mfqgNS z0?Y?@{$IM+?jWiO{&MpVpTE4Wxg*~w|6hnh=fh*^ia(WR z5f6@=fB5k^XTyR1{0ASO-%MzfzklHI`OOEJ?DsVupP$~$T35r|Z-1VR`_B(Hx%zp$ zVn06e+WfPT9ykN)BpU=@(Z0FIXcR*DJjJ%qZ2#affZ!Ipr$Hum{>6ziS%Yl$f?RuKU~( zH$n6V*Cjjo1rs|C)Row_cIqtbN5`^ zDlS%Wp66~v!j^S=H;fuNl*01g&!66J?%&_cyhOZ(^;J?&VhB^_}_fn{&4)~ z0NZAWy48hm&o>1v>Hullare^t$`$h*fuH!{f!Lp64?miY>)nB%74|E1E@WxdB4>4 ze>xhfT{l3S+q!>ewZD>V0cziNFupgtUJT~$x6|s*I>S7){@7M~Zm&)yuQW`QrBf zzUfBN0xk|5>pCwx?(?w_^DedCfB*e?`TG5r56*u)Uw+5^#^3hcujL!;zp*m^NOoAa zzkz{S=52%jm*Zk;i?(wx^L*I;Z+hil>-X8;b(i!g@uV3v|6lra;=FEyX2~-NZ1rcF z>N%hMx*t_jy0``01m>s78W;^S7@4&(9#{>%ZAP92b*$(9gJYn|wmUV%ZO! z|85*|`68Csz_{<9?&Ezc8zCud-nX+iOPL|5vS#n%ceAA;JS1Hu&OFGxKPT2);N?yZ z;~599TfZ9BFd9mbq;V&&U<==&^QIrF-yM~cF=Z$Sxn7;va6egWuf&5D>%Jdi*neE7 z%J4v7?E6;6y8g&7Nezp4-Q#BdBe(g*Q3m(Yz2a;?Y|}1wGxoiz(&zb5eP)q3v&_}6 xaS{){&v4aevsm?dufc)$hTU;IL(bqo@;Tg*QBemp4gf0_22WQ%mvv4FO#s`d>ihrz literal 0 HcmV?d00001 diff --git a/AsyncDisplayKitTests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testAlignContentUnderflow_alignContentSpaceBetween@2x.png b/AsyncDisplayKitTests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testAlignContentUnderflow_alignContentSpaceBetween@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..b8ef3d2aee9496f19097996c9c3269141b50abbd GIT binary patch literal 3600 zcmeAS@N?(olHy`uVBq!ia0vp^6$}ha5gcql*2-|Dvp|ZmILO_JVcj{Imp~3@fk$L9 z1A~|<2s3&HseAwm%4E9uhX83NAO-?P9U!3rp+Ri+XS&N8{{ZPto-U3d6^w7L2IfgS z@GuA7|9>g7y78!h5|i*;rk_UPqFF~>evty0kbAiCtQ+yYg>#@h~~MvR8VuuB)e86}kuiwjgp z^smt~*m*kGty_Cmd99BTSbzBATk}=l zszibuQ?PsK`pP9!J&%F(|H+E^y?M!8m28k>^0t35?cZW_@&-tMd~bIBv@MfUfa2Q@ zzMZy@1ke0sW?=aLe{O)EGXn$bc~I^B_F7<8iYE`tfqnm%-u}kgBGjTeX-4Y)c`ICJ zcFp@+V|Raj{r!^-`9_R^*71?W)1$3+#2(rNh{*uMcyBsb#D?DeG@y( z74{+e|Mbeg<@d7R>jtpV-G39$_DYgV(*5e;3nO zEC@8PLa6+&?xUR&Am^-~>IZkF%DZ3DM~V%Bido{d=iZB6(BsCTHsgWkov+(Fo+kkn zbM6bX+PmFBvR448X7Y~L`GV(<0u>9_tSv!z_{00Zzb?0AWHsOZd-v=n(U^G-KdV@@ zci1$3`pOYjP|5W4wcwfupIA;^SB^ONlyk~@_YF;_g*D>)6IiFK2ketO$ff7MpvHDo z<8TZdyZP(u~crE?)-joB7>~neV&$77= zkR*QH{MBB~Mo9iyUj6y4RU;&gug||y9n1vDK>pvK-7aNv1u`}PIC_vO?9XLOspv-7WWx`6X%P3_`Q4}i)Tdd9$X?thFq+>udzUw&zUx@?}V KelF{r5}E+4V`mEh literal 0 HcmV?d00001 diff --git a/AsyncDisplayKitTests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testAlignContentUnderflow_alignContentStart@2x.png b/AsyncDisplayKitTests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testAlignContentUnderflow_alignContentStart@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..d7907975ae19e95b610632a21f66ae6b94b18edf GIT binary patch literal 3239 zcmeAS@N?(olHy`uVBq!ia0vp^6$}ha5gcql*2-|Dvp|ZmILO_JVcj{Imp~3@fk$L9 z1A~|<2s3&HseAwm%4E9uhX83NAO-?P9U!3rp+RhpU_sMc20&Wg)5S5Qg7MANz&=?^ zo+H2hO|SFROAuM;q`+||Y5%OCHw!I)*4_X9`M;}ubNu)7%kQ`r{XZJ^Z}EfN{Rc(% ztl9C6IdDVMN$C&aCA$y2v`f-Y5VaA#G>?CYpv%Er$IG=x;tpt4Xf3go_fYO=y4|$w zcgNm_&<`OV)piry1X#DT`hFLzW?cPX<%DlF3cU(kdEB$!DSu-MI~c6^_NRlCqsSdG zle_M3Sl2Wyce?et(WFUZhi=BM{u|s8tp2TAo-=2%1QtYJD3RYNzJY5VSLS*4OB{(0)1e88i~^>0_xvP~2BGK6htD&PAdQ}OomW(~olgJL-c z|8K6ElFkslffb1KS3jud%KRUDFh92Chj!J?$1dNPA`WtWyZ_+Z`iIx|DR{L+0M&h1 zz2loXR8PU~OQYhzFd3=g@aOODKlzva8E$eXu6-^~kbm~S-c{-`sZohRYaDFD>y`~LU8 z$bfl_IS0jR_RsP)=UL>^0o1tcd+mQ6jq?oK61exppIs)+7N~TjNw?zi?|t=RkQ97> zecEMSNM6|UviAO{v*{k%|ADQB|NpODsfc1=VEzeeD!jeSCQj?|Kt=kMx0$Q3Z{K%ocIndIot6$FmQz9^-vbp}XV24n~>v ztl|>p4Ue}zV0z3}ut#iE(`Z19rij6tH7Xvky9Y!@S AU;qFB literal 0 HcmV?d00001 diff --git a/AsyncDisplayKitTests/ReferenceImages_iOS_10/ASStackLayoutSpecSnapshotTests/testAlignContentOverflow_alignContentCenter@2x.png b/AsyncDisplayKitTests/ReferenceImages_iOS_10/ASStackLayoutSpecSnapshotTests/testAlignContentOverflow_alignContentCenter@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..032e9fac3a5280787b015463deedd2ba95bb0ee1 GIT binary patch literal 1627 zcmeAS@N?(olHy`uVBq!ia0vp^0SpXG92{&w*3aNfIUvPY9OUlAu%K52Vd}}xj^YFo-U3d6^w7L9qc^f zAi(VKeEt_Xzk@C^9&Ac{`GzM&fmw2f z)g~${`+xiY{P)#tOg!8w4vcm2``Kb44&&MYaoDlqgs(gx*YL!AU2SL%cA}Y1q-`_M zT?)+Mf3wb<1v{44JM4S{(1j-sgzlFr20J_9@T9B85<-uiH#VdUD&uBI z{n#V;tBI#Yyyg2e{|2T#x{YjE3)uYf4}SUZ{~@Ou$njO&%i7ZuUi34tR&ehC$?j@B ztShj9Az$*M1MigsFLh_g-E+43;ttf!8pCWFzq?s^y(?uj%DO+a=ERAQKM&Z8yE3E0fm$n@<0>C3V7$_Kad9|_5c4n=BffS z4AT!#`hI)OG3$tf$dQ2i|1Zs*apyo(374GW=_a=q*?;Cdm-&BR(&j;J`}a-E25|}v z4BLd)a3%v&g2i9Sr!8|g2$voD8k|tM9^`SU9d|bTDg(Lt%7I%GpY8>@FY7_k;a9go z?haticAxegq-;gwY2MZO;IQCbBO7@S>}xhX^ELZoLCQ=wl%)(Z1MTx!{<1P9a{cjU zIN+ghE|McbMc_FPi-Ax_F&mQv=MhV0#vYbOa~c`kmzqppkzjwC!#_quIo^1}WELsr%u!5C=slr?tRL(44pYZFW g-4b%OPhbClebbJExt8Uu{Gig`)78&qol`;+0J(8*N&o-= literal 0 HcmV?d00001 diff --git a/AsyncDisplayKitTests/ReferenceImages_iOS_10/ASStackLayoutSpecSnapshotTests/testAlignContentOverflow_alignContentEnd@2x.png b/AsyncDisplayKitTests/ReferenceImages_iOS_10/ASStackLayoutSpecSnapshotTests/testAlignContentOverflow_alignContentEnd@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..6d08d18e4a2e04adb83abf446b71d70ec2781d3f GIT binary patch literal 1650 zcmeAS@N?(olHy`uVBq!ia0vp^0SpXG92{&w*3aNfIUvPY9OUlAu%Lmgz%F^KY`LuJY5_^Dj46~J($a4 z$isZ#?f#eE+gg5qj zCd&!G_2G$Dh9c)<(*mkuZ z))iR5kS}@Bf%gj7_*#l5Wk`vktffpQo>9R;B3YYAZ0 zb!?HKpNm0vjRdFA}?U;o=Sv+sHTu=WO{ zguZ|S!)?xMEr!5UHs@bY(ILMZoYoJ2X>Qm94hY_uyolP}pdimmczfbg94K&23w9=c zeG3Yi6^&b!PgjF{b>+a*wyWF0VZ(NISpf5N>9G6Y@Ro|O-bA#4zn=>`);BuL zVEV7fkf0*)T!+O#sH2#TNrLl;B{O3W%cD7s3~o$?=NuT68!gfm8YVi-F%~%Bp>R%; z1Eid11Szk%_g%c{|9Z(9Tb956HVluyGIBh~Jq$`fe(?vtJpO-pxkv*rmM(`I{H@^a z`O7G9;C6#CFs8p~%dBT`V8}N<1T0YsesL|6fA4JdjU6b$d=4mbVST|KCI-emXE{KH z&`a$Zb$j?<-jD~1@LB*x9QGgj$i=|&!Au!cZY^J*@av=9lH_`j$ODkbU#{cY3_z=T qKtY`Q@-_)IORQt)*3b6Mw<&;$S@(|>mW literal 0 HcmV?d00001 diff --git a/AsyncDisplayKitTests/ReferenceImages_iOS_10/ASStackLayoutSpecSnapshotTests/testAlignContentOverflow_alignContentSpaceAround@2x.png b/AsyncDisplayKitTests/ReferenceImages_iOS_10/ASStackLayoutSpecSnapshotTests/testAlignContentOverflow_alignContentSpaceAround@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..032e9fac3a5280787b015463deedd2ba95bb0ee1 GIT binary patch literal 1627 zcmeAS@N?(olHy`uVBq!ia0vp^0SpXG92{&w*3aNfIUvPY9OUlAu%K52Vd}}xj^YFo-U3d6^w7L9qc^f zAi(VKeEt_Xzk@C^9&Ac{`GzM&fmw2f z)g~${`+xiY{P)#tOg!8w4vcm2``Kb44&&MYaoDlqgs(gx*YL!AU2SL%cA}Y1q-`_M zT?)+Mf3wb<1v{44JM4S{(1j-sgzlFr20J_9@T9B85<-uiH#VdUD&uBI z{n#V;tBI#Yyyg2e{|2T#x{YjE3)uYf4}SUZ{~@Ou$njO&%i7ZuUi34tR&ehC$?j@B ztShj9Az$*M1MigsFLh_g-E+43;ttf!8pCWFzq?s^y(?uj%DO+a=ERAQKM&Z8yE3E0fm$n@<0>C3V7$_Kad9|_5c4n=BffS z4AT!#`hI)OG3$tf$dQ2i|1Zs*apyo(374GW=_a=q*?;Cdm-&BR(&j;J`}a-E25|}v z4BLd)a3%v&g2i9Sr!8|g2$voD8k|tM9^`SU9d|bTDg(Lt%7I%GpY8>@FY7_k;a9go z?haticAxegq-;gwY2MZO;IQCbBO7@S>}xhX^ELZoLCQ=wl%)(Z1MTx!{<1P9a{cjU zIN+ghE|McbMc_FPi-Ax_F&mQv=MhV0#vYbOa~c`kmzqppkzjwC!#_quIo^1}WELsr%u!5C=slr?tRL(44pYZFW g-4b%OPhbClebbJExt8Uu{Gig`)78&qol`;+0J(8*N&o-= literal 0 HcmV?d00001 diff --git a/AsyncDisplayKitTests/ReferenceImages_iOS_10/ASStackLayoutSpecSnapshotTests/testAlignContentOverflow_alignContentSpaceBetween@2x.png b/AsyncDisplayKitTests/ReferenceImages_iOS_10/ASStackLayoutSpecSnapshotTests/testAlignContentOverflow_alignContentSpaceBetween@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..4df2a81184c678df497e9022753bccd61f1284b7 GIT binary patch literal 1649 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 HM4tXT);94P literal 0 HcmV?d00001 diff --git a/AsyncDisplayKitTests/ReferenceImages_iOS_10/ASStackLayoutSpecSnapshotTests/testAlignContentUnderflow_alignContentSpaceBetween@2x.png b/AsyncDisplayKitTests/ReferenceImages_iOS_10/ASStackLayoutSpecSnapshotTests/testAlignContentUnderflow_alignContentSpaceBetween@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..17b8dbfbc4e90f1575cb36eb4a85125a302db3a2 GIT binary patch literal 5130 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 LAUU&~4k!N;Inr}8 literal 0 HcmV?d00001 diff --git a/AsyncDisplayKitTests/ReferenceImages_iOS_10/ASStackLayoutSpecSnapshotTests/testAlignContentUnderflow_alignContentStart@2x.png b/AsyncDisplayKitTests/ReferenceImages_iOS_10/ASStackLayoutSpecSnapshotTests/testAlignContentUnderflow_alignContentStart@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..ab969dd638ad25571cbad06b2b150c95e42cde60 GIT binary patch literal 4971 zcmeHLeKb_-8s9TxjE0$}6LB<{b1R>jXrV#MxJp_WjjxMQ)b038gP0J;h?MTs3MctG zk_^h^l#Y!sQb!ckF(eJPglaO#5&6j7n@-MJ_uu>9F>CF4Kla|w`}}^-?|I&L=B%@m z9RaV72LOOzzt5Hlw-NwA|4Tt0?yY%~`Xk(+BA9kIfUuqX4w+-`^Naw14O@^4WzXC^ z4o{K;Z5gg`i-s@wgW3vz=`OcOU&*)iZ2UjqcB{QD-Ss5uL(lWjjn&`DJ)2)-_?s&} zN%M6G0Nr{EFHn_!xOllJ%fcnMWnpsq>CX-E@}rIYcQ@=#`s8bPeS8Y{E4^EorWMlk zslfTwV!uOEThK{}+rewxAV;J@+_^eCAG=mhJJI`1)hQipb*H94FuQm%<95x}c#Hh6 zGc*EcxGwkGKrm40S3n3Z=)HC_;A6)ba_6FzEy`B<;C9{99huo2sH}1R#Jy3ES zFtbgsTNo*>=iZQ??3mb_e2h6>+l7|Y0K&H2Q#deloXR@XrYNa~aXl)j@MIWPNsmv9 zi)X}RSV={ilc70O+z5j+1o`fISt69GqPM;{eFO!=HIaGBxnATN5`*P93-v}0-kP3i&Dx|-QLtwe8UlnQ~u$ipxDmQiSw z0&~07u>M`cYETo*Sa|Xh)0g5Caqb~%#|IvU%vLu_{SK~27}<26mj$!Ex<975?$)0b zsjztR+2+(h!8%hUVd)tn6pqa1XYe!pckO)sRoh6ynkiNKt*eYxW9TJ~4(S~Q9?i~e ztt0g^&-Eo@gk}YuiDR0#6ndDix{e-O!Vw`#kQl1-4+^=T~z{o~8NT- zGCvcEQMaf(z^O(}vh;eWyGX~cwu?EBhU-ZFMNHL4VTdzq&U zm)5joLzVX}lK||Yy6o*-S!--%!hf(XqzI-zkTqjTH4zbl=@2(*`!cKT=}#ipGQ;GO^-=>z>JKz^ z%GIKqEq?=+mb1Jc{Ti;5IM(y9un{zBdA*5m>+L>;GY$k*dX+QEqV@0k97@oUi`SVh z7#>7>s(dIN1THHkG$+f0eufHUwzK>JGz?r6eW_YT*XI`)VU0&6JA%M$Uk~VHvl7v)u*`Ca8i{ly(iU!(LGAvCflWi8t||#9`dW$qj=x7)b@PKkFvx!XRnT&KA}*Ma9lJi`_18#37}pct3Zfmu3r?ucoFrHV`L# zZO|_2MYqO$3nIIEoWly>ZtUX0Z~iGxohwq!qSp#|ShekO*HUh*iNf+Z=V4`R@sM#& zPd+e)y#fmdU$J6~p{f9JEqA?1w?ODtlmToJ zcF?ReIHn5?GlS&-G>8`_4a~H^g9xEf0~WroL_5;)sTZss5F)~nGYYjT-yY}=!=Zv0 z_|$OzRgv(Wry?9}RS62V`CGZbO}h=hVlGE)Y88x}DtMLvY!jTSY%TIci1}(Ry{6dT z5J@XG4z{UQmG6GV#9+nTG3JXuZemxw?ds4^{}H=l&hdSA{#~SseDyZQ>*(({|6q&6HypB6R?Mw}|9eFqOmAGk0l?y73S=4r0P+>c?}pJ1IspI_FvjyanpBya z($+vQXOlH$Js8qLLZR2^>QG2{1?mH{J=W}auC*=&B)w8bHGW2+uW})q5EnVQ5uCzl zG*9st$91G!t<{HD*;v)aYNo7~g4M$K|K27blu=31JN3(40evgx_(8ibdU#nRR Aw*UYD literal 0 HcmV?d00001 diff --git a/AsyncDisplayKitTests/ReferenceImages_iOS_10/ASStackLayoutSpecSnapshotTests/testAlignContentUnderflow_alignContentStretch@2x.png b/AsyncDisplayKitTests/ReferenceImages_iOS_10/ASStackLayoutSpecSnapshotTests/testAlignContentUnderflow_alignContentStretch@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..584aca77dbdfa07fd0a6900eb559dc745ea8b6bf GIT binary patch literal 5089 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 ZBF-jS%nO0;Q&4XPJhpog%UnW}{se=@?a}}M literal 0 HcmV?d00001