From 0c2e92b70dfe70be030ea5593ce4a82eed7003aa Mon Sep 17 00:00:00 2001 From: Huy Nguyen Date: Fri, 28 Apr 2017 11:45:37 +0100 Subject: [PATCH 1/4] Implement ASPageTable - It is a screen page table that can be used to quickly filter out objects in a certain rect without checking each and every one of them. - ASCollectionLayoutState generates and keeps a table that maps page to layout attributes within that page. - ASCollectionLayout (and later, ASCollectionGalleryLayoutDelegate) consults its layout state for `layoutAttributesForElementsInRect:`. This ensures the method can return as quickly as possible, especially on a large data set (I heard some people have galleries with thousands of photos!). --- AsyncDisplayKit.xcodeproj/project.pbxproj | 8 + CHANGELOG.md | 1 + .../Details/ASCollectionFlowLayoutDelegate.h | 8 +- .../Details/ASCollectionFlowLayoutDelegate.m | 16 +- Source/Details/ASCollectionLayoutDelegate.h | 2 + Source/Details/ASCollectionLayoutState.h | 67 ++++++-- Source/Details/ASCollectionLayoutState.m | 90 ++++++++++- Source/Details/ASPageTable.h | 121 +++++++++++++++ Source/Details/ASPageTable.mm | 146 ++++++++++++++++++ Source/Private/ASCollectionLayout.mm | 72 ++++----- 10 files changed, 464 insertions(+), 67 deletions(-) create mode 100644 Source/Details/ASPageTable.h create mode 100644 Source/Details/ASPageTable.mm diff --git a/AsyncDisplayKit.xcodeproj/project.pbxproj b/AsyncDisplayKit.xcodeproj/project.pbxproj index a6f9b79ad..7ae1c465d 100644 --- a/AsyncDisplayKit.xcodeproj/project.pbxproj +++ b/AsyncDisplayKit.xcodeproj/project.pbxproj @@ -389,6 +389,8 @@ DEFAD8131CC48914000527C4 /* ASVideoNode.mm in Sources */ = {isa = PBXBuildFile; fileRef = AEEC47E01C20C2DD00EC1693 /* ASVideoNode.mm */; }; E516FC7F1E9FE24200714FF4 /* ASHashing.h in Headers */ = {isa = PBXBuildFile; fileRef = E516FC7D1E9FE24200714FF4 /* ASHashing.h */; }; E516FC801E9FE24200714FF4 /* ASHashing.m in Sources */ = {isa = PBXBuildFile; fileRef = E516FC7E1E9FE24200714FF4 /* ASHashing.m */; }; + E54E81FC1EB357BD00FFE8E1 /* ASPageTable.h in Headers */ = {isa = PBXBuildFile; fileRef = E54E81FA1EB357BD00FFE8E1 /* ASPageTable.h */; }; + E54E81FD1EB357BD00FFE8E1 /* ASPageTable.mm in Sources */ = {isa = PBXBuildFile; fileRef = E54E81FB1EB357BD00FFE8E1 /* ASPageTable.mm */; }; E55D86331CA8A14000A0C26F /* ASLayoutElement.mm in Sources */ = {isa = PBXBuildFile; fileRef = E55D86311CA8A14000A0C26F /* ASLayoutElement.mm */; }; E5711A2C1C840C81009619D4 /* ASCollectionElement.h in Headers */ = {isa = PBXBuildFile; fileRef = E5711A2A1C840C81009619D4 /* ASCollectionElement.h */; settings = {ATTRIBUTES = (Private, ); }; }; E5711A301C840C96009619D4 /* ASCollectionElement.mm in Sources */ = {isa = PBXBuildFile; fileRef = E5711A2D1C840C96009619D4 /* ASCollectionElement.mm */; }; @@ -822,6 +824,8 @@ E516FC7E1E9FE24200714FF4 /* ASHashing.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASHashing.m; sourceTree = ""; }; E52405B21C8FEF03004DC8E7 /* ASLayoutTransition.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASLayoutTransition.mm; sourceTree = ""; }; E52405B41C8FEF16004DC8E7 /* ASLayoutTransition.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASLayoutTransition.h; sourceTree = ""; }; + E54E81FA1EB357BD00FFE8E1 /* ASPageTable.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASPageTable.h; sourceTree = ""; }; + E54E81FB1EB357BD00FFE8E1 /* ASPageTable.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASPageTable.mm; sourceTree = ""; }; E55D86311CA8A14000A0C26F /* ASLayoutElement.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASLayoutElement.mm; sourceTree = ""; }; E5711A2A1C840C81009619D4 /* ASCollectionElement.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASCollectionElement.h; sourceTree = ""; }; E5711A2D1C840C96009619D4 /* ASCollectionElement.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASCollectionElement.mm; sourceTree = ""; }; @@ -1479,6 +1483,8 @@ E58E9E411E941D74004CFC59 /* ASCollectionLayoutDelegate.h */, E58E9E3D1E941D74004CFC59 /* ASCollectionFlowLayoutDelegate.h */, E58E9E3E1E941D74004CFC59 /* ASCollectionFlowLayoutDelegate.m */, + E54E81FA1EB357BD00FFE8E1 /* ASPageTable.h */, + E54E81FB1EB357BD00FFE8E1 /* ASPageTable.mm */, ); name = "Collection Layout"; sourceTree = ""; @@ -1547,6 +1553,7 @@ B35061F71B010EFD0018CF92 /* ASCollectionViewProtocols.h in Headers */, 68FC85E31CE29B7E00EDD713 /* ASTabBarController.h in Headers */, B35061FA1B010EFD0018CF92 /* ASControlNode+Subclasses.h in Headers */, + E54E81FC1EB357BD00FFE8E1 /* ASPageTable.h in Headers */, B35061F81B010EFD0018CF92 /* ASControlNode.h in Headers */, B35062171B010EFD0018CF92 /* ASDataController.h in Headers */, 34EFC75B1B701BAF00AD841F /* ASDimension.h in Headers */, @@ -2056,6 +2063,7 @@ 509E68601B3AED8E009B9150 /* ASScrollDirection.m in Sources */, B35062091B010EFD0018CF92 /* ASScrollNode.mm in Sources */, 8BDA5FC81CDBDF95007D13B2 /* ASVideoPlayerNode.mm in Sources */, + E54E81FD1EB357BD00FFE8E1 /* ASPageTable.mm in Sources */, 34EFC7721B701D0300AD841F /* ASStackLayoutSpec.mm in Sources */, 7AB338661C55B3420055FDE8 /* ASRelativeLayoutSpec.mm in Sources */, 696F01EE1DD2AF450049FBD5 /* ASEventLog.mm in Sources */, diff --git a/CHANGELOG.md b/CHANGELOG.md index be0b6639c..9b091f3c8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,3 +10,4 @@ - Simplified & optimized hashing code. [Adlai Holler](https://github.com/Adlai-Holler) [#86](https://github.com/TextureGroup/Texture/pull/86) - Improve the performance & safety of ASDisplayNode subnodes. [Adlai Holler](https://github.com/Adlai-Holler) [#223](https://github.com/TextureGroup/Texture/pull/223) - Remove finalLayoutElement [Michael Schneider] (https://github.com/maicki)[#96](https://github.com/TextureGroup/Texture/pull/96) +- Add ASPageTable - A map table for fast retrieval of objects within a certain page [Huy Nguyen](https://github.com/nguyenhuy) diff --git a/Source/Details/ASCollectionFlowLayoutDelegate.h b/Source/Details/ASCollectionFlowLayoutDelegate.h index c6eb4f8ff..a143eaf80 100644 --- a/Source/Details/ASCollectionFlowLayoutDelegate.h +++ b/Source/Details/ASCollectionFlowLayoutDelegate.h @@ -22,9 +22,15 @@ NS_ASSUME_NONNULL_BEGIN AS_SUBCLASSING_RESTRICTED +/** + * A thread-safe, high performant layout delegate that arranges items into a flow layout. + * It uses a concurrent and multi-line ASStackLayoutSpec under the hood. Thus, per-child flex properties (i.e alignSelf, + * flexShrink, flexGrow, etc - see @ASStackLayoutElement) can be set directly on cell nodes to be used + * to calculate the final collection layout. + */ @interface ASCollectionFlowLayoutDelegate : NSObject -- (instancetype)initWithScrollableDirections:(ASScrollDirection)scrollableDirections; +- (instancetype)initWithScrollableDirections:(ASScrollDirection)scrollableDirections NS_DESIGNATED_INITIALIZER; @end diff --git a/Source/Details/ASCollectionFlowLayoutDelegate.m b/Source/Details/ASCollectionFlowLayoutDelegate.m index 57ac5a35a..009ba2123 100644 --- a/Source/Details/ASCollectionFlowLayoutDelegate.m +++ b/Source/Details/ASCollectionFlowLayoutDelegate.m @@ -31,16 +31,12 @@ @implementation ASCollectionFlowLayoutDelegate { - (instancetype)init { - self = [super init]; - if (self) { - _scrollableDirections = ASScrollDirectionVerticalDirections; - } - return self; + return [self initWithScrollableDirections:ASScrollDirectionVerticalDirections]; } - (instancetype)initWithScrollableDirections:(ASScrollDirection)scrollableDirections { - self = [self init]; + self = [super init]; if (self) { _scrollableDirections = scrollableDirections; } @@ -71,9 +67,9 @@ - (ASCollectionLayoutState *)calculateLayoutWithContext:(ASCollectionLayoutConte ASElementMap *elements = context.elements; NSMutableArray *children = ASArrayByFlatMapping(elements.itemElements, ASCollectionElement *element, element.node); if (children.count == 0) { - return [[ASCollectionLayoutState alloc] initWithElements:elements - contentSize:CGSizeZero - elementToLayoutArrtibutesMap:[NSMapTable weakToStrongObjectsMapTable]]; + return [[ASCollectionLayoutState alloc] initWithContext:context + contentSize:CGSizeZero + elementToLayoutAttributesTable:[NSMapTable elementToLayoutAttributesTable]]; } ASStackLayoutSpec *stackSpec = [ASStackLayoutSpec stackLayoutSpecWithDirection:ASStackLayoutDirectionHorizontal @@ -85,7 +81,7 @@ - (ASCollectionLayoutState *)calculateLayoutWithContext:(ASCollectionLayoutConte children:children]; stackSpec.concurrent = YES; ASLayout *layout = [stackSpec layoutThatFits:[self sizeRangeThatFits:context.viewportSize]]; - return [[ASCollectionLayoutState alloc] initWithElements:elements layout:layout]; + return [[ASCollectionLayoutState alloc] initWithContext:context layout:layout]; } @end diff --git a/Source/Details/ASCollectionLayoutDelegate.h b/Source/Details/ASCollectionLayoutDelegate.h index a15df5ad8..7fd17467a 100644 --- a/Source/Details/ASCollectionLayoutDelegate.h +++ b/Source/Details/ASCollectionLayoutDelegate.h @@ -27,6 +27,8 @@ NS_ASSUME_NONNULL_BEGIN /** * @abstract Returns any additional information needed for a coming layout pass with the given elements. * + * @param elements The elements to be laid out later. + * * @discussion The returned object must support equality and hashing (i.e `-isEqual:` and `-hash` must be properly implemented). * * @discussion This method will be called on main thread. diff --git a/Source/Details/ASCollectionLayoutState.h b/Source/Details/ASCollectionLayoutState.h index 31656ec47..30d65daf8 100644 --- a/Source/Details/ASCollectionLayoutState.h +++ b/Source/Details/ASCollectionLayoutState.h @@ -19,45 +19,86 @@ #import #import -@class ASElementMap, ASCollectionElement, ASLayout; +@class ASCollectionLayoutContext, ASLayout, ASCollectionElement; NS_ASSUME_NONNULL_BEGIN +@interface NSMapTable (ASCollectionLayoutConvenience) + ++ (NSMapTable *)elementToLayoutAttributesTable; + +@end + AS_SUBCLASSING_RESTRICTED @interface ASCollectionLayoutState : NSObject -/// The elements used to calculate this object -@property (nonatomic, strong, readonly) ASElementMap *elements; +/// The context used to calculate this object +@property (nonatomic, strong, readonly) ASCollectionLayoutContext *context; +/// The final content size of the collection's layout @property (nonatomic, assign, readonly) CGSize contentSize; -/// Element to layout attributes map. Should use weak pointers for elements. -@property (nonatomic, strong, readonly) NSMapTable *elementToLayoutArrtibutesMap; +/// The final content rect calculated from content size +@property (nonatomic, assign, readonly) CGRect contentRect; - (instancetype)init __unavailable; /** * Designated initializer. * - * @param elements The elements used to calculate this object + * @param context The context used to calculate this object * * @param contentSize The content size of the collection's layout * - * @param elementToLayoutArrtibutesMap Map between elements to their layout attributes. The map may contain all elements, or a subset of them and will be updated later. - * Also, it should have NSMapTableObjectPointerPersonality and NSMapTableWeakMemory as key options. + * @param table A map between elements to their layout attributes. It may contain all elements, or a subset of them that will be updated later. + * It should be initialized using +[NSMapTable elementToLayoutAttributesTable] convenience initializer. */ -- (instancetype)initWithElements:(ASElementMap *)elements contentSize:(CGSize)contentSize elementToLayoutArrtibutesMap:(NSMapTable *)elementToLayoutArrtibutesMap NS_DESIGNATED_INITIALIZER; +- (instancetype)initWithContext:(ASCollectionLayoutContext *)context contentSize:(CGSize)contentSize elementToLayoutAttributesTable:(NSMapTable *)table NS_DESIGNATED_INITIALIZER; /** * Convenience initializer. * - * @param elements The elements used to calculate this object + * @param context The context used to calculate this object + * + * @param layout The layout describes size and position of all elements, or a subset of them and will be updated over time. + * + */ +- (instancetype)initWithContext:(ASCollectionLayoutContext *)context layout:(ASLayout *)layout; + +/** + * Returns all layout attributes present in this object. + */ +- (NSArray *)allLayoutAttributes; + +/** + * Returns layout attributes of elements in the specified rect. + * + * @param rect The rect containing the target elements. + */ +- (nullable NSArray *)layoutAttributesForElementsInRect:(CGRect)rect; + +/** + * Returns layout attributes of the element at the specified index path. + * + * @param indexPath The index path of the item. + */ +- (nullable UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath; + +/** + * Returns layout attributes of the specified supplementary element. + * + * @param kind A string that identifies the type of the supplementary element. * - * @param layout The layout describes size and position of all elements, or a subset of them and will be updated later. + * @param indexPath The index path of the element. + */ +- (nullable UICollectionViewLayoutAttributes *)layoutAttributesForSupplementaryElementOfKind:(NSString *)kind atIndexPath:(NSIndexPath *)indexPath; + +/** + * Returns layout attributes of the specified element. * - * @discussion The sublayouts that describe position of elements must be direct children of the root layout object parameter. + * @element The element. */ -- (instancetype)initWithElements:(ASElementMap *)elements layout:(ASLayout *)layout; +- (nullable UICollectionViewLayoutAttributes *)layoutAttributesForElement:(ASCollectionElement *)element; @end diff --git a/Source/Details/ASCollectionLayoutState.m b/Source/Details/ASCollectionLayoutState.m index 0fe42c855..188a9a277 100644 --- a/Source/Details/ASCollectionLayoutState.m +++ b/Source/Details/ASCollectionLayoutState.m @@ -20,16 +20,37 @@ #import #import #import +#import #import #import +#import -@implementation ASCollectionLayoutState +@implementation NSMapTable (ASCollectionLayoutConvenience) -- (instancetype)initWithElements:(ASElementMap *)elements layout:(ASLayout *)layout ++ (NSMapTable *)elementToLayoutAttributesTable { - NSMapTable *attrsMap = [NSMapTable mapTableWithKeyOptions:(NSMapTableObjectPointerPersonality | NSMapTableWeakMemory) valueOptions:NSMapTableStrongMemory]; + return [NSMapTable mapTableWithKeyOptions:(NSMapTableWeakMemory | NSMapTableObjectPointerPersonality) valueOptions:NSMapTableStrongMemory]; +} + +@end + +@implementation ASCollectionLayoutState { + NSMapTable *_elementToLayoutAttributesTable; + ASPageTable *> *_pageToLayoutAttributesTable; +} + +- (instancetype)initWithContext:(ASCollectionLayoutContext *)context layout:(ASLayout *)layout +{ + ASElementMap *elements = context.elements; + NSMapTable *table = [NSMapTable elementToLayoutAttributesTable]; + for (ASLayout *sublayout in layout.sublayouts) { ASCollectionElement *element = ((ASCellNode *)sublayout.layoutElement).collectionElement; + if (element == nil) { + ASDisplayNodeFailAssert(@"Element not found!"); + continue; + } + NSIndexPath *indexPath = [elements indexPathForElement:element]; NSString *supplementaryElementKind = element.supplementaryElementKind; @@ -41,21 +62,74 @@ - (instancetype)initWithElements:(ASElementMap *)elements layout:(ASLayout *)lay } attrs.frame = sublayout.frame; - [attrsMap setObject:attrs forKey:element]; + [table setObject:attrs forKey:element]; } - return [self initWithElements:elements contentSize:layout.size elementToLayoutArrtibutesMap:attrsMap]; + return [self initWithContext:context contentSize:layout.size elementToLayoutAttributesTable:table]; } -- (instancetype)initWithElements:(ASElementMap *)elements contentSize:(CGSize)contentSize elementToLayoutArrtibutesMap:(NSMapTable *)attrsMap +- (instancetype)initWithContext:(ASCollectionLayoutContext *)context contentSize:(CGSize)contentSize elementToLayoutAttributesTable:(NSMapTable *)table { self = [super init]; if (self) { - _elements = elements; + _context = context; _contentSize = contentSize; - _elementToLayoutArrtibutesMap = attrsMap; + _contentRect = CGRectMake(0.0f, 0.0f, contentSize.width, contentSize.height); + _elementToLayoutAttributesTable = table; + _pageToLayoutAttributesTable = [ASPageTable pageTableWithLayoutAttributes:_elementToLayoutAttributesTable.objectEnumerator pageSize:context.viewportSize]; } return self; } +- (NSArray *)allLayoutAttributes +{ + return [_elementToLayoutAttributesTable.objectEnumerator allObjects]; +} + +- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect +{ + CGSize pageSize = _context.viewportSize; + NSPointerArray *pages = ASPageCoordinatesForPagesThatIntersectRect(rect, _contentSize, pageSize); + if (pages.count == 0) { + return nil; + } + + NSMutableArray *results = [NSMutableArray array]; + for (id pagePtr in pages) { + ASPageCoordinate page = (ASPageCoordinate)pagePtr; + NSSet *allAttrs = [_pageToLayoutAttributesTable objectForPage:page]; + if (allAttrs.count > 0) { + CGRect pageRect = ASPageCoordinateGetPageRect(page, pageSize); + + if (CGRectContainsRect(rect, pageRect)) { + [results addObjectsFromArray:[allAttrs allObjects]]; + } else { + for (UICollectionViewLayoutAttributes *attrs in allAttrs) { + if (CGRectIntersectsRect(rect, attrs.frame)) { + [results addObject:attrs]; + } + } + } + } + } + return results; +} + +- (UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath +{ + ASCollectionElement *element = [_context.elements elementForItemAtIndexPath:indexPath]; + return [_elementToLayoutAttributesTable objectForKey:element]; +} + +- (UICollectionViewLayoutAttributes *)layoutAttributesForSupplementaryElementOfKind:(NSString *)elementKind atIndexPath:(NSIndexPath *)indexPath +{ + ASCollectionElement *element = [_context.elements supplementaryElementOfKind:elementKind atIndexPath:indexPath]; + return [_elementToLayoutAttributesTable objectForKey:element]; +} + +- (UICollectionViewLayoutAttributes *)layoutAttributesForElement:(ASCollectionElement *)element +{ + return [_elementToLayoutAttributesTable objectForKey:element]; +} + @end diff --git a/Source/Details/ASPageTable.h b/Source/Details/ASPageTable.h new file mode 100644 index 000000000..a8c996d28 --- /dev/null +++ b/Source/Details/ASPageTable.h @@ -0,0 +1,121 @@ +// +// ASPageTable.h +// Texture +// +// Copyright (c) 2017-present, Pinterest, Inc. All rights reserved. +// 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 +#import +#import +#import + +@class ASCollectionElement; + +NS_ASSUME_NONNULL_BEGIN + +ASDISPLAYNODE_EXTERN_C_BEGIN + +/** + * Represents x and y coordinates of a page. + */ +typedef uintptr_t ASPageCoordinate; + +/** + * Returns a page coordinate with the given x and y values. Both of them must be less than 65,535. + */ +extern ASPageCoordinate ASPageCoordinateMake(uint16_t x, uint16_t y) AS_WARN_UNUSED_RESULT; + +/** + * Returns coordinate of the page that contains the specified point. + * Similar to CGRectContainsPoint, a point is considered inside a page if its lie inside the page or on the minimum X or minimum Y edge. + * + * @param point The point that the page at the returned should contain. Any negative of the point will be corrected to 0.0 + * + * @param pageSize The size of each page. + */ +extern ASPageCoordinate ASPageCoordinateForPageThatContainsPoint(CGPoint point, CGSize pageSize) AS_WARN_UNUSED_RESULT; + +extern uint16_t ASPageCoordinateGetX(ASPageCoordinate pageCoordinate) AS_WARN_UNUSED_RESULT; + +extern uint16_t ASPageCoordinateGetY(ASPageCoordinate pageCoordinate) AS_WARN_UNUSED_RESULT; + +extern CGRect ASPageCoordinateGetPageRect(ASPageCoordinate pageCoordinate, CGSize pageSize) AS_WARN_UNUSED_RESULT; + +/** + * Returns coordinate pointers for pages that intersect the specified rect. For each pointer, use ASPageCoordinateFromPointer() to get the original coordinate. + * The specified rect is restricted to the bounds of a content rect that has an origin of {0, 0} and a size of the given contentSize. + * + * @param rect The rect intersecting the target pages. + * + * @param contentRect The combined rect of all pages. + * + * @param pageSize The size of each page. + */ +extern NSPointerArray * _Nullable ASPageCoordinatesForPagesThatIntersectRect(CGRect rect, CGSize contentSize, CGSize pageSize) AS_WARN_UNUSED_RESULT; + +ASDISPLAYNODE_EXTERN_C_END + +/** + * An alias for an NSMapTable created to store objects using ASPageCoordinates as keys. + * + * You should not call -objectForKey:, -setObject:forKey:, or -removeObjectForKey: + * on these objects. + */ +typedef NSMapTable ASPageTable; + +/** + * A category for creating & using map tables meant for storing objects using ASPage as keys. + */ +@interface NSMapTable (ASPageTableMethods) + +/** + * Creates a new page table with (NSMapTableStrongMemory | NSMapTableObjectPointerPersonality) for values. + */ ++ (ASPageTable *)pageTableForStrongObjectPointers; + +/** + * Creates a new page table with (NSMapTableWeakMemory | NSMapTableObjectPointerPersonality) for values. + */ ++ (ASPageTable *)pageTableForWeakObjectPointers; + +/** + * Builds a new page to layout attributes from the given layout attributes. + * + * @param layoutAttributesEnumerator The layout attributes to build from + * + * @param pageSize The size of each page. + */ ++ (ASPageTable *> *)pageTableWithLayoutAttributes:(id)layoutAttributesEnumerator pageSize:(CGSize)pageSize; + +/** + * Retrieves the object for a given page, or nil if the page is not found. + * + * @param page A page to lookup the object for. + */ +- (ObjectType)objectForPage:(ASPageCoordinate)page; + +/** + * Sets the given object for the associated page. + * + * @param object The object to store as value. + * + * @param page The page to use for the rect. + */ +- (void)setObject:(ObjectType)object forPage:(ASPageCoordinate)page; + +/** + * Removes the object for the given page, if one exists. + * + * @param page The page to remove. + */ +- (void)removeObjectForPage:(ASPageCoordinate)page; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Source/Details/ASPageTable.mm b/Source/Details/ASPageTable.mm new file mode 100644 index 000000000..7d1552f7a --- /dev/null +++ b/Source/Details/ASPageTable.mm @@ -0,0 +1,146 @@ +// +// ASPageTable.mm +// Texture +// +// Copyright (c) 2017-present, Pinterest, Inc. All rights reserved. +// 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 + +extern ASPageCoordinate ASPageCoordinateMake(uint16_t x, uint16_t y) +{ + // Add 1 to the end result because 0 is not accepted by NSArray and NSMapTable. + // To avoid overflow after adding, x and y can't be UINT16_MAX (0xFFFF) **at the same time**. + // But for API simplification, we enforce the same restriction to both values. + ASDisplayNodeCAssert(x < UINT16_MAX, @"x coordinate must be less than 65,535"); + ASDisplayNodeCAssert(y < UINT16_MAX, @"y coordinate must be less than 65,535"); + return (x << 16) + y + 1; +} + +extern ASPageCoordinate ASPageCoordinateForPageThatContainsPoint(CGPoint point, CGSize pageSize) +{ + return ASPageCoordinateMake((MAX(0.0, point.x) / pageSize.width), (MAX(0.0, point.y) / pageSize.height)); +} + +extern uint16_t ASPageCoordinateGetX(ASPageCoordinate pageCoordinate) +{ + return (pageCoordinate - 1) >> 16; +} + +extern uint16_t ASPageCoordinateGetY(ASPageCoordinate pageCoordinate) +{ + return (pageCoordinate - 1) & ~(0xFFFF<<16); +} + +extern CGRect ASPageCoordinateGetPageRect(ASPageCoordinate pageCoordinate, CGSize pageSize) +{ + CGFloat pageWidth = pageSize.width; + CGFloat pageHeight = pageSize.height; + return CGRectMake(ASPageCoordinateGetX(pageCoordinate) * pageWidth, ASPageCoordinateGetY(pageCoordinate) * pageHeight, pageWidth, pageHeight); +} + +extern NSPointerArray *ASPageCoordinatesForPagesThatIntersectRect(CGRect rect, CGSize contentSize, CGSize pageSize) +{ + CGRect contentRect = CGRectMake(0.0, 0.0, contentSize.width, contentSize.height); + // Make sure the specified rect is within contentRect + rect = CGRectIntersection(rect, contentRect); + if (CGRectIsNull(rect) || CGRectIsEmpty(rect)) { + return nil; + } + + NSPointerArray *results = [NSPointerArray pointerArrayWithOptions:(NSPointerFunctionsIntegerPersonality | NSPointerFunctionsOpaqueMemory)]; + + ASPageCoordinate minPage = ASPageCoordinateForPageThatContainsPoint(CGPointMake(CGRectGetMinX(rect), CGRectGetMinY(rect)), pageSize); + ASPageCoordinate maxPage = ASPageCoordinateForPageThatContainsPoint(CGPointMake(CGRectGetMaxX(rect), CGRectGetMaxY(rect)), pageSize); + if (minPage == maxPage) { + [results addPointer:(void *)minPage]; + return results; + } + + NSUInteger minX = ASPageCoordinateGetX(minPage); + NSUInteger minY = ASPageCoordinateGetY(minPage); + NSUInteger maxX = ASPageCoordinateGetX(maxPage); + NSUInteger maxY = ASPageCoordinateGetY(maxPage); + + for (NSUInteger x = minX; x <= maxX; x++) { + for (NSUInteger y = minY; y <= maxY; y++) { + ASPageCoordinate page = ASPageCoordinateMake(x, y); + [results addPointer:(void *)page]; + } + } + + return results; +} + +@implementation NSMapTable (ASPageTableMethods) + ++ (instancetype)pageTableWithValuePointerFunctions:(NSPointerFunctions *)valueFuncs +{ + static NSPointerFunctions *pageCoordinatesFuncs; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + pageCoordinatesFuncs = [NSPointerFunctions pointerFunctionsWithOptions:NSPointerFunctionsIntegerPersonality | NSPointerFunctionsOpaqueMemory]; + }); + + return [[NSMapTable alloc] initWithKeyPointerFunctions:pageCoordinatesFuncs valuePointerFunctions:valueFuncs capacity:0]; +} + ++ (ASPageTable *)pageTableForStrongObjectPointers +{ + static NSPointerFunctions *strongObjectPointerFuncs; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + strongObjectPointerFuncs = [NSPointerFunctions pointerFunctionsWithOptions:NSPointerFunctionsStrongMemory]; + }); + return [self pageTableWithValuePointerFunctions:strongObjectPointerFuncs]; +} + ++ (ASPageTable *)pageTableForWeakObjectPointers +{ + static NSPointerFunctions *weakObjectPointerFuncs; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + weakObjectPointerFuncs = [NSPointerFunctions pointerFunctionsWithOptions:NSPointerFunctionsWeakMemory]; + }); + return [self pageTableWithValuePointerFunctions:weakObjectPointerFuncs]; +} + ++ (ASPageTable *)pageTableWithLayoutAttributes:(id)layoutAttributesEnumerator pageSize:(CGSize)pageSize +{ + ASPageTable *result = [ASPageTable pageTableForStrongObjectPointers]; + for (UICollectionViewLayoutAttributes *attrs in layoutAttributesEnumerator) { + ASPageCoordinate page = ASPageCoordinateForPageThatContainsPoint(attrs.frame.origin, pageSize); + NSMutableSet *attrsInPage = [result objectForPage:page]; + if (attrsInPage == nil) { + attrsInPage = [NSMutableSet set]; + [result setObject:attrsInPage forPage:page]; + } + [attrsInPage addObject:attrs]; + } + return result; +} + +- (id)objectForPage:(ASPageCoordinate)page +{ + __unsafe_unretained id key = (__bridge id)(void *)page; + return [self objectForKey:key]; +} + +- (void)setObject:(id)object forPage:(ASPageCoordinate)page +{ + __unsafe_unretained id key = (__bridge id)(void *)page; + [self setObject:object forKey:key]; +} + +- (void)removeObjectForPage:(ASPageCoordinate)page +{ + __unsafe_unretained id key = (__bridge id)(void *)page; + [self removeObjectForKey:key]; +} + +@end diff --git a/Source/Private/ASCollectionLayout.mm b/Source/Private/ASCollectionLayout.mm index 529cd64bd..645f7a24f 100644 --- a/Source/Private/ASCollectionLayout.mm +++ b/Source/Private/ASCollectionLayout.mm @@ -33,12 +33,10 @@ @interface ASCollectionLayout () { ASDN::Mutex __instanceLock__; // Non-recursive mutex, ftw! // Main thread only. - ASCollectionLayoutState *_state; + ASCollectionLayoutState *_layout; // The pending state calculated ahead of time, if any. - ASCollectionLayoutState *_pendingState; - // The context used to calculate _pendingState - ASCollectionLayoutContext *_layoutContextForPendingState; + ASCollectionLayoutState *_pendingLayout; BOOL _layoutDelegateImplementsAdditionalInfoForLayoutWithElements; } @@ -63,20 +61,20 @@ - (instancetype)initWithLayoutDelegate:(id)layoutDel - (id)layoutContextWithElements:(ASElementMap *)elements { ASDisplayNodeAssertMainThread(); + CGSize viewportSize = [self viewportSize]; id additionalInfo = nil; if (_layoutDelegateImplementsAdditionalInfoForLayoutWithElements) { additionalInfo = [_layoutDelegate additionalInfoForLayoutWithElements:elements]; } - return [[ASCollectionLayoutContext alloc] initWithViewportSize:[self viewportSize] elements:elements additionalInfo:additionalInfo]; + return [[ASCollectionLayoutContext alloc] initWithViewportSize:viewportSize elements:elements additionalInfo:additionalInfo]; } - (void)prepareLayoutWithContext:(id)context { - ASCollectionLayoutState *state = [_layoutDelegate calculateLayoutWithContext:context]; + ASCollectionLayoutState *layout = [_layoutDelegate calculateLayoutWithContext:context]; ASDN::MutexLocker l(__instanceLock__); - _pendingState = state; - _layoutContextForPendingState = context; + _pendingLayout = layout; } #pragma mark - UICollectionViewLayout overrides @@ -87,70 +85,74 @@ - (void)prepareLayout [super prepareLayout]; ASCollectionLayoutContext *context = [self layoutContextWithElements:_collectionNode.visibleElements]; - ASCollectionLayoutState *state = nil; + ASCollectionLayoutState *layout = nil; { ASDN::MutexLocker l(__instanceLock__); - if (_pendingState != nil && ASObjectIsEqual(_layoutContextForPendingState, context)) { - // Looks like we can use the pending state. Great! - state = _pendingState; - _pendingState = nil; - _layoutContextForPendingState = nil; + if (_pendingLayout != nil && ASObjectIsEqual(_pendingLayout.context, context)) { + // Looks like we can use the pending layout. Great! + layout = _pendingLayout; + _pendingLayout = nil; } } - if (state == nil) { - state = [_layoutDelegate calculateLayoutWithContext:context]; + if (layout == nil) { + layout = [_layoutDelegate calculateLayoutWithContext:context]; } - _state = state; + _layout = layout; } - (void)invalidateLayout { ASDisplayNodeAssertMainThread(); [super invalidateLayout]; - _state = nil; + _layout = nil; } - (CGSize)collectionViewContentSize { ASDisplayNodeAssertMainThread(); - ASDisplayNodeAssertNotNil(_state, @"Collection layout state should not be nil at this point"); - return _state.contentSize; + ASDisplayNodeAssertNotNil(_layout, @"Collection layout state should not be nil at this point"); + return _layout.contentSize; } - (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect { - NSMutableArray *attributesInRect = [NSMutableArray array]; - NSMapTable *attrsMap = _state.elementToLayoutArrtibutesMap; - for (ASCollectionElement *element in attrsMap) { - UICollectionViewLayoutAttributes *attrs = [attrsMap objectForKey:element]; - if (CGRectIntersectsRect(rect, attrs.frame)) { - [ASCollectionLayout setSize:attrs.frame.size toElement:element]; - [attributesInRect addObject:attrs]; - } + ASDisplayNodeAssertMainThread(); + if (CGRectIsEmpty(rect) || (! CGRectIntersectsRect(_layout.contentRect, rect))) { + return @[]; + } + + NSArray *results = [_layout layoutAttributesForElementsInRect:rect]; + ASElementMap *elements = _layout.context.elements; + for (UICollectionViewLayoutAttributes *attrs in results) { + ASCollectionElement *element = [elements elementForLayoutAttributes:attrs]; + [ASCollectionLayout setSize:attrs.frame.size toElement:element]; } - return attributesInRect; + return results; } - (UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath { - ASCollectionLayoutState *state = _state; - ASCollectionElement *element = [state.elements elementForItemAtIndexPath:indexPath]; - UICollectionViewLayoutAttributes *attrs = [state.elementToLayoutArrtibutesMap objectForKey:element]; + ASCollectionElement *element = [_layout.context.elements elementForItemAtIndexPath:indexPath]; + UICollectionViewLayoutAttributes *attrs = [_layout layoutAttributesForElement:element]; [ASCollectionLayout setSize:attrs.frame.size toElement:element]; return attrs; } - (UICollectionViewLayoutAttributes *)layoutAttributesForSupplementaryViewOfKind:(NSString *)elementKind atIndexPath:(NSIndexPath *)indexPath { - ASCollectionLayoutState *state = _state; - ASCollectionElement *element = [state.elements supplementaryElementOfKind:elementKind atIndexPath:indexPath]; - UICollectionViewLayoutAttributes *attrs = [state.elementToLayoutArrtibutesMap objectForKey:element]; + ASCollectionElement *element = [_layout.context.elements supplementaryElementOfKind:elementKind atIndexPath:indexPath]; + UICollectionViewLayoutAttributes *attrs = [_layout layoutAttributesForElement:element]; [ASCollectionLayout setSize:attrs.frame.size toElement:element]; return attrs; } +- (BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds +{ + return (! CGSizeEqualToSize([self viewportSize], newBounds.size)); +} + #pragma mark - Private methods + (void)setSize:(CGSize)size toElement:(ASCollectionElement *)element From 02d7153e2f5b192c25c120c0d590f4846df52be6 Mon Sep 17 00:00:00 2001 From: Huy Nguyen Date: Thu, 4 May 2017 11:51:58 +0100 Subject: [PATCH 2/4] Address comments --- AsyncDisplayKit.xcodeproj/project.pbxproj | 8 ++++---- Source/Details/ASCollectionLayoutState.h | 5 +---- Source/Details/ASCollectionLayoutState.m | 16 ++++++++-------- Source/Details/ASPageTable.h | 4 ++-- .../Details/{ASPageTable.mm => ASPageTable.m} | 17 +++++++++-------- Source/Private/ASCollectionLayout.mm | 10 ++++------ 6 files changed, 28 insertions(+), 32 deletions(-) rename Source/Details/{ASPageTable.mm => ASPageTable.m} (87%) diff --git a/AsyncDisplayKit.xcodeproj/project.pbxproj b/AsyncDisplayKit.xcodeproj/project.pbxproj index 7ae1c465d..8c01b66fc 100644 --- a/AsyncDisplayKit.xcodeproj/project.pbxproj +++ b/AsyncDisplayKit.xcodeproj/project.pbxproj @@ -390,7 +390,7 @@ E516FC7F1E9FE24200714FF4 /* ASHashing.h in Headers */ = {isa = PBXBuildFile; fileRef = E516FC7D1E9FE24200714FF4 /* ASHashing.h */; }; E516FC801E9FE24200714FF4 /* ASHashing.m in Sources */ = {isa = PBXBuildFile; fileRef = E516FC7E1E9FE24200714FF4 /* ASHashing.m */; }; E54E81FC1EB357BD00FFE8E1 /* ASPageTable.h in Headers */ = {isa = PBXBuildFile; fileRef = E54E81FA1EB357BD00FFE8E1 /* ASPageTable.h */; }; - E54E81FD1EB357BD00FFE8E1 /* ASPageTable.mm in Sources */ = {isa = PBXBuildFile; fileRef = E54E81FB1EB357BD00FFE8E1 /* ASPageTable.mm */; }; + E54E81FD1EB357BD00FFE8E1 /* ASPageTable.m in Sources */ = {isa = PBXBuildFile; fileRef = E54E81FB1EB357BD00FFE8E1 /* ASPageTable.m */; }; E55D86331CA8A14000A0C26F /* ASLayoutElement.mm in Sources */ = {isa = PBXBuildFile; fileRef = E55D86311CA8A14000A0C26F /* ASLayoutElement.mm */; }; E5711A2C1C840C81009619D4 /* ASCollectionElement.h in Headers */ = {isa = PBXBuildFile; fileRef = E5711A2A1C840C81009619D4 /* ASCollectionElement.h */; settings = {ATTRIBUTES = (Private, ); }; }; E5711A301C840C96009619D4 /* ASCollectionElement.mm in Sources */ = {isa = PBXBuildFile; fileRef = E5711A2D1C840C96009619D4 /* ASCollectionElement.mm */; }; @@ -825,7 +825,7 @@ E52405B21C8FEF03004DC8E7 /* ASLayoutTransition.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASLayoutTransition.mm; sourceTree = ""; }; E52405B41C8FEF16004DC8E7 /* ASLayoutTransition.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASLayoutTransition.h; sourceTree = ""; }; E54E81FA1EB357BD00FFE8E1 /* ASPageTable.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASPageTable.h; sourceTree = ""; }; - E54E81FB1EB357BD00FFE8E1 /* ASPageTable.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASPageTable.mm; sourceTree = ""; }; + E54E81FB1EB357BD00FFE8E1 /* ASPageTable.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASPageTable.m; sourceTree = ""; }; E55D86311CA8A14000A0C26F /* ASLayoutElement.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASLayoutElement.mm; sourceTree = ""; }; E5711A2A1C840C81009619D4 /* ASCollectionElement.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASCollectionElement.h; sourceTree = ""; }; E5711A2D1C840C96009619D4 /* ASCollectionElement.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASCollectionElement.mm; sourceTree = ""; }; @@ -1484,7 +1484,7 @@ E58E9E3D1E941D74004CFC59 /* ASCollectionFlowLayoutDelegate.h */, E58E9E3E1E941D74004CFC59 /* ASCollectionFlowLayoutDelegate.m */, E54E81FA1EB357BD00FFE8E1 /* ASPageTable.h */, - E54E81FB1EB357BD00FFE8E1 /* ASPageTable.mm */, + E54E81FB1EB357BD00FFE8E1 /* ASPageTable.m */, ); name = "Collection Layout"; sourceTree = ""; @@ -2063,7 +2063,7 @@ 509E68601B3AED8E009B9150 /* ASScrollDirection.m in Sources */, B35062091B010EFD0018CF92 /* ASScrollNode.mm in Sources */, 8BDA5FC81CDBDF95007D13B2 /* ASVideoPlayerNode.mm in Sources */, - E54E81FD1EB357BD00FFE8E1 /* ASPageTable.mm in Sources */, + E54E81FD1EB357BD00FFE8E1 /* ASPageTable.m in Sources */, 34EFC7721B701D0300AD841F /* ASStackLayoutSpec.mm in Sources */, 7AB338661C55B3420055FDE8 /* ASRelativeLayoutSpec.mm in Sources */, 696F01EE1DD2AF450049FBD5 /* ASEventLog.mm in Sources */, diff --git a/Source/Details/ASCollectionLayoutState.h b/Source/Details/ASCollectionLayoutState.h index 30d65daf8..e70b951aa 100644 --- a/Source/Details/ASCollectionLayoutState.h +++ b/Source/Details/ASCollectionLayoutState.h @@ -38,9 +38,6 @@ AS_SUBCLASSING_RESTRICTED /// The final content size of the collection's layout @property (nonatomic, assign, readonly) CGSize contentSize; -/// The final content rect calculated from content size -@property (nonatomic, assign, readonly) CGRect contentRect; - - (instancetype)init __unavailable; /** @@ -75,7 +72,7 @@ AS_SUBCLASSING_RESTRICTED * * @param rect The rect containing the target elements. */ -- (nullable NSArray *)layoutAttributesForElementsInRect:(CGRect)rect; +- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect; /** * Returns layout attributes of the element at the specified index path. diff --git a/Source/Details/ASCollectionLayoutState.m b/Source/Details/ASCollectionLayoutState.m index 188a9a277..b066facbc 100644 --- a/Source/Details/ASCollectionLayoutState.m +++ b/Source/Details/ASCollectionLayoutState.m @@ -36,7 +36,7 @@ @implementation NSMapTable (ASCollectionLayoutConvenience) @implementation ASCollectionLayoutState { NSMapTable *_elementToLayoutAttributesTable; - ASPageTable *> *_pageToLayoutAttributesTable; + ASPageTable *> *_pageToLayoutAttributesTable; } - (instancetype)initWithContext:(ASCollectionLayoutContext *)context layout:(ASLayout *)layout @@ -74,7 +74,6 @@ - (instancetype)initWithContext:(ASCollectionLayoutContext *)context contentSize if (self) { _context = context; _contentSize = contentSize; - _contentRect = CGRectMake(0.0f, 0.0f, contentSize.width, contentSize.height); _elementToLayoutAttributesTable = table; _pageToLayoutAttributesTable = [ASPageTable pageTableWithLayoutAttributes:_elementToLayoutAttributesTable.objectEnumerator pageSize:context.viewportSize]; } @@ -91,28 +90,29 @@ - (instancetype)initWithContext:(ASCollectionLayoutContext *)context contentSize CGSize pageSize = _context.viewportSize; NSPointerArray *pages = ASPageCoordinatesForPagesThatIntersectRect(rect, _contentSize, pageSize); if (pages.count == 0) { - return nil; + return @[]; } - NSMutableArray *results = [NSMutableArray array]; + // TODO make sure items in the result are unique (use mutable set) + NSMutableArray *result = [NSMutableArray array]; for (id pagePtr in pages) { ASPageCoordinate page = (ASPageCoordinate)pagePtr; - NSSet *allAttrs = [_pageToLayoutAttributesTable objectForPage:page]; + NSArray *allAttrs = [_pageToLayoutAttributesTable objectForPage:page]; if (allAttrs.count > 0) { CGRect pageRect = ASPageCoordinateGetPageRect(page, pageSize); if (CGRectContainsRect(rect, pageRect)) { - [results addObjectsFromArray:[allAttrs allObjects]]; + [result addObjectsFromArray:allAttrs]; } else { for (UICollectionViewLayoutAttributes *attrs in allAttrs) { if (CGRectIntersectsRect(rect, attrs.frame)) { - [results addObject:attrs]; + [result addObject:attrs]; } } } } } - return results; + return result; } - (UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath diff --git a/Source/Details/ASPageTable.h b/Source/Details/ASPageTable.h index a8c996d28..d2f277e35 100644 --- a/Source/Details/ASPageTable.h +++ b/Source/Details/ASPageTable.h @@ -91,14 +91,14 @@ typedef NSMapTable ASPageTable; * * @param pageSize The size of each page. */ -+ (ASPageTable *> *)pageTableWithLayoutAttributes:(id)layoutAttributesEnumerator pageSize:(CGSize)pageSize; ++ (ASPageTable *> *)pageTableWithLayoutAttributes:(id)layoutAttributesEnumerator pageSize:(CGSize)pageSize; /** * Retrieves the object for a given page, or nil if the page is not found. * * @param page A page to lookup the object for. */ -- (ObjectType)objectForPage:(ASPageCoordinate)page; +- (nullable ObjectType)objectForPage:(ASPageCoordinate)page; /** * Sets the given object for the associated page. diff --git a/Source/Details/ASPageTable.mm b/Source/Details/ASPageTable.m similarity index 87% rename from Source/Details/ASPageTable.mm rename to Source/Details/ASPageTable.m index 7d1552f7a..0f500fa59 100644 --- a/Source/Details/ASPageTable.mm +++ b/Source/Details/ASPageTable.m @@ -53,13 +53,13 @@ extern CGRect ASPageCoordinateGetPageRect(ASPageCoordinate pageCoordinate, CGSiz return nil; } - NSPointerArray *results = [NSPointerArray pointerArrayWithOptions:(NSPointerFunctionsIntegerPersonality | NSPointerFunctionsOpaqueMemory)]; + NSPointerArray *result = [NSPointerArray pointerArrayWithOptions:(NSPointerFunctionsIntegerPersonality | NSPointerFunctionsOpaqueMemory)]; ASPageCoordinate minPage = ASPageCoordinateForPageThatContainsPoint(CGPointMake(CGRectGetMinX(rect), CGRectGetMinY(rect)), pageSize); ASPageCoordinate maxPage = ASPageCoordinateForPageThatContainsPoint(CGPointMake(CGRectGetMaxX(rect), CGRectGetMaxY(rect)), pageSize); if (minPage == maxPage) { - [results addPointer:(void *)minPage]; - return results; + [result addPointer:(void *)minPage]; + return result; } NSUInteger minX = ASPageCoordinateGetX(minPage); @@ -70,11 +70,11 @@ extern CGRect ASPageCoordinateGetPageRect(ASPageCoordinate pageCoordinate, CGSiz for (NSUInteger x = minX; x <= maxX; x++) { for (NSUInteger y = minY; y <= maxY; y++) { ASPageCoordinate page = ASPageCoordinateMake(x, y); - [results addPointer:(void *)page]; + [result addPointer:(void *)page]; } } - return results; + return result; } @implementation NSMapTable (ASPageTableMethods) @@ -110,14 +110,15 @@ + (ASPageTable *)pageTableForWeakObjectPointers return [self pageTableWithValuePointerFunctions:weakObjectPointerFuncs]; } -+ (ASPageTable *)pageTableWithLayoutAttributes:(id)layoutAttributesEnumerator pageSize:(CGSize)pageSize ++ (ASPageTable *> *)pageTableWithLayoutAttributes:(id)layoutAttributesEnumerator pageSize:(CGSize)pageSize { ASPageTable *result = [ASPageTable pageTableForStrongObjectPointers]; for (UICollectionViewLayoutAttributes *attrs in layoutAttributesEnumerator) { + // TODO this attrs may span multiple pages. Make sure we include all of the pages ASPageCoordinate page = ASPageCoordinateForPageThatContainsPoint(attrs.frame.origin, pageSize); - NSMutableSet *attrsInPage = [result objectForPage:page]; + NSMutableArray *attrsInPage = [result objectForPage:page]; if (attrsInPage == nil) { - attrsInPage = [NSMutableSet set]; + attrsInPage = [NSMutableArray array]; [result setObject:attrsInPage forPage:page]; } [attrsInPage addObject:attrs]; diff --git a/Source/Private/ASCollectionLayout.mm b/Source/Private/ASCollectionLayout.mm index 645f7a24f..2ca86f478 100644 --- a/Source/Private/ASCollectionLayout.mm +++ b/Source/Private/ASCollectionLayout.mm @@ -119,17 +119,15 @@ - (CGSize)collectionViewContentSize - (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect { ASDisplayNodeAssertMainThread(); - if (CGRectIsEmpty(rect) || (! CGRectIntersectsRect(_layout.contentRect, rect))) { - return @[]; - } + NSArray *result = [_layout layoutAttributesForElementsInRect:rect]; - NSArray *results = [_layout layoutAttributesForElementsInRect:rect]; ASElementMap *elements = _layout.context.elements; - for (UICollectionViewLayoutAttributes *attrs in results) { + for (UICollectionViewLayoutAttributes *attrs in result) { ASCollectionElement *element = [elements elementForLayoutAttributes:attrs]; [ASCollectionLayout setSize:attrs.frame.size toElement:element]; } - return results; + + return result; } - (UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath From 492004bfe1a81276d79586f8e338f59c6049f948 Mon Sep 17 00:00:00 2001 From: Huy Nguyen Date: Thu, 4 May 2017 12:06:05 +0100 Subject: [PATCH 3/4] Handle items that span multiple pages --- Source/Details/ASCollectionLayoutState.m | 8 ++++---- Source/Details/ASPageTable.h | 6 ++++-- Source/Details/ASPageTable.m | 20 ++++++++++++-------- 3 files changed, 20 insertions(+), 14 deletions(-) diff --git a/Source/Details/ASCollectionLayoutState.m b/Source/Details/ASCollectionLayoutState.m index b066facbc..ecb7e1503 100644 --- a/Source/Details/ASCollectionLayoutState.m +++ b/Source/Details/ASCollectionLayoutState.m @@ -75,7 +75,7 @@ - (instancetype)initWithContext:(ASCollectionLayoutContext *)context contentSize _context = context; _contentSize = contentSize; _elementToLayoutAttributesTable = table; - _pageToLayoutAttributesTable = [ASPageTable pageTableWithLayoutAttributes:_elementToLayoutAttributesTable.objectEnumerator pageSize:context.viewportSize]; + _pageToLayoutAttributesTable = [ASPageTable pageTableWithLayoutAttributes:_elementToLayoutAttributesTable.objectEnumerator contentSize:contentSize pageSize:context.viewportSize]; } return self; } @@ -93,8 +93,8 @@ - (instancetype)initWithContext:(ASCollectionLayoutContext *)context contentSize return @[]; } - // TODO make sure items in the result are unique (use mutable set) - NSMutableArray *result = [NSMutableArray array]; + // Use a mutable set here because some items may span multiple pages + NSMutableSet *result = [NSMutableSet set]; for (id pagePtr in pages) { ASPageCoordinate page = (ASPageCoordinate)pagePtr; NSArray *allAttrs = [_pageToLayoutAttributesTable objectForPage:page]; @@ -112,7 +112,7 @@ - (instancetype)initWithContext:(ASCollectionLayoutContext *)context contentSize } } } - return result; + return [result allObjects]; } - (UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath diff --git a/Source/Details/ASPageTable.h b/Source/Details/ASPageTable.h index d2f277e35..d7136f8c7 100644 --- a/Source/Details/ASPageTable.h +++ b/Source/Details/ASPageTable.h @@ -53,7 +53,7 @@ extern CGRect ASPageCoordinateGetPageRect(ASPageCoordinate pageCoordinate, CGSiz * * @param rect The rect intersecting the target pages. * - * @param contentRect The combined rect of all pages. + * @param contentSize The combined size of all pages. * * @param pageSize The size of each page. */ @@ -89,9 +89,11 @@ typedef NSMapTable ASPageTable; * * @param layoutAttributesEnumerator The layout attributes to build from * + * @param contentSize The combined size of all pages. + * * @param pageSize The size of each page. */ -+ (ASPageTable *> *)pageTableWithLayoutAttributes:(id)layoutAttributesEnumerator pageSize:(CGSize)pageSize; ++ (ASPageTable *> *)pageTableWithLayoutAttributes:(id)layoutAttributesEnumerator contentSize:(CGSize)contentSize pageSize:(CGSize)pageSize; /** * Retrieves the object for a given page, or nil if the page is not found. diff --git a/Source/Details/ASPageTable.m b/Source/Details/ASPageTable.m index 0f500fa59..a59774cbe 100644 --- a/Source/Details/ASPageTable.m +++ b/Source/Details/ASPageTable.m @@ -110,18 +110,22 @@ + (ASPageTable *)pageTableForWeakObjectPointers return [self pageTableWithValuePointerFunctions:weakObjectPointerFuncs]; } -+ (ASPageTable *> *)pageTableWithLayoutAttributes:(id)layoutAttributesEnumerator pageSize:(CGSize)pageSize ++ (ASPageTable *> *)pageTableWithLayoutAttributes:(id)layoutAttributesEnumerator contentSize:(CGSize)contentSize pageSize:(CGSize)pageSize { ASPageTable *result = [ASPageTable pageTableForStrongObjectPointers]; for (UICollectionViewLayoutAttributes *attrs in layoutAttributesEnumerator) { - // TODO this attrs may span multiple pages. Make sure we include all of the pages - ASPageCoordinate page = ASPageCoordinateForPageThatContainsPoint(attrs.frame.origin, pageSize); - NSMutableArray *attrsInPage = [result objectForPage:page]; - if (attrsInPage == nil) { - attrsInPage = [NSMutableArray array]; - [result setObject:attrsInPage forPage:page]; + // This attrs may span multiple pages. Make sure it's registered to all of them + NSPointerArray *pages = ASPageCoordinatesForPagesThatIntersectRect(attrs.frame, contentSize, pageSize); + + for (id pagePtr in pages) { + ASPageCoordinate page = (ASPageCoordinate)pagePtr; + NSMutableArray *attrsInPage = [result objectForPage:page]; + if (attrsInPage == nil) { + attrsInPage = [NSMutableArray array]; + [result setObject:attrsInPage forPage:page]; + } + [attrsInPage addObject:attrs]; } - [attrsInPage addObject:attrs]; } return result; } From f53b3da0043de34b3a04a6127d1ea49271f7c4b3 Mon Sep 17 00:00:00 2001 From: Huy Nguyen Date: Thu, 4 May 2017 12:13:16 +0100 Subject: [PATCH 4/4] Make danger happy --- Source/Details/ASPageTable.m | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/Details/ASPageTable.m b/Source/Details/ASPageTable.m index a59774cbe..626ca975f 100644 --- a/Source/Details/ASPageTable.m +++ b/Source/Details/ASPageTable.m @@ -1,5 +1,5 @@ // -// ASPageTable.mm +// ASPageTable.m // Texture // // Copyright (c) 2017-present, Pinterest, Inc. All rights reserved.