Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Yoga] Implement ASYogaLayoutSpec, a simplified integration strategy for Yoga. #270

Merged
merged 8 commits into from
May 29, 2017
7 changes: 5 additions & 2 deletions Source/ASDisplayNode+Beta.h
Original file line number Diff line number Diff line change
Expand Up @@ -171,15 +171,18 @@ extern void ASDisplayNodePerformBlockOnEveryYogaChild(ASDisplayNode * _Nullable
@interface ASDisplayNode (Yoga)

@property (nonatomic, strong, nullable) NSArray *yogaChildren;
@property (nonatomic, strong, nullable) ASLayout *yogaCalculatedLayout;

- (void)addYogaChild:(ASDisplayNode *)child;
- (void)removeYogaChild:(ASDisplayNode *)child;

- (void)semanticContentAttributeDidChange:(UISemanticContentAttribute)attribute;

#if YOGA_TREE_CONTIGUOUS
@property (nonatomic, strong, nullable) ASLayout *yogaCalculatedLayout;
// These methods should not normally be called directly.
- (void)invalidateCalculatedYogaLayout;
- (void)calculateLayoutFromYogaRoot:(ASSizeRange)rootConstrainedSize;
- (void)semanticContentAttributeDidChange:(UISemanticContentAttribute)attribute;
#endif

@end

Expand Down
198 changes: 33 additions & 165 deletions Source/ASDisplayNode+Yoga.mm
Original file line number Diff line number Diff line change
Expand Up @@ -19,165 +19,16 @@

#if YOGA /* YOGA */

#import <AsyncDisplayKit/ASDisplayNodeInternal.h>
#import <AsyncDisplayKit/ASDisplayNode+FrameworkPrivate.h>
#import <AsyncDisplayKit/ASYogaLayoutSpec.h>
#import <AsyncDisplayKit/ASYogaUtilities.h>
#import <AsyncDisplayKit/ASDisplayNode+Beta.h>
#import <AsyncDisplayKit/ASDisplayNode+FrameworkPrivate.h>
#import <AsyncDisplayKit/ASDisplayNode+Subclasses.h>
#import <AsyncDisplayKit/ASDisplayNodeInternal.h>
#import <AsyncDisplayKit/ASLayout.h>

#define YOGA_LAYOUT_LOGGING 0

extern void ASDisplayNodePerformBlockOnEveryYogaChild(ASDisplayNode * _Nullable node, void(^block)(ASDisplayNode *node))
{
if (node == nil) {
return;
}
block(node);
for (ASDisplayNode *child in [node yogaChildren]) {
ASDisplayNodePerformBlockOnEveryYogaChild(child, block);
}
}

#pragma mark - Yoga Type Conversion Helpers

YGAlign yogaAlignItems(ASStackLayoutAlignItems alignItems);
YGJustify yogaJustifyContent(ASStackLayoutJustifyContent justifyContent);
YGAlign yogaAlignSelf(ASStackLayoutAlignSelf alignSelf);
YGFlexDirection yogaFlexDirection(ASStackLayoutDirection direction);
float yogaFloatForCGFloat(CGFloat value);
float yogaDimensionToPoints(ASDimension dimension);
float yogaDimensionToPercent(ASDimension dimension);
ASDimension dimensionForEdgeWithEdgeInsets(YGEdge edge, ASEdgeInsets insets);
YGSize ASLayoutElementYogaMeasureFunc(YGNodeRef yogaNode,
float width, YGMeasureMode widthMode,
float height, YGMeasureMode heightMode);

#define YGNODE_STYLE_SET_DIMENSION(yogaNode, property, dimension) \
if (dimension.unit == ASDimensionUnitPoints) { \
YGNodeStyleSet##property(yogaNode, yogaDimensionToPoints(dimension)); \
} else if (dimension.unit == ASDimensionUnitFraction) { \
YGNodeStyleSet##property##Percent(yogaNode, yogaDimensionToPercent(dimension)); \
} else { \
YGNodeStyleSet##property(yogaNode, YGUndefined); \
}\

#define YGNODE_STYLE_SET_DIMENSION_WITH_EDGE(yogaNode, property, dimension, edge) \
if (dimension.unit == ASDimensionUnitPoints) { \
YGNodeStyleSet##property(yogaNode, edge, yogaDimensionToPoints(dimension)); \
} else if (dimension.unit == ASDimensionUnitFraction) { \
YGNodeStyleSet##property##Percent(yogaNode, edge, yogaDimensionToPercent(dimension)); \
} else { \
YGNodeStyleSet##property(yogaNode, edge, YGUndefined); \
} \

#define YGNODE_STYLE_SET_FLOAT_WITH_EDGE(yogaNode, property, dimension, edge) \
if (dimension.unit == ASDimensionUnitPoints) { \
YGNodeStyleSet##property(yogaNode, edge, yogaDimensionToPoints(dimension)); \
} else if (dimension.unit == ASDimensionUnitFraction) { \
ASDisplayNodeAssert(NO, @"Unexpected Fraction value in applying ##property## values to YGNode"); \
} else { \
YGNodeStyleSet##property(yogaNode, edge, YGUndefined); \
} \

YGAlign yogaAlignItems(ASStackLayoutAlignItems alignItems)
{
switch (alignItems) {
case ASStackLayoutAlignItemsNotSet: return YGAlignAuto;
case ASStackLayoutAlignItemsStart: return YGAlignFlexStart;
case ASStackLayoutAlignItemsEnd: return YGAlignFlexEnd;
case ASStackLayoutAlignItemsCenter: return YGAlignCenter;
case ASStackLayoutAlignItemsStretch: return YGAlignStretch;
case ASStackLayoutAlignItemsBaselineFirst: return YGAlignBaseline;
// FIXME: WARNING, Yoga does not currently support last-baseline item alignment.
case ASStackLayoutAlignItemsBaselineLast: return YGAlignBaseline;
}
}

YGJustify yogaJustifyContent(ASStackLayoutJustifyContent justifyContent)
{
switch (justifyContent) {
case ASStackLayoutJustifyContentStart: return YGJustifyFlexStart;
case ASStackLayoutJustifyContentCenter: return YGJustifyCenter;
case ASStackLayoutJustifyContentEnd: return YGJustifyFlexEnd;
case ASStackLayoutJustifyContentSpaceBetween: return YGJustifySpaceBetween;
case ASStackLayoutJustifyContentSpaceAround: return YGJustifySpaceAround;
}
}

YGAlign yogaAlignSelf(ASStackLayoutAlignSelf alignSelf)
{
switch (alignSelf) {
case ASStackLayoutAlignSelfStart: return YGAlignFlexStart;
case ASStackLayoutAlignSelfCenter: return YGAlignCenter;
case ASStackLayoutAlignSelfEnd: return YGAlignFlexEnd;
case ASStackLayoutAlignSelfStretch: return YGAlignStretch;
case ASStackLayoutAlignSelfAuto: return YGAlignAuto;
}
}

YGFlexDirection yogaFlexDirection(ASStackLayoutDirection direction)
{
return direction == ASStackLayoutDirectionVertical ? YGFlexDirectionColumn : YGFlexDirectionRow;
}

float yogaFloatForCGFloat(CGFloat value)
{
if (value < CGFLOAT_MAX / 2) {
return value;
} else {
return YGUndefined;
}
}

float yogaDimensionToPoints(ASDimension dimension)
{
ASDisplayNodeCAssert(dimension.unit == ASDimensionUnitPoints,
@"Dimensions should not be type Fraction for this method: %f", dimension.value);
return yogaFloatForCGFloat(dimension.value);
}

float yogaDimensionToPercent(ASDimension dimension)
{
ASDisplayNodeCAssert(dimension.unit == ASDimensionUnitFraction,
@"Dimensions should not be type Points for this method: %f", dimension.value);
return 100.0 * yogaFloatForCGFloat(dimension.value);

}

ASDimension dimensionForEdgeWithEdgeInsets(YGEdge edge, ASEdgeInsets insets)
{
switch (edge) {
case YGEdgeLeft: return insets.left;
case YGEdgeTop: return insets.top;
case YGEdgeRight: return insets.right;
case YGEdgeBottom: return insets.bottom;
case YGEdgeStart: return insets.start;
case YGEdgeEnd: return insets.end;
case YGEdgeHorizontal: return insets.horizontal;
case YGEdgeVertical: return insets.vertical;
case YGEdgeAll: return insets.all;
default: ASDisplayNodeCAssert(NO, @"YGEdge other than ASEdgeInsets is not supported.");
return ASDimensionAuto;
}
}

YGSize ASLayoutElementYogaMeasureFunc(YGNodeRef yogaNode, float width, YGMeasureMode widthMode,
float height, YGMeasureMode heightMode)
{
id <ASLayoutElement> layoutElement = (__bridge id <ASLayoutElement>)YGNodeGetContext(yogaNode);
ASSizeRange sizeRange;
sizeRange.max = CGSizeMake(width, height);
sizeRange.min = sizeRange.max;
if (widthMode == YGMeasureModeAtMost) {
sizeRange.min.width = 0.0;
}
if (heightMode == YGMeasureModeAtMost) {
sizeRange.min.height = 0.0;
}
CGSize size = [[layoutElement layoutThatFits:sizeRange] size];
return (YGSize){ .width = (float)size.width, .height = (float)size.height };
}

#pragma mark - ASDisplayNode+Yoga

@interface ASDisplayNode (YogaInternal)
Expand Down Expand Up @@ -262,6 +113,16 @@ - (void)addYogaChild:(ASDisplayNode *)child
[_yogaChildren addObject:child];

self.hierarchyState |= ASHierarchyStateYogaLayoutEnabled;

#if !YOGA_TREE_CONTIGUOUS
__weak ASDisplayNode *weakSelf = self;
self.layoutSpecBlock = ^ASLayoutSpec * _Nonnull(__kindof ASDisplayNode * _Nonnull node, ASSizeRange constrainedSize) {
ASYogaLayoutSpec *spec = [[ASYogaLayoutSpec alloc] init];
spec.rootNode = weakSelf;
spec.yogaChildren = weakSelf.yogaChildren;
return spec;
};
#endif
}

- (void)removeYogaChild:(ASDisplayNode *)child
Expand All @@ -275,9 +136,24 @@ - (void)removeYogaChild:(ASDisplayNode *)child

if (_yogaChildren.count == 0 && self.yogaParent == nil) {
self.hierarchyState &= ~ASHierarchyStateYogaLayoutEnabled;
#if !YOGA_TREE_CONTIGUOUS
self.layoutSpecBlock = nil;
#endif
}
}

- (void)semanticContentAttributeDidChange:(UISemanticContentAttribute)attribute
{
if (AS_AT_LEAST_IOS9) {
UIUserInterfaceLayoutDirection layoutDirection =
[UIView userInterfaceLayoutDirectionForSemanticContentAttribute:attribute];
self.style.direction = (layoutDirection == UIUserInterfaceLayoutDirectionLeftToRight
? YGDirectionLTR : YGDirectionRTL);
}
}

#if YOGA_TREE_CONTIGUOUS /* YOGA_TREE_CONTIGUOUS */

- (void)setYogaCalculatedLayout:(ASLayout *)yogaCalculatedLayout
{
_yogaCalculatedLayout = yogaCalculatedLayout;
Expand Down Expand Up @@ -338,16 +214,6 @@ - (void)invalidateCalculatedYogaLayout
self.yogaCalculatedLayout = nil;
}

- (void)semanticContentAttributeDidChange:(UISemanticContentAttribute)attribute
{
if (AS_AT_LEAST_IOS9) {
UIUserInterfaceLayoutDirection layoutDirection =
[UIView userInterfaceLayoutDirectionForSemanticContentAttribute:attribute];
self.style.direction = (layoutDirection == UIUserInterfaceLayoutDirectionLeftToRight
? YGDirectionLTR : YGDirectionRTL);
}
}

- (void)calculateLayoutFromYogaRoot:(ASSizeRange)rootConstrainedSize
{
if (self.yogaParent) {
Expand Down Expand Up @@ -443,7 +309,7 @@ - (void)calculateLayoutFromYogaRoot:(ASSizeRange)rootConstrainedSize
node.hierarchyState &= ~ASHierarchyStateYogaLayoutMeasuring;
});

#if YOGA_LAYOUT_LOGGING
#if YOGA_LAYOUT_LOGGING /* YOGA_LAYOUT_LOGGING */
// Concurrent layouts will interleave the NSLog messages unless we serialize.
// Use @synchornize rather than trampolining to the main thread so the tree state isn't changed.
@synchronized ([ASDisplayNode class]) {
Expand All @@ -458,9 +324,11 @@ - (void)calculateLayoutFromYogaRoot:(ASSizeRange)rootConstrainedSize
YGNodePrint(node.yogaNode, (YGPrintOptions)(YGPrintOptionsStyle | YGPrintOptionsLayout));
});
}
#endif
#endif /* YOGA_LAYOUT_LOGGING */
}

#endif /* YOGA_TREE_CONTIGUOUS */

@end

#endif /* YOGA */
6 changes: 3 additions & 3 deletions Source/ASDisplayNode.mm
Original file line number Diff line number Diff line change
Expand Up @@ -429,7 +429,7 @@ - (void)dealloc
[self _scheduleIvarsForMainDeallocation];
}

#if YOGA
#if YOGA_TREE_CONTIGUOUS
if (_yogaNode != NULL) {
YGNodeFree(_yogaNode);
}
Expand Down Expand Up @@ -897,7 +897,7 @@ - (void)invalidateCalculatedLayout
_pendingDisplayNodeLayout->invalidate();
}

#if YOGA
#if YOGA_TREE_CONTIGUOUS
[self invalidateCalculatedYogaLayout];
#endif
}
Expand Down Expand Up @@ -1072,7 +1072,7 @@ - (ASLayout *)calculateLayoutThatFits:(ASSizeRange)constrainedSize

ASDN::MutexLocker l(__instanceLock__);

#if YOGA /* YOGA */
#if YOGA_TREE_CONTIGUOUS /* YOGA */
if (ASHierarchyStateIncludesYogaLayoutEnabled(_hierarchyState) == YES) {
if (ASHierarchyStateIncludesYogaLayoutMeasuring(_hierarchyState) == NO && self.yogaCalculatedLayout == nil) {
ASDN::MutexUnlocker ul(__instanceLock__);
Expand Down
4 changes: 4 additions & 0 deletions Source/Base/ASAvailability.h
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,10 @@
#define YOGA __has_include(YOGA_HEADER_PATH)
#endif

#ifndef YOGA_TREE_CONTIGUOUS
#define YOGA_TREE_CONTIGUOUS 0 // YOGA // Enabled by default when Yoga is used.
#endif

#define AS_PIN_REMOTE_IMAGE __has_include(<PINRemoteImage/PINRemoteImage.h>)
#define AS_IG_LIST_KIT __has_include(<IGListKit/IGListKit.h>)

Expand Down
21 changes: 21 additions & 0 deletions Source/Layout/ASYogaLayoutSpec.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
//
// ASYogaLayoutSpec.h
// AsyncDisplayKit
//
// Created by Scott Goodson on 5/6/17.
// Copyright © 2017 Facebook. All rights reserved.
//

#import <AsyncDisplayKit/ASAvailability.h>

#if !YOGA_TREE_CONTIGUOUS /* !YOGA_TREE_CONTIGUOUS */

#import <AsyncDisplayKit/ASDisplayNode.h>
#import <AsyncDisplayKit/ASLayoutSpec.h>

@interface ASYogaLayoutSpec : ASLayoutSpec
@property (nonatomic, strong, nonnull) ASDisplayNode *rootNode;
@property (nonatomic, strong, nullable) NSArray<ASDisplayNode *> *yogaChildren;
@end

#endif /* !YOGA_TREE_CONTIGUOUS */
Copy link
Member

@nguyenhuy nguyenhuy May 16, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems like this spec only accepts display nodes. Any particular reasons why it can't just work with generic ASLayoutElement instances? It would be nice if it can do that, and even has the same API with ASStackLayoutSpec, should we want to make it the default stack algorithm spec and to ensure the transition is seamless. Plus, we can leverage existing snapshot tests of ASStackLayoutSpec to test this Yoga spec.

Loading