2020-02-03 17:31:09 +04:00

379 lines
11 KiB
Plaintext

//
// ASLayout.mm
// Texture
//
// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved.
// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved.
// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0
//
#import <AsyncDisplayKit/ASLayout.h>
#import <atomic>
#import <queue>
#import <AsyncDisplayKit/ASCollections.h>
#import <AsyncDisplayKit/ASDimension.h>
#import "ASLayoutSpecUtilities.h"
#import "ASLayoutSpec+Subclasses.h"
#import <AsyncDisplayKit/ASEqualityHelpers.h>
#import <AsyncDisplayKit/ASInternalHelpers.h>
#import <AsyncDisplayKit/ASObjectDescriptionHelpers.h>
NSString *const ASThreadDictMaxConstraintSizeKey = @"kASThreadDictMaxConstraintSizeKey";
CGPoint const ASPointNull = {NAN, NAN};
BOOL ASPointIsNull(CGPoint point)
{
return isnan(point.x) && isnan(point.y);
}
/**
* Creates an defined number of " |" indent blocks for the recursive description.
*/
ASDISPLAYNODE_INLINE AS_WARN_UNUSED_RESULT NSString * descriptionIndents(NSUInteger indents)
{
NSMutableString *description = [NSMutableString string];
for (NSUInteger i = 0; i < indents; i++) {
[description appendString:@" |"];
}
if (indents > 0) {
[description appendString:@" "];
}
return description;
}
ASDISPLAYNODE_INLINE AS_WARN_UNUSED_RESULT BOOL ASLayoutIsDisplayNodeType(ASLayout *layout)
{
return layout.type == ASLayoutElementTypeDisplayNode;
}
@interface ASLayout () <ASDescriptionProvider>
{
ASLayoutElementType _layoutElementType;
std::atomic_bool _retainSublayoutElements;
}
@end
@implementation ASLayout
@dynamic frame, type;
static std::atomic_bool static_retainsSublayoutLayoutElements = ATOMIC_VAR_INIT(NO);
+ (void)setShouldRetainSublayoutLayoutElements:(BOOL)shouldRetain
{
static_retainsSublayoutLayoutElements.store(shouldRetain);
}
+ (BOOL)shouldRetainSublayoutLayoutElements
{
return static_retainsSublayoutLayoutElements.load();
}
- (instancetype)initWithLayoutElement:(id<ASLayoutElement>)layoutElement
size:(CGSize)size
position:(CGPoint)position
sublayouts:(nullable NSArray<ASLayout *> *)sublayouts
{
NSParameterAssert(layoutElement);
self = [super init];
if (self) {
#if ASDISPLAYNODE_ASSERTIONS_ENABLED
for (ASLayout *sublayout in sublayouts) {
ASDisplayNodeAssert(ASPointIsNull(sublayout.position) == NO, @"Invalid position is not allowed in sublayout.");
}
#endif
_layoutElement = layoutElement;
// Read this now to avoid @c weak overhead later.
_layoutElementType = layoutElement.layoutElementType;
if (!ASIsCGSizeValidForSize(size)) {
//ASDisplayNodeFailAssert(@"layoutSize is invalid and unsafe to provide to Core Animation! Release configurations will force to 0, 0. Size = %@, node = %@", NSStringFromCGSize(size), layoutElement);
size = CGSizeZero;
} else {
size = CGSizeMake(ASCeilPixelValue(size.width), ASCeilPixelValue(size.height));
}
_size = size;
if (ASPointIsNull(position) == NO) {
_position = ASCeilPointValues(position);
} else {
_position = position;
}
_sublayouts = [sublayouts copy] ?: @[];
if ([ASLayout shouldRetainSublayoutLayoutElements]) {
[self retainSublayoutElements];
}
}
return self;
}
#pragma mark - Class Constructors
+ (instancetype)layoutWithLayoutElement:(id<ASLayoutElement>)layoutElement
size:(CGSize)size
position:(CGPoint)position
sublayouts:(nullable NSArray<ASLayout *> *)sublayouts NS_RETURNS_RETAINED
{
return [[self alloc] initWithLayoutElement:layoutElement
size:size
position:position
sublayouts:sublayouts];
}
+ (instancetype)layoutWithLayoutElement:(id<ASLayoutElement>)layoutElement
size:(CGSize)size
sublayouts:(nullable NSArray<ASLayout *> *)sublayouts NS_RETURNS_RETAINED
{
return [self layoutWithLayoutElement:layoutElement
size:size
position:ASPointNull
sublayouts:sublayouts];
}
+ (instancetype)layoutWithLayoutElement:(id<ASLayoutElement>)layoutElement size:(CGSize)size NS_RETURNS_RETAINED
{
return [self layoutWithLayoutElement:layoutElement
size:size
position:ASPointNull
sublayouts:nil];
}
- (void)dealloc
{
if (_retainSublayoutElements.load()) {
for (ASLayout *sublayout in _sublayouts) {
// We retained this, so there's no risk of it deallocating on us.
if (CFTypeRef cfElement = (__bridge CFTypeRef)sublayout->_layoutElement) {
CFRelease(cfElement);
}
}
}
}
#pragma mark - Sublayout Elements Caching
- (void)retainSublayoutElements
{
if (_retainSublayoutElements.exchange(true)) {
return;
}
for (ASLayout *sublayout in _sublayouts) {
// CFBridgingRetain atomically casts and retains. We need the atomicity.
CFBridgingRetain(sublayout->_layoutElement);
}
}
#pragma mark - Layout Flattening
- (BOOL)isFlattened
{
// A layout is flattened if its position is null, and all of its sublayouts are of type displaynode with no sublayouts.
if (!ASPointIsNull(_position)) {
return NO;
}
for (ASLayout *sublayout in _sublayouts) {
if (ASLayoutIsDisplayNodeType(sublayout) == NO || sublayout->_sublayouts.count > 0) {
return NO;
}
}
return YES;
}
- (ASLayout *)filteredNodeLayoutTree NS_RETURNS_RETAINED
{
if ([self isFlattened]) {
// All flattened layouts must retain sublayout elements until they are applied.
[self retainSublayoutElements];
return self;
}
struct Context {
unowned ASLayout *layout;
CGPoint absolutePosition;
};
// Queue used to keep track of sublayouts while traversing this layout in a DFS fashion.
std::deque<Context> queue;
for (ASLayout *sublayout in _sublayouts) {
queue.push_back({sublayout, sublayout.position});
}
std::vector<ASLayout *> flattenedSublayouts;
while (!queue.empty()) {
const Context context = std::move(queue.front());
queue.pop_front();
unowned ASLayout *layout = context.layout;
// Direct ivar access to avoid retain/release, use existing +1.
const NSUInteger sublayoutsCount = layout->_sublayouts.count;
const CGPoint absolutePosition = context.absolutePosition;
if (ASLayoutIsDisplayNodeType(layout)) {
if (sublayoutsCount > 0 || CGPointEqualToPoint(ASCeilPointValues(absolutePosition), layout.position) == NO) {
// Only create a new layout if the existing one can't be reused, which means it has either some sublayouts or an invalid absolute position.
const auto newLayout = [ASLayout layoutWithLayoutElement:layout->_layoutElement
size:layout.size
position:absolutePosition
sublayouts:@[]];
flattenedSublayouts.push_back(newLayout);
} else {
flattenedSublayouts.push_back(layout);
}
} else if (sublayoutsCount > 0) {
// Fast-reverse-enumerate the sublayouts array by copying it into a C-array and push_front'ing each into the queue.
unowned ASLayout *rawSublayouts[sublayoutsCount];
[layout->_sublayouts getObjects:rawSublayouts range:NSMakeRange(0, sublayoutsCount)];
for (NSInteger i = sublayoutsCount - 1; i >= 0; i--) {
queue.push_front({rawSublayouts[i], absolutePosition + rawSublayouts[i].position});
}
}
}
NSArray *array = [NSArray arrayByTransferring:flattenedSublayouts.data() count:flattenedSublayouts.size()];
// flattenedSublayouts is now all nils.
ASLayout *layout = [ASLayout layoutWithLayoutElement:_layoutElement size:_size sublayouts:array];
// All flattened layouts must retain sublayout elements until they are applied.
[layout retainSublayoutElements];
return layout;
}
#pragma mark - Equality Checking
- (BOOL)isEqual:(id)object
{
if (self == object) return YES;
ASLayout *layout = ASDynamicCast(object, ASLayout);
if (layout == nil) {
return NO;
}
if (!CGSizeEqualToSize(_size, layout.size)) return NO;
if (!((ASPointIsNull(self.position) && ASPointIsNull(layout.position))
|| CGPointEqualToPoint(self.position, layout.position))) return NO;
if (_layoutElement != layout.layoutElement) return NO;
if (!ASObjectIsEqual(_sublayouts, layout.sublayouts)) {
return NO;
}
return YES;
}
#pragma mark - Accessors
- (ASLayoutElementType)type
{
return _layoutElementType;
}
- (CGRect)frameForElement:(id<ASLayoutElement>)layoutElement
{
for (ASLayout *l in _sublayouts) {
if (l->_layoutElement == layoutElement) {
return l.frame;
}
}
return CGRectNull;
}
- (CGRect)frame
{
CGRect subnodeFrame = CGRectZero;
CGPoint adjustedOrigin = _position;
if (isfinite(adjustedOrigin.x) == NO) {
ASDisplayNodeAssert(0, @"Layout has an invalid position");
adjustedOrigin.x = 0;
}
if (isfinite(adjustedOrigin.y) == NO) {
ASDisplayNodeAssert(0, @"Layout has an invalid position");
adjustedOrigin.y = 0;
}
subnodeFrame.origin = adjustedOrigin;
CGSize adjustedSize = _size;
if (isfinite(adjustedSize.width) == NO) {
ASDisplayNodeAssert(0, @"Layout has an invalid size");
adjustedSize.width = 0;
}
if (isfinite(adjustedSize.height) == NO) {
ASDisplayNodeAssert(0, @"Layout has an invalid position");
adjustedSize.height = 0;
}
subnodeFrame.size = adjustedSize;
return subnodeFrame;
}
#pragma mark - Description
- (NSMutableArray <NSDictionary *> *)propertiesForDescription
{
NSMutableArray *result = [NSMutableArray array];
[result addObject:@{ @"size" : [NSValue valueWithCGSize:self.size] }];
if (id<ASLayoutElement> layoutElement = self.layoutElement) {
[result addObject:@{ @"layoutElement" : layoutElement }];
}
const auto pos = self.position;
if (!ASPointIsNull(pos)) {
[result addObject:@{ @"position" : [NSValue valueWithCGPoint:pos] }];
}
return result;
}
- (NSString *)description
{
return ASObjectDescriptionMake(self, [self propertiesForDescription]);
}
- (NSString *)recursiveDescription
{
return [self _recursiveDescriptionForLayout:self level:0];
}
- (NSString *)_recursiveDescriptionForLayout:(ASLayout *)layout level:(NSUInteger)level
{
NSMutableString *description = [NSMutableString string];
[description appendString:descriptionIndents(level)];
[description appendString:[layout description]];
for (ASLayout *sublayout in layout.sublayouts) {
[description appendString:@"\n"];
[description appendString:[self _recursiveDescriptionForLayout:sublayout level:level + 1]];
}
return description;
}
@end
ASLayout *ASCalculateLayout(id<ASLayoutElement> layoutElement, const ASSizeRange sizeRange, const CGSize parentSize)
{
NSCParameterAssert(layoutElement != nil);
return [layoutElement layoutThatFits:sizeRange parentSize:parentSize];
}
ASLayout *ASCalculateRootLayout(id<ASLayoutElement> rootLayoutElement, const ASSizeRange sizeRange)
{
ASLayout *layout = ASCalculateLayout(rootLayoutElement, sizeRange, sizeRange.max);
// Here could specific verfication happen
return layout;
}