[Layout] Add support for flex factor (#2298)

* Add support for flex factor

* Add snapshot tests

* Respect child specified size from ASLayoutable

* Add new snapshot test images

* Fix rebase conflict
This commit is contained in:
Michael Schneider 2016-09-27 15:39:50 -07:00 committed by Adlai Holler
parent f421787cd3
commit e85583523f
44 changed files with 440 additions and 147 deletions

View File

@ -78,7 +78,7 @@
// of the button node to add a touch handler.
[_titleNode setLayerBacked:YES];
#endif
_titleNode.style.flexShrink = YES;
_titleNode.style.flexShrink = 1;
}
return _titleNode;
}

View File

@ -406,7 +406,7 @@ static void *ASVideoPlayerNodeContext = &ASVideoPlayerNodeContext;
return slider;
}];
_scrubberNode.style.flexShrink = YES;
_scrubberNode.style.flexShrink = 1;
[_cachedControls setObject:_scrubberNode forKey:@(ASVideoPlayerNodeControlTypeScrubber)];
}
@ -418,7 +418,7 @@ static void *ASVideoPlayerNodeContext = &ASVideoPlayerNodeContext;
{
if (_controlFlexGrowSpacerSpec == nil) {
_controlFlexGrowSpacerSpec = [[ASStackLayoutSpec alloc] init];
_controlFlexGrowSpacerSpec.style.flexGrow = YES;
_controlFlexGrowSpacerSpec.style.flexGrow = 1;
}
[_cachedControls setObject:_controlFlexGrowSpacerSpec forKey:@(ASVideoPlayerNodeControlTypeFlexGrowSpacer)];
@ -735,7 +735,7 @@ static void *ASVideoPlayerNodeContext = &ASVideoPlayerNodeContext;
_scrubberNode.style.height = ASDimensionMakeWithPoints(44.0);
ASLayoutSpec *spacer = [[ASLayoutSpec alloc] init];
spacer.style.flexGrow = YES;
spacer.style.flexGrow = 1;
ASStackLayoutSpec *controlbarSpec = [ASStackLayoutSpec stackLayoutSpecWithDirection:ASStackLayoutDirectionHorizontal
spacing:10.0

View File

@ -37,8 +37,8 @@ typedef struct ASEnvironmentStateExtensions {
typedef struct ASEnvironmentLayoutOptionsState {
CGFloat spacingBefore;// = 0;
CGFloat spacingAfter;// = 0;
BOOL flexGrow;// = NO;
BOOL flexShrink;// = NO; Default value is YES if created via ASEnvironmentLayoutOptionsStateMakeDefault
CGFloat flexGrow;// = 0;
CGFloat flexShrink;// = 0;
ASDimension flexBasis;// = ASRelativeDimensionAuto;
ASStackLayoutAlignSelf alignSelf;// = ASStackLayoutAlignSelfAuto;
CGFloat ascender;// = 0;

View File

@ -269,16 +269,18 @@ extern NSString * const ASLayoutableStyleLayoutPositionProperty;
@property (nonatomic, assign) CGFloat spacingAfter;
/**
* @abstract If the sum of childrens' stack dimensions is less than the minimum size, should this object grow?
* Used when attached to a stack layout.
* @abstract If the sum of childrens' stack dimensions is less than the minimum size, how much should this component grow?
* This value represents the "flex grow factor" and determines how much this component should grow in relation to any
* other flexible children.
*/
@property (nonatomic, assign) BOOL flexGrow;
@property (nonatomic, assign) CGFloat flexGrow;
/**
* @abstract If the sum of childrens' stack dimensions is greater than the maximum size, should this object shrink?
* Used when attached to a stack layout.
* @abstract If the sum of childrens' stack dimensions is greater than the maximum size, how much should this component shrink?
* This value represents the "flex shrink factor" and determines how much this component should shink in relation to
* other flexible children.
*/
@property (nonatomic, assign) BOOL flexShrink;
@property (nonatomic, assign) CGFloat flexShrink;
/**
* @abstract Specifies the initial size in the stack dimension for this object.

View File

@ -251,14 +251,14 @@ do {\
ASLayoutableStyleCallDelegate(ASLayoutableStyleSpacingAfterProperty);
}
- (void)setFlexGrow:(BOOL)flexGrow
- (void)setFlexGrow:(CGFloat)flexGrow
{
ASDN::MutexLocker l(__instanceLock__);
_flexGrow = flexGrow;
ASLayoutableStyleCallDelegate(ASLayoutableStyleFlexGrowProperty);
}
- (void)setFlexShrink:(BOOL)flexShrink
- (void)setFlexShrink:(CGFloat)flexShrink
{
ASDN::MutexLocker l(__instanceLock__);
_flexShrink = flexShrink;

View File

@ -31,16 +31,18 @@ NS_ASSUME_NONNULL_BEGIN
@property (nonatomic, readwrite) CGFloat spacingAfter;
/**
* @abstract If the sum of childrens' stack dimensions is less than the minimum size, should this object grow?
* Used when attached to a stack layout.
* @abstract If the sum of childrens' stack dimensions is less than the minimum size, how much should this component grow?
* This value represents the "flex grow factor" and determines how much this component should grow in relation to any
* other flexible children.
*/
@property (nonatomic, readwrite) BOOL flexGrow;
@property (nonatomic, readwrite) CGFloat flexGrow;
/**
* @abstract If the sum of childrens' stack dimensions is greater than the maximum size, should this object shrink?
* Used when attached to a stack layout.
* @abstract If the sum of childrens' stack dimensions is greater than the maximum size, how much should this component shrink?
* This value represents the "flex shrink factor" and determines how much this component should shink in relation to
* other flexible children.
*/
@property (nonatomic, readwrite) BOOL flexShrink;
@property (nonatomic, readwrite) CGFloat flexShrink;
/**
* @abstract Specifies the initial size in the stack dimension for this object.

View File

@ -15,6 +15,31 @@
#import "ASLayoutSpecUtilities.h"
static CGFloat resolveCrossDimensionMaxForStretchChild(const ASStackLayoutSpecStyle &style,
const id<ASLayoutable>child,
const CGFloat stackMax,
const CGFloat crossMax)
{
// stretched children may have a cross direction max that is smaller than the minimum size constraint of the parent.
const CGFloat computedMax = (style.direction == ASStackLayoutDirectionVertical ?
ASLayoutableSizeResolve(child.style.size, ASLayoutableParentSizeUndefined).max.width :
ASLayoutableSizeResolve(child.style.size, ASLayoutableParentSizeUndefined).max.height);
return computedMax == INFINITY ? crossMax : computedMax;
}
static CGFloat resolveCrossDimensionMinForStretchChild(const ASStackLayoutSpecStyle &style,
const id<ASLayoutable>child,
const CGFloat stackMax,
const CGFloat crossMin)
{
// stretched children will have a cross dimension of at least crossMin, unless they explicitly define a child size
// that is smaller than the constraint of the parent.
return (style.direction == ASStackLayoutDirectionVertical ?
ASLayoutableSizeResolve(child.style.size, ASLayoutableParentSizeUndefined).min.width :
ASLayoutableSizeResolve(child.style.size, ASLayoutableParentSizeUndefined).min.height) ?: crossMin;
}
/**
Sizes the child given the parameters specified, and returns the computed layout.
*/
@ -28,8 +53,11 @@ static ASLayout *crossChildLayout(const id<ASLayoutable> child,
{
const ASStackLayoutAlignItems alignItems = alignment(child.style.alignSelf, style.alignItems);
// stretched children will have a cross dimension of at least crossMin
const CGFloat childCrossMin = alignItems == ASStackLayoutAlignItemsStretch ? crossMin : 0;
const ASSizeRange childSizeRange = directionSizeRange(style.direction, stackMin, stackMax, childCrossMin, crossMax);
const CGFloat childCrossMin = (alignItems == ASStackLayoutAlignItemsStretch ? resolveCrossDimensionMinForStretchChild(style, child, stackMax, crossMin) : 0);
const CGFloat childCrossMax = (alignItems == ASStackLayoutAlignItemsStretch ?
resolveCrossDimensionMaxForStretchChild(style, child, stackMax, crossMax) :
crossMax);
const ASSizeRange childSizeRange = directionSizeRange(style.direction, stackMin, stackMax, childCrossMin, childCrossMax);
ASLayout *layout = [child layoutThatFits:childSizeRange parentSize:size];
ASDisplayNodeCAssertNotNil(layout, @"ASLayout returned from measureWithSizeRange: must not be nil: %@", child);
return layout ? : [ASLayout layoutWithLayoutable:child size:{0, 0}];
@ -93,6 +121,120 @@ static void stretchChildrenAlongCrossDimension(std::vector<ASStackUnpositionedIt
}
}
/** The threshold that determines if a violation has actually occurred. */
static const CGFloat kViolationEpsilon = 0.01;
/**
Returns a lambda that computes the relevant flex factor based on the given violation.
@param violation The amount that the stack layout violates its size range. See header for sign interpretation.
*/
static std::function<CGFloat(const ASStackUnpositionedItem &)> flexFactorInViolationDirection(const CGFloat violation)
{
if (fabs(violation) < kViolationEpsilon) {
return [](const ASStackUnpositionedItem &item) { return 0; };
} else if (violation > 0) {
return [](const ASStackUnpositionedItem &item) { return item.child.style.flexGrow; };
} else {
return [](const ASStackUnpositionedItem &item) { return item.child.style.flexShrink; };
}
}
static inline CGFloat scaledFlexShrinkFactor(const ASStackUnpositionedItem &item, const ASStackLayoutSpecStyle &style)
{
return stackDimension(style.direction, item.layout.size) * item.child.style.flexShrink;
}
/**
Returns a lambda that computes a flex shrink adjustment for a given item based on the provided violation.
@param items The unpositioned items from the original unconstrained layout pass.
@param style The layout style to be applied to all children.
@param violation The amount that the stack layout violates its size range.
@return A lambda capable of computing the flex shrink adjustment, if any, for a particular item.
*/
static std::function<CGFloat(const ASStackUnpositionedItem &, BOOL)> flexShrinkAdjustment(const std::vector<ASStackUnpositionedItem> &items,
const ASStackLayoutSpecStyle &style,
const CGFloat violation)
{
const CGFloat scaledFlexShrinkFactorSum = std::accumulate(items.begin(), items.end(), 0, [&](CGFloat x, const ASStackUnpositionedItem &item) {
return x + scaledFlexShrinkFactor(item, style);
});
return [style, scaledFlexShrinkFactorSum, violation](const ASStackUnpositionedItem &item, BOOL isFirstFlex) {
const CGFloat scaledFlexShrinkFactorRatio = scaledFlexShrinkFactor(item, style) / scaledFlexShrinkFactorSum;
// The item should shrink proportionally to the scaled flex shrink factor ratio computed above.
// Unlike the flex grow adjustment the flex shrink adjustment needs to take the size of each item into account.
return -fabs(scaledFlexShrinkFactorRatio * violation);
};
}
/**
Returns a lambda that computes a flex grow adjustment for a given item based on the provided violation.
@param items The unpositioned items from the original unconstrained layout pass.
@param violation The amount that the stack layout violates its size range.
@param flexFactorSum The sum of each item's flex factor as determined by the provided violation.
@return A lambda capable of computing the flex grow adjustment, if any, for a particular item.
*/
static std::function<CGFloat(const ASStackUnpositionedItem &, BOOL)> flexGrowAdjustment(const std::vector<ASStackUnpositionedItem> &items,
const CGFloat violation,
const CGFloat flexFactorSum)
{
const CGFloat violationPerFlexFactor = floorf(violation / flexFactorSum);
const CGFloat remainingViolation = violation - (violationPerFlexFactor * flexFactorSum);
// To compute the flex grow adjustment distribute the violation proportionally based on each item's flex grow factor.
// If there happens to be a violation remaining make sure it is allocated to the first flexible child.
return [violationPerFlexFactor, remainingViolation](const ASStackUnpositionedItem &item, BOOL isFirstFlex) {
// Only apply the remaining violation for the first flexible child that has a flex grow factor.
return violationPerFlexFactor * item.child.style.flexGrow + (isFirstFlex && item.child.style.flexGrow > 0 ? remainingViolation : 0);
};
}
/**
Returns a lambda that computes a flex adjustment for a given item based on the provided violation.
@param items The unpositioned items from the original unconstrained layout pass.
@param style The layout style to be applied to all children.
@param violation The amount that the stack layout violates its size range.
@param flexFactorSum The sum of each item's flex factor as determined by the provided violation.
@return A lambda capable of computing the flex adjustment for a particular item.
*/
static std::function<CGFloat(const ASStackUnpositionedItem &, BOOL)> flexAdjustmentInViolationDirection(const std::vector<ASStackUnpositionedItem> &items,
const ASStackLayoutSpecStyle &style,
const CGFloat violation,
const CGFloat flexFactorSum)
{
if (violation > 0) {
return flexGrowAdjustment(items, violation, flexFactorSum);
} else {
return flexShrinkAdjustment(items, style, violation);
}
}
ASDISPLAYNODE_INLINE BOOL isFlexibleInBothDirections(id<ASLayoutable> child)
{
return child.style.flexGrow > 0 && child.style.flexShrink > 0;
}
/**
The flexible children may have been left not laid out in the initial layout pass, so we may have to go through and size
these children at zero size so that the children layouts are at least present.
*/
static void layoutFlexibleChildrenAtZeroSize(std::vector<ASStackUnpositionedItem> &items,
const ASStackLayoutSpecStyle &style,
const ASSizeRange &sizeRange,
const CGSize size)
{
for (ASStackUnpositionedItem &item : items) {
const id<ASLayoutable> child = item.child;
if (isFlexibleInBothDirections(child)) {
item.layout = crossChildLayout(child,
style,
0,
0,
crossDimension(style.direction, sizeRange.min),
crossDimension(style.direction, sizeRange.max),
size);
}
}
}
/**
Computes the consumed stack dimension length for the given vector of children and stacking style.
@ -172,30 +314,6 @@ static CGFloat computeViolation(const CGFloat stackDimensionSum,
return 0;
}
/** The threshold that determines if a violation has actually occurred. */
static const CGFloat kViolationEpsilon = 0.01;
/**
Returns a lambda that determines if the given unpositioned item's child is flexible in the direction of the violation.
@param violation the amount that the stack layout violates its size range. See header for sign interpretation.
*/
static std::function<BOOL(const ASStackUnpositionedItem &)> isFlexibleInViolationDirection(const CGFloat violation)
{
if (std::fabs(violation) < kViolationEpsilon) {
return [](const ASStackUnpositionedItem &l) { return NO; };
} else if (violation > 0) {
return [](const ASStackUnpositionedItem &l) { return l.child.style.flexGrow; };
} else {
return [](const ASStackUnpositionedItem &l) { return l.child.style.flexShrink; };
}
}
ASDISPLAYNODE_INLINE BOOL isFlexibleInBothDirections(id<ASLayoutable> child)
{
return child.style.flexGrow && child.style.flexShrink;
}
/**
If we have a single flexible (both shrinkable and growable) child, and our allowed size range is set to a specific
number then we may avoid the first "intrinsic" size calculation.
@ -210,29 +328,6 @@ ASDISPLAYNODE_INLINE BOOL useOptimizedFlexing(const std::vector<id<ASLayoutable>
stackDimension(style.direction, sizeRange.max)));
}
/**
The flexible children may have been left not laid out in the initial layout pass, so we may have to go through and size
these children at zero size so that the children layouts are at least present.
*/
static void layoutFlexibleChildrenAtZeroSize(std::vector<ASStackUnpositionedItem> &items,
const ASStackLayoutSpecStyle &style,
const ASSizeRange &sizeRange,
const CGSize size)
{
for (ASStackUnpositionedItem &item : items) {
const id<ASLayoutable> child = item.child;
if (isFlexibleInBothDirections(child)) {
item.layout = crossChildLayout(child,
style,
0,
0,
crossDimension(style.direction, sizeRange.min),
crossDimension(style.direction, sizeRange.max),
size);
}
}
}
/**
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
@ -243,7 +338,8 @@ static void layoutFlexibleChildrenAtZeroSize(std::vector<ASStackUnpositionedItem
@param items Reference to unpositioned 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 spec
@param sizeRange the range of allowable sizes for the stack layout component
@param size Size of the stack layout component. May be undefined in either or both directions.
*/
static void flexChildrenAlongStackDimension(std::vector<ASStackUnpositionedItem> &items,
const ASStackLayoutSpecStyle &style,
@ -251,32 +347,32 @@ static void flexChildrenAlongStackDimension(std::vector<ASStackUnpositionedItem>
const CGSize size,
const BOOL useOptimizedFlexing)
{
const CGFloat stackDimensionSum = computeStackDimensionSum(items, style);
const CGFloat violation = computeViolation(stackDimensionSum, style, sizeRange);
// We count the number of children which are flexible in the direction of the violation
std::function<BOOL(const ASStackUnpositionedItem &)> isFlex = isFlexibleInViolationDirection(violation);
const NSUInteger flexibleChildren = std::count_if(items.begin(), items.end(), isFlex);
if (flexibleChildren == 0) {
// If optimized flexing was used then we have to clean up the unsized children, and lay them out at zero size
const CGFloat violation = computeViolation(computeStackDimensionSum(items, style), style, sizeRange);
std::function<CGFloat(const ASStackUnpositionedItem &)> 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, [&](CGFloat x, const ASStackUnpositionedItem &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, size);
}
return;
}
// Each flexible child along the direction of the violation is expanded or contracted equally
const CGFloat violationPerFlexChild = std::floor(violation / flexibleChildren);
// If the floor operation above left a remainder we may have a remainder after deducting the adjustments from all the
// contributions of the flexible children.
const CGFloat violationRemainder = violation - (violationPerFlexChild * flexibleChildren);
std::function<CGFloat(const ASStackUnpositionedItem &, BOOL)> flexAdjustment = flexAdjustmentInViolationDirection(items,
style,
violation,
flexFactorSum);
BOOL isFirstFlex = YES;
for (ASStackUnpositionedItem &item : items) {
if (isFlex(item)) {
const CGFloat currentFlexAdjustment = flexAdjustment(item, isFirstFlex);
// 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);
// The first flexible child is given the additional violation remainder
const CGFloat flexedStackSize = originalStackSize + violationPerFlexChild + (isFirstFlex ? violationRemainder : 0);
const CGFloat flexedStackSize = originalStackSize + currentFlexAdjustment;
item.layout = crossChildLayout(item.child,
style,
MAX(flexedStackSize, 0),

View File

@ -90,7 +90,7 @@ static NSString *suffixForCenteringOptions(ASCenterLayoutSpecCenteringOptions ce
{
ASDisplayNode *backgroundNode = ASDisplayNodeWithBackgroundColor([UIColor redColor]);
ASDisplayNode *foregroundNode = ASDisplayNodeWithBackgroundColor([UIColor redColor], CGSizeMake(10, 10));
foregroundNode.style.flexGrow = YES;
foregroundNode.style.flexGrow = 1;
ASCenterLayoutSpec *layoutSpec =
[ASCenterLayoutSpec

View File

@ -107,7 +107,7 @@ static NSString *suffixForPositionOptions(ASRelativeLayoutSpecPosition horizonta
{
ASDisplayNode *backgroundNode = ASDisplayNodeWithBackgroundColor([UIColor redColor]);
ASDisplayNode *foregroundNode = ASDisplayNodeWithBackgroundColor([UIColor redColor], CGSizeMake(10, 10));
foregroundNode.style.flexGrow = YES;
foregroundNode.style.flexGrow = 1;
ASLayoutSpec *childSpec =
[ASBackgroundLayoutSpec

View File

@ -25,10 +25,10 @@
static NSArray<ASDisplayNode *> *defaultSubnodes()
{
return defaultSubnodesWithSameSize(CGSizeZero, NO);
return defaultSubnodesWithSameSize(CGSizeZero, 0);
}
static NSArray<ASDisplayNode *> *defaultSubnodesWithSameSize(CGSize subnodeSize, BOOL flex)
static NSArray<ASDisplayNode *> *defaultSubnodesWithSameSize(CGSize subnodeSize, CGFloat flex)
{
NSArray<ASDisplayNode *> *subnodes = @[
ASDisplayNodeWithBackgroundColor([UIColor redColor], subnodeSize),
@ -62,7 +62,7 @@ static void setCGSizeToNode(CGSize size, ASDisplayNode *node)
}
- (void)testStackLayoutSpecWithJustify:(ASStackLayoutJustifyContent)justify
flex:(BOOL)flex
flexFactor:(CGFloat)flex
sizeRange:(ASSizeRange)sizeRange
identifier:(NSString *)identifier
{
@ -81,7 +81,7 @@ static void setCGSizeToNode(CGSize size, ASDisplayNode *node)
itemsVerticalAlignment:(ASVerticalAlignment)verticalAlignment
identifier:(NSString *)identifier
{
NSArray<ASDisplayNode *> *subnodes = defaultSubnodesWithSameSize({50, 50}, NO);
NSArray<ASDisplayNode *> *subnodes = defaultSubnodesWithSameSize({50, 50}, 0);
ASStackLayoutSpec *stackLayoutSpec = [[ASStackLayoutSpec alloc] init];
stackLayoutSpec.direction = direction;
@ -139,34 +139,34 @@ static void setCGSizeToNode(CGSize size, ASDisplayNode *node)
{
// width 300px; height 0-300px
static ASSizeRange kSize = {{300, 0}, {300, 300}};
[self testStackLayoutSpecWithJustify:ASStackLayoutJustifyContentStart flex:NO sizeRange:kSize identifier:@"justifyStart"];
[self testStackLayoutSpecWithJustify:ASStackLayoutJustifyContentCenter flex:NO sizeRange:kSize identifier:@"justifyCenter"];
[self testStackLayoutSpecWithJustify:ASStackLayoutJustifyContentEnd flex:NO sizeRange:kSize identifier:@"justifyEnd"];
[self testStackLayoutSpecWithJustify:ASStackLayoutJustifyContentStart flex:YES sizeRange:kSize identifier:@"flex"];
[self testStackLayoutSpecWithJustify:ASStackLayoutJustifyContentSpaceBetween flex:NO sizeRange:kSize identifier:@"justifySpaceBetween"];
[self testStackLayoutSpecWithJustify:ASStackLayoutJustifyContentSpaceAround flex:NO sizeRange:kSize identifier:@"justifySpaceAround"];
[self testStackLayoutSpecWithJustify:ASStackLayoutJustifyContentStart flexFactor:0 sizeRange:kSize identifier:@"justifyStart"];
[self testStackLayoutSpecWithJustify:ASStackLayoutJustifyContentCenter flexFactor:0 sizeRange:kSize identifier:@"justifyCenter"];
[self testStackLayoutSpecWithJustify:ASStackLayoutJustifyContentEnd flexFactor:0 sizeRange:kSize identifier:@"justifyEnd"];
[self testStackLayoutSpecWithJustify:ASStackLayoutJustifyContentStart flexFactor:1 sizeRange:kSize identifier:@"flex"];
[self testStackLayoutSpecWithJustify:ASStackLayoutJustifyContentSpaceBetween flexFactor:0 sizeRange:kSize identifier:@"justifySpaceBetween"];
[self testStackLayoutSpecWithJustify:ASStackLayoutJustifyContentSpaceAround flexFactor:0 sizeRange:kSize identifier:@"justifySpaceAround"];
}
- (void)testOverflowBehaviors
{
// width 110px; height 0-300px
static ASSizeRange kSize = {{110, 0}, {110, 300}};
[self testStackLayoutSpecWithJustify:ASStackLayoutJustifyContentStart flex:NO sizeRange:kSize identifier:@"justifyStart"];
[self testStackLayoutSpecWithJustify:ASStackLayoutJustifyContentCenter flex:NO sizeRange:kSize identifier:@"justifyCenter"];
[self testStackLayoutSpecWithJustify:ASStackLayoutJustifyContentEnd flex:NO sizeRange:kSize identifier:@"justifyEnd"];
[self testStackLayoutSpecWithJustify:ASStackLayoutJustifyContentStart flex:YES sizeRange:kSize identifier:@"flex"];
[self testStackLayoutSpecWithJustify:ASStackLayoutJustifyContentStart flexFactor:0 sizeRange:kSize identifier:@"justifyStart"];
[self testStackLayoutSpecWithJustify:ASStackLayoutJustifyContentCenter flexFactor:0 sizeRange:kSize identifier:@"justifyCenter"];
[self testStackLayoutSpecWithJustify:ASStackLayoutJustifyContentEnd flexFactor:0 sizeRange:kSize identifier:@"justifyEnd"];
[self testStackLayoutSpecWithJustify:ASStackLayoutJustifyContentStart flexFactor:1 sizeRange:kSize identifier:@"flex"];
// On overflow, "space between" is identical to "content start"
[self testStackLayoutSpecWithJustify:ASStackLayoutJustifyContentSpaceBetween flex:NO sizeRange:kSize identifier:@"justifyStart"];
[self testStackLayoutSpecWithJustify:ASStackLayoutJustifyContentSpaceBetween flexFactor:0 sizeRange:kSize identifier:@"justifyStart"];
// On overflow, "space around" is identical to "content center"
[self testStackLayoutSpecWithJustify:ASStackLayoutJustifyContentSpaceAround flex:NO sizeRange:kSize identifier:@"justifyCenter"];
[self testStackLayoutSpecWithJustify:ASStackLayoutJustifyContentSpaceAround flexFactor:0 sizeRange:kSize identifier:@"justifyCenter"];
}
- (void)testOverflowBehaviorsWhenAllFlexShrinkChildrenHaveBeenClampedToZeroButViolationStillExists
{
ASStackLayoutSpecStyle style = {.direction = ASStackLayoutDirectionHorizontal};
NSArray<ASDisplayNode *> *subnodes = defaultSubnodesWithSameSize({50, 50}, NO);
subnodes[1].style.flexShrink = YES;
NSArray<ASDisplayNode *> *subnodes = defaultSubnodesWithSameSize({50, 50}, 0);
subnodes[1].style.flexShrink = 1;
// Width is 75px--that's less than the sum of the widths of the children, which is 100px.
static ASSizeRange kSize = {{75, 0}, {75, 150}};
@ -177,7 +177,7 @@ static void setCGSizeToNode(CGSize size, ASDisplayNode *node)
{
ASStackLayoutSpecStyle style = {.direction = ASStackLayoutDirectionHorizontal};
NSArray<ASDisplayNode *> *subnodes = defaultSubnodesWithSameSize({50, 50}, YES);
NSArray<ASDisplayNode *> *subnodes = defaultSubnodesWithSameSize({50, 50}, 1);
setCGSizeToNode({150, 150}, subnodes[1]);
// width 300px; height 0-150px.
@ -333,27 +333,27 @@ static void setCGSizeToNode(CGSize size, ASDisplayNode *node)
{
// width 301px; height 0-300px; 1px remaining
static ASSizeRange kSize = {{301, 0}, {301, 300}};
[self testStackLayoutSpecWithJustify:ASStackLayoutJustifyContentSpaceBetween flex:NO sizeRange:kSize identifier:nil];
[self testStackLayoutSpecWithJustify:ASStackLayoutJustifyContentSpaceBetween flexFactor:0 sizeRange:kSize identifier:nil];
}
- (void)testJustifiedSpaceAroundWithRemainingSpace
{
// width 305px; height 0-300px; 5px remaining
static ASSizeRange kSize = {{305, 0}, {305, 300}};
[self testStackLayoutSpecWithJustify:ASStackLayoutJustifyContentSpaceAround flex:NO sizeRange:kSize identifier:nil];
[self testStackLayoutSpecWithJustify:ASStackLayoutJustifyContentSpaceAround flexFactor:0 sizeRange:kSize identifier:nil];
}
- (void)testChildThatChangesCrossSizeWhenMainSizeIsFlexed
{
ASStackLayoutSpecStyle style = {.direction = ASStackLayoutDirectionHorizontal};
ASDisplayNode * subnode1 = ASDisplayNodeWithBackgroundColor([UIColor blueColor]);
ASDisplayNode * subnode2 = ASDisplayNodeWithBackgroundColor([UIColor redColor], {50, 50});
ASDisplayNode *subnode1 = ASDisplayNodeWithBackgroundColor([UIColor blueColor]);
ASDisplayNode *subnode2 = ASDisplayNodeWithBackgroundColor([UIColor redColor], {50, 50});
ASRatioLayoutSpec *child1 = [ASRatioLayoutSpec ratioLayoutSpecWithRatio:1.5 child:subnode1];
child1.style.flexBasis = ASDimensionMakeWithFraction(1);
child1.style.flexGrow = YES;
child1.style.flexShrink = YES;
child1.style.flexGrow = 1;
child1.style.flexShrink = 1;
static ASSizeRange kFixedWidth = {{150, 0}, {150, INFINITY}};
[self testStackLayoutSpecWithStyle:style children:@[child1, subnode2] sizeRange:kFixedWidth subnodes:@[subnode1, subnode2] identifier:nil];
@ -366,13 +366,13 @@ static void setCGSizeToNode(CGSize size, ASDisplayNode *node)
.alignItems = ASStackLayoutAlignItemsCenter
};
ASDisplayNode *subnode1 = ASDisplayNodeWithBackgroundColor([UIColor redColor], {100, 100});
subnode1.style.flexShrink = YES;
NSArray<ASDisplayNode *> *subnodes = @[
ASDisplayNodeWithBackgroundColor([UIColor redColor], {100, 100}),
ASDisplayNodeWithBackgroundColor([UIColor blueColor], {50, 50})
];
subnodes[0].style.flexShrink = 1;
subnodes[1].style.flexShrink = 1;
ASDisplayNode *subnode2 = ASDisplayNodeWithBackgroundColor([UIColor blueColor], {50, 50});
subnode2.style.flexShrink = YES;
NSArray<ASDisplayNode *> *subnodes = @[subnode1, subnode2];
static ASSizeRange kFixedWidth = {{150, 0}, {150, 100}};
[self testStackLayoutSpecWithStyle:style sizeRange:kFixedWidth subnodes:subnodes identifier:nil];
}
@ -508,11 +508,11 @@ static void setCGSizeToNode(CGSize size, ASDisplayNode *node)
{
ASStackLayoutSpecStyle style = {.direction = ASStackLayoutDirectionHorizontal};
NSArray<ASDisplayNode *> *subnodes = defaultSubnodesWithSameSize({50, 50}, NO);
NSArray<ASDisplayNode *> *subnodes = defaultSubnodesWithSameSize({50, 50}, 0);
setCGSizeToNode({150, 150}, subnodes[1]);
for (ASDisplayNode *subnode in subnodes) {
subnode.style.flexGrow = YES;
subnode.style.flexGrow = 1;
subnode.style.flexBasis = ASDimensionMakeWithPoints(10);
}
@ -529,9 +529,9 @@ static void setCGSizeToNode(CGSize size, ASDisplayNode *node)
{
ASStackLayoutSpecStyle style = {.direction = ASStackLayoutDirectionHorizontal};
NSArray<ASDisplayNode *> *subnodes = defaultSubnodesWithSameSize({50, 50}, NO);
NSArray<ASDisplayNode *> *subnodes = defaultSubnodesWithSameSize({50, 50}, 0);
for (ASDisplayNode *subnode in subnodes) {
subnode.style.flexGrow = YES;
subnode.style.flexGrow = 1;
}
// This should override the intrinsic size of 50pts and instead compute to 50% = 100pts.
@ -561,15 +561,20 @@ static void setCGSizeToNode(CGSize size, ASDisplayNode *node)
- (void)testCrossAxisStretchingOccursAfterStackAxisFlexing
{
// If cross axis stretching occurred *before* flexing, then the blue child would be stretched to 3000 points tall.
// Instead it should be stretched to 300 points tall, matching the red child and not overlapping the green inset.
NSArray<ASDisplayNode *> *subnodes = @[
ASDisplayNodeWithBackgroundColor([UIColor greenColor]),
ASDisplayNodeWithBackgroundColor([UIColor blueColor], {10, 0}),
ASDisplayNodeWithBackgroundColor([UIColor redColor], {3000, 3000})
ASDisplayNodeWithBackgroundColor([UIColor greenColor]), // Inset background node
ASDisplayNodeWithBackgroundColor([UIColor blueColor]), // child1 of stack
ASDisplayNodeWithBackgroundColor([UIColor redColor], {500, 500}) // child2 of stack
];
ASRatioLayoutSpec *child2 = [ASRatioLayoutSpec ratioLayoutSpecWithRatio:1.0 child:subnodes[2]];
child2.style.flexGrow = YES;
child2.style.flexShrink = YES;
subnodes[1].style.width = ASDimensionMake(10);
ASDisplayNode *child2 = subnodes[2];
child2.style.flexGrow = 1;
child2.style.flexShrink = 1;
// If cross axis stretching occurred *before* flexing, then the blue child would be stretched to 3000 points tall.
// Instead it should be stretched to 300 points tall, matching the red child and not overlapping the green inset.
@ -592,29 +597,217 @@ static void setCGSizeToNode(CGSize size, ASDisplayNode *node)
[self testLayoutSpec:layoutSpec sizeRange:kSize subnodes:subnodes identifier:nil];
}
- (void)testViolationIsDistributedEquallyAmongFlexibleChildren
- (void)testPositiveViolationIsDistributedEquallyAmongFlexibleChildren
{
ASStackLayoutSpecStyle style = {.direction = ASStackLayoutDirectionHorizontal};
NSArray<ASDisplayNode *> *subnodes = defaultSubnodes();
NSArray<ASDisplayNode *> *subnodes = defaultSubnodesWithSameSize({50, 50}, 0);
subnodes[0].style.flexGrow = 0;
subnodes[2].style.flexGrow = 0;
setCGSizeToNode({300, 50}, subnodes[0]);
setCGSizeToNode({100, 50}, subnodes[1]);
setCGSizeToNode({200, 50}, subnodes[2]);
subnodes[0].style.flexShrink = YES;
subnodes[1].style.flexShrink = NO;
subnodes[2].style.flexShrink = YES;
// A width of 400px results in a violation of 200px. This is distributed equally among each flexible child,
// causing both of them to be shrunk by 100px, resulting in widths of 300px, 100px, and 50px.
// In the W3 flexbox standard, flexible children are shrunk proportionate to their original sizes,
// resulting in widths of 180px, 100px, and 120px.
// This test verifies the current behavior--the snapshot contains widths 300px, 100px, and 50px.
static ASSizeRange kSize = {{400, 0}, {400, 150}};
// In this scenario a width of 350 results in a positive violation of 200.
// Due to each flexible subnode specifying a flex grow factor of 1 the violation will be distributed evenly.
static ASSizeRange kSize = {{350, 350}, {350, 350}};
[self testStackLayoutSpecWithStyle:style sizeRange:kSize subnodes:subnodes identifier:nil];
}
- (void)testPositiveViolationIsDistributedProportionallyAmongFlexibleChildren
{
ASStackLayoutSpecStyle style = {.direction = ASStackLayoutDirectionVertical};
NSArray<ASDisplayNode *> *subnodes = defaultSubnodesWithSameSize({50, 50}, 0);
subnodes[0].style.flexGrow = 1;
subnodes[1].style.flexGrow = 2;
subnodes[2].style.flexGrow = 1;
// In this scenario a width of 350 results in a positive violation of 200.
// The first and third subnodes specify a flex grow factor of 1 and will flex by 50.
// The second subnode specifies a flex grow factor of 2 and will flex by 100.
static ASSizeRange kSize = {{350, 350}, {350, 350}};
[self testStackLayoutSpecWithStyle:style sizeRange:kSize subnodes:subnodes identifier:nil];
}
- (void)testPositiveViolationIsDistributedEquallyAmongGrowingAndShrinkingFlexibleChildren
{
ASStackLayoutSpecStyle style = {.direction = ASStackLayoutDirectionHorizontal};
const CGSize kSubnodeSize = {50, 50};
NSArray<ASDisplayNode *> *subnodes = defaultSubnodesWithSameSize(kSubnodeSize, 0);
subnodes = [subnodes arrayByAddingObject:ASDisplayNodeWithBackgroundColor([UIColor yellowColor], kSubnodeSize)];
subnodes[0].style.flexShrink = 1;
subnodes[1].style.flexGrow = 1;
subnodes[2].style.flexShrink = 0;
subnodes[3].style.flexGrow = 1;
// In this scenario a width of 400 results in a positive violation of 200.
// The first and third subnode specify a flex shrink factor of 1 and 0, respectively. They won't flex.
// The second and fourth subnode specify a flex grow factor of 1 and will flex by 100.
static ASSizeRange kSize = {{400, 400}, {400, 400}};
[self testStackLayoutSpecWithStyle:style sizeRange:kSize subnodes:subnodes identifier:nil];
}
- (void)testPositiveViolationIsDistributedProportionallyAmongGrowingAndShrinkingFlexibleChildren
{
ASStackLayoutSpecStyle style = {.direction = ASStackLayoutDirectionVertical};
const CGSize kSubnodeSize = {50, 50};
NSArray<ASDisplayNode *> *subnodes = defaultSubnodesWithSameSize(kSubnodeSize, 0);
subnodes = [subnodes arrayByAddingObject:ASDisplayNodeWithBackgroundColor([UIColor yellowColor], kSubnodeSize)];
subnodes[0].style.flexShrink = 1;
subnodes[1].style.flexGrow = 3;
subnodes[2].style.flexShrink = 0;
subnodes[3].style.flexGrow = 1;
// In this scenario a width of 400 results in a positive violation of 200.
// The first and third subnodes specify a flex shrink factor of 1 and 0, respectively. They won't flex.
// The second child subnode specifies a flex grow factor of 3 and will flex by 150.
// The fourth child subnode specifies a flex grow factor of 1 and will flex by 50.
static ASSizeRange kSize = {{400, 400}, {400, 400}};
[self testStackLayoutSpecWithStyle:style sizeRange:kSize subnodes:subnodes identifier:nil];
}
- (void)testRemainingViolationIsAppliedProperlyToFirstFlexibleChild
{
ASStackLayoutSpecStyle style = {.direction = ASStackLayoutDirectionVertical};
NSArray<ASDisplayNode *> *subnodes = @[
ASDisplayNodeWithBackgroundColor([UIColor greenColor], {50, 25}),
ASDisplayNodeWithBackgroundColor([UIColor blueColor], {50, 0}),
ASDisplayNodeWithBackgroundColor([UIColor redColor], {50, 100})
];
subnodes[0].style.flexGrow = 0;
subnodes[1].style.flexGrow = 1;
subnodes[2].style.flexGrow = 1;
// In this scenario a width of 300 results in a positive violation of 175.
// The second and third subnodes specify a flex grow factor of 1 and will flex by 88 and 87, respectively.
static ASSizeRange kSize = {{300, 300}, {300, 300}};
[self testStackLayoutSpecWithStyle:style sizeRange:kSize subnodes:subnodes identifier:nil];
}
- (void)testNegativeViolationIsDistributedProportionallyBasedOnSizeAmongFlexibleChildren
{
ASStackLayoutSpecStyle style = {.direction = ASStackLayoutDirectionHorizontal};
NSArray<ASDisplayNode *> *subnodes = @[
ASDisplayNodeWithBackgroundColor([UIColor greenColor], {300, 50}),
ASDisplayNodeWithBackgroundColor([UIColor blueColor], {100, 50}),
ASDisplayNodeWithBackgroundColor([UIColor redColor], {200, 50})
];
subnodes[0].style.flexShrink = 1;
subnodes[1].style.flexShrink = 0;
subnodes[2].style.flexShrink = 1;
// In this scenario a width of 400 results in a negative violation of 200.
// The first and third subnodes specify a flex shrink factor of 1 and will flex by -120 and -80, respectively.
static ASSizeRange kSize = {{400, 400}, {400, 400}};
[self testStackLayoutSpecWithStyle:style sizeRange:kSize subnodes:subnodes identifier:nil];
}
- (void)testNegativeViolationIsDistributedProportionallyBasedOnSizeAndFlexFactorAmongFlexibleChildren
{
ASStackLayoutSpecStyle style = {.direction = ASStackLayoutDirectionVertical};
NSArray<ASDisplayNode *> *subnodes = @[
ASDisplayNodeWithBackgroundColor([UIColor greenColor], {50, 300}),
ASDisplayNodeWithBackgroundColor([UIColor blueColor], {50, 100}),
ASDisplayNodeWithBackgroundColor([UIColor redColor], {50, 200})
];
subnodes[0].style.flexShrink = 2;
subnodes[1].style.flexShrink = 1;
subnodes[2].style.flexShrink = 2;
// In this scenario a width of 400 results in a negative violation of 200.
// The first and third subnodes specify a flex shrink factor of 2 and will flex by -109 and -72, respectively.
// The second subnode specifies a flex shrink factor of 1 and will flex by -18.
static ASSizeRange kSize = {{400, 400}, {400, 400}};
[self testStackLayoutSpecWithStyle:style sizeRange:kSize subnodes:subnodes identifier:nil];
}
- (void)testNegativeViolationIsDistributedProportionallyBasedOnSizeAmongGrowingAndShrinkingFlexibleChildren
{
ASStackLayoutSpecStyle style = {.direction = ASStackLayoutDirectionHorizontal};
const CGSize kSubnodeSize = {150, 50};
NSArray<ASDisplayNode *> *subnodes = defaultSubnodesWithSameSize(kSubnodeSize, 0);
subnodes = [subnodes arrayByAddingObject:ASDisplayNodeWithBackgroundColor([UIColor yellowColor], kSubnodeSize)];
subnodes[0].style.flexGrow = 1;
subnodes[1].style.flexShrink = 1;
subnodes[2].style.flexGrow = 0;
subnodes[3].style.flexShrink = 1;
// In this scenario a width of 400 results in a negative violation of 200.
// The first and third subnodes specify a flex grow factor of 1 and 0, respectively. They won't flex.
// The second and fourth subnodes specify a flex grow factor of 1 and will flex by -100.
static ASSizeRange kSize = {{400, 400}, {400, 400}};
[self testStackLayoutSpecWithStyle:style sizeRange:kSize subnodes:subnodes identifier:nil];
}
- (void)testNegativeViolationIsDistributedProportionallyBasedOnSizeAndFlexFactorAmongGrowingAndShrinkingFlexibleChildren
{
ASStackLayoutSpecStyle style = {.direction = ASStackLayoutDirectionVertical};
NSArray<ASDisplayNode *> *subnodes = @[
ASDisplayNodeWithBackgroundColor([UIColor greenColor], {50, 150}),
ASDisplayNodeWithBackgroundColor([UIColor blueColor], {50, 100}),
ASDisplayNodeWithBackgroundColor([UIColor redColor], {50, 150}),
ASDisplayNodeWithBackgroundColor([UIColor yellowColor], {50, 200})
];
subnodes[0].style.flexGrow = 1;
subnodes[1].style.flexShrink = 1;
subnodes[2].style.flexGrow = 0;
subnodes[3].style.flexShrink = 3;
// In this scenario a width of 400 results in a negative violation of 200.
// The first and third subnodes specify a flex grow factor of 1 and 0, respectively. They won't flex.
// The second subnode specifies a flex grow factor of 1 and will flex by -28.
// The fourth subnode specifies a flex grow factor of 3 and will flex by -171.
static ASSizeRange kSize = {{400, 400}, {400, 400}};
[self testStackLayoutSpecWithStyle:style sizeRange:kSize subnodes:subnodes identifier:nil];
}
- (void)testNegativeViolationIsDistributedProportionallyBasedOnSizeAndFlexFactorDoesNotShrinkToZeroWidth
{
ASStackLayoutSpecStyle style = {.direction = ASStackLayoutDirectionHorizontal};
NSArray<ASDisplayNode *> *subnodes = @[
ASDisplayNodeWithBackgroundColor([UIColor greenColor], {300, 50}),
ASDisplayNodeWithBackgroundColor([UIColor blueColor], {100, 50}),
ASDisplayNodeWithBackgroundColor([UIColor redColor], {200, 50})
];
subnodes[0].style.flexShrink = 1;
subnodes[1].style.flexShrink = 2;
subnodes[2].style.flexShrink = 1;
// In this scenario a width of 400 results in a negative violation of 200.
// The first and third subnodes specify a flex shrink factor of 1 and will flex by 50.
// The second subnode specifies a flex shrink factor of 2 and will flex by -57. It will have a width of 43.
static ASSizeRange kSize = {{400, 400}, {400, 400}};
[self testStackLayoutSpecWithStyle:style sizeRange:kSize subnodes:subnodes identifier:nil];
}
- (void)testNestedStackLayoutStretchDoesNotViolateWidth
{
ASStackLayoutSpec *stackLayoutSpec = [[ASStackLayoutSpec alloc] init]; // Default direction is horizontal
stackLayoutSpec.direction = ASStackLayoutDirectionHorizontal;
stackLayoutSpec.alignItems = ASStackLayoutAlignItemsStretch;
[stackLayoutSpec.style setSizeWithCGSize:{100, 100}];
ASDisplayNode *child = ASDisplayNodeWithBackgroundColor([UIColor redColor], {50, 50});
stackLayoutSpec.children = @[child];
static ASSizeRange kSize = {{0, 0}, {300, INFINITY}};
[self testStackLayoutSpec:stackLayoutSpec sizeRange:kSize subnodes:@[child] identifier:nil];
}
- (void)testHorizontalAndVerticalAlignments
{
[self testStackLayoutSpecWithDirection:ASStackLayoutDirectionHorizontal itemsHorizontalAlignment:ASHorizontalAlignmentLeft itemsVerticalAlignment:ASVerticalAlignmentTop identifier:@"horizontalTopLeft"];

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.1 KiB

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.5 KiB

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.6 KiB

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.3 KiB

After

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.5 KiB

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.4 KiB

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.2 KiB

After

Width:  |  Height:  |  Size: 8.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB