Skip to content

Commit

Permalink
[Yoga] Add insertYogaNode:atIndex: method. Improve handling of relayo…
Browse files Browse the repository at this point in the history
…uts. (TextureGroup#469)

* [Yoga] Add insertYogaNode:atIndex: method. Improve handling of relayouts.

* Add new "version" parameter to Yoga initialization of ASDisplayNodeLayout C++ struct.
  • Loading branch information
appleguy authored and bernieperez committed Apr 25, 2018
1 parent 73d0376 commit a026505
Show file tree
Hide file tree
Showing 6 changed files with 114 additions and 31 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
## master

* Add your own contributions to the next release on the line below this with your name.
- [Yoga] Add insertYogaNode:atIndex: method. Improve handling of relayouts. [Scott Goodson](https://github.com/appleguy)
- [ASCollectionNode] Add -isProcessingUpdates and -onDidFinishProcessingUpdates: APIs. [#522](https://github.com/TextureGroup/Texture/pull/522) [Scott Goodson](https://github.com/appleguy)
- [Accessibility] Add .isAccessibilityContainer property, allowing automatic aggregation of children's a11y labels. [#468][Scott Goodson](https://github.com/appleguy)
- [ASImageNode] Enabled .clipsToBounds by default, fixing the use of .cornerRadius and clipping of GIFs. [Scott Goodson](https://github.com/appleguy) [#466](https://github.com/TextureGroup/Texture/pull/466)
Expand Down
5 changes: 4 additions & 1 deletion Source/ASDisplayNode+Beta.h
Original file line number Diff line number Diff line change
Expand Up @@ -175,12 +175,15 @@ extern void ASDisplayNodePerformBlockOnEveryYogaChild(ASDisplayNode * _Nullable

- (void)addYogaChild:(ASDisplayNode *)child;
- (void)removeYogaChild:(ASDisplayNode *)child;
- (void)insertYogaChild:(ASDisplayNode *)child atIndex:(NSUInteger)index;

- (void)semanticContentAttributeDidChange:(UISemanticContentAttribute)attribute;

@property (nonatomic, assign) BOOL yogaLayoutInProgress;
@property (nonatomic, strong, nullable) ASLayout *yogaCalculatedLayout;
// These methods should not normally be called directly.

// These methods are intended to be used internally to Texture, and should not be called directly.
- (BOOL)shouldHaveYogaMeasureFunc;
- (void)invalidateCalculatedYogaLayout;
- (void)calculateLayoutFromYogaRoot:(ASSizeRange)rootConstrainedSize;

Expand Down
87 changes: 68 additions & 19 deletions Source/ASDisplayNode+Yoga.mm
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,23 @@ - (NSArray *)yogaChildren
}

- (void)addYogaChild:(ASDisplayNode *)child
{
[self insertYogaChild:child atIndex:_yogaChildren.count];
}

- (void)removeYogaChild:(ASDisplayNode *)child
{
if (child == nil) {
return;
}

[_yogaChildren removeObjectIdenticalTo:child];

// YGNodeRef removal is done in setParent:
child.yogaParent = nil;
}

- (void)insertYogaChild:(ASDisplayNode *)child atIndex:(NSUInteger)index
{
if (child == nil) {
return;
Expand All @@ -69,24 +86,12 @@ - (void)addYogaChild:(ASDisplayNode *)child
// Clean up state in case this child had another parent.
[self removeYogaChild:child];

[_yogaChildren addObject:child];
[_yogaChildren insertObject:child atIndex:index];

// YGNodeRef insertion is done in setParent:
child.yogaParent = self;
}

- (void)removeYogaChild:(ASDisplayNode *)child
{
if (child == nil) {
return;
}

[_yogaChildren removeObjectIdenticalTo:child];

// YGNodeRef removal is done in setParent:
child.yogaParent = nil;
}

- (void)semanticContentAttributeDidChange:(UISemanticContentAttribute)attribute
{
if (AS_AT_LEAST_IOS9) {
Expand Down Expand Up @@ -168,28 +173,72 @@ - (void)setupYogaCalculatedLayout
CGSize size = CGSizeMake(YGNodeLayoutGetWidth(yogaNode), YGNodeLayoutGetHeight(yogaNode));
ASLayout *layout = [ASLayout layoutWithLayoutElement:self size:size sublayouts:sublayouts];

self.yogaCalculatedLayout = layout;
#if ASDISPLAYNODE_ASSERTIONS_ENABLED
// Assert that the sublayout is already flattened.
for (ASLayout *sublayout in layout.sublayouts) {
if (sublayout.sublayouts.count > 0 || ASDynamicCast(sublayout.layoutElement, ASDisplayNode) == nil) {
ASDisplayNodeAssert(NO, @"Yoga sublayout is not flattened! %@, %@", self, sublayout);
}
}
#endif

// Because this layout won't go through the rest of the logic in calculateLayoutThatFits:, flatten it now.
layout = [layout filteredNodeLayoutTree];

if ([self.yogaCalculatedLayout isEqual:layout] == NO) {
self.yogaCalculatedLayout = layout;
} else {
layout = self.yogaCalculatedLayout;
ASYogaLog("-setupYogaCalculatedLayout: applying identical ASLayout: %@", layout);
}

// Setup _pendingDisplayNodeLayout to reference the Yoga-calculated ASLayout, *unless* we are a leaf node.
// Leaf yoga nodes may have their own .sublayouts, if they use a layout spec (such as ASButtonNode).
// Their _pending variable is set after passing the Yoga checks at the start of -calculateLayoutThatFits:

// For other Yoga nodes, there is no code that will set _pending unless we do it here. Why does it need to be set?
// When CALayer triggers the -[ASDisplayNode __layout] call, we will check if our current _pending layout
// has a size which matches our current bounds size. If it does, that layout will be used without recomputing it.

// NOTE: Yoga does not make the constrainedSize available to intermediate nodes in the tree (e.g. not root or leaves).
// Although the size range provided here is not accurate, this will only affect caching of calls to layoutThatFits:
// These calls will behave as if they are not cached, starting a new Yoga layout pass, but this will tap into Yoga's
// own internal cache.

if ([self shouldHaveYogaMeasureFunc] == NO) {
YGNodeRef parentNode = YGNodeGetParent(yogaNode);
CGSize parentSize = CGSizeZero;
if (parentNode) {
parentSize.width = YGNodeLayoutGetWidth(parentNode);
parentSize.height = YGNodeLayoutGetHeight(parentNode);
}
_pendingDisplayNodeLayout = std::make_shared<ASDisplayNodeLayout>(layout, ASSizeRangeUnconstrained, parentSize, 0);
}
}

- (void)updateYogaMeasureFuncIfNeeded
- (BOOL)shouldHaveYogaMeasureFunc
{
// Size calculation via calculateSizeThatFits: or layoutSpecThatFits:
// This will be used for ASTextNode, as well as any other node that has no Yoga children
BOOL isLeafNode = (self.yogaChildren.count == 0);
BOOL definesCustomLayout = [self implementsLayoutMethod];
return (isLeafNode && definesCustomLayout);
}

- (void)updateYogaMeasureFuncIfNeeded
{
// We set the measure func only during layout. Otherwise, a cycle is created:
// The YGNodeRef Context will retain the ASDisplayNode, which retains the style, which owns the YGNodeRef.
BOOL shouldHaveMeasureFunc = (isLeafNode && definesCustomLayout && checkFlag(YogaLayoutInProgress));
BOOL shouldHaveMeasureFunc = ([self shouldHaveYogaMeasureFunc] && checkFlag(YogaLayoutInProgress));

ASLayoutElementYogaUpdateMeasureFunc(self.style.yogaNode, shouldHaveMeasureFunc ? self : nil);
}

- (void)invalidateCalculatedYogaLayout
{
// Yoga internally asserts that this method may only be called on nodes with a measurement function.
YGNodeRef yogaNode = self.style.yogaNode;
if (yogaNode && YGNodeGetMeasureFunc(yogaNode)) {
// Yoga internally asserts that MarkDirty() may only be called on nodes with a measurement function.
YGNodeMarkDirty(yogaNode);
}
self.yogaCalculatedLayout = nil;
Expand All @@ -200,7 +249,7 @@ - (void)calculateLayoutFromYogaRoot:(ASSizeRange)rootConstrainedSize
ASDisplayNode *yogaParent = self.yogaParent;

if (yogaParent) {
ASYogaLog(@"ESCALATING to Yoga root: %@", self);
ASYogaLog("ESCALATING to Yoga root: %@", self);
// TODO(appleguy): Consider how to get the constrainedSize for the yogaRoot when escalating manually.
[yogaParent calculateLayoutFromYogaRoot:ASSizeRangeUnconstrained];
return;
Expand All @@ -217,7 +266,7 @@ - (void)calculateLayoutFromYogaRoot:(ASSizeRange)rootConstrainedSize
rootConstrainedSize = [self _locked_constrainedSizeForLayoutPass];
}

ASYogaLog(@"CALCULATING at Yoga root with constraint = {%@, %@}: %@",
ASYogaLog("CALCULATING at Yoga root with constraint = {%@, %@}: %@",
NSStringFromCGSize(rootConstrainedSize.min), NSStringFromCGSize(rootConstrainedSize.max), self);

YGNodeRef rootYogaNode = self.style.yogaNode;
Expand Down
27 changes: 17 additions & 10 deletions Source/ASDisplayNode.mm
Original file line number Diff line number Diff line change
Expand Up @@ -967,23 +967,30 @@ - (ASLayout *)calculateLayoutThatFits:(ASSizeRange)constrainedSize
// - 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)) {
if (usesYoga) {
// This node has some connection to a Yoga tree.
ASDN::MutexUnlocker ul(__instanceLock__);

if (self.yogaLayoutInProgress == NO) {
[self calculateLayoutFromYogaRoot:constrainedSize];
if ([self shouldHaveYogaMeasureFunc] == NO) {
// If we're a yoga root, tree node, or leaf with no measure func (e.g. spacer), then
// initiate a new Yoga calculation pass from root.
ASDN::MutexUnlocker ul(__instanceLock__);
as_activity_create_for_scope("Yoga layout calculation");
if (self.yogaLayoutInProgress == NO) {
ASYogaLog("Calculating yoga layout from root %@, %@", self, NSStringFromASSizeRange(constrainedSize));
[self calculateLayoutFromYogaRoot:constrainedSize];
} else {
ASYogaLog("Reusing existing yoga layout %@", _yogaCalculatedLayout);
}
ASDisplayNodeAssert(_yogaCalculatedLayout, @"Yoga node should have a non-nil layout at this stage: %@", self);
return _yogaCalculatedLayout;
} else {
// If we're a yoga leaf node with custom measurement function, proceed with normal layout so layoutSpecs can run (e.g. ASButtonNode).
ASYogaLog("PROCEEDING past Yoga check to calculate ASLayout for: %@", self);
}
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:
Expand Down
21 changes: 21 additions & 0 deletions Source/Layout/ASLayout.mm
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,27 @@ - (ASLayout *)filteredNodeLayoutTree
return layout;
}

#pragma mark - Equality Checking

- (BOOL)isEqual:(id)object
{
ASLayout *layout = ASDynamicCast(object, ASLayout);
if (layout == nil) {
return NO;
}

if (!CGSizeEqualToSize(_size, layout.size)) return NO;
if (!CGPointEqualToPoint(_position, layout.position)) return NO;
if (_layoutElement != layout.layoutElement) return NO;

NSArray *sublayouts = layout.sublayouts;
if (sublayouts != _sublayouts && (sublayouts == nil || _sublayouts == nil || ![_sublayouts isEqual:sublayouts])) {
return NO;
}

return YES;
}

#pragma mark - Accessors

- (ASLayoutElementType)type
Expand Down
4 changes: 3 additions & 1 deletion Source/Layout/ASYogaUtilities.h
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,11 @@
#if YOGA /* YOGA */

#import <AsyncDisplayKit/ASLayout.h>
#import <AsyncDisplayKit/ASLog.h>
#import <AsyncDisplayKit/ASDisplayNode+Beta.h>

#define ASYogaLog(...) //NSLog(__VA_ARGS__)
// Should pass a string literal, not an NSString as the first argument to ASYogaLog
#define ASYogaLog(x, ...) as_log_verbose(ASLayoutLog(), x, ##__VA_ARGS__);

@interface ASDisplayNode (YogaHelpers)

Expand Down

0 comments on commit a026505

Please sign in to comment.