From f91265757e545a475e12bbee433cc3533851d32e Mon Sep 17 00:00:00 2001 From: appleguy Date: Thu, 9 Feb 2017 16:10:29 -0800 Subject: [PATCH] [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. --- AsyncDisplayKit.podspec | 7 + AsyncDisplayKit.xcodeproj/project.pbxproj | 4 + AsyncDisplayKit/ASDisplayNode+Beta.h | 43 +- AsyncDisplayKit/ASDisplayNode+Subclasses.h | 4 +- AsyncDisplayKit/ASDisplayNode+Yoga.mm | 407 ++++++++++++++++++ AsyncDisplayKit/ASDisplayNode.mm | 25 +- AsyncDisplayKit/AsyncDisplayKit-Prefix.pch | 9 +- AsyncDisplayKit/Layout/ASDimension.h | 9 + AsyncDisplayKit/Layout/ASLayoutElement.h | 4 +- AsyncDisplayKit/Layout/ASLayoutElement.mm | 52 ++- .../Private/ASDisplayNode+FrameworkPrivate.h | 16 +- .../Private/ASDisplayNodeInternal.h | 17 +- Base/ASAvailability.h | 19 +- examples/ASDKLayoutTransition/Podfile | 3 +- 14 files changed, 585 insertions(+), 34 deletions(-) create mode 100644 AsyncDisplayKit/ASDisplayNode+Yoga.mm diff --git a/AsyncDisplayKit.podspec b/AsyncDisplayKit.podspec index cb0b0d4526..c6b60eb710 100644 --- a/AsyncDisplayKit.podspec +++ b/AsyncDisplayKit.podspec @@ -44,6 +44,7 @@ Pod::Spec.new do |spec| end spec.subspec 'PINRemoteImage' do |pin| + # Note: The core.prefix_header_file includes setup of PIN_REMOTE_IMAGE, so the line below could be removed. pin.xcconfig = { 'GCC_PREPROCESSOR_DEFINITIONS' => '$(inherited) PIN_REMOTE_IMAGE=1' } pin.dependency 'PINRemoteImage/iOS', '= 3.0.0-beta.7' pin.dependency 'PINRemoteImage/PINCache' @@ -51,11 +52,17 @@ Pod::Spec.new do |spec| end spec.subspec 'IGListKit' do |igl| + # Note: The core.prefix_header_file includes setup of IG_LIST_KIT, so the line below could be removed. igl.xcconfig = { 'GCC_PREPROCESSOR_DEFINITIONS' => '$(inherited) IG_LIST_KIT=1' } igl.dependency 'IGListKit', '2.1.0' igl.dependency 'AsyncDisplayKit/Core' end + spec.subspec 'Yoga' do |yoga| + yoga.dependency 'Yoga', '1.0.2' + yoga.dependency 'AsyncDisplayKit/Core' + end + # Include optional PINRemoteImage module spec.default_subspec = 'PINRemoteImage' diff --git a/AsyncDisplayKit.xcodeproj/project.pbxproj b/AsyncDisplayKit.xcodeproj/project.pbxproj index 6fdef80577..d4ced2799a 100644 --- a/AsyncDisplayKit.xcodeproj/project.pbxproj +++ b/AsyncDisplayKit.xcodeproj/project.pbxproj @@ -177,6 +177,7 @@ 8BBBAB8D1CEBAF1E00107FC6 /* ASDefaultPlaybackButton.m in Sources */ = {isa = PBXBuildFile; fileRef = 8B0768B21CE752EC002E1453 /* ASDefaultPlaybackButton.m */; }; 8BDA5FC71CDBDF91007D13B2 /* ASVideoPlayerNode.h in Headers */ = {isa = PBXBuildFile; fileRef = 8BDA5FC31CDBDDE1007D13B2 /* ASVideoPlayerNode.h */; settings = {ATTRIBUTES = (Public, ); }; }; 8BDA5FC81CDBDF95007D13B2 /* ASVideoPlayerNode.mm in Sources */ = {isa = PBXBuildFile; fileRef = 8BDA5FC41CDBDDE1007D13B2 /* ASVideoPlayerNode.mm */; }; + 90FC784F1E4BFE1B00383C5A /* ASDisplayNode+Yoga.mm in Sources */ = {isa = PBXBuildFile; fileRef = 90FC784E1E4BFE1B00383C5A /* ASDisplayNode+Yoga.mm */; }; 92DD2FE61BF4D05E0074C9DD /* MapKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 92DD2FE51BF4D05E0074C9DD /* MapKit.framework */; settings = {ATTRIBUTES = (Weak, ); }; }; 92DD2FE71BF4D0850074C9DD /* ASMapNode.mm in Sources */ = {isa = PBXBuildFile; fileRef = 92DD2FE21BF4B97E0074C9DD /* ASMapNode.mm */; }; 92DD2FE81BF4D0A80074C9DD /* ASMapNode.h in Headers */ = {isa = PBXBuildFile; fileRef = 92DD2FE11BF4B97E0074C9DD /* ASMapNode.h */; settings = {ATTRIBUTES = (Public, ); }; }; @@ -613,6 +614,7 @@ 8B0768B21CE752EC002E1453 /* ASDefaultPlaybackButton.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASDefaultPlaybackButton.m; sourceTree = ""; }; 8BDA5FC31CDBDDE1007D13B2 /* ASVideoPlayerNode.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASVideoPlayerNode.h; sourceTree = ""; }; 8BDA5FC41CDBDDE1007D13B2 /* ASVideoPlayerNode.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASVideoPlayerNode.mm; sourceTree = ""; }; + 90FC784E1E4BFE1B00383C5A /* ASDisplayNode+Yoga.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = "ASDisplayNode+Yoga.mm"; sourceTree = ""; }; 92DD2FE11BF4B97E0074C9DD /* ASMapNode.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASMapNode.h; sourceTree = ""; }; 92DD2FE21BF4B97E0074C9DD /* ASMapNode.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASMapNode.mm; sourceTree = ""; }; 92DD2FE51BF4D05E0074C9DD /* MapKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = MapKit.framework; path = System/Library/Frameworks/MapKit.framework; sourceTree = SDKROOT; }; @@ -893,6 +895,7 @@ 058D09D8195D050800B7D73C /* ASDisplayNode.h */, 058D09D9195D050800B7D73C /* ASDisplayNode.mm */, 68B027791C1A79CC0041016B /* ASDisplayNode+Beta.h */, + 90FC784E1E4BFE1B00383C5A /* ASDisplayNode+Yoga.mm */, 683489271D70DE3400327501 /* ASDisplayNode+Deprecated.h */, 058D09DA195D050800B7D73C /* ASDisplayNode+Subclasses.h */, 058D09DB195D050800B7D73C /* ASDisplayNodeExtras.h */, @@ -1902,6 +1905,7 @@ 34EFC76F1B701CF700AD841F /* ASRatioLayoutSpec.mm in Sources */, 254C6B8B1BF94F8A003EC431 /* ASTextKitShadower.mm in Sources */, 254C6B851BF94F8A003EC431 /* ASTextKitAttributes.mm in Sources */, + 90FC784F1E4BFE1B00383C5A /* ASDisplayNode+Yoga.mm in Sources */, 509E68601B3AED8E009B9150 /* ASScrollDirection.m in Sources */, B35062091B010EFD0018CF92 /* ASScrollNode.mm in Sources */, 8BDA5FC81CDBDF95007D13B2 /* ASVideoPlayerNode.mm in Sources */, diff --git a/AsyncDisplayKit/ASDisplayNode+Beta.h b/AsyncDisplayKit/ASDisplayNode+Beta.h index 6cc244191b..f465d033a5 100644 --- a/AsyncDisplayKit/ASDisplayNode+Beta.h +++ b/AsyncDisplayKit/ASDisplayNode+Beta.h @@ -8,6 +8,7 @@ // of patent rights can be found in the PATENTS file in the same directory. // +#import #import #import #import @@ -20,15 +21,15 @@ void ASPerformBlockOnBackgroundThread(void (^block)()); // DISPATCH_QUEUE_PRIORI ASDISPLAYNODE_EXTERN_C_END #if ASEVENTLOG_ENABLE -#define ASDisplayNodeLogEvent(node, ...) [node.eventLog logEventWithBacktrace:(AS_SAVE_EVENT_BACKTRACES ? [NSThread callStackSymbols] : nil) format:__VA_ARGS__] + #define ASDisplayNodeLogEvent(node, ...) [node.eventLog logEventWithBacktrace:(AS_SAVE_EVENT_BACKTRACES ? [NSThread callStackSymbols] : nil) format:__VA_ARGS__] #else -#define ASDisplayNodeLogEvent(node, ...) + #define ASDisplayNodeLogEvent(node, ...) #endif #if ASEVENTLOG_ENABLE -#define ASDisplayNodeGetEventLog(node) node.eventLog + #define ASDisplayNodeGetEventLog(node) node.eventLog #else -#define ASDisplayNodeGetEventLog(node) nil + #define ASDisplayNodeGetEventLog(node) nil #endif /** @@ -147,4 +148,38 @@ typedef struct { @end +#pragma mark - Yoga Layout Support + +#if YOGA + +@interface ASDisplayNode (Yoga) + +@property (nonatomic, strong) NSArray *yogaChildren; + +- (void)addYogaChild:(ASDisplayNode *)child; +- (void)removeYogaChild:(ASDisplayNode *)child; + +// This method should not normally be called directly. +- (ASLayout *)calculateLayoutFromYogaRoot:(ASSizeRange)rootConstrainedSize; + +@end + +@interface ASLayoutElementStyle (Yoga) + +@property (nonatomic, assign, readwrite) ASStackLayoutDirection 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; +@property (nonatomic, assign, readwrite) ASEdgeInsets position; +@property (nonatomic, assign, readwrite) ASEdgeInsets margin; +@property (nonatomic, assign, readwrite) ASEdgeInsets padding; +@property (nonatomic, assign, readwrite) ASEdgeInsets border; +@property (nonatomic, assign, readwrite) CGFloat aspectRatio; +@property (nonatomic, assign, readwrite) YGWrap flexWrap; + +@end + +#endif + NS_ASSUME_NONNULL_END diff --git a/AsyncDisplayKit/ASDisplayNode+Subclasses.h b/AsyncDisplayKit/ASDisplayNode+Subclasses.h index 2b831a3562..3d5e042494 100644 --- a/AsyncDisplayKit/ASDisplayNode+Subclasses.h +++ b/AsyncDisplayKit/ASDisplayNode+Subclasses.h @@ -232,8 +232,8 @@ NS_ASSUME_NONNULL_BEGIN /** @name Observing node state changes */ /** - * Declare methods as requiring super calls (this can't be required in the protocol). - * For descriptions, see definition. + * Declare methods as requiring super calls (this can't be required in the protocol). + * For descriptions, see definition. */ - (void)didEnterVisibleState ASDISPLAYNODE_REQUIRES_SUPER; diff --git a/AsyncDisplayKit/ASDisplayNode+Yoga.mm b/AsyncDisplayKit/ASDisplayNode+Yoga.mm new file mode 100644 index 0000000000..d798e49277 --- /dev/null +++ b/AsyncDisplayKit/ASDisplayNode+Yoga.mm @@ -0,0 +1,407 @@ +// +// ASDisplayNode+Yoga.mm +// AsyncDisplayKit +// +// Created by Scott Goodson on 2/8/17. +// Copyright © 2017 Facebook. All rights reserved. +// + +#import + +#if YOGA /* YOGA */ + +#import +#import +#import +#import + +// 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 layoutElement = (__bridge id )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 */ diff --git a/AsyncDisplayKit/ASDisplayNode.mm b/AsyncDisplayKit/ASDisplayNode.mm index 177288a7a5..b7f4dfbd18 100644 --- a/AsyncDisplayKit/ASDisplayNode.mm +++ b/AsyncDisplayKit/ASDisplayNode.mm @@ -427,6 +427,12 @@ static ASDisplayNodeMethodOverrides GetASDisplayNodeMethodOverrides(Class c) _subnodes = nil; +#if YOGA + 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]; } @@ -1070,7 +1076,8 @@ ASLayoutElementFinalLayoutElementDefault restrictedToSize:(ASLayoutElementSize)size relativeToParentSize:(CGSize)parentSize { - const ASSizeRange resolvedRange = ASSizeRangeIntersect(constrainedSize, ASLayoutElementSizeResolve(self.style.size, parentSize)); + ASSizeRange styleAndParentSize = ASLayoutElementSizeResolve(self.style.size, parentSize); + const ASSizeRange resolvedRange = ASSizeRangeIntersect(constrainedSize, styleAndParentSize); return [self calculateLayoutThatFits:resolvedRange]; } @@ -1080,6 +1087,14 @@ ASLayoutElementFinalLayoutElementDefault ASDN::MutexLocker l(__instanceLock__); +#if YOGA /* YOGA */ + if (ASHierarchyStateIncludesYogaLayoutEnabled(_hierarchyState) == YES && + ASHierarchyStateIncludesYogaLayoutMeasuring(_hierarchyState) == NO) { + ASDN::MutexUnlocker ul(__instanceLock__); + return [self calculateLayoutFromYogaRoot:constrainedSize]; + } +#endif /* YOGA */ + // Manual size calculation via calculateSizeThatFits: if (((_methodOverrides & ASDisplayNodeMethodOverrideLayoutSpecThatFits) || (_layoutSpecBlock != NULL)) == NO) { @@ -3189,11 +3204,9 @@ ASDISPLAYNODE_INLINE BOOL nodeIsInRasterizedTree(ASDisplayNode *node) { // Entering layout pending state } else { // Leaving layout pending state, reset related properties - { - ASDN::MutexLocker l(__instanceLock__); - _pendingTransitionID = ASLayoutElementContextInvalidTransitionID; - _pendingLayoutTransition = nil; - } + ASDN::MutexLocker l(__instanceLock__); + _pendingTransitionID = ASLayoutElementContextInvalidTransitionID; + _pendingLayoutTransition = nil; } } diff --git a/AsyncDisplayKit/AsyncDisplayKit-Prefix.pch b/AsyncDisplayKit/AsyncDisplayKit-Prefix.pch index 64ec2a6a4e..374bad0162 100644 --- a/AsyncDisplayKit/AsyncDisplayKit-Prefix.pch +++ b/AsyncDisplayKit/AsyncDisplayKit-Prefix.pch @@ -8,16 +8,17 @@ #import #endif +// Some build systems (Cocoapods, Buck, Bazel, etc) will define these flags manually if the functionality +// is needed by the app. Carthage in particular, or if a user forgets to set the build flag, benefit from +// checking if each flag is not defined and then setting it to whether or not the header is accessible. -// CocoaPods has a preproceessor macro for PIN_REMOTE_IMAGE, if already defined, okay #ifndef PIN_REMOTE_IMAGE - // For Carthage or manual builds, this will define PIN_REMOTE_IMAGE if the header is available in the // search path e.g. they've dragged in the framework (technically this will not be able to detect if // a user does not include the framework in the link binary with build step). -#define PIN_REMOTE_IMAGE __has_include() + #define PIN_REMOTE_IMAGE __has_include() #endif #ifndef IG_LIST_KIT -#define IG_LIST_KIT __has_include() + #define IG_LIST_KIT __has_include() #endif diff --git a/AsyncDisplayKit/Layout/ASDimension.h b/AsyncDisplayKit/Layout/ASDimension.h index dd402b7e64..10237f9db6 100644 --- a/AsyncDisplayKit/Layout/ASDimension.h +++ b/AsyncDisplayKit/Layout/ASDimension.h @@ -191,6 +191,15 @@ ASDISPLAYNODE_INLINE AS_WARN_UNUSED_RESULT NSString *NSStringFromASLayoutSize(AS NSStringFromASDimension(size.height)]; } +#pragma mark - ASEdgeInsets + +typedef struct { + ASDimension top; + ASDimension left; + ASDimension bottom; + ASDimension right; +} ASEdgeInsets; + #pragma mark - ASSizeRange /** diff --git a/AsyncDisplayKit/Layout/ASLayoutElement.h b/AsyncDisplayKit/Layout/ASLayoutElement.h index 51b8af3261..0e982a7b38 100644 --- a/AsyncDisplayKit/Layout/ASLayoutElement.h +++ b/AsyncDisplayKit/Layout/ASLayoutElement.h @@ -157,6 +157,8 @@ ASDISPLAYNODE_EXTERN_C_END #pragma mark - Deprecated +#define ASLayoutable ASLayoutElement + /** * @abstract Calculate a layout based on given size range. * @@ -259,7 +261,6 @@ extern NSString * const ASLayoutElementStyleLayoutPositionProperty; */ @property (nonatomic, assign, readwrite) ASDimension maxWidth; - #pragma mark - ASLayoutElementStyleSizeHelpers /** @@ -325,7 +326,6 @@ extern NSString * const ASLayoutElementStyleLayoutPositionProperty; @end - #pragma mark - ASLayoutElementStylability @protocol ASLayoutElementStylability diff --git a/AsyncDisplayKit/Layout/ASLayoutElement.mm b/AsyncDisplayKit/Layout/ASLayoutElement.mm index 3a0b500936..d2907b27b4 100644 --- a/AsyncDisplayKit/Layout/ASLayoutElement.mm +++ b/AsyncDisplayKit/Layout/ASLayoutElement.mm @@ -10,13 +10,14 @@ // of patent rights can be found in the PATENTS file in the same directory. // +#import "ASDisplayNode+FrameworkPrivate.h" +#import #import +#import +#import #import #import -#import - -#import extern void ASLayoutElementPerformBlockOnEveryElement(id element, void(^block)(id element)) { @@ -138,6 +139,20 @@ do {\ std::atomic _ascender; std::atomic _descender; std::atomic _layoutPosition; + +#if YOGA + std::atomic _direction; + std::atomic _spacing; + std::atomic _justifyContent; + std::atomic _alignItems; + std::atomic _positionType; + std::atomic _position; + std::atomic _margin; + std::atomic _padding; + std::atomic _border; + std::atomic _aspectRatio; + std::atomic _flexWrap; +#endif } @dynamic width, height, minWidth, maxWidth, minHeight, maxHeight; @@ -350,7 +365,6 @@ do {\ ASLayoutElementStyleCallDelegate(ASLayoutElementStyleMaxHeightProperty); } - #pragma mark - ASStackLayoutElement - (void)setSpacingBefore:(CGFloat)spacingBefore @@ -573,6 +587,36 @@ do {\ return result; } +#pragma mark - Yoga Flexbox Properties + +#if YOGA + +- (ASStackLayoutDirection)direction { return _direction.load(); } +- (CGFloat)spacing { return _spacing.load(); } +- (ASStackLayoutJustifyContent)justifyContent { return _justifyContent.load(); } +- (ASStackLayoutAlignItems)alignItems { return _alignItems.load(); } +- (YGPositionType)positionType { return _positionType.load(); } +- (ASEdgeInsets)position { return _position.load(); } +- (ASEdgeInsets)margin { return _margin.load(); } +- (ASEdgeInsets)padding { return _padding.load(); } +- (ASEdgeInsets)border { return _border.load(); } +- (CGFloat)aspectRatio { return _aspectRatio.load(); } +- (YGWrap)flexWrap { return _flexWrap.load(); } + +- (void)setDirection:(ASStackLayoutDirection)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); } + +#endif + #pragma mark Deprecated #pragma clang diagnostic push diff --git a/AsyncDisplayKit/Private/ASDisplayNode+FrameworkPrivate.h b/AsyncDisplayKit/Private/ASDisplayNode+FrameworkPrivate.h index 922fa4e28f..7e0a47bbb0 100644 --- a/AsyncDisplayKit/Private/ASDisplayNode+FrameworkPrivate.h +++ b/AsyncDisplayKit/Private/ASDisplayNode+FrameworkPrivate.h @@ -49,7 +49,9 @@ 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, - ASHierarchyStateVisualizeLayout = 1 << 4 + ASHierarchyStateYogaLayoutEnabled = 1 << 4, + ASHierarchyStateYogaLayoutMeasuring = 1 << 5, + ASHierarchyStateVisualizeLayout = 1 << 6 }; ASDISPLAYNODE_INLINE BOOL ASHierarchyStateIncludesLayoutPending(ASHierarchyState hierarchyState) @@ -59,7 +61,17 @@ ASDISPLAYNODE_INLINE BOOL ASHierarchyStateIncludesLayoutPending(ASHierarchyState ASDISPLAYNODE_INLINE BOOL ASHierarchyStateIncludesRangeManaged(ASHierarchyState hierarchyState) { - return ((hierarchyState & ASHierarchyStateRangeManaged) == ASHierarchyStateRangeManaged); + 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 ASHierarchyStateIncludesVisualizeLayout(ASHierarchyState hierarchyState) diff --git a/AsyncDisplayKit/Private/ASDisplayNodeInternal.h b/AsyncDisplayKit/Private/ASDisplayNodeInternal.h index 3e8159488c..25ba0c1f90 100644 --- a/AsyncDisplayKit/Private/ASDisplayNodeInternal.h +++ b/AsyncDisplayKit/Private/ASDisplayNodeInternal.h @@ -15,14 +15,13 @@ #import #import -#import -#import +#import #import #import +#import +#import #import -#import - NS_ASSUME_NONNULL_BEGIN @protocol _ASDisplayLayerDelegate; @@ -107,8 +106,8 @@ FOUNDATION_EXPORT NSString * const ASRenderingEngineDidDisplayNodesScheduledBefo @protected ASDisplayNode * __weak _supernode; - NSMutableArray *_subnodes; - + NSMutableArray *_subnodes; + ASLayoutElementStyle *_style; ASPrimitiveTraitCollection _primitiveTraitCollection; @@ -179,6 +178,12 @@ FOUNDATION_EXPORT NSString * const ASRenderingEngineDidDisplayNodesScheduledBefo NSTimeInterval _layoutComputationTotalTime; NSInteger _layoutComputationNumberOfPasses; +#if YOGA + YGNodeRef _yogaNode; + ASDisplayNode *_yogaParent; + NSMutableArray *_yogaChildren; +#endif + #if TIME_DISPLAYNODE_OPS @public NSTimeInterval _debugTimeToCreateView; diff --git a/Base/ASAvailability.h b/Base/ASAvailability.h index 44d8c9fa18..5f0d0d559a 100644 --- a/Base/ASAvailability.h +++ b/Base/ASAvailability.h @@ -11,11 +11,11 @@ #import #ifndef kCFCoreFoundationVersionNumber_iOS_9_0 -#define kCFCoreFoundationVersionNumber_iOS_9_0 1240.10 + #define kCFCoreFoundationVersionNumber_iOS_9_0 1240.10 #endif #ifndef kCFCoreFoundationVersionNumber_iOS_10_0 -#define kCFCoreFoundationVersionNumber_iOS_10_0 1348.00 + #define kCFCoreFoundationVersionNumber_iOS_10_0 1348.00 #endif #define AS_AT_LEAST_IOS9 (kCFCoreFoundationVersionNumber >= kCFCoreFoundationVersionNumber_iOS_9_0) @@ -24,6 +24,20 @@ #define AS_TARGET_OS_OSX (!(TARGET_OS_IOS || TARGET_OS_TV || TARGET_OS_WATCH)) #define AS_TARGET_OS_IOS TARGET_OS_IPHONE +#ifndef YOGA_HEADER_PATH + #define YOGA_HEADER_PATH +#endif + +#ifndef YOGA + #define YOGA __has_include(YOGA_HEADER_PATH) +#endif + +// If Yoga is available, make it available anywhere we use ASAvailability. +// This reduces Yoga-specific code in other files. +#if YOGA + #import YOGA_HEADER_PATH +#endif + #if AS_TARGET_OS_OSX #define UIEdgeInsets NSEdgeInsets @@ -65,5 +79,4 @@ } @end - #endif diff --git a/examples/ASDKLayoutTransition/Podfile b/examples/ASDKLayoutTransition/Podfile index defaf55058..32b4c3336e 100644 --- a/examples/ASDKLayoutTransition/Podfile +++ b/examples/ASDKLayoutTransition/Podfile @@ -1,5 +1,6 @@ source 'https://github.com/CocoaPods/Specs.git' platform :ios, '8.0' target 'Sample' do - pod 'AsyncDisplayKit', :path => '../..' + pod 'AsyncDisplayKit', :path => '../..' + pod 'AsyncDisplayKit/Yoga', :path => '../..' end