Swiftgram/AsyncDisplayKit/ASDisplayNode+Yoga.mm
appleguy f91265757e [Yoga] Initial commit for supporting Yoga-powered layout calculation. (#2982)
* [Yoga + AsyncDisplayKit] Initial commit for supporting Yoga-powered layout calculation.

Because this results in ASLayout objects, it preserve support for automaticallyManagesSubnodes
as well as the animated transition system. More work remains to vet performance of the new mode,
and it will remain in +Beta for the forseeable future.

I'm not sure that this should ever be used as the primary ASDK layout system, but it should remain an
option for some apps to experiment with if they require an implementation that more strictly mirrors
W3C standard Flexbox.

* [Yoga] Improve usage of ASHierarchyState to ensure simultaneous yoga layouts can't happen.

* [Yoga] Strictly minimize the impact of the Yoga integration on existing code.

Created new file ASDisplayNode+Yoga.mm, reduced size and number of integration points in core code.

* [Yoga] Figured out how to further reduce ASDisplayNode.mm impact by allocating _yogaNode in property accessor, and changing all accesses to use the property.
2017-02-09 16:10:29 -08:00

408 lines
15 KiB
Plaintext

//
// ASDisplayNode+Yoga.mm
// AsyncDisplayKit
//
// Created by Scott Goodson on 2/8/17.
// Copyright © 2017 Facebook. All rights reserved.
//
#import <AsyncDisplayKit/ASAvailability.h>
#if YOGA /* YOGA */
#import <AsyncDisplayKit/ASDisplayNodeInternal.h>
#import <AsyncDisplayKit/ASDisplayNode+FrameworkPrivate.h>
#import <AsyncDisplayKit/ASDisplayNode+Beta.h>
#import <AsyncDisplayKit/ASLayout.h>
// If Yoga support becomes a supported feature, move this traversal to ASDisplayNodeExtras.
void ASDisplayNodePerformBlockOnEveryYogaChild(ASDisplayNode * _Nullable node, void(^block)(ASDisplayNode *node));
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 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;
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)
@property (nonatomic, weak) ASDisplayNode *yogaParent;
@property (nonatomic, assign) YGNodeRef yogaNode;
@end
@implementation ASDisplayNode (Yoga)
- (void)setYogaNode:(YGNodeRef)yogaNode
{
_yogaNode = yogaNode;
}
- (YGNodeRef)yogaNode
{
if (_yogaNode == NULL) {
_yogaNode = YGNodeNew();
}
return _yogaNode;
}
- (void)setYogaParent:(ASDisplayNode *)yogaParent
{
if (_yogaParent == yogaParent) {
return;
}
YGNodeRef yogaNode = self.yogaNode; // Use property to assign Ref if needed.
YGNodeRef oldParentRef = YGNodeGetParent(yogaNode);
if (oldParentRef != NULL) {
YGNodeRemoveChild(oldParentRef, yogaNode);
}
_yogaParent = yogaParent;
if (yogaParent) {
self.hierarchyState |= ASHierarchyStateYogaLayoutEnabled;
YGNodeRef newParentRef = yogaParent.yogaNode;
YGNodeInsertChild(newParentRef, yogaNode, YGNodeGetChildCount(newParentRef));
} else {
self.hierarchyState &= ~ASHierarchyStateYogaLayoutEnabled;
}
}
- (ASDisplayNode *)yogaParent
{
return _yogaParent;
}
- (void)setYogaChildren:(NSArray *)yogaChildren
{
for (ASDisplayNode *child in _yogaChildren) {
// Make sure to un-associate the YGNodeRef tree before replacing _yogaChildren
// If this becomes a performance bottleneck, it can be optimized by not doing the NSArray removals here.
[self removeYogaChild:child];
}
_yogaChildren = nil;
for (ASDisplayNode *child in yogaChildren) {
[self addYogaChild:child];
}
}
- (NSArray *)yogaChildren
{
return _yogaChildren;
}
- (void)addYogaChild:(ASDisplayNode *)child
{
if (child == nil) {
return;
}
if (_yogaChildren == nil) {
_yogaChildren = [NSMutableArray array];
}
// Clean up state in case this child had another parent.
[self removeYogaChild:child];
// YGNodeRef insertion is done in setParent:
child.yogaParent = self;
[_yogaChildren addObject:child];
self.hierarchyState |= ASHierarchyStateYogaLayoutEnabled;
}
- (void)removeYogaChild:(ASDisplayNode *)child
{
if (child == nil) {
return;
}
// YGNodeRef removal is done in setParent:
child.yogaParent = nil;
[_yogaChildren removeObjectIdenticalTo:child];
if (_yogaChildren.count == 0 && self.yogaParent == nil) {
self.hierarchyState &= ~ASHierarchyStateYogaLayoutEnabled;
}
}
- (ASLayout *)layoutTreeForYogaNode
{
YGNodeRef yogaNode = self.yogaNode; // Use property to assign Ref if needed.
uint32_t childCount = YGNodeGetChildCount(yogaNode);
ASDisplayNodeAssert(childCount == self.yogaChildren.count,
@"Yoga tree should always be in sync with .yogaNodes array! %@", self.yogaChildren);
NSMutableArray *sublayouts = [NSMutableArray arrayWithCapacity:childCount];
for (ASDisplayNode *subnode in self.yogaChildren) {
[sublayouts addObject:[subnode layoutTreeForYogaNode]];
}
CGSize size = CGSizeMake(YGNodeLayoutGetWidth(yogaNode), YGNodeLayoutGetHeight(yogaNode));
CGPoint position = CGPointMake(YGNodeLayoutGetLeft(yogaNode), YGNodeLayoutGetTop(yogaNode));
ASDisplayNodeLogEvent(self, @"Yoga calculatedSize: %@", NSStringFromCGSize(size));
ASLayout *layout = [ASLayout layoutWithLayoutElement:self
size:size
position:position
sublayouts:sublayouts];
return layout;
}
- (void)setYogaMeasureFuncIfNeeded
{
// Manual size calculation via calculateSizeThatFits:
// This will be used for ASTextNode, as well as any other leaf node that has no layout spec.
if ((self.methodOverrides & ASDisplayNodeMethodOverrideLayoutSpecThatFits) == NO
&& self.layoutSpecBlock == NULL && self.yogaChildren.count == 0) {
YGNodeRef yogaNode = self.yogaNode; // Use property to assign Ref if needed.
YGNodeSetContext(yogaNode, (__bridge void *)self);
YGNodeSetMeasureFunc(yogaNode, &ASLayoutElementYogaMeasureFunc);
}
}
- (ASLayout *)calculateLayoutFromYogaRoot:(ASSizeRange)rootConstrainedSize
{
if (ASHierarchyStateIncludesYogaLayoutMeasuring(self.hierarchyState)) {
ASDisplayNodeAssert(NO, @"A Yoga layout is being performed by a parent; children must not perform their own until it is done! %@", [self displayNodeRecursiveDescription]);
return [ASLayout layoutWithLayoutElement:self size:CGSizeZero];
}
ASDisplayNodePerformBlockOnEveryYogaChild(self, ^(ASDisplayNode * _Nonnull node) {
node.hierarchyState |= ASHierarchyStateYogaLayoutMeasuring;
});
YGNodeRef rootYogaNode = self.yogaNode;
// Apply the constrainedSize as a base, known frame of reference.
// If the root node also has style.*Size set, these will be overridden below.
YGNodeStyleSetMinWidth (rootYogaNode, yogaFloatForCGFloat(rootConstrainedSize.min.width));
YGNodeStyleSetMinHeight(rootYogaNode, yogaFloatForCGFloat(rootConstrainedSize.min.height));
// TODO(appleguy); Is it sufficient to set only Width OR MaxWidth (+ same for height), or do we need all four?
YGNodeStyleSetMaxWidth (rootYogaNode, yogaFloatForCGFloat(rootConstrainedSize.max.width));
YGNodeStyleSetMaxHeight(rootYogaNode, yogaFloatForCGFloat(rootConstrainedSize.max.height));
YGNodeStyleSetWidth (rootYogaNode, yogaFloatForCGFloat(rootConstrainedSize.max.width));
YGNodeStyleSetHeight (rootYogaNode, yogaFloatForCGFloat(rootConstrainedSize.max.height));
// TODO(appleguy): We need this on all containers, not just the root. Need to be able to check for "unset"
YGNodeStyleSetAlignItems(rootYogaNode, yogaAlignItems(self.style.alignItems));
ASDisplayNodePerformBlockOnEveryYogaChild(self, ^(ASDisplayNode * _Nonnull node) {
ASLayoutElementStyle *style = node.style;
YGNodeRef yogaNode = node.yogaNode;
YGNodeStyleSetDirection (yogaNode, YGDirectionInherit);
YGNodeStyleSetFlexWrap (yogaNode, style.flexWrap);
YGNodeStyleSetFlexGrow (yogaNode, style.flexGrow);
YGNodeStyleSetFlexShrink (yogaNode, style.flexShrink);
YGNODE_STYLE_SET_DIMENSION (yogaNode, FlexBasis, style.flexBasis);
YGNodeStyleSetFlexDirection (yogaNode, yogaFlexDirection(style.direction));
YGNodeStyleSetAlignSelf (yogaNode, yogaAlignSelf(style.alignSelf));
YGNodeStyleSetAlignItems (yogaNode, yogaAlignItems(style.alignItems));
YGNodeStyleSetJustifyContent(yogaNode, yogaJustifyContent(style.justifyContent));
YGNodeStyleSetPositionType (yogaNode, style.positionType);
ASEdgeInsets position = style.position;
ASEdgeInsets margin = style.margin;
ASEdgeInsets padding = style.padding;
ASEdgeInsets border = style.border;
YGEdge edge = YGEdgeLeft;
for (int i = 0; i < 4; i++) {
YGNODE_STYLE_SET_DIMENSION_WITH_EDGE(yogaNode, Position, dimensionForEdgeWithEdgeInsets(edge, position), edge);
YGNODE_STYLE_SET_DIMENSION_WITH_EDGE(yogaNode, Margin, dimensionForEdgeWithEdgeInsets(edge, margin), edge);
YGNODE_STYLE_SET_DIMENSION_WITH_EDGE(yogaNode, Padding, dimensionForEdgeWithEdgeInsets(edge, padding), edge);
YGNODE_STYLE_SET_FLOAT_WITH_EDGE(yogaNode, Border, dimensionForEdgeWithEdgeInsets(edge, border), edge);
edge = (edge == YGEdgeLeft ? YGEdgeTop : (edge == YGEdgeTop ? YGEdgeRight : YGEdgeBottom));
}
CGFloat aspectRatio = style.aspectRatio;
if (aspectRatio > FLT_EPSILON && aspectRatio < CGFLOAT_MAX / 2.0) {
YGNodeStyleSetAspectRatio(yogaNode, aspectRatio);
}
// For the root node, we use rootConstrainedSize above. For children, consult the style for their size.
if (node != self) {
YGNODE_STYLE_SET_DIMENSION(yogaNode, Width, style.width);
YGNODE_STYLE_SET_DIMENSION(yogaNode, Height, style.height);
YGNODE_STYLE_SET_DIMENSION(yogaNode, MinWidth, style.minWidth);
YGNODE_STYLE_SET_DIMENSION(yogaNode, MinHeight, style.minHeight);
YGNODE_STYLE_SET_DIMENSION(yogaNode, MaxWidth, style.maxWidth);
YGNODE_STYLE_SET_DIMENSION(yogaNode, MaxHeight, style.maxHeight);
}
[node setYogaMeasureFuncIfNeeded];
/* TODO(appleguy): STYLE SETTER METHODS LEFT TO IMPLEMENT
void YGNodeStyleSetFlexDirection(YGNodeRef node, YGFlexDirection flexDirection);
void YGNodeStyleSetOverflow(YGNodeRef node, YGOverflow overflow);
void YGNodeStyleSetFlex(YGNodeRef node, float flex);
*/
});
// It is crucial to use yogaFloat... to convert CGFLOAT_MAX into YGUndefined here.
YGNodeCalculateLayout(rootYogaNode,
yogaFloatForCGFloat(rootConstrainedSize.max.width),
yogaFloatForCGFloat(rootConstrainedSize.max.height),
YGDirectionInherit);
#if ASEVENTLOG_ENABLE
YGNodePrint(rootYogaNode, (YGPrintOptions)(YGPrintOptionsChildren | YGPrintOptionsStyle | YGPrintOptionsLayout));
#endif
ASLayout *yogaLayout = [self layoutTreeForYogaNode];
ASDisplayNodePerformBlockOnEveryYogaChild(self, ^(ASDisplayNode * _Nonnull node) {
node.hierarchyState &= ~ASHierarchyStateYogaLayoutMeasuring;
});
return yogaLayout;
}
@end
#endif /* YOGA */