[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
This commit is contained in:
Huy Nguyen
2017-02-26 19:48:32 +00:00
committed by Adlai Holler
parent 61cab6f643
commit e48edef4e7
34 changed files with 616 additions and 203 deletions

View File

@@ -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";

View File

@@ -75,6 +75,13 @@
ReferencedContainer = "container:AsyncDisplayKit.xcodeproj">
</BuildableReference>
</MacroExpansion>
<EnvironmentVariables>
<EnvironmentVariable
key = "FB_REFERENCE_IMAGE_DIR"
value = "$(SOURCE_ROOT)/$(PROJECT_NAME)Tests/ReferenceImages"
isEnabled = "YES">
</EnvironmentVariable>
</EnvironmentVariables>
<AdditionalOptions>
</AdditionalOptions>
</LaunchAction>

View File

@@ -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 */

View File

@@ -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<id<ASLayoutElement>> *)children AS_WARN_UNUSED_RESULT;
+ (instancetype)stackLayoutSpecWithDirection:(ASStackLayoutDirection)direction
spacing:(CGFloat)spacing
justifyContent:(ASStackLayoutJustifyContent)justifyContent
alignItems:(ASStackLayoutAlignItems)alignItems
children:(NSArray<id<ASLayoutElement>> *)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<id<ASLayoutElement>> *)children AS_WARN_UNUSED_RESULT;
/**
* @return A stack layout spec with direction of ASStackLayoutDirectionVertical

View File

@@ -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<id<ASLayoutElement>> *)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

View File

@@ -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,

View File

@@ -15,7 +15,9 @@
/** Represents a set of laid out and positioned stack layout children. */
struct ASStackPositionedLayout {
const std::vector<ASStackLayoutSpecItem> 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,

View File

@@ -11,24 +11,26 @@
#import <AsyncDisplayKit/ASStackPositionedLayout.h>
#import <tgmath.h>
#import <numeric>
#import <AsyncDisplayKit/ASDimension.h>
#import <AsyncDisplayKit/ASInternalHelpers.h>
#import <AsyncDisplayKit/ASLayoutSpecUtilities.h>
#import <AsyncDisplayKit/ASLayoutSpec+Subclasses.h>
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<ASStackLayoutSpecItem> 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)};
}

View File

@@ -14,6 +14,9 @@
#import <AsyncDisplayKit/ASStackLayoutSpecUtilities.h>
#import <AsyncDisplayKit/ASStackLayoutSpec.h>
/** The threshold that determines if a violation has actually occurred. */
extern CGFloat const kViolationEpsilon;
struct ASStackLayoutSpecChild {
/** The original source child. */
id<ASLayoutElement> 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<ASStackLayoutSpecItem> 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<ASStackLayoutSpecItem> 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<ASStackUnpositionedLine> 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<ASStackLayoutSpecChild> &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);
};

View File

@@ -16,6 +16,8 @@
#import <AsyncDisplayKit/ASLayoutSpecUtilities.h>
#import <AsyncDisplayKit/ASLayoutElementStylePrivate.h>
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<ASStackUnpositionedLine> &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<ASStackLayoutSpecItem> &items,
const ASStackLayoutSpecStyle &style,
const CGSize parentSize,
const CGFloat crossSize)
static void stretchItemsAlongCrossDimension(std::vector<ASStackLayoutSpecItem> &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<ASStackLayoutSpecItem
}
}
/**
* Stretch lines and their items according to alignContent, alignItems and alignSelf.
* https://www.w3.org/TR/css-flexbox-1/#algo-line-stretch
* https://www.w3.org/TR/css-flexbox-1/#algo-stretch
*/
static void stretchLinesAlongCrossDimension(std::vector<ASStackUnpositionedLine> &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<ASStackLayoutSpecItem> &items,
const ASStackLayoutSpecStyle &style,
const ASSizeRange &sizeRange,
CGFloat &crossSize,
CGFloat &baseline)
static void computeLinesCrossSizeAndBaseline(std::vector<ASStackUnpositionedLine> &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 items baseline and its hypothetical outer cross-start edge (aka. its ascender value),
// and the largest of the distances between each items 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 items baseline and its hypothetical outer cross-start edge (aka. its baseline value),
// and the largest of the distances between each items 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 lines 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<ASStackLayoutSpecItem>
@param items unpositioned layouts for items
@param style the layout style of the overall stack layout
*/
static CGFloat computeStackDimensionSum(const std::vector<ASStackLayoutSpecItem> &items,
const ASStackLayoutSpecStyle &style)
static CGFloat computeItemsStackDimensionSum(const std::vector<ASStackLayoutSpecItem> &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<ASStackLayoutSpecItem>
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<ASStackLayoutSpecItem>
@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<ASStackLayoutSpe
/**
Flexes children in the stack axis to resolve a min or max stack size violation. First, determines which children are
flexible (see computeViolation and isFlexibleInViolationDirection). Then computes how much to flex each flexible child
flexible (see computeStackViolation and isFlexibleInViolationDirection). Then computes how much to flex each flexible child
and performs re-layout. Note that there may still be a non-zero violation even after flexing.
The actual CSS flexbox spec describes an iterative looping algorithm here, which may be adopted in t5837937:
http://www.w3.org/TR/css3-flexbox/#resolve-flexible-lengths
@param items Reference to unpositioned items from the original, unconstrained layout pass; modified in-place
@param lines reference to unpositioned lines and items from the original, unconstrained layout pass; modified in-place
@param style layout style to be applied to all children
@param sizeRange the range of allowable sizes for the stack layout component
@param parentSize Size of the stack layout component. May be undefined in either or both directions.
*/
static void flexChildrenAlongStackDimension(std::vector<ASStackLayoutSpecItem> &items,
const ASStackLayoutSpecStyle &style,
const ASSizeRange &sizeRange,
const CGSize parentSize,
const BOOL useOptimizedFlexing)
static void flexLinesAlongStackDimension(std::vector<ASStackUnpositionedLine> &lines,
const ASStackLayoutSpecStyle &style,
const ASSizeRange &sizeRange,
const CGSize parentSize,
const BOOL useOptimizedFlexing)
{
const CGFloat violation = computeViolation(computeStackDimensionSum(items, style), style, sizeRange);
std::function<CGFloat(const ASStackLayoutSpecItem &)> 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<CGFloat(const ASStackLayoutSpecItem &)> 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<CGFloat(const ASStackLayoutSpecItem &)> 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<CGFloat(const ASStackLayoutSpecItem &)> 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<ASStackUnpositionedLine> collectChildrenIntoLines(const std::vector<ASStackLayoutSpecItem> &items,
const ASStackLayoutSpecStyle &style,
const ASSizeRange &sizeRange)
{
//TODO if infinite max stack size, fast path
if (style.flexWrap == ASStackLayoutFlexWrapNoWrap) {
return std::vector<ASStackUnpositionedLine> (1, {.items = std::move(items)});
}
std::vector<ASStackUnpositionedLine> lines;
std::vector<ASStackLayoutSpecItem> 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<ASStackLayoutSpecItem> (lineItems)});
lineItems.clear();
lineStackDimensionSum = 0;
}
lineItems.push_back(std::move(item));
lineStackDimensionSum += itemStackDimension;
}
// Handle last line
lines.push_back({.items = std::vector<ASStackLayoutSpecItem> (lineItems)});
return lines;
}
/**
@@ -507,25 +669,34 @@ ASStackUnpositionedLayout ASStackUnpositionedLayout::compute(const std::vector<A
// the stack dimension. This allows us to compute the "intrinsic" size of each child and find the available violation
// which determines whether we must grow or shrink the flexible children.
std::vector<ASStackLayoutSpecItem> 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<ASStackUnpositionedLine> 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};
}

View File

@@ -22,15 +22,6 @@
@implementation ASStackLayoutSpecSnapshotTests
#pragma mark - XCTestCase
- (void)setUp
{
[super setUp];
self.recordMode = NO;
}
#pragma mark - Utility methods
static NSArray<ASDisplayNode *> *defaultSubnodes()
@@ -110,6 +101,8 @@ static NSArray<ASTextNode*> *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<ASTextNode*> *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<ASDisplayNode *> *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<ASTextNode*> *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<ASDisplayNode *> *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