Implement horizontal and vertical alignments for stack layout

This commit is contained in:
Huy Nguyen
2015-11-20 17:50:05 +02:00
parent 50a52e7112
commit 0077c3eec6
11 changed files with 235 additions and 8 deletions

View File

@@ -67,3 +67,27 @@ typedef NS_ENUM(NSUInteger, ASStackLayoutAlignSelf) {
/** Expand to fill cross axis */ /** Expand to fill cross axis */
ASStackLayoutAlignSelfStretch, ASStackLayoutAlignSelfStretch,
}; };
/** Orientation of children along horizontal axis */
typedef NS_ENUM(NSUInteger, ASHorizontalAlignment) {
/** No alignment specified. Default value */
ASHorizontalAlignmentNone,
/** Left aligned */
ASAlignmentLeft,
/** Center aligned */
ASAlignmentMiddle,
/** Right aligned */
ASAlignmentRight,
};
/** Orientation of children along vertical axis */
typedef NS_ENUM(NSUInteger, ASVerticalAlignment) {
/** No alignment specified. Default value */
ASVerticalAlignmentNone,
/** Top aligned */
ASAlignmentTop,
/** Center aligned */
ASAlignmentCenter,
/** Bottom aligned */
ASAlignmentBottom,
};

View File

@@ -35,10 +35,25 @@
*/ */
@interface ASStackLayoutSpec : ASLayoutSpec @interface ASStackLayoutSpec : ASLayoutSpec
/** Specifies the direction children are stacked in. */ /**
Specifies the direction children are stacked in. If horizontalAlignment and verticalAlignment were set,
they will be resolved again, causing justifyContent and alignItems to be updated accordingly
*/
@property (nonatomic, assign) ASStackLayoutDirection direction; @property (nonatomic, assign) ASStackLayoutDirection direction;
/** The amount of space between each child. */ /** The amount of space between each child. */
@property (nonatomic, assign) CGFloat spacing; @property (nonatomic, assign) CGFloat spacing;
/**
Specifies how children are aligned horizontally. Depends on the stack direction, setting the alignment causes either
justifyContent or alignItems to be updated. The alignment will remain valid after future direction changes.
Thus, it is preferred to those properties
*/
@property (nonatomic, assign) ASHorizontalAlignment horizontalAlignment;
/**
Specifies how children are aligned vertically. Depends on the stack direction, setting the alignment causes either
justifyContent or alignItems to be updated. The alignment will remain valid after future direction changes.
Thus, it is preferred to those properties
*/
@property (nonatomic, assign) ASVerticalAlignment verticalAlignment;
/** The amount of space between each child. */ /** The amount of space between each child. */
@property (nonatomic, assign) ASStackLayoutJustifyContent justifyContent; @property (nonatomic, assign) ASStackLayoutJustifyContent justifyContent;
/** Orientation of children along cross axis */ /** Orientation of children along cross axis */

View File

@@ -25,7 +25,7 @@
@implementation ASStackLayoutSpec @implementation ASStackLayoutSpec
{ {
ASDN::RecursiveMutex _propertyLock; ASDN::RecursiveMutex _propertyLock;
} }
- (instancetype)init - (instancetype)init
@@ -58,8 +58,10 @@
return nil; return nil;
} }
_direction = direction; _direction = direction;
_alignItems = alignItems;
_spacing = spacing; _spacing = spacing;
_horizontalAlignment = ASHorizontalAlignmentNone;
_verticalAlignment = ASVerticalAlignmentNone;
_alignItems = alignItems;
_justifyContent = justifyContent; _justifyContent = justifyContent;
[self setChildren:children]; [self setChildren:children];
@@ -69,18 +71,44 @@
- (void)setDirection:(ASStackLayoutDirection)direction - (void)setDirection:(ASStackLayoutDirection)direction
{ {
ASDisplayNodeAssert(self.isMutable, @"Cannot set properties when layout spec is not mutable"); ASDisplayNodeAssert(self.isMutable, @"Cannot set properties when layout spec is not mutable");
_direction = direction; if (_direction != direction) {
_direction = direction;
[self resolveHorizontalAlignment];
[self resolveVerticalAlignment];
}
}
- (void)setHorizontalAlignment:(ASHorizontalAlignment)horizontalAlignment
{
ASDisplayNodeAssert(self.isMutable, @"Cannot set properties when layout spec is not mutable");
if (_horizontalAlignment != horizontalAlignment) {
_horizontalAlignment = horizontalAlignment;
[self resolveHorizontalAlignment];
}
}
- (void)setVerticalAlignment:(ASVerticalAlignment)verticalAlignment
{
ASDisplayNodeAssert(self.isMutable, @"Cannot set properties when layout spec is not mutable");
if (_verticalAlignment != verticalAlignment) {
_verticalAlignment = verticalAlignment;
[self resolveVerticalAlignment];
}
} }
- (void)setAlignItems:(ASStackLayoutAlignItems)alignItems - (void)setAlignItems:(ASStackLayoutAlignItems)alignItems
{ {
ASDisplayNodeAssert(self.isMutable, @"Cannot set properties when layout spec is not mutable"); ASDisplayNodeAssert(self.isMutable, @"Cannot set properties when layout spec is not mutable");
ASDisplayNodeAssert(_horizontalAlignment == ASHorizontalAlignmentNone, @"Cannot set this property directly because horizontalAlignment is being used");
ASDisplayNodeAssert(_verticalAlignment == ASVerticalAlignmentNone, @"Cannot set this property directly because verticalAlignment is being used");
_alignItems = alignItems; _alignItems = alignItems;
} }
- (void)setJustifyContent:(ASStackLayoutJustifyContent)justifyContent - (void)setJustifyContent:(ASStackLayoutJustifyContent)justifyContent
{ {
ASDisplayNodeAssert(self.isMutable, @"Cannot set properties when layout spec is not mutable"); ASDisplayNodeAssert(self.isMutable, @"Cannot set properties when layout spec is not mutable");
ASDisplayNodeAssert(_horizontalAlignment == ASHorizontalAlignmentNone, @"Cannot set this property directly because horizontalAlignment is being used");
ASDisplayNodeAssert(_verticalAlignment == ASVerticalAlignmentNone, @"Cannot set this property directly because verticalAlignment is being used");
_justifyContent = justifyContent; _justifyContent = justifyContent;
} }
@@ -149,6 +177,24 @@
sublayouts:sublayouts]; sublayouts:sublayouts];
} }
- (void)resolveHorizontalAlignment
{
if (_direction == ASStackLayoutDirectionHorizontal) {
_justifyContent = justifyContent(_horizontalAlignment, _justifyContent);
} else {
_alignItems = alignment(_horizontalAlignment, _alignItems);
}
}
- (void)resolveVerticalAlignment
{
if (_direction == ASStackLayoutDirectionHorizontal) {
_alignItems = alignment(_verticalAlignment, _alignItems);
} else {
_justifyContent = justifyContent(_verticalAlignment, _justifyContent);
}
}
@end @end
@implementation ASStackLayoutSpec (Debugging) @implementation ASStackLayoutSpec (Debugging)

View File

@@ -68,3 +68,63 @@ inline ASStackLayoutAlignItems alignment(ASStackLayoutAlignSelf childAlignment,
return stackAlignment; return stackAlignment;
} }
} }
inline ASStackLayoutAlignItems alignment(ASHorizontalAlignment alignment, ASStackLayoutAlignItems defaultAlignment)
{
switch (alignment) {
case ASAlignmentLeft:
return ASStackLayoutAlignItemsStart;
case ASAlignmentMiddle:
return ASStackLayoutAlignItemsCenter;
case ASAlignmentRight:
return ASStackLayoutAlignItemsEnd;
case ASHorizontalAlignmentNone:
default:
return defaultAlignment;
}
}
inline ASStackLayoutAlignItems alignment(ASVerticalAlignment alignment, ASStackLayoutAlignItems defaultAlignment)
{
switch (alignment) {
case ASAlignmentTop:
return ASStackLayoutAlignItemsStart;
case ASAlignmentCenter:
return ASStackLayoutAlignItemsCenter;
case ASAlignmentBottom:
return ASStackLayoutAlignItemsEnd;
case ASVerticalAlignmentNone:
default:
return defaultAlignment;
}
}
inline ASStackLayoutJustifyContent justifyContent(ASHorizontalAlignment alignment, ASStackLayoutJustifyContent defaultJustifyContent)
{
switch (alignment) {
case ASAlignmentLeft:
return ASStackLayoutJustifyContentStart;
case ASAlignmentMiddle:
return ASStackLayoutJustifyContentCenter;
case ASAlignmentRight:
return ASStackLayoutJustifyContentEnd;
case ASHorizontalAlignmentNone:
default:
return defaultJustifyContent;
}
}
inline ASStackLayoutJustifyContent justifyContent(ASVerticalAlignment alignment, ASStackLayoutJustifyContent defaultJustifyContent)
{
switch (alignment) {
case ASAlignmentTop:
return ASStackLayoutJustifyContentStart;
case ASAlignmentCenter:
return ASStackLayoutJustifyContentCenter;
case ASAlignmentBottom:
return ASStackLayoutJustifyContentEnd;
case ASVerticalAlignmentNone:
default:
return defaultJustifyContent;
}
}

View File

@@ -28,6 +28,8 @@
self.recordMode = NO; self.recordMode = NO;
} }
#pragma mark - Utility methods
static NSArray *defaultSubnodes() static NSArray *defaultSubnodes()
{ {
return defaultSubnodesWithSameSize(CGSizeZero, NO); return defaultSubnodesWithSameSize(CGSizeZero, NO);
@@ -63,6 +65,24 @@ static NSArray *defaultSubnodesWithSameSize(CGSize subnodeSize, BOOL flex)
[self testStackLayoutSpecWithStyle:style sizeRange:sizeRange subnodes:subnodes identifier:identifier]; [self testStackLayoutSpecWithStyle:style sizeRange:sizeRange subnodes:subnodes identifier:identifier];
} }
- (void)testStackLayoutSpecWithDirection:(ASStackLayoutDirection)direction
itemsHorizontalAlignment:(ASHorizontalAlignment)horizontalAlignment
itemsVerticalAlignment:(ASVerticalAlignment)verticalAlignment
identifier:(NSString *)identifier
{
NSArray *subnodes = defaultSubnodesWithSameSize({50, 50}, NO);
ASStackLayoutSpec *stackLayoutSpec = [[ASStackLayoutSpec alloc] init];
stackLayoutSpec.direction = direction;
stackLayoutSpec.children = subnodes;
[stackLayoutSpec setHorizontalAlignment:horizontalAlignment];
[stackLayoutSpec setVerticalAlignment:verticalAlignment];
CGSize exactSize = CGSizeMake(200, 200);
static ASSizeRange kSize = ASSizeRangeMake(exactSize, exactSize);
[self testStackLayoutSpec:stackLayoutSpec sizeRange:kSize subnodes:subnodes identifier:identifier];
}
- (void)testStackLayoutSpecWithStyle:(ASStackLayoutSpecStyle)style - (void)testStackLayoutSpecWithStyle:(ASStackLayoutSpecStyle)style
sizeRange:(ASSizeRange)sizeRange sizeRange:(ASSizeRange)sizeRange
subnodes:(NSArray *)subnodes subnodes:(NSArray *)subnodes
@@ -76,13 +96,23 @@ static NSArray *defaultSubnodesWithSameSize(CGSize subnodeSize, BOOL flex)
sizeRange:(ASSizeRange)sizeRange sizeRange:(ASSizeRange)sizeRange
subnodes:(NSArray *)subnodes subnodes:(NSArray *)subnodes
identifier:(NSString *)identifier identifier:(NSString *)identifier
{
ASStackLayoutSpec *stackLayoutSpec = [ASStackLayoutSpec stackLayoutSpecWithDirection:style.direction
spacing:style.spacing
justifyContent:style.justifyContent
alignItems:style.alignItems
children:children];
[self testStackLayoutSpec:stackLayoutSpec sizeRange:sizeRange subnodes:subnodes identifier:identifier];
}
- (void)testStackLayoutSpec:(ASStackLayoutSpec *)stackLayoutSpec
sizeRange:(ASSizeRange)sizeRange
subnodes:(NSArray *)subnodes
identifier:(NSString *)identifier
{ {
ASDisplayNode *backgroundNode = ASDisplayNodeWithBackgroundColor([UIColor whiteColor]); ASDisplayNode *backgroundNode = ASDisplayNodeWithBackgroundColor([UIColor whiteColor]);
ASLayoutSpec *layoutSpec = ASLayoutSpec *layoutSpec = [ASBackgroundLayoutSpec backgroundLayoutSpecWithChild:stackLayoutSpec background:backgroundNode];
[ASBackgroundLayoutSpec
backgroundLayoutSpecWithChild:[ASStackLayoutSpec stackLayoutSpecWithDirection:style.direction spacing:style.spacing justifyContent:style.justifyContent alignItems:style.alignItems children:children]
background:backgroundNode];
NSMutableArray *newSubnodes = [NSMutableArray arrayWithObject:backgroundNode]; NSMutableArray *newSubnodes = [NSMutableArray arrayWithObject:backgroundNode];
[newSubnodes addObjectsFromArray:subnodes]; [newSubnodes addObjectsFromArray:subnodes];
@@ -90,6 +120,8 @@ static NSArray *defaultSubnodesWithSameSize(CGSize subnodeSize, BOOL flex)
[self testLayoutSpec:layoutSpec sizeRange:sizeRange subnodes:newSubnodes identifier:identifier]; [self testLayoutSpec:layoutSpec sizeRange:sizeRange subnodes:newSubnodes identifier:identifier];
} }
#pragma mark -
- (void)testUnderflowBehaviors - (void)testUnderflowBehaviors
{ {
// width 300px; height 0-300px // width 300px; height 0-300px
@@ -522,4 +554,54 @@ static NSArray *defaultSubnodesWithSameSize(CGSize subnodeSize, BOOL flex)
[self testStackLayoutSpecWithStyle:style sizeRange:kSize subnodes:subnodes identifier:nil]; [self testStackLayoutSpecWithStyle:style sizeRange:kSize subnodes:subnodes identifier:nil];
} }
- (void)testHorizontalAndVerticalAlignments
{
[self testStackLayoutSpecWithDirection:ASStackLayoutDirectionHorizontal itemsHorizontalAlignment:ASAlignmentLeft itemsVerticalAlignment:ASAlignmentTop identifier:@"horizontalTopLeft"];
[self testStackLayoutSpecWithDirection:ASStackLayoutDirectionHorizontal itemsHorizontalAlignment:ASAlignmentMiddle itemsVerticalAlignment:ASAlignmentCenter identifier:@"horizontalCenter"];
[self testStackLayoutSpecWithDirection:ASStackLayoutDirectionHorizontal itemsHorizontalAlignment:ASAlignmentRight itemsVerticalAlignment:ASAlignmentBottom identifier:@"horizontalBottomRight"];
[self testStackLayoutSpecWithDirection:ASStackLayoutDirectionVertical itemsHorizontalAlignment:ASAlignmentLeft itemsVerticalAlignment:ASAlignmentTop identifier:@"verticalTopLeft"];
[self testStackLayoutSpecWithDirection:ASStackLayoutDirectionVertical itemsHorizontalAlignment:ASAlignmentMiddle itemsVerticalAlignment:ASAlignmentCenter identifier:@"verticalCenter"];
[self testStackLayoutSpecWithDirection:ASStackLayoutDirectionVertical itemsHorizontalAlignment:ASAlignmentRight itemsVerticalAlignment:ASAlignmentBottom identifier:@"verticalBottomRight"];
}
- (void)testDirectionChangeAfterSettingHorizontalAndVerticalAlignments
{
ASStackLayoutSpec *stackLayoutSpec = [[ASStackLayoutSpec alloc] init]; // Default direction is horizontal
stackLayoutSpec.horizontalAlignment = ASAlignmentRight;
stackLayoutSpec.verticalAlignment = ASAlignmentCenter;
XCTAssertEqual(stackLayoutSpec.alignItems, ASStackLayoutAlignItemsCenter);
XCTAssertEqual(stackLayoutSpec.justifyContent, ASStackLayoutJustifyContentEnd);
stackLayoutSpec.direction = ASStackLayoutDirectionVertical;
XCTAssertEqual(stackLayoutSpec.alignItems, ASStackLayoutAlignItemsEnd);
XCTAssertEqual(stackLayoutSpec.justifyContent, ASStackLayoutJustifyContentCenter);
}
- (void)testAlignItemsAndJustifyContentRestrictionsIfHorizontalAndVerticalAlignmentsAreUsed
{
ASStackLayoutSpec *stackLayoutSpec = [[ASStackLayoutSpec alloc] init];
// No assertions should be thrown here because alignments are not used
stackLayoutSpec.alignItems = ASStackLayoutAlignItemsEnd;
stackLayoutSpec.justifyContent = ASStackLayoutJustifyContentEnd;
// Set alignments and assert that assertions are thrown
stackLayoutSpec.horizontalAlignment = ASAlignmentMiddle;
stackLayoutSpec.verticalAlignment = ASAlignmentCenter;
XCTAssertThrows(stackLayoutSpec.alignItems = ASStackLayoutAlignItemsEnd);
XCTAssertThrows(stackLayoutSpec.justifyContent = ASStackLayoutJustifyContentEnd);
// Unset alignments. alignItems and justifyContent should not be changed
stackLayoutSpec.horizontalAlignment = ASHorizontalAlignmentNone;
stackLayoutSpec.verticalAlignment = ASVerticalAlignmentNone;
XCTAssertEqual(stackLayoutSpec.alignItems, ASStackLayoutAlignItemsCenter);
XCTAssertEqual(stackLayoutSpec.justifyContent, ASStackLayoutJustifyContentCenter);
// Now that alignments are none, setting alignItems and justifyContent should be allowed again
stackLayoutSpec.alignItems = ASStackLayoutAlignItemsEnd;
stackLayoutSpec.justifyContent = ASStackLayoutJustifyContentEnd;
XCTAssertEqual(stackLayoutSpec.alignItems, ASStackLayoutAlignItemsEnd);
XCTAssertEqual(stackLayoutSpec.justifyContent, ASStackLayoutJustifyContentEnd);
}
@end @end