Skip to content

Commit

Permalink
Implement ASPageTable (#81)
Browse files Browse the repository at this point in the history
* 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!).

* Address comments

* Handle items that span multiple pages

* Make danger happy
  • Loading branch information
nguyenhuy authored May 4, 2017
1 parent 4cbf278 commit ca2885c
Show file tree
Hide file tree
Showing 10 changed files with 467 additions and 68 deletions.
8 changes: 8 additions & 0 deletions AsyncDisplayKit.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -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.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 */; };
Expand Down Expand Up @@ -822,6 +824,8 @@
E516FC7E1E9FE24200714FF4 /* ASHashing.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASHashing.m; sourceTree = "<group>"; };
E52405B21C8FEF03004DC8E7 /* ASLayoutTransition.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASLayoutTransition.mm; sourceTree = "<group>"; };
E52405B41C8FEF16004DC8E7 /* ASLayoutTransition.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASLayoutTransition.h; sourceTree = "<group>"; };
E54E81FA1EB357BD00FFE8E1 /* ASPageTable.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASPageTable.h; sourceTree = "<group>"; };
E54E81FB1EB357BD00FFE8E1 /* ASPageTable.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASPageTable.m; sourceTree = "<group>"; };
E55D86311CA8A14000A0C26F /* ASLayoutElement.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASLayoutElement.mm; sourceTree = "<group>"; };
E5711A2A1C840C81009619D4 /* ASCollectionElement.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASCollectionElement.h; sourceTree = "<group>"; };
E5711A2D1C840C96009619D4 /* ASCollectionElement.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASCollectionElement.mm; sourceTree = "<group>"; };
Expand Down Expand Up @@ -1479,6 +1483,8 @@
E58E9E411E941D74004CFC59 /* ASCollectionLayoutDelegate.h */,
E58E9E3D1E941D74004CFC59 /* ASCollectionFlowLayoutDelegate.h */,
E58E9E3E1E941D74004CFC59 /* ASCollectionFlowLayoutDelegate.m */,
E54E81FA1EB357BD00FFE8E1 /* ASPageTable.h */,
E54E81FB1EB357BD00FFE8E1 /* ASPageTable.m */,
);
name = "Collection Layout";
sourceTree = "<group>";
Expand Down Expand Up @@ -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 */,
Expand Down Expand Up @@ -2056,6 +2063,7 @@
509E68601B3AED8E009B9150 /* ASScrollDirection.m in Sources */,
B35062091B010EFD0018CF92 /* ASScrollNode.mm in Sources */,
8BDA5FC81CDBDF95007D13B2 /* ASVideoPlayerNode.mm in Sources */,
E54E81FD1EB357BD00FFE8E1 /* ASPageTable.m in Sources */,
34EFC7721B701D0300AD841F /* ASStackLayoutSpec.mm in Sources */,
7AB338661C55B3420055FDE8 /* ASRelativeLayoutSpec.mm in Sources */,
696F01EE1DD2AF450049FBD5 /* ASEventLog.mm in Sources */,
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
8 changes: 7 additions & 1 deletion Source/Details/ASCollectionFlowLayoutDelegate.h
Original file line number Diff line number Diff line change
Expand Up @@ -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 <ASCollectionLayoutDelegate>

- (instancetype)initWithScrollableDirections:(ASScrollDirection)scrollableDirections;
- (instancetype)initWithScrollableDirections:(ASScrollDirection)scrollableDirections NS_DESIGNATED_INITIALIZER;

@end

Expand Down
16 changes: 6 additions & 10 deletions Source/Details/ASCollectionFlowLayoutDelegate.m
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down Expand Up @@ -71,9 +67,9 @@ - (ASCollectionLayoutState *)calculateLayoutWithContext:(ASCollectionLayoutConte
ASElementMap *elements = context.elements;
NSMutableArray<ASCellNode *> *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
Expand All @@ -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
2 changes: 2 additions & 0 deletions Source/Details/ASCollectionLayoutDelegate.h
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
66 changes: 52 additions & 14 deletions Source/Details/ASCollectionLayoutState.h
Original file line number Diff line number Diff line change
Expand Up @@ -19,45 +19,83 @@
#import <UIKit/UIKit.h>
#import <AsyncDisplayKit/ASBaseDefines.h>

@class ASElementMap, ASCollectionElement, ASLayout;
@class ASCollectionLayoutContext, ASLayout, ASCollectionElement;

NS_ASSUME_NONNULL_BEGIN

@interface NSMapTable (ASCollectionLayoutConvenience)

+ (NSMapTable<ASCollectionElement *, UICollectionViewLayoutAttributes *> *)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<ASCollectionElement *, UICollectionViewLayoutAttributes *> *elementToLayoutArrtibutesMap;

- (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<ASCollectionElement *, UICollectionViewLayoutAttributes *> *)elementToLayoutArrtibutesMap NS_DESIGNATED_INITIALIZER;
- (instancetype)initWithContext:(ASCollectionLayoutContext *)context contentSize:(CGSize)contentSize elementToLayoutAttributesTable:(NSMapTable<ASCollectionElement *, UICollectionViewLayoutAttributes *> *)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<UICollectionViewLayoutAttributes *> *)allLayoutAttributes;

/**
* Returns layout attributes of elements in the specified rect.
*
* @param layout The layout describes size and position of all elements, or a subset of them and will be updated later.
* @param rect The rect containing the target elements.
*/
- (NSArray<UICollectionViewLayoutAttributes *> *)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 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

Expand Down
90 changes: 82 additions & 8 deletions Source/Details/ASCollectionLayoutState.m
Original file line number Diff line number Diff line change
Expand Up @@ -20,16 +20,37 @@
#import <AsyncDisplayKit/ASAssert.h>
#import <AsyncDisplayKit/ASCellNode+Internal.h>
#import <AsyncDisplayKit/ASCollectionElement.h>
#import <AsyncDisplayKit/ASCollectionLayoutContext.h>
#import <AsyncDisplayKit/ASElementMap.h>
#import <AsyncDisplayKit/ASLayout.h>
#import <AsyncDisplayKit/ASPageTable.h>

@implementation ASCollectionLayoutState
@implementation NSMapTable (ASCollectionLayoutConvenience)

- (instancetype)initWithElements:(ASElementMap *)elements layout:(ASLayout *)layout
+ (NSMapTable<ASCollectionElement *,UICollectionViewLayoutAttributes *> *)elementToLayoutAttributesTable
{
NSMapTable<ASCollectionElement *, UICollectionViewLayoutAttributes *> *attrsMap = [NSMapTable mapTableWithKeyOptions:(NSMapTableObjectPointerPersonality | NSMapTableWeakMemory) valueOptions:NSMapTableStrongMemory];
return [NSMapTable mapTableWithKeyOptions:(NSMapTableWeakMemory | NSMapTableObjectPointerPersonality) valueOptions:NSMapTableStrongMemory];
}

@end

@implementation ASCollectionLayoutState {
NSMapTable<ASCollectionElement *,UICollectionViewLayoutAttributes *> *_elementToLayoutAttributesTable;
ASPageTable<id, NSMutableArray<UICollectionViewLayoutAttributes *> *> *_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;

Expand All @@ -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<ASCollectionElement *,UICollectionViewLayoutAttributes *> *)attrsMap
- (instancetype)initWithContext:(ASCollectionLayoutContext *)context contentSize:(CGSize)contentSize elementToLayoutAttributesTable:(NSMapTable<ASCollectionElement *,UICollectionViewLayoutAttributes *> *)table
{
self = [super init];
if (self) {
_elements = elements;
_context = context;
_contentSize = contentSize;
_elementToLayoutArrtibutesMap = attrsMap;
_elementToLayoutAttributesTable = table;
_pageToLayoutAttributesTable = [ASPageTable pageTableWithLayoutAttributes:_elementToLayoutAttributesTable.objectEnumerator contentSize:contentSize pageSize:context.viewportSize];
}
return self;
}

- (NSArray<UICollectionViewLayoutAttributes *> *)allLayoutAttributes
{
return [_elementToLayoutAttributesTable.objectEnumerator allObjects];
}

- (NSArray<UICollectionViewLayoutAttributes *> *)layoutAttributesForElementsInRect:(CGRect)rect
{
CGSize pageSize = _context.viewportSize;
NSPointerArray *pages = ASPageCoordinatesForPagesThatIntersectRect(rect, _contentSize, pageSize);
if (pages.count == 0) {
return @[];
}

// Use a mutable set here because some items may span multiple pages
NSMutableSet<UICollectionViewLayoutAttributes *> *result = [NSMutableSet set];
for (id pagePtr in pages) {
ASPageCoordinate page = (ASPageCoordinate)pagePtr;
NSArray<UICollectionViewLayoutAttributes *> *allAttrs = [_pageToLayoutAttributesTable objectForPage:page];
if (allAttrs.count > 0) {
CGRect pageRect = ASPageCoordinateGetPageRect(page, pageSize);

if (CGRectContainsRect(rect, pageRect)) {
[result addObjectsFromArray:allAttrs];
} else {
for (UICollectionViewLayoutAttributes *attrs in allAttrs) {
if (CGRectIntersectsRect(rect, attrs.frame)) {
[result addObject:attrs];
}
}
}
}
}
return [result allObjects];
}

- (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
Loading

0 comments on commit ca2885c

Please sign in to comment.