mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-08-19 12:10:55 +00:00
This brings back the concept of a window store for nodes that are in the working range (reverting #127). It turns out that due to the system architecture if there are nodes who fetch remote content (e.g. `ASNetworkImageNode`), calls to `-display` will occur before fetching has been completed. The next chance the nodes have to decode and display content is then when they are actually on the screen, thus defeating the purpose of a working range. With the reintroduction of the working range window, nodes are "stored" in the window and when content is finished being fetched, CA triggers `-display` since they are part of a view hierarchy. This can be tested in the Kittens project by insuring that before `ASRangeController` adds a node to [a visible view](https://github.com/facebook/AsyncDisplayKit/blob/master/AsyncDisplayKit/Details/ASRangeController.mm#L57) that the image node (with remote content) has set its layer's contents.
260 lines
10 KiB
Plaintext
260 lines
10 KiB
Plaintext
/* 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 root directory of this source tree. An additional grant
|
|
* of patent rights can be found in the PATENTS file in the same directory.
|
|
*/
|
|
|
|
#import "ASRangeController.h"
|
|
|
|
#import "ASAssert.h"
|
|
#import "ASDisplayNodeExtras.h"
|
|
#import "ASDisplayNodeInternal.h"
|
|
#import "ASMultiDimensionalArrayUtils.h"
|
|
#import "ASRangeHandlerRender.h"
|
|
#import "ASRangeHandlerPreload.h"
|
|
|
|
@interface ASRangeController () {
|
|
BOOL _rangeIsValid;
|
|
|
|
// keys should be ASLayoutRangeTypes and values NSSets containing NSIndexPaths
|
|
NSMutableDictionary *_rangeTypeIndexPaths;
|
|
NSDictionary *_rangeTypeHandlers;
|
|
BOOL _queuedRangeUpdate;
|
|
|
|
ASScrollDirection _scrollDirection;
|
|
}
|
|
|
|
@end
|
|
|
|
@implementation ASRangeController
|
|
|
|
- (instancetype)init {
|
|
if (self = [super init]) {
|
|
|
|
_rangeIsValid = YES;
|
|
_rangeTypeIndexPaths = [[NSMutableDictionary alloc] init];
|
|
|
|
_rangeTypeHandlers = @{
|
|
@(ASLayoutRangeTypeRender): [[ASRangeHandlerRender alloc] init],
|
|
@(ASLayoutRangeTypePreload): [[ASRangeHandlerPreload alloc] init],
|
|
};
|
|
}
|
|
|
|
return self;
|
|
}
|
|
|
|
|
|
#pragma mark - View manipulation
|
|
|
|
- (void)moveNode:(ASCellNode *)node toView:(UIView *)view
|
|
{
|
|
ASDisplayNodeAssertMainThread();
|
|
ASDisplayNodeAssert(node, @"Cannot move a nil node to a view");
|
|
ASDisplayNodeAssert(view, @"Cannot move a node to a non-existent view");
|
|
|
|
[view addSubview:node.view];
|
|
}
|
|
|
|
|
|
#pragma mark - API
|
|
|
|
- (void)visibleNodeIndexPathsDidChangeWithScrollDirection:(ASScrollDirection)scrollDirection
|
|
{
|
|
_scrollDirection = scrollDirection;
|
|
|
|
if (_queuedRangeUpdate) {
|
|
return;
|
|
}
|
|
|
|
// coalesce these events -- handling them multiple times per runloop is noisy and expensive
|
|
_queuedRangeUpdate = YES;
|
|
[self performSelector:@selector(updateVisibleNodeIndexPaths)
|
|
withObject:nil
|
|
afterDelay:0
|
|
inModes:@[ NSRunLoopCommonModes ]];
|
|
}
|
|
|
|
- (void)updateVisibleNodeIndexPaths
|
|
{
|
|
if (!_queuedRangeUpdate) {
|
|
return;
|
|
}
|
|
|
|
NSArray *visibleNodePaths = [_delegate rangeControllerVisibleNodeIndexPaths:self];
|
|
NSSet *visibleNodePathsSet = [NSSet setWithArray:visibleNodePaths];
|
|
CGSize viewportSize = [_delegate rangeControllerViewportSize:self];
|
|
|
|
// the layout controller needs to know what the current visible indices are to calculate range offsets
|
|
[_layoutController setVisibleNodeIndexPaths:visibleNodePaths];
|
|
|
|
for (NSInteger i = 0; i < ASLayoutRangeTypeCount; i++) {
|
|
ASLayoutRangeType rangeType = (ASLayoutRangeType)i;
|
|
id rangeKey = @(rangeType);
|
|
|
|
// this delegate decide what happens when a node is added or removed from a range
|
|
id<ASRangeHandler> rangeDelegate = _rangeTypeHandlers[rangeKey];
|
|
|
|
if ([_layoutController shouldUpdateForVisibleIndexPaths:visibleNodePaths viewportSize:viewportSize rangeType:rangeType]) {
|
|
NSSet *indexPaths = [_layoutController indexPathsForScrolling:_scrollDirection viewportSize:viewportSize rangeType:rangeType];
|
|
|
|
// Notify to remove indexpaths that are leftover that are not visible or included in the _layoutController calculated paths
|
|
NSMutableSet *removedIndexPaths = _rangeIsValid ? [[_rangeTypeIndexPaths objectForKey:rangeKey] mutableCopy] : [NSMutableSet set];
|
|
[removedIndexPaths minusSet:indexPaths];
|
|
[removedIndexPaths minusSet:visibleNodePathsSet];
|
|
if (removedIndexPaths.count) {
|
|
NSArray *removedNodes = [_delegate rangeController:self nodesAtIndexPaths:[removedIndexPaths allObjects]];
|
|
[removedNodes enumerateObjectsUsingBlock:^(ASCellNode *node, NSUInteger idx, BOOL *stop) {
|
|
// since this class usually manages large or infinite data sets, the working range
|
|
// directly bounds memory usage by requiring redrawing any content that falls outside the range.
|
|
[rangeDelegate node:node exitedRangeOfType:rangeType];
|
|
}];
|
|
}
|
|
|
|
// Notify to add indexpaths that are not currently in _rangeTypeIndexPaths
|
|
NSMutableSet *addedIndexPaths = [indexPaths mutableCopy];
|
|
[addedIndexPaths minusSet:[_rangeTypeIndexPaths objectForKey:rangeKey]];
|
|
|
|
// The preload range (for example) should include nodes that are visible
|
|
// TODO: remove this once we have removed the dependency on Core Animation's -display
|
|
if ([self shouldSkipVisibleNodesForRangeType:rangeType]) {
|
|
[addedIndexPaths minusSet:visibleNodePathsSet];
|
|
}
|
|
|
|
if (addedIndexPaths.count) {
|
|
NSArray *addedNodes = [_delegate rangeController:self nodesAtIndexPaths:[addedIndexPaths allObjects]];
|
|
[addedNodes enumerateObjectsUsingBlock:^(ASCellNode *node, NSUInteger idx, BOOL *stop) {
|
|
[rangeDelegate node:node enteredRangeOfType:rangeType];
|
|
}];
|
|
}
|
|
|
|
// set the range indexpaths so that we can remove/add on the next update pass
|
|
[_rangeTypeIndexPaths setObject:indexPaths forKey:rangeKey];
|
|
}
|
|
}
|
|
|
|
_rangeIsValid = YES;
|
|
_queuedRangeUpdate = NO;
|
|
}
|
|
|
|
- (BOOL)shouldSkipVisibleNodesForRangeType:(ASLayoutRangeType)rangeType
|
|
{
|
|
return rangeType == ASLayoutRangeTypeRender;
|
|
}
|
|
|
|
- (void)configureContentView:(UIView *)contentView forCellNode:(ASCellNode *)cellNode
|
|
{
|
|
if (cellNode.view.superview == contentView) {
|
|
// this content view is already correctly configured
|
|
return;
|
|
}
|
|
|
|
// clean the content view
|
|
for (UIView *view in contentView.subviews) {
|
|
[view removeFromSuperview];
|
|
}
|
|
|
|
[self moveNode:cellNode toView:contentView];
|
|
}
|
|
|
|
|
|
#pragma mark - ASDataControllerDelegete
|
|
|
|
- (void)dataControllerBeginUpdates:(ASDataController *)dataController {
|
|
ASDisplayNodePerformBlockOnMainThread(^{
|
|
[_delegate rangeControllerBeginUpdates:self];
|
|
});
|
|
}
|
|
|
|
- (void)dataControllerEndUpdates:(ASDataController *)dataController completion:(void (^)(BOOL))completion {
|
|
ASDisplayNodePerformBlockOnMainThread(^{
|
|
[_delegate rangeControllerEndUpdates:self completion:completion];
|
|
});
|
|
}
|
|
|
|
- (void)dataController:(ASDataController *)dataController willInsertNodes:(NSArray *)nodes atIndexPaths:(NSArray *)indexPaths withAnimationOption:(ASDataControllerAnimationOptions)animationOption {
|
|
ASDisplayNodePerformBlockOnMainThread(^{
|
|
if ([_delegate respondsToSelector:@selector(rangeController:willInsertNodesAtIndexPaths:withAnimationOption:)]) {
|
|
[_delegate rangeController:self willInsertNodesAtIndexPaths:indexPaths withAnimationOption:animationOption];
|
|
}
|
|
});
|
|
}
|
|
|
|
- (void)dataController:(ASDataController *)dataController didInsertNodes:(NSArray *)nodes atIndexPaths:(NSArray *)indexPaths withAnimationOption:(ASDataControllerAnimationOptions)animationOption {
|
|
ASDisplayNodeAssert(nodes.count == indexPaths.count, @"Invalid index path");
|
|
|
|
NSMutableArray *nodeSizes = [NSMutableArray arrayWithCapacity:nodes.count];
|
|
[nodes enumerateObjectsUsingBlock:^(ASCellNode *node, NSUInteger idx, BOOL *stop) {
|
|
[nodeSizes addObject:[NSValue valueWithCGSize:node.calculatedSize]];
|
|
}];
|
|
|
|
ASDisplayNodePerformBlockOnMainThread(^{
|
|
[_layoutController insertNodesAtIndexPaths:indexPaths withSizes:nodeSizes];
|
|
[_delegate rangeController:self didInsertNodesAtIndexPaths:indexPaths withAnimationOption:animationOption];
|
|
_rangeIsValid = NO;
|
|
});
|
|
}
|
|
|
|
- (void)dataController:(ASDataController *)dataController willDeleteNodesAtIndexPaths:(NSArray *)indexPaths withAnimationOption:(ASDataControllerAnimationOptions)animationOption {
|
|
ASDisplayNodePerformBlockOnMainThread(^{
|
|
if ([_delegate respondsToSelector:@selector(rangeController:willDeleteNodesAtIndexPaths:withAnimationOption:)]) {
|
|
[_delegate rangeController:self willDeleteNodesAtIndexPaths:indexPaths withAnimationOption:animationOption];
|
|
}
|
|
});
|
|
}
|
|
|
|
- (void)dataController:(ASDataController *)dataController didDeleteNodesAtIndexPaths:(NSArray *)indexPaths withAnimationOption:(ASDataControllerAnimationOptions)animationOption {
|
|
ASDisplayNodePerformBlockOnMainThread(^{
|
|
[_layoutController deleteNodesAtIndexPaths:indexPaths];
|
|
[_delegate rangeController:self didDeleteNodesAtIndexPaths:indexPaths withAnimationOption:animationOption];
|
|
_rangeIsValid = NO;
|
|
});
|
|
}
|
|
|
|
- (void)dataController:(ASDataController *)dataController willInsertSections:(NSArray *)sections atIndexSet:(NSIndexSet *)indexSet withAnimationOption:(ASDataControllerAnimationOptions)animationOption {
|
|
ASDisplayNodePerformBlockOnMainThread(^{
|
|
if ([_delegate respondsToSelector:@selector(rangeController:willInsertSectionsAtIndexSet:withAnimationOption:)]) {
|
|
[_delegate rangeController:self willInsertSectionsAtIndexSet:indexSet withAnimationOption:animationOption];
|
|
}
|
|
});
|
|
}
|
|
|
|
- (void)dataController:(ASDataController *)dataController didInsertSections:(NSArray *)sections atIndexSet:(NSIndexSet *)indexSet withAnimationOption:(ASDataControllerAnimationOptions)animationOption {
|
|
ASDisplayNodeAssert(sections.count == indexSet.count, @"Invalid sections");
|
|
|
|
NSMutableArray *sectionNodeSizes = [NSMutableArray arrayWithCapacity:sections.count];
|
|
|
|
[sections enumerateObjectsUsingBlock:^(NSArray *nodes, NSUInteger idx, BOOL *stop) {
|
|
NSMutableArray *nodeSizes = [NSMutableArray arrayWithCapacity:nodes.count];
|
|
[nodes enumerateObjectsUsingBlock:^(ASCellNode *node, NSUInteger idx2, BOOL *stop2) {
|
|
[nodeSizes addObject:[NSValue valueWithCGSize:node.calculatedSize]];
|
|
}];
|
|
[sectionNodeSizes addObject:nodeSizes];
|
|
}];
|
|
|
|
ASDisplayNodePerformBlockOnMainThread(^{
|
|
[_layoutController insertSections:sectionNodeSizes atIndexSet:indexSet];
|
|
[_delegate rangeController:self didInsertSectionsAtIndexSet:indexSet withAnimationOption:animationOption];
|
|
_rangeIsValid = NO;
|
|
});
|
|
}
|
|
|
|
- (void)dataController:(ASDataController *)dataController willDeleteSectionsAtIndexSet:(NSIndexSet *)indexSet withAnimationOption:(ASDataControllerAnimationOptions)animationOption {
|
|
ASDisplayNodePerformBlockOnMainThread(^{
|
|
if ([_delegate respondsToSelector:@selector(rangeController:willDeleteSectionsAtIndexSet:withAnimationOption:)]) {
|
|
[_delegate rangeController:self willDeleteSectionsAtIndexSet:indexSet withAnimationOption:animationOption];
|
|
}
|
|
});
|
|
}
|
|
|
|
- (void)dataController:(ASDataController *)dataController didDeleteSectionsAtIndexSet:(NSIndexSet *)indexSet withAnimationOption:(ASDataControllerAnimationOptions)animationOption {
|
|
ASDisplayNodePerformBlockOnMainThread(^{
|
|
[_layoutController deleteSectionsAtIndexSet:indexSet];
|
|
[_delegate rangeController:self didDeleteSectionsAtIndexSet:indexSet withAnimationOption:animationOption];
|
|
_rangeIsValid = NO;
|
|
});
|
|
}
|
|
|
|
@end
|