[Yoga] Rewrite YOGA_TREE_CONTIGUOUS mode with improved behavior and cleaner integration (#343)

* [Yoga] Rewrite YOGA_TREE_CONTIGUOUS mode with support for mixing with ASLayoutSpec.

After experimentation with the ASYogaLayoutSpec (or non-contiguous) approach to
integrating Yoga, test results and feedback from the authors of Yoga have shown
that this approach can't be made completely correct,

There are issues with some of the features required to represent Web-style
flexbox; in particular: padding, margins, and border handling have varience.

This diff is a first step towards a truly correct and elegant implementation of
Yoga integration with Texture. In addition to reducing the footprint of
the integration, which is an explicit goal of work at this stage, these changes
already support improved behavior - including mixing between ASLayoutSpecs
even as subnodes of Yoga layout-driven nodes, in addition to above them. Yoga
may be used for any set of nodes.

Because Yoga usage is limited at this time, it's safe to merge this diff and
further improvements will be refinements in this direction.

* [ASDKgram] Add Yoga layout implementation for PhotoCellNode.

* [Yoga] Final fixes for the upgraded implementation of the Contiguous layout mode.

* [Yoga] Add CHANGELOG.md entry and fix for Yoga rounding to screen scale.

* [Yoga] Minor cleanup to remove old comments and generalize utility methods.
This commit is contained in:
appleguy
2017-06-14 19:36:13 -07:00
committed by GitHub
parent 13f6f14e9f
commit 55928f343d
15 changed files with 450 additions and 155 deletions

View File

@@ -1,6 +1,7 @@
## master
* Add your own contributions to the next release on the line below this with your name.
- [Yoga] Rewrite YOGA_TREE_CONTIGUOUS mode with improved behavior and cleaner integration [Scott Goodson](https://github.com/appleguy)
- [ASTraitCollection] Convert ASPrimitiveTraitCollection from lock to atomic. [Scott Goodson](https://github.com/appleguy)
- Add a synchronous mode to ASCollectionNode, for colletion view data source debugging. [Hannah Troisi](https://github.com/hannahmbanana)
- [ASDisplayNode+Layout] Add check for orphaned nodes after layout transition to clean up. #336. [Scott Goodson](https://github.com/appleguy)

View File

@@ -22,6 +22,7 @@
#if YOGA
#import YOGA_HEADER_PATH
#import <AsyncDisplayKit/ASYogaUtilities.h>
#endif
NS_ASSUME_NONNULL_BEGIN
@@ -178,6 +179,7 @@ extern void ASDisplayNodePerformBlockOnEveryYogaChild(ASDisplayNode * _Nullable
- (void)semanticContentAttributeDidChange:(UISemanticContentAttribute)attribute;
#if YOGA_TREE_CONTIGUOUS
@property (nonatomic, assign) BOOL yogaLayoutInProgress;
@property (nonatomic, strong, nullable) ASLayout *yogaCalculatedLayout;
// These methods should not normally be called directly.
- (void)invalidateCalculatedYogaLayout;
@@ -188,9 +190,11 @@ extern void ASDisplayNodePerformBlockOnEveryYogaChild(ASDisplayNode * _Nullable
@interface ASLayoutElementStyle (Yoga)
- (YGNodeRef)yogaNodeCreateIfNeeded;
@property (nonatomic, assign, readonly) YGNodeRef yogaNode;
@property (nonatomic, assign, readwrite) ASStackLayoutDirection flexDirection;
@property (nonatomic, assign, readwrite) YGDirection direction;
@property (nonatomic, assign, readwrite) CGFloat spacing;
@property (nonatomic, assign, readwrite) ASStackLayoutJustifyContent justifyContent;
@property (nonatomic, assign, readwrite) ASStackLayoutAlignItems alignItems;
@property (nonatomic, assign, readwrite) YGPositionType positionType;

View File

@@ -23,7 +23,7 @@
#import <AsyncDisplayKit/ASYogaUtilities.h>
#import <AsyncDisplayKit/ASDisplayNode+Beta.h>
#import <AsyncDisplayKit/ASDisplayNode+FrameworkPrivate.h>
#import <AsyncDisplayKit/ASDisplayNode+Subclasses.h>
#import <AsyncDisplayKit/ASDisplayNode+FrameworkSubclasses.h>
#import <AsyncDisplayKit/ASDisplayNodeInternal.h>
#import <AsyncDisplayKit/ASLayout.h>
@@ -35,7 +35,11 @@
@interface ASDisplayNode (YogaInternal)
@property (nonatomic, weak) ASDisplayNode *yogaParent;
@property (nonatomic, assign) YGNodeRef yogaNode;
- (ASSizeRange)_locked_constrainedSizeForLayoutPass;
@end
@interface ASLayout (YogaInternal)
@property (nonatomic, getter=isFlattened) BOOL flattened;
@end
#endif /* YOGA_TREE_CONTIGUOUS */
@@ -71,12 +75,18 @@
// Clean up state in case this child had another parent.
[self removeYogaChild:child];
BOOL hadZeroChildren = (_yogaChildren.count == 0);
[_yogaChildren addObject:child];
#if YOGA_TREE_CONTIGUOUS
// Ensure any measure function is removed before inserting the YGNodeRef child.
if (hadZeroChildren) {
[self updateYogaMeasureFuncIfNeeded];
}
// YGNodeRef insertion is done in setParent:
child.yogaParent = self;
self.hierarchyState |= ASHierarchyStateYogaLayoutEnabled;
#else
// When using non-contiguous Yoga layout, each level in the node hierarchy independently uses an ASYogaLayoutSpec
__weak ASDisplayNode *weakSelf = self;
@@ -94,13 +104,16 @@
if (child == nil) {
return;
}
BOOL hadChildren = (_yogaChildren.count > 0);
[_yogaChildren removeObjectIdenticalTo:child];
#if YOGA_TREE_CONTIGUOUS
// YGNodeRef removal is done in setParent:
child.yogaParent = nil;
if (_yogaChildren.count == 0 && self.yogaParent == nil) {
self.hierarchyState &= ~ASHierarchyStateYogaLayoutEnabled;
// Ensure any measure function is re-added after removing the YGNodeRef child.
if (hadChildren && _yogaChildren.count == 0) {
[self updateYogaMeasureFuncIfNeeded];
}
#else
if (_yogaChildren.count == 0) {
@@ -121,26 +134,13 @@
#if YOGA_TREE_CONTIGUOUS /* YOGA_TREE_CONTIGUOUS */
- (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 yogaNode = [self.style yogaNodeCreateIfNeeded];
YGNodeRef oldParentRef = YGNodeGetParent(yogaNode);
if (oldParentRef != NULL) {
YGNodeRemoveChild(oldParentRef, yogaNode);
@@ -148,11 +148,8 @@
_yogaParent = yogaParent;
if (yogaParent) {
self.hierarchyState |= ASHierarchyStateYogaLayoutEnabled;
YGNodeRef newParentRef = yogaParent.yogaNode;
YGNodeRef newParentRef = [yogaParent.style yogaNodeCreateIfNeeded];
YGNodeInsertChild(newParentRef, yogaNode, YGNodeGetChildCount(newParentRef));
} else {
self.hierarchyState &= ~ASHierarchyStateYogaLayoutEnabled;
}
}
@@ -171,51 +168,60 @@
return _yogaCalculatedLayout;
}
- (void)setYogaLayoutInProgress:(BOOL)yogaLayoutInProgress
{
setFlag(YogaLayoutInProgress, yogaLayoutInProgress);
}
- (BOOL)yogaLayoutInProgress
{
return checkFlag(YogaLayoutInProgress);
}
- (ASLayout *)layoutForYogaNode
{
YGNodeRef yogaNode = self.yogaNode;
YGNodeRef yogaNode = self.style.yogaNode;
CGSize size = CGSizeMake(YGNodeLayoutGetWidth(yogaNode), YGNodeLayoutGetHeight(yogaNode));
CGPoint position = CGPointMake(YGNodeLayoutGetLeft(yogaNode), YGNodeLayoutGetTop(yogaNode));
// TODO: If it were possible to set .flattened = YES, it would be valid to do so here.
return [ASLayout layoutWithLayoutElement:self size:size position:position sublayouts:nil];
}
- (void)setupYogaCalculatedLayout
{
YGNodeRef yogaNode = self.yogaNode; // Use property to assign Ref if needed.
YGNodeRef yogaNode = self.style.yogaNode;
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 layoutForYogaNode]];
ASLayout *sublayout = [subnode layoutForYogaNode];
sublayout.flattened = YES;
[sublayouts addObject:sublayout];
}
// The layout for self should have position CGPointNull, but include the calculated size.
CGSize size = CGSizeMake(YGNodeLayoutGetWidth(yogaNode), YGNodeLayoutGetHeight(yogaNode));
ASLayout *layout = [ASLayout layoutWithLayoutElement:self size:size sublayouts:sublayouts];
self.yogaCalculatedLayout = layout;
}
- (void)setYogaMeasureFuncIfNeeded
- (void)updateYogaMeasureFuncIfNeeded
{
// Size calculation via calculateSizeThatFits: or layoutSpecThatFits:
// This will be used for ASTextNode, as well as any other node that has no Yoga children
if (self.yogaChildren.count == 0) {
YGNodeRef yogaNode = self.yogaNode; // Use property to assign Ref if needed.
YGNodeSetContext(yogaNode, (__bridge void *)self);
YGNodeSetMeasureFunc(yogaNode, &ASLayoutElementYogaMeasureFunc);
}
id <ASLayoutElement> layoutElementToMeasure = (self.yogaChildren.count == 0 ? self : nil);
ASLayoutElementYogaUpdateMeasureFunc(self.style.yogaNode, layoutElementToMeasure);
}
- (void)invalidateCalculatedYogaLayout
{
// Yoga internally asserts that this method may only be called on nodes with a measurement function.
YGNodeRef yogaNode = self.yogaNode;
if (YGNodeGetMeasureFunc(yogaNode)) {
YGNodeRef yogaNode = self.style.yogaNode;
if (yogaNode && YGNodeGetMeasureFunc(yogaNode)) {
YGNodeMarkDirty(yogaNode);
}
self.yogaCalculatedLayout = nil;
@@ -223,88 +229,40 @@
- (void)calculateLayoutFromYogaRoot:(ASSizeRange)rootConstrainedSize
{
if (self.yogaParent) {
if (self.yogaCalculatedLayout == nil) {
[self _setNeedsLayoutFromAbove];
}
return;
}
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]);
ASDisplayNode *yogaParent = self.yogaParent;
if (yogaParent) {
ASYogaLog(@"ESCALATING to Yoga root: %@", self);
// TODO(appleguy): Consider how to get the constrainedSize for the yogaRoot when escalating manually.
[yogaParent calculateLayoutFromYogaRoot:ASSizeRangeUnconstrained];
return;
}
ASDN::MutexLocker l(__instanceLock__);
// Prepare all children for the layout pass with the current Yoga tree configuration.
ASDisplayNodePerformBlockOnEveryYogaChild(self, ^(ASDisplayNode * _Nonnull node) {
node.hierarchyState |= ASHierarchyStateYogaLayoutMeasuring;
node.yogaLayoutInProgress = YES;
[node updateYogaMeasureFuncIfNeeded];
});
YGNodeRef rootYogaNode = self.yogaNode;
if (ASSizeRangeEqualToSizeRange(rootConstrainedSize, ASSizeRangeUnconstrained)) {
rootConstrainedSize = [self _locked_constrainedSizeForLayoutPass];
}
ASYogaLog(@"CALCULATING at Yoga root with constraint = {%@, %@}: %@",
NSStringFromCGSize(rootConstrainedSize.min), NSStringFromCGSize(rootConstrainedSize.max), self);
YGNodeRef rootYogaNode = self.style.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.
// YGNodeCalculateLayout currently doesn't offer the ability to pass a minimum size (max is passed there).
// TODO(appleguy): Reconcile the self.style.*Size properties with rootConstrainedSize
YGNodeStyleSetMinWidth (rootYogaNode, yogaFloatForCGFloat(rootConstrainedSize.min.width));
YGNodeStyleSetMinHeight(rootYogaNode, yogaFloatForCGFloat(rootConstrainedSize.min.height));
ASDisplayNodePerformBlockOnEveryYogaChild(self, ^(ASDisplayNode * _Nonnull node) {
ASLayoutElementStyle *style = node.style;
YGNodeRef yogaNode = node.yogaNode;
YGNodeStyleSetDirection (yogaNode, style.direction);
YGNodeStyleSetFlexWrap (yogaNode, style.flexWrap);
YGNodeStyleSetFlexGrow (yogaNode, style.flexGrow);
YGNodeStyleSetFlexShrink (yogaNode, style.flexShrink);
YGNODE_STYLE_SET_DIMENSION (yogaNode, FlexBasis, style.flexBasis);
YGNodeStyleSetFlexDirection (yogaNode, yogaFlexDirection(style.flexDirection));
YGNodeStyleSetJustifyContent(yogaNode, yogaJustifyContent(style.justifyContent));
YGNodeStyleSetAlignSelf (yogaNode, yogaAlignSelf(style.alignSelf));
ASStackLayoutAlignItems alignItems = style.alignItems;
if (alignItems != ASStackLayoutAlignItemsNotSet) {
YGNodeStyleSetAlignItems(yogaNode, yogaAlignItems(alignItems));
}
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 < YGEdgeAll + 1; ++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 = (YGEdge)(edge + 1);
}
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 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),
@@ -313,7 +271,7 @@
ASDisplayNodePerformBlockOnEveryYogaChild(self, ^(ASDisplayNode * _Nonnull node) {
[node setupYogaCalculatedLayout];
node.hierarchyState &= ~ASHierarchyStateYogaLayoutMeasuring;
node.yogaLayoutInProgress = NO;
});
#if YOGA_LAYOUT_LOGGING /* YOGA_LAYOUT_LOGGING */

View File

@@ -424,12 +424,6 @@ static ASDisplayNodeMethodOverrides GetASDisplayNodeMethodOverrides(Class c)
[self _scheduleIvarsForMainDeallocation];
}
#if YOGA_TREE_CONTIGUOUS
if (_yogaNode != NULL) {
YGNodeFree(_yogaNode);
}
#endif
// TODO: Remove this? If supernode isn't already nil, this method isn't dealloc-safe anyway.
[self _setSupernode:nil];
}
@@ -966,22 +960,32 @@ static ASDisplayNodeMethodOverrides GetASDisplayNodeMethodOverrides(Class c)
ASDN::MutexLocker l(__instanceLock__);
#if YOGA_TREE_CONTIGUOUS /* YOGA */
if (ASHierarchyStateIncludesYogaLayoutEnabled(_hierarchyState) == YES) {
if (ASHierarchyStateIncludesYogaLayoutMeasuring(_hierarchyState) == NO && self.yogaCalculatedLayout == nil) {
ASDN::MutexUnlocker ul(__instanceLock__);
// There are several cases where Yoga could arrive here:
// - This node is not in a Yoga tree: it has neither a yogaParent nor yogaChildren.
// - This node is a Yoga tree root: it has no yogaParent, but has yogaChildren.
// - This node is a Yoga tree node: it has both a yogaParent and yogaChildren.
// - This node is a Yoga tree leaf: it has a yogaParent, but no yogaChidlren.
// If we're a leaf node, we are probably being called by a measure function and proceed as normal.
// If we're a yoga root or tree node, initiate a new Yoga calculation pass from root.
YGNodeRef yogaNode = _style.yogaNode;
BOOL hasYogaParent = (_yogaParent != nil);
BOOL hasYogaChildren = (_yogaChildren.count > 0);
BOOL usesYoga = (yogaNode != NULL && (hasYogaParent || hasYogaChildren));
if (usesYoga && (_yogaParent == nil || _yogaChildren.count > 0)) {
// This node has some connection to a Yoga tree.
ASDN::MutexUnlocker ul(__instanceLock__);
if (self.yogaLayoutInProgress == NO) {
[self calculateLayoutFromYogaRoot:constrainedSize];
}
// The call above may set yogaCalculatedLayout, even if it tested as nil to enter it.
if (self.yogaCalculatedLayout && self.yogaChildren.count > 0) {
return self.yogaCalculatedLayout;
}
ASDisplayNodeAssert(_yogaCalculatedLayout, @"Yoga node should have a non-nil layout at this stage: %@", self);
return _yogaCalculatedLayout;
}
ASYogaLog(@"PROCEEDING past Yoga check to calculate ASLayout for: %@", self);
#endif /* YOGA */
// Manual size calculation via calculateSizeThatFits:
if (((_methodOverrides & ASDisplayNodeMethodOverrideLayoutSpecThatFits) ||
(_layoutSpecBlock != NULL)) == NO) {
if (_layoutSpecBlock == NULL && (_methodOverrides & ASDisplayNodeMethodOverrideLayoutSpecThatFits) == 0) {
CGSize size = [self calculateSizeThatFits:constrainedSize.max];
ASDisplayNodeLogEvent(self, @"calculatedSize: %@", NSStringFromCGSize(size));
return [ASLayout layoutWithLayoutElement:self size:ASSizeRangeClamp(constrainedSize, size) sublayouts:nil];

View File

@@ -45,7 +45,7 @@
// in the ASDisplayNode tree (based on .yogaChildren). When disabled, ASYogaLayoutSpec is used, with a
// disjoint Yoga tree for each level in the hierarchy. Currently, both modes are experimental.
#ifndef YOGA_TREE_CONTIGUOUS
#define YOGA_TREE_CONTIGUOUS 0 // To enable, set to YOGA, as the code depends on YOGA also being set.
#define YOGA_TREE_CONTIGUOUS YOGA // To enable, set to YOGA, as the code depends on YOGA also being set.
#endif
#define AS_PIN_REMOTE_IMAGE __has_include(<PINRemoteImage/PINRemoteImage.h>)

View File

@@ -415,7 +415,7 @@ typedef void (^ASDataControllerCompletionBlock)(NSArray<ASCollectionElement *> *
nodeBlock = [dataSource dataController:self supplementaryNodeBlockOfKind:kind atIndexPath:indexPath];
}
ASSizeRange constrainedSize;
ASSizeRange constrainedSize = ASSizeRangeUnconstrained;
if (shouldFetchSizeRanges) {
constrainedSize = [self constrainedSizeForNodeOfKind:kind atIndexPath:indexPath];
}

View File

@@ -316,6 +316,8 @@ typedef struct {
extern ASEdgeInsets const ASEdgeInsetsZero;
extern ASEdgeInsets ASEdgeInsetsMake(UIEdgeInsets edgeInsets);
#endif
NS_ASSUME_NONNULL_END

View File

@@ -115,4 +115,14 @@ NSString *NSStringFromASSizeRange(ASSizeRange sizeRange)
#if YOGA
#pragma mark - Yoga - ASEdgeInsets
ASEdgeInsets const ASEdgeInsetsZero = {};
extern ASEdgeInsets ASEdgeInsetsMake(UIEdgeInsets edgeInsets)
{
ASEdgeInsets asEdgeInsets = ASEdgeInsetsZero;
asEdgeInsets.top = ASDimensionMake(edgeInsets.top);
asEdgeInsets.left = ASDimensionMake(edgeInsets.left);
asEdgeInsets.bottom = ASDimensionMake(edgeInsets.bottom);
asEdgeInsets.right = ASDimensionMake(edgeInsets.right);
return asEdgeInsets;
}
#endif

View File

@@ -15,17 +15,19 @@
// http://www.apache.org/licenses/LICENSE-2.0
//
#import "ASDisplayNode+FrameworkPrivate.h"
#import <AsyncDisplayKit/ASDisplayNode+FrameworkPrivate.h>
#import <AsyncDisplayKit/ASAvailability.h>
#import <AsyncDisplayKit/ASLayout.h>
#import <AsyncDisplayKit/ASLayoutElement.h>
#import <AsyncDisplayKit/ASThread.h>
#import <AsyncDisplayKit/ASObjectDescriptionHelpers.h>
#import <AsyncDisplayKit/ASInternalHelpers.h>
#import <atomic>
#if YOGA
#import YOGA_HEADER_PATH
#import <AsyncDisplayKit/ASYogaUtilities.h>
#endif
#pragma mark - ASLayoutElementContext
@@ -110,6 +112,21 @@ NSString * const ASLayoutElementStyleDescenderProperty = @"ASLayoutElementStyleD
NSString * const ASLayoutElementStyleLayoutPositionProperty = @"ASLayoutElementStyleLayoutPositionProperty";
#if YOGA
NSString * const ASYogaFlexWrapProperty = @"ASLayoutElementStyleLayoutFlexWrapProperty";
NSString * const ASYogaFlexDirectionProperty = @"ASYogaFlexDirectionProperty";
NSString * const ASYogaDirectionProperty = @"ASYogaDirectionProperty";
NSString * const ASYogaSpacingProperty = @"ASYogaSpacingProperty";
NSString * const ASYogaJustifyContentProperty = @"ASYogaJustifyContentProperty";
NSString * const ASYogaAlignItemsProperty = @"ASYogaAlignItemsProperty";
NSString * const ASYogaPositionTypeProperty = @"ASYogaPositionTypeProperty";
NSString * const ASYogaPositionProperty = @"ASYogaPositionProperty";
NSString * const ASYogaMarginProperty = @"ASYogaMarginProperty";
NSString * const ASYogaPaddingProperty = @"ASYogaPaddingProperty";
NSString * const ASYogaBorderProperty = @"ASYogaBorderProperty";
NSString * const ASYogaAspectRatioProperty = @"ASYogaAspectRatioProperty";
#endif
#define ASLayoutElementStyleSetSizeWithScope(x) \
__instanceLock__.lock(); \
ASLayoutElementSize newSize = _size.load(); \
@@ -119,6 +136,7 @@ NSString * const ASLayoutElementStyleLayoutPositionProperty = @"ASLayoutElementS
#define ASLayoutElementStyleCallDelegate(propertyName)\
do {\
[self propertyDidChange:propertyName];\
[_delegate style:self propertyDidChange:propertyName];\
} while(0)
@@ -138,9 +156,10 @@ do {\
std::atomic<CGPoint> _layoutPosition;
#if YOGA
YGNodeRef _yogaNode;
std::atomic<YGWrap> _flexWrap;
std::atomic<ASStackLayoutDirection> _flexDirection;
std::atomic<YGDirection> _direction;
std::atomic<CGFloat> _spacing;
std::atomic<ASStackLayoutJustifyContent> _justifyContent;
std::atomic<ASStackLayoutAlignItems> _alignItems;
std::atomic<YGPositionType> _positionType;
@@ -149,7 +168,6 @@ do {\
std::atomic<ASEdgeInsets> _padding;
std::atomic<ASEdgeInsets> _border;
std::atomic<CGFloat> _aspectRatio;
std::atomic<YGWrap> _flexWrap;
#endif
}
@@ -590,13 +608,153 @@ do {\
return result;
}
- (void)propertyDidChange:(NSString *)propertyName
{
#if YOGA
/* TODO(appleguy): STYLE SETTER METHODS LEFT TO IMPLEMENT
void YGNodeStyleSetOverflow(YGNodeRef node, YGOverflow overflow);
void YGNodeStyleSetFlex(YGNodeRef node, float flex);
*/
if (_yogaNode == NULL) {
return;
}
// Because the NSStrings used to identify each property are const, use efficient pointer comparison.
if (propertyName == ASLayoutElementStyleWidthProperty) {
YGNODE_STYLE_SET_DIMENSION(_yogaNode, Width, self.width);
}
else if (propertyName == ASLayoutElementStyleMinWidthProperty) {
YGNODE_STYLE_SET_DIMENSION(_yogaNode, MinWidth, self.minWidth);
}
else if (propertyName == ASLayoutElementStyleMaxWidthProperty) {
YGNODE_STYLE_SET_DIMENSION(_yogaNode, MaxWidth, self.maxWidth);
}
else if (propertyName == ASLayoutElementStyleHeightProperty) {
YGNODE_STYLE_SET_DIMENSION(_yogaNode, Height, self.height);
}
else if (propertyName == ASLayoutElementStyleMinHeightProperty) {
YGNODE_STYLE_SET_DIMENSION(_yogaNode, MinHeight, self.minHeight);
}
else if (propertyName == ASLayoutElementStyleMaxHeightProperty) {
YGNODE_STYLE_SET_DIMENSION(_yogaNode, MaxHeight, self.maxHeight);
}
else if (propertyName == ASLayoutElementStyleFlexGrowProperty) {
YGNodeStyleSetFlexGrow(_yogaNode, self.flexGrow);
}
else if (propertyName == ASLayoutElementStyleFlexShrinkProperty) {
YGNodeStyleSetFlexShrink(_yogaNode, self.flexShrink);
}
else if (propertyName == ASLayoutElementStyleFlexBasisProperty) {
YGNODE_STYLE_SET_DIMENSION(_yogaNode, FlexBasis, self.flexBasis);
}
else if (propertyName == ASLayoutElementStyleAlignSelfProperty) {
YGNodeStyleSetAlignSelf(_yogaNode, yogaAlignSelf(self.alignSelf));
}
else if (propertyName == ASYogaFlexWrapProperty) {
YGNodeStyleSetFlexWrap(_yogaNode, self.flexWrap);
}
else if (propertyName == ASYogaFlexDirectionProperty) {
YGNodeStyleSetFlexDirection(_yogaNode, yogaFlexDirection(self.flexDirection));
}
else if (propertyName == ASYogaDirectionProperty) {
YGNodeStyleSetDirection(_yogaNode, self.direction);
}
else if (propertyName == ASYogaJustifyContentProperty) {
YGNodeStyleSetJustifyContent(_yogaNode, yogaJustifyContent(self.justifyContent));
}
else if (propertyName == ASYogaAlignItemsProperty) {
ASStackLayoutAlignItems alignItems = self.alignItems;
if (alignItems != ASStackLayoutAlignItemsNotSet) {
YGNodeStyleSetAlignItems(_yogaNode, yogaAlignItems(alignItems));
}
}
else if (propertyName == ASYogaPositionTypeProperty) {
YGNodeStyleSetPositionType(_yogaNode, self.positionType);
}
else if (propertyName == ASYogaPositionProperty) {
ASEdgeInsets position = self.position;
YGEdge edge = YGEdgeLeft;
for (int i = 0; i < YGEdgeAll + 1; ++i) {
YGNODE_STYLE_SET_DIMENSION_WITH_EDGE(_yogaNode, Position, dimensionForEdgeWithEdgeInsets(edge, position), edge);
edge = (YGEdge)(edge + 1);
}
}
else if (propertyName == ASYogaMarginProperty) {
ASEdgeInsets margin = self.margin;
YGEdge edge = YGEdgeLeft;
for (int i = 0; i < YGEdgeAll + 1; ++i) {
YGNODE_STYLE_SET_DIMENSION_WITH_EDGE(_yogaNode, Margin, dimensionForEdgeWithEdgeInsets(edge, margin), edge);
edge = (YGEdge)(edge + 1);
}
}
else if (propertyName == ASYogaPaddingProperty) {
ASEdgeInsets padding = self.padding;
YGEdge edge = YGEdgeLeft;
for (int i = 0; i < YGEdgeAll + 1; ++i) {
YGNODE_STYLE_SET_DIMENSION_WITH_EDGE(_yogaNode, Padding, dimensionForEdgeWithEdgeInsets(edge, padding), edge);
edge = (YGEdge)(edge + 1);
}
}
else if (propertyName == ASYogaBorderProperty) {
ASEdgeInsets border = self.border;
YGEdge edge = YGEdgeLeft;
for (int i = 0; i < YGEdgeAll + 1; ++i) {
YGNODE_STYLE_SET_FLOAT_WITH_EDGE(_yogaNode, Border, dimensionForEdgeWithEdgeInsets(edge, border), edge);
edge = (YGEdge)(edge + 1);
}
}
else if (propertyName == ASYogaAspectRatioProperty) {
CGFloat aspectRatio = self.aspectRatio;
if (aspectRatio > FLT_EPSILON && aspectRatio < CGFLOAT_MAX / 2.0) {
YGNodeStyleSetAspectRatio(_yogaNode, aspectRatio);
}
}
#endif
}
#pragma mark - Yoga Flexbox Properties
#if YOGA
+ (void)initialize
{
[super initialize];
YGConfigSetPointScaleFactor(YGConfigGetDefault(), ASScreenScale());
// Yoga recommends using Web Defaults for all new projects. This will be enabled for Texture very soon.
//YGConfigSetUseWebDefaults(YGConfigGetDefault(), true);
}
- (YGNodeRef)yogaNode
{
return _yogaNode;
}
- (YGNodeRef)yogaNodeCreateIfNeeded
{
if (_yogaNode == NULL) {
_yogaNode = YGNodeNew();
}
return _yogaNode;
}
- (void)destroyYogaNode
{
if (_yogaNode != NULL) {
// Release the __bridge_retained Context object.
ASLayoutElementYogaUpdateMeasureFunc(_yogaNode, nil);
YGNodeFree(_yogaNode);
_yogaNode = NULL;
}
}
- (void)dealloc
{
[self destroyYogaNode];
}
- (YGWrap)flexWrap { return _flexWrap.load(); }
- (ASStackLayoutDirection)flexDirection { return _flexDirection.load(); }
- (YGDirection)direction { return _direction.load(); }
- (CGFloat)spacing { return _spacing.load(); }
- (ASStackLayoutJustifyContent)justifyContent { return _justifyContent.load(); }
- (ASStackLayoutAlignItems)alignItems { return _alignItems.load(); }
- (YGPositionType)positionType { return _positionType.load(); }
@@ -605,22 +763,53 @@ do {\
- (ASEdgeInsets)padding { return _padding.load(); }
- (ASEdgeInsets)border { return _border.load(); }
- (CGFloat)aspectRatio { return _aspectRatio.load(); }
- (YGWrap)flexWrap { return _flexWrap.load(); }
- (void)setFlexDirection:(ASStackLayoutDirection)flexDirection { _flexDirection.store(flexDirection); }
- (void)setDirection:(YGDirection)direction { _direction.store(direction); }
- (void)setSpacing:(CGFloat)spacing { _spacing.store(spacing); }
- (void)setJustifyContent:(ASStackLayoutJustifyContent)justify { _justifyContent.store(justify); }
- (void)setAlignItems:(ASStackLayoutAlignItems)alignItems { _alignItems.store(alignItems); }
- (void)setPositionType:(YGPositionType)positionType { _positionType.store(positionType); }
- (void)setPosition:(ASEdgeInsets)position { _position.store(position); }
- (void)setMargin:(ASEdgeInsets)margin { _margin.store(margin); }
- (void)setPadding:(ASEdgeInsets)padding { _padding.store(padding); }
- (void)setBorder:(ASEdgeInsets)border { _border.store(border); }
- (void)setAspectRatio:(CGFloat)aspectRatio { _aspectRatio.store(aspectRatio); }
- (void)setFlexWrap:(YGWrap)flexWrap { _flexWrap.store(flexWrap); }
- (void)setFlexWrap:(YGWrap)flexWrap {
_flexWrap.store(flexWrap);
ASLayoutElementStyleCallDelegate(ASYogaFlexWrapProperty);
}
- (void)setFlexDirection:(ASStackLayoutDirection)flexDirection {
_flexDirection.store(flexDirection);
ASLayoutElementStyleCallDelegate(ASYogaFlexDirectionProperty);
}
- (void)setDirection:(YGDirection)direction {
_direction.store(direction);
ASLayoutElementStyleCallDelegate(ASYogaDirectionProperty);
}
- (void)setJustifyContent:(ASStackLayoutJustifyContent)justify {
_justifyContent.store(justify);
ASLayoutElementStyleCallDelegate(ASYogaJustifyContentProperty);
}
- (void)setAlignItems:(ASStackLayoutAlignItems)alignItems {
_alignItems.store(alignItems);
ASLayoutElementStyleCallDelegate(ASYogaAlignItemsProperty);
}
- (void)setPositionType:(YGPositionType)positionType {
_positionType.store(positionType);
ASLayoutElementStyleCallDelegate(ASYogaPositionTypeProperty);
}
- (void)setPosition:(ASEdgeInsets)position {
_position.store(position);
ASLayoutElementStyleCallDelegate(ASYogaPositionProperty);
}
- (void)setMargin:(ASEdgeInsets)margin {
_margin.store(margin);
ASLayoutElementStyleCallDelegate(ASYogaMarginProperty);
}
- (void)setPadding:(ASEdgeInsets)padding {
_padding.store(padding);
ASLayoutElementStyleCallDelegate(ASYogaPaddingProperty);
}
- (void)setBorder:(ASEdgeInsets)border {
_border.store(border);
ASLayoutElementStyleCallDelegate(ASYogaBorderProperty);
}
- (void)setAspectRatio:(CGFloat)aspectRatio {
_aspectRatio.store(aspectRatio);
ASLayoutElementStyleCallDelegate(ASYogaAspectRatioProperty);
}
#endif
#endif /* YOGA */
#pragma mark Deprecated

View File

@@ -17,6 +17,16 @@
#import <AsyncDisplayKit/ASLayout.h>
#import <AsyncDisplayKit/ASDisplayNode+Beta.h>
#define ASYogaLog(...) //NSLog(__VA_ARGS__)
@interface ASDisplayNode (YogaHelpers)
+ (ASDisplayNode *)yogaNode;
+ (ASDisplayNode *)verticalYogaStack;
+ (ASDisplayNode *)horizontalYogaStack;
@end
extern void ASDisplayNodePerformBlockOnEveryYogaChild(ASDisplayNode *node, void(^block)(ASDisplayNode *node));
ASDISPLAYNODE_EXTERN_C_BEGIN
@@ -32,6 +42,7 @@ float yogaDimensionToPoints(ASDimension dimension);
float yogaDimensionToPercent(ASDimension dimension);
ASDimension dimensionForEdgeWithEdgeInsets(YGEdge edge, ASEdgeInsets insets);
void ASLayoutElementYogaUpdateMeasureFunc(YGNodeRef yogaNode, id <ASLayoutElement> layoutElement);
YGSize ASLayoutElementYogaMeasureFunc(YGNodeRef yogaNode,
float width, YGMeasureMode widthMode,
float height, YGMeasureMode heightMode);

View File

@@ -14,6 +14,32 @@
#if YOGA /* YOGA */
@implementation ASDisplayNode (YogaHelpers)
+ (ASDisplayNode *)yogaNode
{
ASDisplayNode *node = [[ASDisplayNode alloc] init];
node.automaticallyManagesSubnodes = YES;
[node.style yogaNodeCreateIfNeeded];
return node;
}
+ (ASDisplayNode *)verticalYogaStack
{
ASDisplayNode *stack = [self yogaNode];
stack.style.flexDirection = ASStackLayoutDirectionVertical;
return stack;
}
+ (ASDisplayNode *)horizontalYogaStack
{
ASDisplayNode *stack = [self yogaNode];
stack.style.flexDirection = ASStackLayoutDirectionHorizontal;
return stack;
}
@end
extern void ASDisplayNodePerformBlockOnEveryYogaChild(ASDisplayNode *node, void(^block)(ASDisplayNode *node))
{
if (node == nil) {
@@ -109,6 +135,27 @@ ASDimension dimensionForEdgeWithEdgeInsets(YGEdge edge, ASEdgeInsets insets)
}
}
void ASLayoutElementYogaUpdateMeasureFunc(YGNodeRef yogaNode, id <ASLayoutElement> layoutElement)
{
if (yogaNode == NULL) {
return;
}
BOOL hasMeasureFunc = (YGNodeGetMeasureFunc(yogaNode) != NULL);
if (layoutElement != nil && hasMeasureFunc == NO) {
// TODO(appleguy): Add override detection for calculateSizeThatFits: and calculateLayoutThatFits:,
// then we can set the MeasureFunc only for nodes that override one of the trio of measurement methods.
// if (_layoutSpecBlock == NULL && (_methodOverrides & ASDisplayNodeMethodOverrideLayoutSpecThatFits) == 0 && ...) {
// Retain the Context object. This must be explicitly released with a __bridge_transfer; YGNodeFree() is not sufficient.
YGNodeSetContext(yogaNode, (__bridge_retained void *)layoutElement);
YGNodeSetMeasureFunc(yogaNode, &ASLayoutElementYogaMeasureFunc);
} else if (layoutElement == nil && hasMeasureFunc == YES){
// Release the __bridge_retained Context object.
__unused id <ASLayoutElement> element = (__bridge_transfer id)YGNodeGetContext(yogaNode);
YGNodeSetContext(yogaNode, NULL);
YGNodeSetMeasureFunc(yogaNode, NULL);
}
}
YGSize ASLayoutElementYogaMeasureFunc(YGNodeRef yogaNode, float width, YGMeasureMode widthMode,
float height, YGMeasureMode heightMode)
{
@@ -133,6 +180,14 @@ YGSize ASLayoutElementYogaMeasureFunc(YGNodeRef yogaNode, float width, YGMeasure
sizeRange.min.height = (minHeight.unit == ASDimensionUnitPoints ? yogaDimensionToPoints(minHeight) : 0.0);
}
ASDisplayNodeCAssert(isnan(sizeRange.min.width) == NO && isnan(sizeRange.min.height) == NO, @"Yoga size range for measurement should not have NaN in minimum");
if (isnan(sizeRange.max.width)) {
sizeRange.max.width = CGFLOAT_MAX;
}
if (isnan(sizeRange.max.height)) {
sizeRange.max.height = CGFLOAT_MAX;
}
CGSize size = [[layoutElement layoutThatFits:sizeRange] size];
return (YGSize){ .width = (float)size.width, .height = (float)size.height };
}

View File

@@ -56,8 +56,6 @@ typedef NS_OPTIONS(NSUInteger, ASHierarchyState)
/** One of the supernodes of this node is performing a transition.
Any layout calculated during this state should not be applied immediately, but pending until later. */
ASHierarchyStateLayoutPending = 1 << 3,
ASHierarchyStateYogaLayoutEnabled = 1 << 4,
ASHierarchyStateYogaLayoutMeasuring = 1 << 5
};
ASDISPLAYNODE_INLINE BOOL ASHierarchyStateIncludesLayoutPending(ASHierarchyState hierarchyState)
@@ -70,16 +68,6 @@ ASDISPLAYNODE_INLINE BOOL ASHierarchyStateIncludesRangeManaged(ASHierarchyState
return ((hierarchyState & ASHierarchyStateRangeManaged) == ASHierarchyStateRangeManaged);
}
ASDISPLAYNODE_INLINE BOOL ASHierarchyStateIncludesYogaLayoutMeasuring(ASHierarchyState hierarchyState)
{
return ((hierarchyState & ASHierarchyStateYogaLayoutMeasuring) == ASHierarchyStateYogaLayoutMeasuring);
}
ASDISPLAYNODE_INLINE BOOL ASHierarchyStateIncludesYogaLayoutEnabled(ASHierarchyState hierarchyState)
{
return ((hierarchyState & ASHierarchyStateYogaLayoutEnabled) == ASHierarchyStateYogaLayoutEnabled);
}
ASDISPLAYNODE_INLINE BOOL ASHierarchyStateIncludesRasterized(ASHierarchyState hierarchyState)
{
return ((hierarchyState & ASHierarchyStateRasterized) == ASHierarchyStateRasterized);

View File

@@ -58,6 +58,7 @@ typedef NS_OPTIONS(NSUInteger, ASDisplayNodeMethodOverrides)
typedef NS_OPTIONS(uint_least32_t, ASDisplayNodeAtomicFlags)
{
Synchronous = 1 << 0,
YogaLayoutInProgress = 1 << 1,
};
#define checkFlag(flag) ((_atomicFlags.load() & flag) != 0)
@@ -204,7 +205,6 @@ FOUNDATION_EXPORT NSString * const ASRenderingEngineDidDisplayNodesScheduledBefo
NSMutableArray<ASDisplayNode *> *_yogaChildren;
#endif
#if YOGA_TREE_CONTIGUOUS
YGNodeRef _yogaNode;
__weak ASDisplayNode *_yogaParent;
ASLayout *_yogaCalculatedLayout;
#endif

View File

@@ -3,4 +3,5 @@ platform :ios, '8.0'
target 'Sample' do
pod 'Texture/IGListKit', :path => '../..'
pod 'Texture/PINRemoteImage', :path => '../..'
pod 'Texture/Yoga', :path => '../..'
end

View File

@@ -29,6 +29,7 @@
// There are many ways to format ASLayoutSpec code. In this example, we offer two different formats:
// A flatter, more ordinary Objective-C style; or a more structured, "visually" declarative style.
#define YOGA_LAYOUT 0
#define FLAT_LAYOUT 0
#define DEBUG_PHOTOCELL_LAYOUT 0
@@ -106,6 +107,8 @@
// instead of adding everything addSubnode:
self.automaticallyManagesSubnodes = YES;
[self setupYogaLayoutIfNeeded];
#if DEBUG_PHOTOCELL_LAYOUT
_userAvatarImageNode.backgroundColor = [UIColor greenColor];
@@ -121,6 +124,7 @@
return self;
}
#if !YOGA_LAYOUT
- (ASLayoutSpec *)layoutSpecThatFits:(ASSizeRange)constrainedSize
{
// There are many ways to format ASLayoutSpec code. In this example, we offer two different formats:
@@ -267,6 +271,7 @@
]];
}
}
#endif
#pragma mark - Instance Methods
@@ -298,4 +303,71 @@
}
}
- (void)setupYogaLayoutIfNeeded
{
#if YOGA_LAYOUT
[self.style yogaNodeCreateIfNeeded];
[_userAvatarImageNode.style yogaNodeCreateIfNeeded];
[_userNameLabel.style yogaNodeCreateIfNeeded];
[_photoImageNode.style yogaNodeCreateIfNeeded];
[_photoCommentsNode.style yogaNodeCreateIfNeeded];
[_photoLikesLabel.style yogaNodeCreateIfNeeded];
[_photoDescriptionLabel.style yogaNodeCreateIfNeeded];
[_photoLocationLabel.style yogaNodeCreateIfNeeded];
[_photoTimeIntervalSincePostLabel.style yogaNodeCreateIfNeeded];
ASDisplayNode *headerStack = [ASDisplayNode horizontalYogaStack];
headerStack.style.margin = ASEdgeInsetsMake(InsetForHeader);
headerStack.style.alignItems = ASStackLayoutAlignItemsCenter;
headerStack.style.flexGrow = 1.0;
// Avatar Image, with inset - first thing in the header stack.
_userAvatarImageNode.style.preferredSize = CGSizeMake(USER_IMAGE_HEIGHT, USER_IMAGE_HEIGHT);
_userAvatarImageNode.style.margin = ASEdgeInsetsMake(InsetForAvatar);
[headerStack addYogaChild:_userAvatarImageNode];
// User Name and Photo Location stack is next
ASDisplayNode *userPhotoLocationStack = [ASDisplayNode verticalYogaStack];
userPhotoLocationStack.style.flexShrink = 1.0;
[headerStack addYogaChild:userPhotoLocationStack];
// Setup the inside of the User Name and Photo Location stack.
_userNameLabel.style.flexShrink = 1.0;
[userPhotoLocationStack addYogaChild:_userNameLabel];
if (_photoLocationLabel.attributedText) {
_photoLocationLabel.style.flexShrink = 1.0;
[userPhotoLocationStack addYogaChild:_photoLocationLabel];
}
/* TODO: These parameters aren't working as expected. For now the timestamp is next to the username.
// Add a spacer to allow a flexible space between the User Name / Location stack, and the Timestamp.
ASDisplayNode *spacer = [ASDisplayNode new];
spacer.style.flexShrink = 1.0;
spacer.style.width = ASDimensionMakeWithFraction(1.0);
[headerStack addYogaChild:spacer];
*/
// Photo Timestamp Label.
_photoTimeIntervalSincePostLabel.style.spacingBefore = HORIZONTAL_BUFFER;
[headerStack addYogaChild:_photoTimeIntervalSincePostLabel];
// Create the last stack before assembling everything: the Footer Stack contains the description and comments.
ASDisplayNode *footerStack = [ASDisplayNode verticalYogaStack];
footerStack.style.margin = ASEdgeInsetsMake(InsetForFooter);
footerStack.style.padding = ASEdgeInsetsMake(UIEdgeInsetsMake(0.0, 0.0, VERTICAL_BUFFER, 0.0));
footerStack.yogaChildren = @[_photoLikesLabel, _photoDescriptionLabel, _photoCommentsNode];
// Main Vertical Stack: contains header, large main photo with fixed aspect ratio, and footer.
_photoImageNode.style.aspectRatio = 1.0;
ASDisplayNode *verticalStack = self;
self.style.flexDirection = ASStackLayoutDirectionVertical;
[verticalStack addYogaChild:headerStack];
[verticalStack addYogaChild:_photoImageNode];
[verticalStack addYogaChild:footerStack];
#endif
}
@end