forked from TextureGroup/Texture
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathASCollectionLayout.mm
More file actions
342 lines (295 loc) · 13.9 KB
/
Copy pathASCollectionLayout.mm
File metadata and controls
342 lines (295 loc) · 13.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
//
// ASCollectionLayout.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/ASCollectionLayout.h>
#import <AsyncDisplayKit/ASAssert.h>
#import <AsyncDisplayKit/ASAbstractLayoutController.h>
#import <AsyncDisplayKit/ASCellNode.h>
#import <AsyncDisplayKit/ASCollectionElement.h>
#import <AsyncDisplayKit/ASCollectionLayoutCache.h>
#import <AsyncDisplayKit/ASCollectionLayoutContext+Private.h>
#import <AsyncDisplayKit/ASCollectionLayoutDelegate.h>
#import <AsyncDisplayKit/ASCollectionLayoutState+Private.h>
#import <AsyncDisplayKit/ASCollectionNode+Beta.h>
#import <AsyncDisplayKit/ASDispatch.h>
#import <AsyncDisplayKit/ASDisplayNode+FrameworkPrivate.h>
#import <AsyncDisplayKit/ASElementMap.h>
#import <AsyncDisplayKit/ASEqualityHelpers.h>
#import <AsyncDisplayKit/ASPageTable.h>
static const ASRangeTuningParameters kASDefaultMeasureRangeTuningParameters = {
.leadingBufferScreenfuls = 2.0,
.trailingBufferScreenfuls = 2.0
};
static const ASScrollDirection kASStaticScrollDirection = (ASScrollDirectionRight | ASScrollDirectionDown);
@interface ASCollectionLayout () <ASDataControllerLayoutDelegate> {
ASCollectionLayoutCache *_layoutCache;
ASCollectionLayoutState *_layout; // Main thread only.
struct {
unsigned int implementsAdditionalInfoForLayoutWithElements:1;
} _layoutDelegateFlags;
}
@end
@implementation ASCollectionLayout
- (instancetype)initWithLayoutDelegate:(id<ASCollectionLayoutDelegate>)layoutDelegate
{
self = [super init];
if (self) {
ASDisplayNodeAssertNotNil(layoutDelegate, @"Collection layout delegate cannot be nil");
_layoutDelegate = layoutDelegate;
_layoutDelegateFlags.implementsAdditionalInfoForLayoutWithElements = [layoutDelegate respondsToSelector:@selector(additionalInfoForLayoutWithElements:)];
_layoutCache = [[ASCollectionLayoutCache alloc] init];
}
return self;
}
#pragma mark - ASDataControllerLayoutDelegate
- (ASCollectionLayoutContext *)layoutContextWithElements:(ASElementMap *)elements
{
ASDisplayNodeAssertMainThread();
CGSize viewportSize = [self _viewportSize];
CGPoint contentOffset = _collectionNode.contentOffset;
id additionalInfo = nil;
if (_layoutDelegateFlags.implementsAdditionalInfoForLayoutWithElements) {
additionalInfo = [_layoutDelegate additionalInfoForLayoutWithElements:elements];
}
return [[ASCollectionLayoutContext alloc] initWithViewportSize:viewportSize
initialContentOffset:contentOffset
scrollableDirections:[_layoutDelegate scrollableDirections]
elements:elements
layoutDelegateClass:[_layoutDelegate class]
layoutCache:_layoutCache
additionalInfo:additionalInfo];
}
+ (ASCollectionLayoutState *)calculateLayoutWithContext:(ASCollectionLayoutContext *)context
{
if (context.elements == nil) {
return [[ASCollectionLayoutState alloc] initWithContext:context];
}
ASCollectionLayoutState *layout = [context.layoutDelegateClass calculateLayoutWithContext:context];
[context.layoutCache setLayout:layout forContext:context];
// Measure elements in the measure range ahead of time
CGSize viewportSize = context.viewportSize;
CGPoint contentOffset = context.initialContentOffset;
CGRect initialRect = CGRectMake(contentOffset.x, contentOffset.y, viewportSize.width, viewportSize.height);
CGRect measureRect = CGRectExpandToRangeWithScrollableDirections(initialRect,
kASDefaultMeasureRangeTuningParameters,
context.scrollableDirections,
kASStaticScrollDirection);
// The first call to -layoutAttributesForElementsInRect: will be with a rect that is way bigger than initialRect here.
// If we only block on initialRect, a few elements that are outside of initialRect but inside measureRect
// may not be available by the time -layoutAttributesForElementsInRect: is called.
// Since this method is usually run off main, let's spawn more threads to measure and block on all elements in measureRect.
[self _measureElementsInRect:measureRect blockingRect:measureRect layout:layout];
return layout;
}
#pragma mark - UICollectionViewLayout overrides
- (void)prepareLayout
{
ASDisplayNodeAssertMainThread();
[super prepareLayout];
ASCollectionLayoutContext *context = [self layoutContextWithElements:_collectionNode.visibleElements];
if (_layout != nil && ASObjectIsEqual(_layout.context, context)) {
// The existing layout is still valid. No-op
return;
}
if (ASCollectionLayoutState *cachedLayout = [_layoutCache layoutForContext:context]) {
_layout = cachedLayout;
} else {
// A new layout is needed now. Calculate and apply it immediately
_layout = [ASCollectionLayout calculateLayoutWithContext:context];
}
}
- (void)invalidateLayout
{
ASDisplayNodeAssertMainThread();
[super invalidateLayout];
if (_layout != nil) {
[_layoutCache removeLayoutForContext:_layout.context];
_layout = nil;
}
}
- (CGSize)collectionViewContentSize
{
ASDisplayNodeAssertMainThread();
// The content size can be queried right after a layout invalidation (https://github.com/TextureGroup/Texture/pull/509).
// In that case, return zero.
return _layout ? _layout.contentSize : CGSizeZero;
}
- (NSArray<UICollectionViewLayoutAttributes *> *)layoutAttributesForElementsInRect:(CGRect)blockingRect
{
ASDisplayNodeAssertMainThread();
if (CGRectIsEmpty(blockingRect)) {
return nil;
}
// Measure elements in the measure range, block on the requested rect
CGRect measureRect = CGRectExpandToRangeWithScrollableDirections(blockingRect,
kASDefaultMeasureRangeTuningParameters,
_layout.context.scrollableDirections,
kASStaticScrollDirection);
[ASCollectionLayout _measureElementsInRect:measureRect blockingRect:blockingRect layout:_layout];
NSArray<UICollectionViewLayoutAttributes *> *result = [_layout layoutAttributesForElementsInRect:blockingRect];
ASElementMap *elements = _layout.context.elements;
for (UICollectionViewLayoutAttributes *attrs in result) {
ASCollectionElement *element = [elements elementForLayoutAttributes:attrs];
ASCollectionLayoutSetSizeToElement(attrs.frame.size, element);
}
return result;
}
- (UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath
{
ASDisplayNodeAssertMainThread();
ASCollectionElement *element = [_layout.context.elements elementForItemAtIndexPath:indexPath];
UICollectionViewLayoutAttributes *attrs = [_layout layoutAttributesForElement:element];
ASCellNode *node = element.node;
CGSize elementSize = attrs.frame.size;
if (! CGSizeEqualToSize(elementSize, node.calculatedSize)) {
[node layoutThatFits:ASCollectionLayoutElementSizeRangeFromSize(elementSize)];
}
ASCollectionLayoutSetSizeToElement(attrs.frame.size, element);
return attrs;
}
- (UICollectionViewLayoutAttributes *)layoutAttributesForSupplementaryViewOfKind:(NSString *)elementKind atIndexPath:(NSIndexPath *)indexPath
{
ASCollectionElement *element = [_layout.context.elements supplementaryElementOfKind:elementKind atIndexPath:indexPath];
UICollectionViewLayoutAttributes *attrs = [_layout layoutAttributesForElement:element];
ASCellNode *node = element.node;
CGSize elementSize = attrs.frame.size;
if (! CGSizeEqualToSize(elementSize, node.calculatedSize)) {
[node layoutThatFits:ASCollectionLayoutElementSizeRangeFromSize(elementSize)];
}
ASCollectionLayoutSetSizeToElement(attrs.frame.size, element);
return attrs;
}
- (BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds
{
return (! CGSizeEqualToSize([self _viewportSize], newBounds.size));
}
#pragma mark - Private methods
- (CGSize)_viewportSize
{
ASCollectionNode *collectionNode = _collectionNode;
if (collectionNode != nil && !collectionNode.isNodeLoaded) {
// TODO consider calculatedSize as well
return collectionNode.threadSafeBounds.size;
} else {
ASDisplayNodeAssertMainThread();
return self.collectionView.bounds.size;
}
}
/**
* Measures all elements in the specified rect and blocks the calling thread while measuring those in the blocking rect.
*/
+ (void)_measureElementsInRect:(CGRect)rect blockingRect:(CGRect)blockingRect layout:(ASCollectionLayoutState *)layout
{
if (CGRectIsEmpty(rect) || layout.context.elements == nil) {
return;
}
BOOL hasBlockingRect = !CGRectIsEmpty(blockingRect);
if (hasBlockingRect && CGRectContainsRect(rect, blockingRect) == NO) {
ASDisplayNodeCAssert(NO, @"Blocking rect, if specified, must be within the other (outer) rect");
return;
}
// Step 1: Clamp the specified rects between the bounds of content rect
CGSize contentSize = layout.contentSize;
CGRect contentRect = CGRectMake(0, 0, contentSize.width, contentSize.height);
rect = CGRectIntersection(contentRect, rect);
if (CGRectIsNull(rect)) {
return;
}
if (hasBlockingRect) {
blockingRect = CGRectIntersection(contentRect, blockingRect);
hasBlockingRect = !CGRectIsNull(blockingRect);
}
// Step 2: Get layout attributes of all elements within the specified outer rect
ASPageToLayoutAttributesTable *attrsTable = [layout getAndRemoveUnmeasuredLayoutAttributesPageTableInRect:rect];
if (attrsTable.count == 0) {
// No elements in this rect! Bail early
return;
}
// Step 3: Split all those attributes into blocking and non-blocking buckets
// Use ordered sets here because some items may span multiple pages, and the sets will be accessed by indexes later on.
ASCollectionLayoutContext *context = layout.context;
CGSize pageSize = context.viewportSize;
NSMutableOrderedSet<UICollectionViewLayoutAttributes *> *blockingAttrs = hasBlockingRect ? [NSMutableOrderedSet orderedSet] : nil;
NSMutableOrderedSet<UICollectionViewLayoutAttributes *> *nonBlockingAttrs = [NSMutableOrderedSet orderedSet];
for (id pagePtr in attrsTable) {
ASPageCoordinate page = (ASPageCoordinate)pagePtr;
NSArray<UICollectionViewLayoutAttributes *> *attrsInPage = [attrsTable objectForPage:page];
// Calculate the page's rect but only if it's going to be used.
CGRect pageRect = hasBlockingRect ? ASPageCoordinateGetPageRect(page, pageSize) : CGRectZero;
if (hasBlockingRect && CGRectContainsRect(blockingRect, pageRect)) {
// The page fits well within the blocking rect. All attributes in this page are blocking.
[blockingAttrs addObjectsFromArray:attrsInPage];
} else if (hasBlockingRect && CGRectIntersectsRect(blockingRect, pageRect)) {
// The page intersects the blocking rect. Some elements in this page are blocking, some are not.
for (UICollectionViewLayoutAttributes *attrs in attrsInPage) {
if (CGRectIntersectsRect(blockingRect, attrs.frame)) {
[blockingAttrs addObject:attrs];
} else {
[nonBlockingAttrs addObject:attrs];
}
}
} else {
// The page doesn't intersect the blocking rect. All elements in this page are non-blocking.
[nonBlockingAttrs addObjectsFromArray:attrsInPage];
}
}
// Step 4: Allocate and measure blocking elements' node
ASElementMap *elements = context.elements;
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
if (NSUInteger count = blockingAttrs.count) {
ASDispatchApply(count, queue, 0, ^(size_t i) {
UICollectionViewLayoutAttributes *attrs = blockingAttrs[i];
ASCellNode *node = [elements elementForItemAtIndexPath:attrs.indexPath].node;
CGSize expectedSize = attrs.frame.size;
if (! CGSizeEqualToSize(expectedSize, node.calculatedSize)) {
[node layoutThatFits:ASCollectionLayoutElementSizeRangeFromSize(expectedSize)];
}
});
}
// Step 5: Allocate and measure non-blocking ones
if (NSUInteger count = nonBlockingAttrs.count) {
__weak ASElementMap *weakElements = elements;
ASDispatchAsync(count, queue, 0, ^(size_t i) {
__strong ASElementMap *strongElements = weakElements;
if (strongElements) {
UICollectionViewLayoutAttributes *attrs = nonBlockingAttrs[i];
ASCellNode *node = [elements elementForItemAtIndexPath:attrs.indexPath].node;
CGSize expectedSize = attrs.frame.size;
if (! CGSizeEqualToSize(expectedSize, node.calculatedSize)) {
[node layoutThatFits:ASCollectionLayoutElementSizeRangeFromSize(expectedSize)];
}
}
});
}
}
# pragma mark - Convenient inline functions
ASDISPLAYNODE_INLINE ASSizeRange ASCollectionLayoutElementSizeRangeFromSize(CGSize size)
{
// The layout delegate consulted us that this element must fit within this size,
// and the only way to achieve that without asking it again is to use an exact size range here.
return ASSizeRangeMake(size);
}
ASDISPLAYNODE_INLINE void ASCollectionLayoutSetSizeToElement(CGSize size, ASCollectionElement *element)
{
if (ASCellNode *node = element.node) {
if (! CGSizeEqualToSize(size, node.frame.size)) {
CGRect frame = CGRectZero;
frame.size = size;
node.frame = frame;
}
}
}
@end