Merge pull request #1156 from levi/implicitNodeMembership

[ASDisplayNode] Implicit node hierarchy handling with ASLayouts
This commit is contained in:
appleguy
2016-02-01 18:33:32 -08:00
9 changed files with 481 additions and 30 deletions

View File

@@ -11,6 +11,9 @@
+ (BOOL)shouldUseNewRenderingRange;
+ (void)setShouldUseNewRenderingRange:(BOOL)shouldUseNewRenderingRange;
+ (BOOL)usesImplicitHierarchyManagement;
+ (void)setUsesImplicitHierarchyManagement:(BOOL)enabled;
/** @name Layout */

View File

@@ -21,6 +21,7 @@
#import "_ASCoreAnimationExtras.h"
#import "ASDisplayNodeExtras.h"
#import "ASEqualityHelpers.h"
#import "NSArray+Diffing.h"
#import "ASInternalHelpers.h"
#import "ASLayout.h"
@@ -29,6 +30,36 @@
NSInteger const ASDefaultDrawingPriority = ASDefaultTransactionPriority;
@interface _ASDisplayNodePosition : NSObject
@property (nonatomic, assign) NSUInteger index;
@property (nonatomic, strong) ASDisplayNode *node;
+ (instancetype)positionWithNode:(ASDisplayNode *)node atIndex:(NSUInteger)index;
- (instancetype)initWithNode:(ASDisplayNode *)node atIndex:(NSUInteger)index;
@end
@implementation _ASDisplayNodePosition
+ (instancetype)positionWithNode:(ASDisplayNode *)node atIndex:(NSUInteger)index
{
return [[self alloc] initWithNode:node atIndex:index];
}
- (instancetype)initWithNode:(ASDisplayNode *)node atIndex:(NSUInteger)index
{
self = [super init];
if (self) {
_node = node;
_index = index;
}
return self;
}
@end
@interface ASDisplayNode () <UIGestureRecognizerDelegate>
/**
@@ -52,6 +83,9 @@ NSInteger const ASDefaultDrawingPriority = ASDefaultTransactionPriority;
#endif
@interface ASDisplayNode () <_ASDisplayLayerDelegate>
@property (assign, nonatomic) BOOL implicitNodeHierarchyManagement;
@end
@implementation ASDisplayNode
@@ -62,9 +96,21 @@ NSInteger const ASDefaultDrawingPriority = ASDefaultTransactionPriority;
@synthesize preferredFrameSize = _preferredFrameSize;
@synthesize isFinalLayoutable = _isFinalLayoutable;
static BOOL usesImplicitHierarchyManagement = FALSE;
+ (BOOL)usesImplicitHierarchyManagement
{
return usesImplicitHierarchyManagement;
}
+ (void)setUsesImplicitHierarchyManagement:(BOOL)enabled
{
usesImplicitHierarchyManagement = enabled;
}
BOOL ASDisplayNodeSubclassOverridesSelector(Class subclass, SEL selector)
{
return ASSubclassOverridesSelector([ASDisplayNode class], subclass, selector);
return ASSubclassOverridesSelector([ASDisplayNode class], subclass, selector);
}
void ASDisplayNodeRespectThreadAffinityOfNode(ASDisplayNode *node, void (^block)())
@@ -582,14 +628,9 @@ static ASDisplayNodeMethodOverrides GetASDisplayNodeMethodOverrides(Class c)
}
- (ASLayout *)measureWithSizeRange:(ASSizeRange)constrainedSize
{
ASDN::MutexLocker l(_propertyLock);
return [self __measureWithSizeRange:constrainedSize];
}
- (ASLayout *)__measureWithSizeRange:(ASSizeRange)constrainedSize
{
ASDisplayNodeAssertThreadAffinity(self);
ASDN::MutexLocker l(_propertyLock);
if (![self __shouldSize])
return nil;
@@ -598,7 +639,22 @@ static ASDisplayNodeMethodOverrides GetASDisplayNodeMethodOverrides(Class c)
// - we haven't already
// - the constrained size range is different
if (!_flags.isMeasured || !ASSizeRangeEqualToSizeRange(constrainedSize, _constrainedSize)) {
_layout = [self calculateLayoutThatFits:constrainedSize];
ASLayout *newLayout = [self calculateLayoutThatFits:constrainedSize];
if (_layout) {
NSIndexSet *insertions, *deletions;
[_layout.sublayouts asdk_diffWithArray:newLayout.sublayouts insertions:&insertions deletions:&deletions compareBlock:^BOOL(ASLayout *lhs, ASLayout *rhs) {
return ASObjectIsEqual(lhs.layoutableObject, rhs.layoutableObject);
}];
_insertedSubnodes = [self _filterNodesInLayouts:newLayout.sublayouts withIndexes:insertions];
_deletedSubnodes = [self _filterNodesInLayouts:_layout.sublayouts withIndexes:deletions];
} else {
NSIndexSet *indexes = [NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, [newLayout.sublayouts count])];
_insertedSubnodes = [self _filterNodesInLayouts:newLayout.sublayouts withIndexes:indexes];
_deletedSubnodes = @[];
}
_layout = newLayout;
_constrainedSize = constrainedSize;
_flags.isMeasured = YES;
[self calculatedLayoutDidChange];
@@ -608,23 +664,36 @@ static ASDisplayNodeMethodOverrides GetASDisplayNodeMethodOverrides(Class c)
ASDisplayNodeAssertTrue(_layout.size.width >= 0.0);
ASDisplayNodeAssertTrue(_layout.size.height >= 0.0);
// we generate placeholders at measureWithSizeRange: time so that a node is guaranteed to have a placeholder ready to go
// also if a node has no size, it should not have a placeholder
if (self.placeholderEnabled && [self _displaysAsynchronously] && _layout.size.width > 0.0 && _layout.size.height > 0.0) {
// we generate placeholders at measureWithSizeRange: time so that a node is guaranteed
// to have a placeholder ready to go. Also, if a node has no size it should not have a placeholder
if (self.placeholderEnabled && [self _displaysAsynchronously] &&
_layout.size.width > 0.0 && _layout.size.height > 0.0) {
if (!_placeholderImage) {
_placeholderImage = [self placeholderImage];
}
if (_placeholderLayer) {
[self setupPlaceholderLayerContents];
[self _setupPlaceholderLayerContents];
}
}
return _layout;
}
- (NSArray<_ASDisplayNodePosition *> *)_filterNodesInLayouts:(NSArray<ASLayout *> *)layouts withIndexes:(NSIndexSet *)indexes
{
NSMutableArray<_ASDisplayNodePosition *> *result = [NSMutableArray array];
[indexes enumerateIndexesUsingBlock:^(NSUInteger idx, BOOL * _Nonnull stop) {
ASDisplayNode *node = (ASDisplayNode *)layouts[idx].layoutableObject;
ASDisplayNodeAssertNotNil(node, @"A flattened layout must consist exclusively of node sublayouts");
[result addObject:[_ASDisplayNodePosition positionWithNode:node atIndex:idx]];
}];
return result;
}
- (void)calculatedLayoutDidChange
{
// subclass override
}
- (BOOL)displaysAsynchronously
@@ -805,7 +874,10 @@ static ASDisplayNodeMethodOverrides GetASDisplayNodeMethodOverrides(Class c)
ASDisplayNodeAssertMainThread();
ASDN::MutexLocker l(_propertyLock);
if (CGRectEqualToRect(self.bounds, CGRectZero)) {
return; // Performing layout on a zero-bounds view often results in frame calculations with negative sizes after applying margins, which will cause measureWithSizeRange: on subnodes to assert.
// Performing layout on a zero-bounds view often results in frame calculations
// with negative sizes after applying margins, which will cause
// measureWithSizeRange: on subnodes to assert.
return;
}
_placeholderLayer.frame = self.bounds;
[self layout];
@@ -1606,7 +1678,11 @@ static BOOL ShouldUseNewRenderingRange = YES;
layout = [ASLayout layoutWithLayoutableObject:self size:layout.size sublayouts:@[layout]];
}
return [layout flattenedLayoutUsingPredicateBlock:^BOOL(ASLayout *evaluatedLayout) {
return [_subnodes containsObject:evaluatedLayout.layoutableObject];
if ([[self class] usesImplicitHierarchyManagement]) {
return ASObjectIsEqual(layout, evaluatedLayout) == NO && [evaluatedLayout.layoutableObject isKindOfClass:[ASDisplayNode class]];
} else {
return [_subnodes containsObject:evaluatedLayout.layoutableObject];
}
}];
} else {
// If neither -layoutSpecThatFits: nor -calculateSizeThatFits: is overridden by subclassses, preferredFrameSize should be used,
@@ -1974,7 +2050,9 @@ static BOOL ShouldUseNewRenderingRange = YES;
ASDisplayNode *subnode = nil;
CGRect subnodeFrame = CGRectZero;
for (ASLayout *subnodeLayout in _layout.sublayouts) {
ASDisplayNodeAssert([_subnodes containsObject:subnodeLayout.layoutableObject], @"Cached sublayouts must only contain subnodes' layout. self = %@, subnodes = %@", self, _subnodes);
if (![[self class] usesImplicitHierarchyManagement]) {
ASDisplayNodeAssert([_subnodes containsObject:subnodeLayout.layoutableObject], @"Sublayouts must only contain subnodes' layout. self = %@, subnodes = %@", self, _subnodes);
}
CGPoint adjustedOrigin = subnodeLayout.position;
if (isfinite(adjustedOrigin.x) == NO) {
ASDisplayNodeAssert(0, @"subnodeLayout has an invalid position");
@@ -2000,6 +2078,45 @@ static BOOL ShouldUseNewRenderingRange = YES;
subnode = ((ASDisplayNode *)subnodeLayout.layoutableObject);
[subnode setFrame:subnodeFrame];
}
if ([[self class] usesImplicitHierarchyManagement]) {
for (_ASDisplayNodePosition *position in _deletedSubnodes) {
[self _implicitlyRemoveSubnode:position.node atIndex:position.index];
}
for (_ASDisplayNodePosition *position in _insertedSubnodes) {
[self _implicitlyInsertSubnode:position.node atIndex:position.index];
}
}
}
- (void)_implicitlyInsertSubnode:(ASDisplayNode *)node atIndex:(NSUInteger)idx
{
ASDisplayNodeAssertThreadAffinity(self);
if (!_managedSubnodes) {
_managedSubnodes = [NSMutableArray array];
}
ASDisplayNodeAssert(idx <= [_managedSubnodes count], @"index needs to be in range of the current managed subnodes");
if (idx == [_managedSubnodes count]) {
[_managedSubnodes addObject:node];
} else {
[_managedSubnodes insertObject:node atIndex:idx];
}
[self addSubnode:node];
}
- (void)_implicitlyRemoveSubnode:(ASDisplayNode *)node atIndex:(NSUInteger)idx
{
ASDisplayNodeAssertThreadAffinity(self);
if (!_managedSubnodes) {
_managedSubnodes = [NSMutableArray array];
}
[_managedSubnodes removeObjectAtIndex:idx];
[node removeFromSupernode];
}
- (void)displayWillStart
@@ -2012,14 +2129,14 @@ static BOOL ShouldUseNewRenderingRange = YES;
if (_placeholderImage && _placeholderLayer && self.layer.contents == nil) {
[CATransaction begin];
[CATransaction setDisableActions:YES];
[self setupPlaceholderLayerContents];
[self _setupPlaceholderLayerContents];
_placeholderLayer.opacity = 1.0;
[CATransaction commit];
[self.layer addSublayer:_placeholderLayer];
}
}
- (void)setupPlaceholderLayerContents
- (void)_setupPlaceholderLayerContents
{
BOOL stretchable = !UIEdgeInsetsEqualToEdgeInsets(_placeholderImage.capInsets, UIEdgeInsetsZero);
if (stretchable) {

View File

@@ -12,7 +12,7 @@
#import "ASAssert.h"
#import "ASLayoutSpecUtilities.h"
#import "ASInternalHelpers.h"
#import <stack>
#import <queue>
CGPoint const CGPointNull = {NAN, NAN};
@@ -71,14 +71,14 @@ extern BOOL CGPointIsNull(CGPoint point)
BOOL visited;
};
// Stack of Contexts, used to keep track of sublayouts while traversing this layout in a DFS fashion.
std::stack<Context> stack;
stack.push({self, CGPointMake(0, 0), NO});
// Stack of Contexts, used to keep track of sublayouts while traversing this layout in a BFS fashion.
std::queue<Context> queue;
queue.push({self, CGPointMake(0, 0), NO});
while (!stack.empty()) {
Context &context = stack.top();
while (!queue.empty()) {
Context &context = queue.front();
if (context.visited) {
stack.pop();
queue.pop();
} else {
context.visited = YES;
@@ -90,11 +90,11 @@ extern BOOL CGPointIsNull(CGPoint point)
}
for (ASLayout *sublayout in context.layout.sublayouts) {
stack.push({sublayout, context.absolutePosition + sublayout.position, NO});
queue.push({sublayout, context.absolutePosition + sublayout.position, NO});
}
}
}
return [ASLayout layoutWithLayoutableObject:_layoutableObject size:_size sublayouts:flattenedSublayouts];
}

View File

@@ -35,6 +35,7 @@ typedef NS_OPTIONS(NSUInteger, ASDisplayNodeMethodOverrides)
};
@class _ASPendingState;
@class _ASDisplayNodePosition;
// Allow 2^n increments of begin disabling hierarchy notifications
#define VISIBILITY_NOTIFICATIONS_DISABLED_BITS 4
@@ -60,6 +61,12 @@ typedef NS_OPTIONS(NSUInteger, ASDisplayNodeMethodOverrides)
UIEdgeInsets _hitTestSlop;
NSMutableArray *_subnodes;
// Subnodes implicitly managed by layout changes
NSMutableArray<ASDisplayNode *> *_managedSubnodes;
NSArray<_ASDisplayNodePosition *> *_insertedSubnodes;
NSArray<_ASDisplayNodePosition *> *_deletedSubnodes;
ASDisplayNodeViewBlock _viewBlock;
ASDisplayNodeLayerBlock _layerBlock;
ASDisplayNodeDidLoadBlock _nodeLoadedBlock;
@@ -131,10 +138,11 @@ typedef NS_OPTIONS(NSUInteger, ASDisplayNodeMethodOverrides)
- (BOOL)__shouldLoadViewOrLayer;
- (BOOL)__shouldSize;
// Core implementation of -measureWithSizeRange:. Must be called with _propertyLock held.
- (ASLayout *)__measureWithSizeRange:(ASSizeRange)constrainedSize;
/**
Invoked by a call to setNeedsLayout to the underlying view
*/
- (void)__setNeedsLayout;
- (void)__layout;
- (void)__setSupernode:(ASDisplayNode *)supernode;

View File

@@ -0,0 +1,29 @@
//
// NSArray+Diffing.h
// AsyncDisplayKit
//
// Created by Levi McCallum on 1/29/16.
// Copyright © 2016 Facebook. All rights reserved.
//
#import <Foundation/Foundation.h>
@interface NSArray (Diffing)
/**
* @abstract Compares two arrays, providing the insertion and deletion indexes needed to transform into the target array.
* @discussion This compares the equality of each object with `isEqual:`.
* This diffing algorithm uses a bottom-up memoized longest common subsequence solution to identify differences.
* It runs in O(mn) complexity.
*/
- (void)asdk_diffWithArray:(NSArray *)array insertions:(NSIndexSet **)insertions deletions:(NSIndexSet **)deletions;
/**
* @abstract Compares two arrays, providing the insertion and deletion indexes needed to transform into the target array.
* @discussion The `compareBlock` is used to identify the equality of the objects within the arrays.
* This diffing algorithm uses a bottom-up memoized longest common subsequence solution to identify differences.
* It runs in O(mn) complexity.
*/
- (void)asdk_diffWithArray:(NSArray *)array insertions:(NSIndexSet **)insertions deletions:(NSIndexSet **)deletions compareBlock:(BOOL (^)(id lhs, id rhs))comparison;
@end

View File

@@ -0,0 +1,78 @@
//
// NSArray+Diffing.m
// AsyncDisplayKit
//
// Created by Levi McCallum on 1/29/16.
// Copyright © 2016 Facebook. All rights reserved.
//
#import "NSArray+Diffing.h"
@implementation NSArray (Diffing)
- (void)asdk_diffWithArray:(NSArray *)array insertions:(NSIndexSet **)insertions deletions:(NSIndexSet **)deletions
{
[self asdk_diffWithArray:array insertions:insertions deletions:deletions compareBlock:^BOOL(id lhs, id rhs) {
return [lhs isEqual:rhs];
}];
}
- (void)asdk_diffWithArray:(NSArray *)array insertions:(NSIndexSet **)insertions deletions:(NSIndexSet **)deletions compareBlock:(BOOL (^)(id lhs, id rhs))comparison
{
NSIndexSet *commonIndexes = [self _asdk_commonIndexesWithArray:array compareBlock:comparison];
if (insertions) {
NSArray *commonObjects = [self objectsAtIndexes:commonIndexes];
NSMutableIndexSet *insertionIndexes = [NSMutableIndexSet indexSet];
for (NSInteger i = 0, j = 0; i < commonObjects.count || j < array.count;) {
if (i < commonObjects.count && j < array.count && comparison(commonObjects[i], array[j])) {
i++; j++;
} else {
[insertionIndexes addIndex:j];
j++;
}
}
*insertions = insertionIndexes;
}
if (deletions) {
NSMutableIndexSet *deletionIndexes = [NSMutableIndexSet indexSet];
for (NSInteger i = 0; i < self.count; i++) {
if (![commonIndexes containsIndex:i]) {
[deletionIndexes addIndex:i];
}
}
*deletions = deletionIndexes;
}
}
- (NSIndexSet *)_asdk_commonIndexesWithArray:(NSArray *)array compareBlock:(BOOL (^)(id lhs, id rhs))comparison
{
NSInteger lengths[self.count+1][array.count+1];
for (NSInteger i = self.count; i >= 0; i--) {
for (NSInteger j = array.count; j >= 0; j--) {
if (i == self.count || j == array.count) {
lengths[i][j] = 0;
} else if ([self[i] isEqual:array[j]]) {
lengths[i][j] = 1 + lengths[i+1][j+1];
} else {
lengths[i][j] = MAX(lengths[i+1][j], lengths[i][j+1]);
}
}
}
NSMutableIndexSet *common = [NSMutableIndexSet indexSet];
for (NSInteger i = 0, j = 0; i < self.count && j < array.count;) {
if (comparison(self[i], array[j])) {
[common addIndex:i];
i++; j++;
} else if (lengths[i+1][j] >= lengths[i][j+1]) {
i++;
} else {
j++;
}
}
return common;
}
@end