Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

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

Merged
merged 4 commits into from
Sep 18, 2017
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)
- [ASCollectionView] Add delegate bridging and index space translation for missing UICollectionViewLayout properties. [Scott Goodson](https://github.com/appleguy)
- [ASTextNode2] Add initial implementation for link handling. [Scott Goodson](https://github.com/appleguy) [#396](https://github.com/TextureGroup/Texture/pull/396)
- [ASTextNode2] Provide compile flag to globally enable new implementation of ASTextNode: ASTEXTNODE_EXPERIMENT_GLOBAL_ENABLE. [Scott Goodson](https://github.com/appleguy) [#396](https://github.com/TextureGroup/Texture/pull/410)
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 @@ -57,6 +57,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 @@ -68,24 +85,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 @@ -167,28 +172,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);
}
}

- (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 @@ -199,7 +248,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 @@ -216,7 +265,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 @@ -985,23 +985,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