diff --git a/CHANGELOG.md b/CHANGELOG.md index 648ec0b0bc..c1a53764fe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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) diff --git a/Source/ASDisplayNode+Beta.h b/Source/ASDisplayNode+Beta.h index e8b08a9a57..ea241bd164 100644 --- a/Source/ASDisplayNode+Beta.h +++ b/Source/ASDisplayNode+Beta.h @@ -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; diff --git a/Source/ASDisplayNode+Yoga.mm b/Source/ASDisplayNode+Yoga.mm index 281bf1e781..6ef80ccf14 100644 --- a/Source/ASDisplayNode+Yoga.mm +++ b/Source/ASDisplayNode+Yoga.mm @@ -59,20 +59,7 @@ - (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]; - - [_yogaChildren addObject:child]; - - // YGNodeRef insertion is done in setParent: - child.yogaParent = self; + [self insertYogaChild:child atIndex:_yogaChildren.count]; } - (void)removeYogaChild:(ASDisplayNode *)child @@ -87,6 +74,24 @@ child.yogaParent = nil; } +- (void)insertYogaChild:(ASDisplayNode *)child atIndex:(NSUInteger)index +{ + if (child == nil) { + return; + } + if (_yogaChildren == nil) { + _yogaChildren = [NSMutableArray array]; + } + + // Clean up state in case this child had another parent. + [self removeYogaChild:child]; + + [_yogaChildren insertObject:child atIndex:index]; + + // YGNodeRef insertion is done in setParent: + child.yogaParent = self; +} + - (void)semanticContentAttributeDidChange:(UISemanticContentAttribute)attribute { if (AS_AT_LEAST_IOS9) { @@ -168,28 +173,72 @@ 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(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; @@ -200,7 +249,7 @@ 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; @@ -217,7 +266,7 @@ 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; diff --git a/Source/ASDisplayNode.mm b/Source/ASDisplayNode.mm index e1f1d143ce..2b2041e2a4 100644 --- a/Source/ASDisplayNode.mm +++ b/Source/ASDisplayNode.mm @@ -967,23 +967,30 @@ static ASDisplayNodeMethodOverrides GetASDisplayNodeMethodOverrides(Class c) // - 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: diff --git a/Source/Layout/ASLayout.mm b/Source/Layout/ASLayout.mm index 031120100c..4e8d2f5026 100644 --- a/Source/Layout/ASLayout.mm +++ b/Source/Layout/ASLayout.mm @@ -271,6 +271,27 @@ static std::atomic_bool static_retainsSublayoutLayoutElements = ATOMIC_VAR_INIT( 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 diff --git a/Source/Layout/ASYogaUtilities.h b/Source/Layout/ASYogaUtilities.h index 2986c0195f..b229b34358 100644 --- a/Source/Layout/ASYogaUtilities.h +++ b/Source/Layout/ASYogaUtilities.h @@ -15,9 +15,11 @@ #if YOGA /* YOGA */ #import +#import #import -#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)