Swiftgram/Source/ASScrollNode.mm
Huy Nguyen bccde6cf0f
[ASScrollNode] Fix small bugs and add unit tests (#637)
* Add unit tests for ASScrollNode

* Make sure ASScrollNode's size is clamped against its size range

* Invalidate ASScrollNode's calculated layout if its scrollable directions changed

* Update comment

* Update CHANGELOG

* Address Adlai's comments
2017-12-01 15:41:06 +00:00

173 lines
6.0 KiB
Plaintext

//
// ASScrollNode.mm
// Texture
//
// Copyright (c) 2014-present, Facebook, Inc. All rights reserved.
// This source code is licensed under the BSD-style license found in the
// LICENSE file in the /ASDK-Licenses directory of this source tree. An additional
// grant of patent rights can be found in the PATENTS file in the same directory.
//
// Modifications to this file made after 4/13/2017 are: Copyright (c) 2017-present,
// Pinterest, Inc. Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
#import <AsyncDisplayKit/ASScrollNode.h>
#import <AsyncDisplayKit/ASDisplayNodeExtras.h>
#import <AsyncDisplayKit/ASDisplayNode+FrameworkPrivate.h>
#import <AsyncDisplayKit/ASDisplayNode+FrameworkSubclasses.h>
#import <AsyncDisplayKit/ASLayout.h>
#import <AsyncDisplayKit/_ASDisplayLayer.h>
@interface ASScrollView : UIScrollView
@end
@implementation ASScrollView
// This special +layerClass allows ASScrollNode to get -layout calls from -layoutSublayers.
+ (Class)layerClass
{
return [_ASDisplayLayer class];
}
- (ASScrollNode *)scrollNode
{
return (ASScrollNode *)ASViewToDisplayNode(self);
}
#pragma mark - _ASDisplayView behavior substitutions
// Need these to drive interfaceState so we know when we are visible, if not nested in another range-managing element.
// Because our superclass is a true UIKit class, we cannot also subclass _ASDisplayView.
- (void)willMoveToWindow:(UIWindow *)newWindow
{
ASDisplayNode *node = self.scrollNode; // Create strong reference to weak ivar.
BOOL visible = (newWindow != nil);
if (visible && !node.inHierarchy) {
[node __enterHierarchy];
}
}
- (void)didMoveToWindow
{
ASDisplayNode *node = self.scrollNode; // Create strong reference to weak ivar.
BOOL visible = (self.window != nil);
if (!visible && node.inHierarchy) {
[node __exitHierarchy];
}
}
@end
@implementation ASScrollNode
{
ASScrollDirection _scrollableDirections;
BOOL _automaticallyManagesContentSize;
CGSize _contentCalculatedSizeFromLayout;
}
@dynamic view;
- (instancetype)init
{
if (self = [super init]) {
[self setViewBlock:^UIView *{ return [[ASScrollView alloc] init]; }];
}
return self;
}
- (ASLayout *)calculateLayoutThatFits:(ASSizeRange)constrainedSize
restrictedToSize:(ASLayoutElementSize)size
relativeToParentSize:(CGSize)parentSize
{
ASDN::MutexLocker l(__instanceLock__); // Lock for using our instance variables.
ASSizeRange contentConstrainedSize = constrainedSize;
if (ASScrollDirectionContainsVerticalDirection(_scrollableDirections)) {
contentConstrainedSize.max.height = CGFLOAT_MAX;
}
if (ASScrollDirectionContainsHorizontalDirection(_scrollableDirections)) {
contentConstrainedSize.max.width = CGFLOAT_MAX;
}
ASLayout *layout = [super calculateLayoutThatFits:contentConstrainedSize
restrictedToSize:size
relativeToParentSize:parentSize];
if (_automaticallyManagesContentSize) {
// To understand this code, imagine we're containing a horizontal stack set within a vertical table node.
// Our parentSize is fixed ~375pt width, but 0 - INF height. Our stack measures 1000pt width, 50pt height.
// In this case, we want our scrollNode.bounds to be 375pt wide, and 50pt high. ContentSize 1000pt, 50pt.
// We can achieve this behavior by:
// 1. Always set contentSize to layout.size.
// 2. Set bounds to a size that is calculated by clamping parentSize against constrained size,
// unless one dimension is not defined, in which case adopt the contentSize for that dimension.
_contentCalculatedSizeFromLayout = layout.size;
CGSize selfSize = ASSizeRangeClamp(constrainedSize, parentSize);
if (ASPointsValidForLayout(selfSize.width) == NO) {
selfSize.width = _contentCalculatedSizeFromLayout.width;
}
if (ASPointsValidForLayout(selfSize.height) == NO) {
selfSize.height = _contentCalculatedSizeFromLayout.height;
}
// Don't provide a position, as that should be set by the parent.
layout = [ASLayout layoutWithLayoutElement:self
size:selfSize
sublayouts:layout.sublayouts];
}
return layout;
}
- (void)layout
{
[super layout];
ASDN::MutexLocker l(__instanceLock__); // Lock for using our two instance variables.
if (_automaticallyManagesContentSize) {
CGSize contentSize = _contentCalculatedSizeFromLayout;
if (ASIsCGSizeValidForLayout(contentSize) == NO) {
NSLog(@"%@ calculated a size in its layout spec that can't be applied to .contentSize: %@. Applying parentSize (scrollNode's bounds) instead: %@.", self, NSStringFromCGSize(contentSize), NSStringFromCGSize(self.calculatedSize));
contentSize = self.calculatedSize;
}
self.view.contentSize = contentSize;
}
}
- (BOOL)automaticallyManagesContentSize
{
ASDN::MutexLocker l(__instanceLock__);
return _automaticallyManagesContentSize;
}
- (void)setAutomaticallyManagesContentSize:(BOOL)automaticallyManagesContentSize
{
ASDN::MutexLocker l(__instanceLock__);
_automaticallyManagesContentSize = automaticallyManagesContentSize;
if (_automaticallyManagesContentSize == YES
&& ASScrollDirectionContainsVerticalDirection(_scrollableDirections) == NO
&& ASScrollDirectionContainsHorizontalDirection(_scrollableDirections) == NO) {
// Set the @default value, for more user-friendly behavior of the most
// common use cases of .automaticallyManagesContentSize.
_scrollableDirections = ASScrollDirectionVerticalDirections;
}
}
- (ASScrollDirection)scrollableDirections
{
ASDN::MutexLocker l(__instanceLock__);
return _scrollableDirections;
}
- (void)setScrollableDirections:(ASScrollDirection)scrollableDirections
{
ASDN::MutexLocker l(__instanceLock__);
if (_scrollableDirections != scrollableDirections) {
_scrollableDirections = scrollableDirections;
[self setNeedsLayout];
}
}
@end