mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-08-19 04:00:54 +00:00
265 lines
10 KiB
Plaintext
265 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");
|
|
|
|
// force any nodes that are about to come into view to have display enabled
|
|
if (node.displaySuspended) {
|
|
[node recursivelySetDisplaySuspended:NO];
|
|
}
|
|
|
|
[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
|