[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
@@ -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";
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)};
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
@@ -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 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<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};
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
|
After Width: | Height: | Size: 1021 B |
|
After Width: | Height: | Size: 890 B |
|
After Width: | Height: | Size: 1021 B |
|
After Width: | Height: | Size: 916 B |
|
After Width: | Height: | Size: 916 B |
|
After Width: | Height: | Size: 3.5 KiB |
|
After Width: | Height: | Size: 3.2 KiB |
|
After Width: | Height: | Size: 3.6 KiB |
|
After Width: | Height: | Size: 3.5 KiB |
|
After Width: | Height: | Size: 3.2 KiB |
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 4.8 KiB |
|
After Width: | Height: | Size: 5.0 KiB |
|
After Width: | Height: | Size: 4.9 KiB |
|
After Width: | Height: | Size: 4.9 KiB |
|
After Width: | Height: | Size: 5.0 KiB |
|
After Width: | Height: | Size: 4.9 KiB |
|
After Width: | Height: | Size: 5.0 KiB |
|
After Width: | Height: | Size: 3.2 KiB |