From 169641ec1924294c7a7b6bd32de62fb0b67d4a78 Mon Sep 17 00:00:00 2001 From: Huy Nguyen Date: Wed, 19 Apr 2017 18:38:26 +0100 Subject: [PATCH 01/25] Implement ASCollectionGalleryLayoutDelegate - It arranges items of the same size into a multi-line stack (say photo gallery or pager). It takes advantage of the fact that its items always have a fixed size to measure as few items as possible while still being able to track their positions at all time. This helps reduce startup/reloadData time, as well as memory footprint. - It then uses a measure range, which also works as a allocate range, to figure out which items to measure ahead of time. And it guarantees that each item is scheduled to measure only once. - Lastly, ASCollectionLayoutDelegate has some new methods that allow delegates to hook up and stay ahead of layout attributes requests from the backing view. ASCollectionGalleryLayoutDelegate for example uses these methods to ensure elements that have their layout attributes requested are always ready for consumption, and to measure more elements in the background. --- AsyncDisplayKit.xcodeproj/project.pbxproj | 32 +- CHANGELOG.md | 1 + Source/AsyncDisplayKit.h | 1 + .../Details/ASCollectionFlowLayoutDelegate.m | 24 +- .../ASCollectionGalleryLayoutDelegate.h | 32 ++ .../ASCollectionGalleryLayoutDelegate.mm | 387 ++++++++++++++++++ Source/Details/ASCollectionLayoutDelegate.h | 50 +++ Source/Details/ASCollectionLayoutState.h | 16 +- Source/Details/ASCollectionLayoutState.m | 10 +- Source/Private/ASCollectionLayout.mm | 27 +- Source/Private/ASCollectionLayoutDefines.h | 27 ++ Source/Private/ASCollectionLayoutDefines.m | 27 ++ .../ASCollectionView/Sample/ViewController.m | 23 +- 13 files changed, 615 insertions(+), 42 deletions(-) create mode 100644 Source/Details/ASCollectionGalleryLayoutDelegate.h create mode 100644 Source/Details/ASCollectionGalleryLayoutDelegate.mm create mode 100644 Source/Private/ASCollectionLayoutDefines.h create mode 100644 Source/Private/ASCollectionLayoutDefines.m diff --git a/AsyncDisplayKit.xcodeproj/project.pbxproj b/AsyncDisplayKit.xcodeproj/project.pbxproj index 58ac2eec4..5647edee0 100644 --- a/AsyncDisplayKit.xcodeproj/project.pbxproj +++ b/AsyncDisplayKit.xcodeproj/project.pbxproj @@ -432,6 +432,8 @@ 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 */; }; + E5855DEF1EBB4D83003639AE /* ASCollectionLayoutDefines.m in Sources */ = {isa = PBXBuildFile; fileRef = E5855DED1EBB4D83003639AE /* ASCollectionLayoutDefines.m */; }; + E5855DF01EBB4D83003639AE /* ASCollectionLayoutDefines.h in Headers */ = {isa = PBXBuildFile; fileRef = E5855DEE1EBB4D83003639AE /* ASCollectionLayoutDefines.h */; }; E58E9E421E941D74004CFC59 /* ASCollectionFlowLayoutDelegate.h in Headers */ = {isa = PBXBuildFile; fileRef = E58E9E3D1E941D74004CFC59 /* ASCollectionFlowLayoutDelegate.h */; settings = {ATTRIBUTES = (Public, ); }; }; E58E9E431E941D74004CFC59 /* ASCollectionFlowLayoutDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = E58E9E3E1E941D74004CFC59 /* ASCollectionFlowLayoutDelegate.m */; }; E58E9E441E941D74004CFC59 /* ASCollectionLayoutContext.h in Headers */ = {isa = PBXBuildFile; fileRef = E58E9E3F1E941D74004CFC59 /* ASCollectionLayoutContext.h */; settings = {ATTRIBUTES = (Public, ); }; }; @@ -448,6 +450,8 @@ E5C347B31ECB40AA00EC4BE4 /* ASTableNode+Beta.h in Headers */ = {isa = PBXBuildFile; fileRef = E5C347B21ECB40AA00EC4BE4 /* ASTableNode+Beta.h */; }; E5E281741E71C833006B67C2 /* ASCollectionLayoutState.h in Headers */ = {isa = PBXBuildFile; fileRef = E5E281731E71C833006B67C2 /* ASCollectionLayoutState.h */; settings = {ATTRIBUTES = (Public, ); }; }; E5E281761E71C845006B67C2 /* ASCollectionLayoutState.m in Sources */ = {isa = PBXBuildFile; fileRef = E5E281751E71C845006B67C2 /* ASCollectionLayoutState.m */; }; + E5E2D72E1EA780C4005C24C6 /* ASCollectionGalleryLayoutDelegate.h in Headers */ = {isa = PBXBuildFile; fileRef = E5E2D72D1EA780C4005C24C6 /* ASCollectionGalleryLayoutDelegate.h */; settings = {ATTRIBUTES = (Public, ); }; }; + E5E2D7301EA780DF005C24C6 /* ASCollectionGalleryLayoutDelegate.mm in Sources */ = {isa = PBXBuildFile; fileRef = E5E2D72F1EA780DF005C24C6 /* ASCollectionGalleryLayoutDelegate.mm */; }; F711994E1D20C21100568860 /* ASDisplayNodeExtrasTests.m in Sources */ = {isa = PBXBuildFile; fileRef = F711994D1D20C21100568860 /* ASDisplayNodeExtrasTests.m */; }; /* End PBXBuildFile section */ @@ -909,6 +913,8 @@ 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 = ""; }; + E5855DED1EBB4D83003639AE /* ASCollectionLayoutDefines.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASCollectionLayoutDefines.m; sourceTree = ""; }; + E5855DEE1EBB4D83003639AE /* ASCollectionLayoutDefines.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASCollectionLayoutDefines.h; sourceTree = ""; }; E58E9E3D1E941D74004CFC59 /* ASCollectionFlowLayoutDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASCollectionFlowLayoutDelegate.h; sourceTree = ""; }; E58E9E3E1E941D74004CFC59 /* ASCollectionFlowLayoutDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASCollectionFlowLayoutDelegate.m; sourceTree = ""; }; E58E9E3F1E941D74004CFC59 /* ASCollectionLayoutContext.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASCollectionLayoutContext.h; sourceTree = ""; }; @@ -925,6 +931,8 @@ E5C347B21ECB40AA00EC4BE4 /* ASTableNode+Beta.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "ASTableNode+Beta.h"; sourceTree = ""; }; E5E281731E71C833006B67C2 /* ASCollectionLayoutState.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASCollectionLayoutState.h; sourceTree = ""; }; E5E281751E71C845006B67C2 /* ASCollectionLayoutState.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASCollectionLayoutState.m; sourceTree = ""; }; + E5E2D72D1EA780C4005C24C6 /* ASCollectionGalleryLayoutDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASCollectionGalleryLayoutDelegate.h; sourceTree = ""; }; + E5E2D72F1EA780DF005C24C6 /* ASCollectionGalleryLayoutDelegate.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; lineEnding = 0; path = ASCollectionGalleryLayoutDelegate.mm; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.objcpp; }; EFA731F0396842FF8AB635EE /* libPods-AsyncDisplayKitTests.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-AsyncDisplayKitTests.a"; sourceTree = BUILT_PRODUCTS_DIR; }; F711994D1D20C21100568860 /* ASDisplayNodeExtrasTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASDisplayNodeExtrasTests.m; sourceTree = ""; }; FB07EABBCF28656C6297BC2D /* Pods-AsyncDisplayKitTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-AsyncDisplayKitTests.debug.xcconfig"; path = "Pods/Target Support Files/Pods-AsyncDisplayKitTests/Pods-AsyncDisplayKitTests.debug.xcconfig"; sourceTree = ""; }; @@ -1334,6 +1342,7 @@ CC55A7101E52A0F200594372 /* ASResponderChainEnumerator.m */, 6947B0BB1E36B4E30007C478 /* Layout */, CCE04B2A1E313EDA006AEBBB /* Collection Data Adapter */, + E52F8AEE1EAE659600B5A912 /* Collection Layout */, 058D0A03195D050800B7D73C /* _ASCoreAnimationExtras.h */, 058D0A04195D050800B7D73C /* _ASCoreAnimationExtras.mm */, AC026B6D1BD57DBF00BBC17E /* _ASHierarchyChangeSet.h */, @@ -1347,9 +1356,6 @@ 044285051BAA63FE00D16268 /* ASBatchFetching.h */, 044285061BAA63FE00D16268 /* ASBatchFetching.m */, CC87BB941DA8193C0090E380 /* ASCellNode+Internal.h */, - E58E9E471E941DA5004CFC59 /* ASCollectionLayout.h */, - E58E9E481E941DA5004CFC59 /* ASCollectionLayout.mm */, - E5B5B9D01E9BAD9800A6B726 /* ASCollectionLayoutContext+Private.h */, CC2E317F1DAC353700EEE891 /* ASCollectionView+Undeprecated.h */, CC0F885A1E42807F00576FED /* ASCollectionViewFlowLayoutInspector.h */, CC0F88591E42807F00576FED /* ASCollectionViewFlowLayoutInspector.m */, @@ -1637,6 +1643,18 @@ path = Debug; sourceTree = ""; }; + E52F8AEE1EAE659600B5A912 /* Collection Layout */ = { + isa = PBXGroup; + children = ( + E58E9E471E941DA5004CFC59 /* ASCollectionLayout.h */, + E58E9E481E941DA5004CFC59 /* ASCollectionLayout.mm */, + E5B5B9D01E9BAD9800A6B726 /* ASCollectionLayoutContext+Private.h */, + E5855DEE1EBB4D83003639AE /* ASCollectionLayoutDefines.h */, + E5855DED1EBB4D83003639AE /* ASCollectionLayoutDefines.m */, + ); + name = "Collection Layout"; + sourceTree = ""; + }; E5B077EB1E6843AF00C24B5B /* Collection Layout */ = { isa = PBXGroup; children = ( @@ -1647,6 +1665,8 @@ E58E9E411E941D74004CFC59 /* ASCollectionLayoutDelegate.h */, E58E9E3D1E941D74004CFC59 /* ASCollectionFlowLayoutDelegate.h */, E58E9E3E1E941D74004CFC59 /* ASCollectionFlowLayoutDelegate.m */, + E5E2D72D1EA780C4005C24C6 /* ASCollectionGalleryLayoutDelegate.h */, + E5E2D72F1EA780DF005C24C6 /* ASCollectionGalleryLayoutDelegate.mm */, E54E81FA1EB357BD00FFE8E1 /* ASPageTable.h */, E54E81FB1EB357BD00FFE8E1 /* ASPageTable.m */, ); @@ -1672,6 +1692,7 @@ files = ( CC034A131E649F1300626263 /* AsyncDisplayKit+IGListKitMethods.h in Headers */, 693A1DCA1ECC944E00D0C9D2 /* IGListAdapter+AsyncDisplayKit.h in Headers */, + E5E2D72E1EA780C4005C24C6 /* ASCollectionGalleryLayoutDelegate.h in Headers */, E58E9E461E941D74004CFC59 /* ASCollectionLayoutDelegate.h in Headers */, E5E281741E71C833006B67C2 /* ASCollectionLayoutState.h in Headers */, E5B077FF1E69F4EB00C24B5B /* ASElementMap.h in Headers */, @@ -1695,6 +1716,7 @@ B13CA1011C52004900E031AB /* ASCollectionNode+Beta.h in Headers */, 68C215581DE10D330019C4BC /* ASCollectionViewLayoutInspector.h in Headers */, B35062411B010EFD0018CF92 /* _ASAsyncTransactionGroup.h in Headers */, + E5855DF01EBB4D83003639AE /* ASCollectionLayoutDefines.h in Headers */, B350620F1B010EFD0018CF92 /* _ASDisplayLayer.h in Headers */, CCCCCCD71EC3EF060087FE10 /* ASTextInput.h in Headers */, B35062111B010EFD0018CF92 /* _ASDisplayView.h in Headers */, @@ -1789,6 +1811,7 @@ B350624F1B010EFD0018CF92 /* ASDisplayNode+DebugTiming.h in Headers */, CC57EAF71E3939350034C595 /* ASCollectionView+Undeprecated.h in Headers */, B35062521B010EFD0018CF92 /* ASDisplayNodeInternal.h in Headers */, + E52F8AF21EAE6ADD00B5A912 /* (null) in Headers */, AC7A2C181BDE11DF0093FE1A /* ASTableViewInternal.h in Headers */, B35062531B010EFD0018CF92 /* ASImageNode+CGExtras.h in Headers */, E58E9E491E941DA5004CFC59 /* ASCollectionLayout.h in Headers */, @@ -2226,6 +2249,7 @@ E58E9E4A1E941DA5004CFC59 /* ASCollectionLayout.mm in Sources */, 6947B0C01E36B4E30007C478 /* ASStackUnpositionedLayout.mm in Sources */, 68355B401CB57A69001D4E68 /* ASImageContainerProtocolCategories.m in Sources */, + E5855DEF1EBB4D83003639AE /* ASCollectionLayoutDefines.m in Sources */, B35062031B010EFD0018CF92 /* ASImageNode.mm in Sources */, 254C6B821BF94F8A003EC431 /* ASTextKitComponents.mm in Sources */, 34EFC7601B701C8B00AD841F /* ASInsetLayoutSpec.mm in Sources */, @@ -2236,6 +2260,7 @@ CCCCCCE01EC3EF060087FE10 /* ASTextRunDelegate.m in Sources */, CCCCCCDA1EC3EF060087FE10 /* ASTextLayout.m in Sources */, 254C6B841BF94F8A003EC431 /* ASTextNodeWordKerner.m in Sources */, + E5E2D7301EA780DF005C24C6 /* ASCollectionGalleryLayoutDelegate.mm in Sources */, 34EFC76B1B701CEB00AD841F /* ASLayoutSpec.mm in Sources */, CC3B20861C3F76D600798563 /* ASPendingStateController.mm in Sources */, 254C6B8C1BF94F8A003EC431 /* ASTextKitTailTruncater.mm in Sources */, @@ -2259,6 +2284,7 @@ E5ABAC7C1E8564EE007AC15C /* ASRectTable.m in Sources */, CCA282C91E9EB64B0037E8B7 /* ASDisplayNodeTipState.m in Sources */, 509E68601B3AED8E009B9150 /* ASScrollDirection.m in Sources */, + E52F8AFA1EAF600300B5A912 /* (null) in Sources */, B35062091B010EFD0018CF92 /* ASScrollNode.mm in Sources */, 69BCE3D91EC6513B007DCCAD /* ASDisplayNode+Layout.mm in Sources */, 8BDA5FC81CDBDF95007D13B2 /* ASVideoPlayerNode.mm in Sources */, diff --git a/CHANGELOG.md b/CHANGELOG.md index 3f585d2bc..b39354c6d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ * Add your own contributions to the next release on the line below this with your name. - [ASTextNode2] Add initial implementation for link handling. [Scott Goodson](https://github.com/appleguy) [#396](https://github.com/TextureGroup/Texture/pull/396) - [ASTextNode2] Provide compile flag to globally enable new implementation of ASTextNode: ASTEXTNODE_EXPERIMENT_GLOBAL_ENABLE. [Scott Goodson](https://github.com/appleguy) [#396](https://github.com/TextureGroup/Texture/pull/410) + - Add ASCollectionGalleryLayoutDelegate - an async collection layout that makes same-size collections (e.g photo galleries, pagers, etc) fast and lightweight! [Huy Nguyen](https://github.com/nguyenhuy/) [#76](https://github.com/TextureGroup/Texture/pull/76) ##2.3.5 - Fix an issue where inserting/deleting sections could lead to inconsistent supplementary element behavior. [Adlai Holler](https://github.com/Adlai-Holler) diff --git a/Source/AsyncDisplayKit.h b/Source/AsyncDisplayKit.h index 2c319b0f2..665f9b422 100644 --- a/Source/AsyncDisplayKit.h +++ b/Source/AsyncDisplayKit.h @@ -52,6 +52,7 @@ #import #import #import +#import #import #import diff --git a/Source/Details/ASCollectionFlowLayoutDelegate.m b/Source/Details/ASCollectionFlowLayoutDelegate.m index 009ba2123..40c0e2e9d 100644 --- a/Source/Details/ASCollectionFlowLayoutDelegate.m +++ b/Source/Details/ASCollectionFlowLayoutDelegate.m @@ -17,10 +17,11 @@ #import -#import +#import #import #import #import +#import #import #import #import @@ -43,20 +44,6 @@ - (instancetype)initWithScrollableDirections:(ASScrollDirection)scrollableDirect return self; } -- (ASSizeRange)sizeRangeThatFits:(CGSize)viewportSize -{ - ASSizeRange sizeRange = ASSizeRangeUnconstrained; - if (ASScrollDirectionContainsVerticalDirection(_scrollableDirections) == NO) { - sizeRange.min.height = viewportSize.height; - sizeRange.max.height = viewportSize.height; - } - if (ASScrollDirectionContainsHorizontalDirection(_scrollableDirections) == NO) { - sizeRange.min.width = viewportSize.width; - sizeRange.max.width = viewportSize.width; - } - return sizeRange; -} - - (id)additionalInfoForLayoutWithElements:(ASElementMap *)elements { return nil; @@ -69,6 +56,7 @@ - (ASCollectionLayoutState *)calculateLayoutWithContext:(ASCollectionLayoutConte if (children.count == 0) { return [[ASCollectionLayoutState alloc] initWithContext:context contentSize:CGSizeZero + additionalInfo:nil elementToLayoutAttributesTable:[NSMapTable elementToLayoutAttributesTable]]; } @@ -80,8 +68,10 @@ - (ASCollectionLayoutState *)calculateLayoutWithContext:(ASCollectionLayoutConte alignContent:ASStackLayoutAlignContentStart children:children]; stackSpec.concurrent = YES; - ASLayout *layout = [stackSpec layoutThatFits:[self sizeRangeThatFits:context.viewportSize]]; - return [[ASCollectionLayoutState alloc] initWithContext:context layout:layout]; + ASLayout *layout = [stackSpec layoutThatFits:ASSizeRangeForCollectionLayoutThatFitsViewportSize(context.viewportSize, _scrollableDirections)]; + return [[ASCollectionLayoutState alloc] initWithContext:context layout:layout additionalInfo:nil getElementBlock:^ASCollectionElement * _Nonnull(ASLayout * _Nonnull sublayout) { + return ((ASCellNode *)sublayout.layoutElement).collectionElement; + }]; } @end diff --git a/Source/Details/ASCollectionGalleryLayoutDelegate.h b/Source/Details/ASCollectionGalleryLayoutDelegate.h new file mode 100644 index 000000000..2b9e64c7c --- /dev/null +++ b/Source/Details/ASCollectionGalleryLayoutDelegate.h @@ -0,0 +1,32 @@ +// +// ASCollectionGalleryLayoutDelegate.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 + +NS_ASSUME_NONNULL_BEGIN + +/** + * A thread-safe layout delegate that arranges items with the same size into a flow layout. + * + * @note Supplemenraty elements are not supported. + */ +AS_SUBCLASSING_RESTRICTED +@interface ASCollectionGalleryLayoutDelegate : NSObject + +- (instancetype)initWithScrollableDirections:(ASScrollDirection)scrollableDirections itemSize:(CGSize)itemSize NS_DESIGNATED_INITIALIZER; + +- (instancetype)init __unavailable; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Source/Details/ASCollectionGalleryLayoutDelegate.mm b/Source/Details/ASCollectionGalleryLayoutDelegate.mm new file mode 100644 index 000000000..1c64569c8 --- /dev/null +++ b/Source/Details/ASCollectionGalleryLayoutDelegate.mm @@ -0,0 +1,387 @@ +// +// ASCollectionGalleryLayoutDelegate.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 + +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import + +static const ASRangeTuningParameters kASDefaultMeasureRangeTuningParameters = { + .leadingBufferScreenfuls = 2.0, + .trailingBufferScreenfuls = 2.0 +}; + +static const ASScrollDirection kASStaticScrollDirection = (ASScrollDirectionRight | ASScrollDirectionDown); + +#pragma mark - _ASGalleryLayoutItem + +NS_ASSUME_NONNULL_BEGIN + +/** + * A dummy item that represents a collection element to participate in the collection layout calculation process + * without triggering measurement on the actual node of the collection element. + * + * This item always has a fixed size that is the item size passed to it. + */ +AS_SUBCLASSING_RESTRICTED +@interface _ASGalleryLayoutItem : NSObject + +@property (nonatomic, assign, readonly) CGSize itemSize; +@property (nonatomic, weak, readonly) ASCollectionElement *collectionElement; + +- (instancetype)initWithItemSize:(CGSize)itemSize collectionElement:(ASCollectionElement *)collectionElement; +- (instancetype)init __unavailable; + +@end + +NS_ASSUME_NONNULL_END + +@implementation _ASGalleryLayoutItem { + ASPrimitiveTraitCollection _primitiveTraitCollection; +} + +@synthesize style; + +- (instancetype)initWithItemSize:(CGSize)itemSize collectionElement:(ASCollectionElement *)collectionElement +{ + self = [super init]; + if (self) { + ASDisplayNodeAssert(! CGSizeEqualToSize(CGSizeZero, itemSize), @"Item size should not be zero"); + ASDisplayNodeAssertNotNil(collectionElement, @"Collection element should not be nil"); + _itemSize = itemSize; + _collectionElement = collectionElement; + } + return self; +} + +ASLayoutElementFinalLayoutElementDefault +ASLayoutElementStyleExtensibilityForwarding +ASPrimitiveTraitCollectionDefaults +ASPrimitiveTraitCollectionDeprecatedImplementation + +- (ASTraitCollection *)asyncTraitCollection +{ + ASDisplayNodeAssertNotSupported(); + return nil; +} + +- (ASLayoutElementType)layoutElementType +{ + return ASLayoutElementTypeLayoutSpec; +} + +- (NSArray> *)sublayoutElements +{ + ASDisplayNodeAssertNotSupported(); + return nil; +} + +ASLayoutElementLayoutCalculationDefaults + +- (ASLayout *)calculateLayoutThatFits:(ASSizeRange)constrainedSize +{ + ASDisplayNodeAssert(CGSizeEqualToSize(_itemSize, ASSizeRangeClamp(constrainedSize, _itemSize)), + @"Item size %@ can't fit within the bounds of constrained size %@", NSStringFromCGSize(_itemSize), NSStringFromASSizeRange(constrainedSize)); + return [ASLayout layoutWithLayoutElement:self size:_itemSize]; +} + +@end + +#pragma mark - _ASGalleryLayoutStateAdditionInfo + +NS_ASSUME_NONNULL_BEGIN + +/** + * A thread-safe object that contains additional information of a collection layout. + * + * It keeps track of layout attributes of all unmeasured elements in a layout and facilitates highly-optimized lookups + * for unmeasured elements within a given rect. + */ +AS_SUBCLASSING_RESTRICTED +@interface _ASGalleryLayoutStateAdditionInfo : NSObject + +/// Sets unmeasured layout attributes to this object. +- (void)setUnmeasuredLayoutAttributes:(NSArray *)layoutAttributes withPageSize:(CGSize)pageSize; + +/// Removes and returns unmeasured layout attributes that intersect the specified rect +- (nullable ASPageTable *> *)getAndRemoveUnmeasuredLayoutAttributesInRect:(CGRect)rect contentSize:(CGSize)contentSize pageSize:(CGSize)pageSize; + +@end + +NS_ASSUME_NONNULL_END + +@implementation _ASGalleryLayoutStateAdditionInfo { + ASDN::Mutex __instanceLock__; + ASPageTable *> *_pageToUnmeasuredLayoutAttributesTable; +} + +- (void)setUnmeasuredLayoutAttributes:(NSArray *)layoutAttributes withPageSize:(CGSize)pageSize +{ + ASDN::MutexLocker l(__instanceLock__); + _pageToUnmeasuredLayoutAttributesTable = [ASPageTable pageTableWithLayoutAttributes:layoutAttributes pageSize:pageSize]; +} + +- (ASPageTable *> *)getAndRemoveUnmeasuredLayoutAttributesInRect:(CGRect)rect contentSize:(CGSize)contentSize pageSize:(CGSize)pageSize +{ + if (CGRectIsNull(rect) || CGRectIsEmpty(rect) || CGSizeEqualToSize(CGSizeZero, pageSize) || CGSizeEqualToSize(CGSizeZero, contentSize)) { + return nil; + } + + ASDN::MutexLocker l(__instanceLock__); + ASDisplayNodeAssertNotNil(_pageToUnmeasuredLayoutAttributesTable, @"Unmeasured page map hasn't been set"); + if (_pageToUnmeasuredLayoutAttributesTable.count == 0) { + return nil; + } + + // Step 1: Determine all the pages that intersect the specified rect + NSPointerArray *pagesInRect = ASPageCoordinatesForPagesThatIntersectRect(rect, contentSize, pageSize); + if (pagesInRect.count == 0) { + return nil; + } + + // Step 2: Filter out attributes in these pages that intersect the specified rect. Remove them from the internal table as we go + ASPageTable *results = [ASPageTable pageTableForStrongObjectPointers]; + for (id pagePtr in pagesInRect) { + ASPageCoordinate page = (ASPageCoordinate)pagePtr; + NSMutableSet *attrsInPage = [_pageToUnmeasuredLayoutAttributesTable objectForPage:page]; + NSUInteger attrsCount = attrsInPage.count; + + if (attrsCount > 0) { + NSMutableSet *interesectingAttrsInPage = nil; + + CGRect pageRect = ASPageCoordinateGetPageRect(page, pageSize); + if (CGRectContainsRect(rect, pageRect)) { + // The page fits well within the specified rect. Simply return all attributes in this page. + interesectingAttrsInPage = [attrsInPage copy]; + } else { + // The page intersects the specified rect. Some attributes in this page are to be returned, some are not. + for (UICollectionViewLayoutAttributes *attrs in attrsInPage) { + if (CGRectIntersectsRect(rect, attrs.frame)) { + if (interesectingAttrsInPage == nil) { + interesectingAttrsInPage = [NSMutableSet set]; + } + [interesectingAttrsInPage addObject:attrs]; + } + } + } + + NSUInteger interesectingAttrsCount = interesectingAttrsInPage.count; + if (interesectingAttrsCount > 0) { + [results setObject:interesectingAttrsInPage forPage:page]; + if (attrsCount == interesectingAttrsCount) { + // All attributes in this page intersect the specified rect. Remove the whole page. + [_pageToUnmeasuredLayoutAttributesTable removeObjectForPage:page]; + } else { + [attrsInPage minusSet:interesectingAttrsInPage]; + } + } + } + } + + return results; +} + +@end + +#pragma mark - ASCollectionGalleryLayoutDelegate + +@implementation ASCollectionGalleryLayoutDelegate { + ASScrollDirection _scrollableDirections; + CGSize _itemSize; +} + +- (instancetype)initWithScrollableDirections:(ASScrollDirection)scrollableDirections itemSize:(CGSize)itemSize +{ + self = [super init]; + if (self) { + ASDisplayNodeAssertFalse(CGSizeEqualToSize(CGSizeZero, itemSize)); + _scrollableDirections = scrollableDirections; + _itemSize = itemSize; + } + return self; +} + +- (id)additionalInfoForLayoutWithElements:(ASElementMap *)elements +{ + return nil; +} + +- (ASCollectionLayoutState *)calculateLayoutWithContext:(ASCollectionLayoutContext *)context +{ + ASElementMap *elements = context.elements; + CGSize pageSize = context.viewportSize; + NSMutableArray<_ASGalleryLayoutItem *> *children = ASArrayByFlatMapping(elements.itemElements, + ASCollectionElement *element, + [[_ASGalleryLayoutItem alloc] initWithItemSize:_itemSize collectionElement:element]); + if (children.count == 0) { + return [[ASCollectionLayoutState alloc] initWithContext:context contentSize:CGSizeZero additionalInfo:nil elementToLayoutAttributesTable:[NSMapTable weakToStrongObjectsMapTable]]; + } + + // Step 1: Use a stack spec to calculate layout content size and frames of all elements without actually measuring each element + ASStackLayoutSpec *stackSpec = [ASStackLayoutSpec stackLayoutSpecWithDirection:ASStackLayoutDirectionHorizontal + spacing:0 + justifyContent:ASStackLayoutJustifyContentStart + alignItems:ASStackLayoutAlignItemsStart + flexWrap:ASStackLayoutFlexWrapWrap + alignContent:ASStackLayoutAlignContentStart + children:children]; + // TODO Profile to see if a concurrent stack helps here? + ASLayout *layout = [stackSpec layoutThatFits:ASSizeRangeForCollectionLayoutThatFitsViewportSize(pageSize, _scrollableDirections)]; + + // Step 2: Create neccessary objects to hold information extracted from the layout + ASCollectionElement * _Nonnull(^getElementBlock)(ASLayout * _Nonnull) = ^ASCollectionElement *(ASLayout *sublayout) { + return ((_ASGalleryLayoutItem *)sublayout.layoutElement).collectionElement; + }; + ASCollectionLayoutState *collectionLayout = [[ASCollectionLayoutState alloc] initWithContext:context + layout:layout + additionalInfo:[[_ASGalleryLayoutStateAdditionInfo alloc] init] + getElementBlock:getElementBlock]; + if (CGSizeEqualToSize(collectionLayout.contentSize, CGSizeZero)) { + return collectionLayout; + } + + // Step 3: Since _ASGalleryLayoutItem is a dummy layout object, register all elements as unmeasured. + [collectionLayout.additionalInfo setUnmeasuredLayoutAttributes:[collectionLayout allLayoutAttributes] withPageSize:pageSize]; + + // Step 4: Measure elements in the measure range ahead of time, block on the initial rect as it'll be visible shortly + // TODO Consider content offset of the collection node + CGRect initialRect = CGRectMake(0, 0, pageSize.width, pageSize.height); + CGRect measureRect = CGRectExpandToRangeWithScrollableDirections(initialRect, kASDefaultMeasureRangeTuningParameters, _scrollableDirections, kASStaticScrollDirection); + [ASCollectionGalleryLayoutDelegate _measureElementsInRect:measureRect blockingRect:initialRect layout:collectionLayout elementSize:_itemSize]; + + return collectionLayout; +} + +- (void)ensureLayoutAttributesForElementsInRect:(CGRect)rect withLayout:(ASCollectionLayoutState *)layout +{ + ASDisplayNodeAssertMainThread(); + if (CGRectIsEmpty(rect) || (! CGRectIntersectsRect(layout.contentRect, rect))) { + return; + } + + // Measure elements in the measure range, block on the requested rect + CGRect measureRect = CGRectExpandToRangeWithScrollableDirections(rect, kASDefaultMeasureRangeTuningParameters, _scrollableDirections, kASStaticScrollDirection); + [ASCollectionGalleryLayoutDelegate _measureElementsInRect:measureRect blockingRect:rect layout:layout elementSize:_itemSize]; +} + +- (void)ensureLayoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath withLayout:(ASCollectionLayoutState *)layout +{ + ASDisplayNodeAssertMainThread(); + ASCollectionElement *element = [layout.context.elements elementForItemAtIndexPath:indexPath]; + ASCellNode *node = element.node; + if (! CGSizeEqualToSize(_itemSize, node.calculatedSize)) { + [node layoutThatFits:ASSizeRangeMake(_itemSize, _itemSize)]; + } +} + +/** + * 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 elementSize:(CGSize)elementSize +{ + if (CGRectIsEmpty(rect)) { + return; + } + BOOL hasBlockingRect = !CGRectIsEmpty(blockingRect); + ASDisplayNodeAssert(! hasBlockingRect || CGRectContainsRect(rect, blockingRect), @"Blocking rect, if specified, must be within the other (outer) rect"); + + // Step 1: Clamp the specified rects between the bounds of content rect + CGRect contentRect = layout.contentRect; + rect = CGRectIntersection(contentRect, rect); + if (CGRectIsNull(rect)) { + return; + } + if (hasBlockingRect) { + blockingRect = CGRectIntersection(contentRect, blockingRect); + hasBlockingRect = !CGRectIsNull(blockingRect); + } + + // Step 2: Get layout attributes of all unmeasured elements within the specified outer rect + ASCollectionLayoutContext *context = layout.context; + CGSize pageSize = context.viewportSize; + ASPageTable *unmeasuredAttrsTable = [layout.additionalInfo getAndRemoveUnmeasuredLayoutAttributesInRect:rect contentSize:layout.contentSize pageSize:pageSize]; + if (unmeasuredAttrsTable.count == 0) { + // No unmeasured elements in this rect! Bail early + return; + } + + // Step 3: Split all those attributes into blocking and non-blocking buckets + NSMutableArray *blockingAttrs = hasBlockingRect ? [NSMutableArray array] : nil; + NSMutableArray *nonBlockingAttrs = [NSMutableArray array]; + for (id pagePtr in unmeasuredAttrsTable) { + ASPageCoordinate page = (ASPageCoordinate)pagePtr; + NSArray *attrsInPage = [[unmeasuredAttrsTable objectForPage:page] allObjects]; + // Calcualte 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; + ASSizeRange sizeRange = ASSizeRangeMake(elementSize); + dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); + if (NSUInteger count = blockingAttrs.count > 0) { + ASDispatchApply(count, queue, 0, ^(size_t i) { + ASCollectionElement *element = [elements elementForItemAtIndexPath:blockingAttrs[i].indexPath]; + ASCellNode *node = element.node; + if (! CGSizeEqualToSize(elementSize, node.calculatedSize)) { + [node layoutThatFits:sizeRange]; + } + }); + } + + // Step 5: Allocate and measure non-blocking ones + for (UICollectionViewLayoutAttributes *attrs in nonBlockingAttrs) { + __weak ASCollectionElement *weakElement = [elements elementForItemAtIndexPath:attrs.indexPath]; + dispatch_async(queue, ^{ + __strong ASCollectionElement *strongElement = weakElement; + if (strongElement != nil) { + ASCellNode *node = strongElement.node; + if (! CGSizeEqualToSize(elementSize, node.calculatedSize)) { + [node layoutThatFits:sizeRange]; + } + } + }); + } +} + +@end diff --git a/Source/Details/ASCollectionLayoutDelegate.h b/Source/Details/ASCollectionLayoutDelegate.h index 7fd17467a..852604758 100644 --- a/Source/Details/ASCollectionLayoutDelegate.h +++ b/Source/Details/ASCollectionLayoutDelegate.h @@ -24,6 +24,8 @@ NS_ASSUME_NONNULL_BEGIN @protocol ASCollectionLayoutDelegate +// TODO Add delegate method to inform whether this delegate can allocate nodes itself + /** * @abstract Returns any additional information needed for a coming layout pass with the given elements. * @@ -51,6 +53,54 @@ NS_ASSUME_NONNULL_BEGIN */ - (ASCollectionLayoutState *)calculateLayoutWithContext:(ASCollectionLayoutContext *)context; +@optional + +/** + * @abstract Ensures all elements within a given rect have valid and final layout attributes. + * + * @discussion This gives the layout delegate a chance to update the layout on the fly, as opposed to measuring everything + * in `-calculateLayoutWithContext:`. + * + * @discussion This method will be called on main thread, on every frame and must block the thread until all elements + * within the specified rect are ready. As a result, it's crucial to minimize the blocking time + * and do as much work ahead of time as possible. + * + * @param rect The rect in which elements should have valid and final layout attributes. + * + * @param layout The layout object previously returned by `-calculateLayoutWithContext:`. + */ +- (void)ensureLayoutAttributesForElementsInRect:(CGRect)rect withLayout:(ASCollectionLayoutState *)layout; + +/** + * @abstract Ensures the item at the index path has a valid and final layout attributes. + * + * @discussion This gives the layout delegate a chance to update the layout on the fly, as opposed to measuring everything + * in `-calculateLayoutWithContext:`. + * + * @discussion This method will be called on main thread and must block the thread until the layout attribues is ready. + * + * @param indexPath The index path of the item. + * + * @param layout The layout object previously returned by `-calculateLayoutWithContext:`. + */ +- (void)ensureLayoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath withLayout:(ASCollectionLayoutState *)layout; + +/** + * @abstract Ensures the supplementary element at the index path has a valid and final layout attributes. + * + * @discussion This gives the layout delegate a chance to update the layout on the fly, as opposed to measuring everything + * in `-calculateLayoutWithContext:`. + * + * @discussion This method will be called on main thread and must block the thread until the layout attribues is ready. + * + * @param elementKind A string that identifies the type of supplementary element. + * + * @param indexPath The index path of the element. + * + * @param layout The layout object previously returned by `-calculateLayoutWithContext:`. + */ +- (void)ensureLayoutAttributesForSupplementaryElementOfKind:(NSString *)kind atIndexPath:(NSIndexPath *)indexPath withLayout:(ASCollectionLayoutState *)layout; + @end NS_ASSUME_NONNULL_END diff --git a/Source/Details/ASCollectionLayoutState.h b/Source/Details/ASCollectionLayoutState.h index e70b951aa..6cb314e58 100644 --- a/Source/Details/ASCollectionLayoutState.h +++ b/Source/Details/ASCollectionLayoutState.h @@ -38,6 +38,9 @@ AS_SUBCLASSING_RESTRICTED /// The final content size of the collection's layout @property (nonatomic, assign, readonly) CGSize contentSize; +/// Any additional information can be stored here. Clients are responsible for the thread-safety of this object. +@property (nonatomic, strong, readonly, nullable) id additionalInfo; + - (instancetype)init __unavailable; /** @@ -47,10 +50,12 @@ AS_SUBCLASSING_RESTRICTED * * @param contentSize The content size of the collection's layout * - * @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. + * @param additionalInfo Any additional information to be stored in this object. + * + * @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 have NSMapTableObjectPointerPersonality and NSMapTableWeakMemory as key options. */ -- (instancetype)initWithContext:(ASCollectionLayoutContext *)context contentSize:(CGSize)contentSize elementToLayoutAttributesTable:(NSMapTable *)table NS_DESIGNATED_INITIALIZER; +- (instancetype)initWithContext:(ASCollectionLayoutContext *)context contentSize:(CGSize)contentSize additionalInfo:(nullable id)additionalInfo elementToLayoutAttributesTable:(NSMapTable *)table NS_DESIGNATED_INITIALIZER; /** * Convenience initializer. @@ -59,8 +64,11 @@ AS_SUBCLASSING_RESTRICTED * * @param layout The layout describes size and position of all elements, or a subset of them and will be updated over time. * + * @param additionalInfo Any additional information to be stored in this object. + * + * @param getElementBlock A block that can retrieve the collection element from a direct sublayout of the root layout. */ -- (instancetype)initWithContext:(ASCollectionLayoutContext *)context layout:(ASLayout *)layout; +- (instancetype)initWithContext:(ASCollectionLayoutContext *)context layout:(ASLayout *)layout additionalInfo:(nullable id)additionalInfo getElementBlock:(ASCollectionElement *(^)(ASLayout *))getElementBlock; /** * Returns all layout attributes present in this object. diff --git a/Source/Details/ASCollectionLayoutState.m b/Source/Details/ASCollectionLayoutState.m index ecb7e1503..0ff0316bf 100644 --- a/Source/Details/ASCollectionLayoutState.m +++ b/Source/Details/ASCollectionLayoutState.m @@ -18,7 +18,6 @@ #import #import -#import #import #import #import @@ -39,13 +38,13 @@ @implementation ASCollectionLayoutState { ASPageTable *> *_pageToLayoutAttributesTable; } -- (instancetype)initWithContext:(ASCollectionLayoutContext *)context layout:(ASLayout *)layout +- (instancetype)initWithContext:(ASCollectionLayoutContext *)context layout:(ASLayout *)layout additionalInfo:(nullable id)additionalInfo getElementBlock:(ASCollectionElement *(^)(ASLayout *))getElementBlock { ASElementMap *elements = context.elements; NSMapTable *table = [NSMapTable elementToLayoutAttributesTable]; for (ASLayout *sublayout in layout.sublayouts) { - ASCollectionElement *element = ((ASCellNode *)sublayout.layoutElement).collectionElement; + ASCollectionElement *element = getElementBlock(sublayout); if (element == nil) { ASDisplayNodeFailAssert(@"Element not found!"); continue; @@ -65,10 +64,10 @@ - (instancetype)initWithContext:(ASCollectionLayoutContext *)context layout:(ASL [table setObject:attrs forKey:element]; } - return [self initWithContext:context contentSize:layout.size elementToLayoutAttributesTable:table]; + return [self initWithContext:context contentSize:layout.size additionalInfo:additionalInfo elementToLayoutAttributesTable:table]; } -- (instancetype)initWithContext:(ASCollectionLayoutContext *)context contentSize:(CGSize)contentSize elementToLayoutAttributesTable:(NSMapTable *)table +- (instancetype)initWithContext:(ASCollectionLayoutContext *)context contentSize:(CGSize)contentSize additionalInfo:(id)additionalInfo elementToLayoutAttributesTable:(NSMapTable *)table { self = [super init]; if (self) { @@ -76,6 +75,7 @@ - (instancetype)initWithContext:(ASCollectionLayoutContext *)context contentSize _contentSize = contentSize; _elementToLayoutAttributesTable = table; _pageToLayoutAttributesTable = [ASPageTable pageTableWithLayoutAttributes:_elementToLayoutAttributesTable.objectEnumerator contentSize:contentSize pageSize:context.viewportSize]; + _additionalInfo = additionalInfo; } return self; } diff --git a/Source/Private/ASCollectionLayout.mm b/Source/Private/ASCollectionLayout.mm index 2ca86f478..d7a16063f 100644 --- a/Source/Private/ASCollectionLayout.mm +++ b/Source/Private/ASCollectionLayout.mm @@ -38,7 +38,12 @@ @interface ASCollectionLayout () { // The pending state calculated ahead of time, if any. ASCollectionLayoutState *_pendingLayout; - BOOL _layoutDelegateImplementsAdditionalInfoForLayoutWithElements; + struct { + unsigned int additionalInfoForLayoutWithElements:1; + unsigned int ensureLayoutAttributesForElementsInRect:1; + unsigned int ensureLayoutAttributesForItemAtIndexPath:1; + unsigned int ensureLayoutAttributesForSupplementaryElementOfKind:1; + } _layoutDelegateFlags; } @end @@ -51,7 +56,10 @@ - (instancetype)initWithLayoutDelegate:(id)layoutDel if (self) { ASDisplayNodeAssertNotNil(layoutDelegate, @"Collection layout delegate cannot be nil"); _layoutDelegate = layoutDelegate; - _layoutDelegateImplementsAdditionalInfoForLayoutWithElements = [layoutDelegate respondsToSelector:@selector(additionalInfoForLayoutWithElements:)]; + _layoutDelegateFlags.additionalInfoForLayoutWithElements = [layoutDelegate respondsToSelector:@selector(additionalInfoForLayoutWithElements:)]; + _layoutDelegateFlags.ensureLayoutAttributesForElementsInRect = [layoutDelegate respondsToSelector:@selector(ensureLayoutAttributesForElementsInRect:withLayout:)]; + _layoutDelegateFlags.ensureLayoutAttributesForItemAtIndexPath = [layoutDelegate respondsToSelector:@selector(ensureLayoutAttributesForItemAtIndexPath:withLayout:)]; + _layoutDelegateFlags.ensureLayoutAttributesForSupplementaryElementOfKind = [layoutDelegate respondsToSelector:@selector(ensureLayoutAttributesForSupplementaryElementOfKind:atIndexPath:withLayout:)]; } return self; } @@ -63,7 +71,7 @@ - (id)layoutContextWithElements:(ASElementMap *)elements ASDisplayNodeAssertMainThread(); CGSize viewportSize = [self viewportSize]; id additionalInfo = nil; - if (_layoutDelegateImplementsAdditionalInfoForLayoutWithElements) { + if (_layoutDelegateFlags.additionalInfoForLayoutWithElements) { additionalInfo = [_layoutDelegate additionalInfoForLayoutWithElements:elements]; } return [[ASCollectionLayoutContext alloc] initWithViewportSize:viewportSize elements:elements additionalInfo:additionalInfo]; @@ -119,6 +127,11 @@ - (CGSize)collectionViewContentSize - (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect { ASDisplayNodeAssertMainThread(); + + if (_layoutDelegate != nil && _layoutDelegateFlags.ensureLayoutAttributesForElementsInRect) { + [_layoutDelegate ensureLayoutAttributesForElementsInRect:rect withLayout:_layout]; + } + NSArray *result = [_layout layoutAttributesForElementsInRect:rect]; ASElementMap *elements = _layout.context.elements; @@ -132,6 +145,10 @@ - (CGSize)collectionViewContentSize - (UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath { + if (_layoutDelegate != nil && _layoutDelegateFlags.ensureLayoutAttributesForItemAtIndexPath) { + [_layoutDelegate ensureLayoutAttributesForItemAtIndexPath:indexPath withLayout:_layout]; + } + ASCollectionElement *element = [_layout.context.elements elementForItemAtIndexPath:indexPath]; UICollectionViewLayoutAttributes *attrs = [_layout layoutAttributesForElement:element]; [ASCollectionLayout setSize:attrs.frame.size toElement:element]; @@ -140,6 +157,10 @@ - (UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSInde - (UICollectionViewLayoutAttributes *)layoutAttributesForSupplementaryViewOfKind:(NSString *)elementKind atIndexPath:(NSIndexPath *)indexPath { + if (_layoutDelegate != nil && _layoutDelegateFlags.ensureLayoutAttributesForSupplementaryElementOfKind) { + [_layoutDelegate ensureLayoutAttributesForSupplementaryElementOfKind:elementKind atIndexPath:indexPath withLayout:_layout]; + } + ASCollectionElement *element = [_layout.context.elements supplementaryElementOfKind:elementKind atIndexPath:indexPath]; UICollectionViewLayoutAttributes *attrs = [_layout layoutAttributesForElement:element]; [ASCollectionLayout setSize:attrs.frame.size toElement:element]; diff --git a/Source/Private/ASCollectionLayoutDefines.h b/Source/Private/ASCollectionLayoutDefines.h new file mode 100644 index 000000000..07be880a0 --- /dev/null +++ b/Source/Private/ASCollectionLayoutDefines.h @@ -0,0 +1,27 @@ +// +// ASCollectionLayoutDefines.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 + +NS_ASSUME_NONNULL_BEGIN + +ASDISPLAYNODE_EXTERN_C_BEGIN + +FOUNDATION_EXPORT ASSizeRange ASSizeRangeForCollectionLayoutThatFitsViewportSize(CGSize viewportSize, ASScrollDirection scrollableDirections) AS_WARN_UNUSED_RESULT; + +ASDISPLAYNODE_EXTERN_C_END + +NS_ASSUME_NONNULL_END diff --git a/Source/Private/ASCollectionLayoutDefines.m b/Source/Private/ASCollectionLayoutDefines.m new file mode 100644 index 000000000..b8c9c21cc --- /dev/null +++ b/Source/Private/ASCollectionLayoutDefines.m @@ -0,0 +1,27 @@ +// +// ASCollectionLayoutDefines.m +// 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 ASSizeRange ASSizeRangeForCollectionLayoutThatFitsViewportSize(CGSize viewportSize, ASScrollDirection scrollableDirections) +{ + ASSizeRange sizeRange = ASSizeRangeUnconstrained; + if (ASScrollDirectionContainsVerticalDirection(scrollableDirections) == NO) { + sizeRange.min.height = viewportSize.height; + sizeRange.max.height = viewportSize.height; + } + if (ASScrollDirectionContainsHorizontalDirection(scrollableDirections) == NO) { + sizeRange.min.width = viewportSize.width; + sizeRange.max.width = viewportSize.width; + } + return sizeRange; +} diff --git a/examples/ASCollectionView/Sample/ViewController.m b/examples/ASCollectionView/Sample/ViewController.m index d00ea60df..270765802 100644 --- a/examples/ASCollectionView/Sample/ViewController.m +++ b/examples/ASCollectionView/Sample/ViewController.m @@ -1,18 +1,18 @@ // // ViewController.m -// Sample +// 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 root directory of this source tree. An additional grant -// of patent rights can be found in the PATENTS file in the same directory. +// 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. // -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL -// FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN -// ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN -// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// 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 "ViewController.h" @@ -44,7 +44,10 @@ - (void)viewDidLoad { [super viewDidLoad]; - self.collectionNode = [[ASCollectionNode alloc] initWithLayoutDelegate:[[ASCollectionFlowLayoutDelegate alloc] init] layoutFacilitator:nil]; + id layoutDelegate = [[ASCollectionGalleryLayoutDelegate alloc] initWithScrollableDirections:ASScrollDirectionVerticalDirections itemSize:CGSizeMake(180, 90)]; +// id layoutDelegate = [[ASCollectionFlowLayoutDelegate alloc] init]; + + self.collectionNode = [[ASCollectionNode alloc] initWithLayoutDelegate:layoutDelegate layoutFacilitator:nil]; self.collectionNode.dataSource = self; self.collectionNode.delegate = self; From ff06ad55577c22bf6533852de96b40fb6ea1bba0 Mon Sep 17 00:00:00 2001 From: Huy Nguyen Date: Thu, 4 May 2017 13:29:14 +0100 Subject: [PATCH 02/25] Handle items that span multiple pages and other improvements in gallery delegate --- .../ASCollectionGalleryLayoutDelegate.mm | 54 ++++++++++--------- Source/Details/ASCollectionLayoutState.m | 2 +- 2 files changed, 29 insertions(+), 27 deletions(-) diff --git a/Source/Details/ASCollectionGalleryLayoutDelegate.mm b/Source/Details/ASCollectionGalleryLayoutDelegate.mm index 1c64569c8..bea69dbc9 100644 --- a/Source/Details/ASCollectionGalleryLayoutDelegate.mm +++ b/Source/Details/ASCollectionGalleryLayoutDelegate.mm @@ -77,7 +77,6 @@ - (instancetype)initWithItemSize:(CGSize)itemSize collectionElement:(ASCollectio return self; } -ASLayoutElementFinalLayoutElementDefault ASLayoutElementStyleExtensibilityForwarding ASPrimitiveTraitCollectionDefaults ASPrimitiveTraitCollectionDeprecatedImplementation @@ -124,10 +123,10 @@ - (ASLayout *)calculateLayoutThatFits:(ASSizeRange)constrainedSize @interface _ASGalleryLayoutStateAdditionInfo : NSObject /// Sets unmeasured layout attributes to this object. -- (void)setUnmeasuredLayoutAttributes:(NSArray *)layoutAttributes withPageSize:(CGSize)pageSize; +- (void)setUnmeasuredLayoutAttributes:(NSArray *)layoutAttributes contentSize:(CGSize)contentSize pageSize:(CGSize)pageSize; /// Removes and returns unmeasured layout attributes that intersect the specified rect -- (nullable ASPageTable *> *)getAndRemoveUnmeasuredLayoutAttributesInRect:(CGRect)rect contentSize:(CGSize)contentSize pageSize:(CGSize)pageSize; +- (nullable ASPageTable *> *)getAndRemoveUnmeasuredLayoutAttributesInRect:(CGRect)rect contentSize:(CGSize)contentSize pageSize:(CGSize)pageSize; @end @@ -135,16 +134,16 @@ - (void)setUnmeasuredLayoutAttributes:(NSArray *> *_pageToUnmeasuredLayoutAttributesTable; + ASPageTable *> *_pageToUnmeasuredLayoutAttributesTable; } -- (void)setUnmeasuredLayoutAttributes:(NSArray *)layoutAttributes withPageSize:(CGSize)pageSize +- (void)setUnmeasuredLayoutAttributes:(NSArray *)layoutAttributes contentSize:(CGSize)contentSize pageSize:(CGSize)pageSize { ASDN::MutexLocker l(__instanceLock__); - _pageToUnmeasuredLayoutAttributesTable = [ASPageTable pageTableWithLayoutAttributes:layoutAttributes pageSize:pageSize]; + _pageToUnmeasuredLayoutAttributesTable = [ASPageTable pageTableWithLayoutAttributes:layoutAttributes contentSize:contentSize pageSize:pageSize]; } -- (ASPageTable *> *)getAndRemoveUnmeasuredLayoutAttributesInRect:(CGRect)rect contentSize:(CGSize)contentSize pageSize:(CGSize)pageSize +- (ASPageTable *> *)getAndRemoveUnmeasuredLayoutAttributesInRect:(CGRect)rect contentSize:(CGSize)contentSize pageSize:(CGSize)pageSize { if (CGRectIsNull(rect) || CGRectIsEmpty(rect) || CGSizeEqualToSize(CGSizeZero, pageSize) || CGSizeEqualToSize(CGSizeZero, contentSize)) { return nil; @@ -163,45 +162,44 @@ - (void)setUnmeasuredLayoutAttributes:(NSArray 0) { - NSMutableSet *interesectingAttrsInPage = nil; + if (NSUInteger attrsCount = attrsInPage.count > 0) { + NSMutableArray *interesectingAttrsInPage = nil; CGRect pageRect = ASPageCoordinateGetPageRect(page, pageSize); if (CGRectContainsRect(rect, pageRect)) { // The page fits well within the specified rect. Simply return all attributes in this page. - interesectingAttrsInPage = [attrsInPage copy]; + // Don't need to make a copy of attrsInPage here because it will be removed from the page table soon anyway. + interesectingAttrsInPage = attrsInPage; } else { // The page intersects the specified rect. Some attributes in this page are to be returned, some are not. for (UICollectionViewLayoutAttributes *attrs in attrsInPage) { if (CGRectIntersectsRect(rect, attrs.frame)) { if (interesectingAttrsInPage == nil) { - interesectingAttrsInPage = [NSMutableSet set]; + interesectingAttrsInPage = [NSMutableArray array]; } [interesectingAttrsInPage addObject:attrs]; } } } - NSUInteger interesectingAttrsCount = interesectingAttrsInPage.count; - if (interesectingAttrsCount > 0) { - [results setObject:interesectingAttrsInPage forPage:page]; + if (NSUInteger interesectingAttrsCount = interesectingAttrsInPage.count > 0) { + [result setObject:interesectingAttrsInPage forPage:page]; if (attrsCount == interesectingAttrsCount) { // All attributes in this page intersect the specified rect. Remove the whole page. [_pageToUnmeasuredLayoutAttributesTable removeObjectForPage:page]; } else { - [attrsInPage minusSet:interesectingAttrsInPage]; + [attrsInPage removeObjectsInArray:interesectingAttrsInPage]; } } } } - return results; + return result; } @end @@ -259,12 +257,13 @@ - (ASCollectionLayoutState *)calculateLayoutWithContext:(ASCollectionLayoutConte layout:layout additionalInfo:[[_ASGalleryLayoutStateAdditionInfo alloc] init] getElementBlock:getElementBlock]; - if (CGSizeEqualToSize(collectionLayout.contentSize, CGSizeZero)) { + CGSize contentSize = collectionLayout.contentSize; + if (CGSizeEqualToSize(contentSize, CGSizeZero)) { return collectionLayout; } // Step 3: Since _ASGalleryLayoutItem is a dummy layout object, register all elements as unmeasured. - [collectionLayout.additionalInfo setUnmeasuredLayoutAttributes:[collectionLayout allLayoutAttributes] withPageSize:pageSize]; + [collectionLayout.additionalInfo setUnmeasuredLayoutAttributes:[collectionLayout allLayoutAttributes] contentSize:contentSize pageSize:pageSize]; // Step 4: Measure elements in the measure range ahead of time, block on the initial rect as it'll be visible shortly // TODO Consider content offset of the collection node @@ -278,7 +277,7 @@ - (ASCollectionLayoutState *)calculateLayoutWithContext:(ASCollectionLayoutConte - (void)ensureLayoutAttributesForElementsInRect:(CGRect)rect withLayout:(ASCollectionLayoutState *)layout { ASDisplayNodeAssertMainThread(); - if (CGRectIsEmpty(rect) || (! CGRectIntersectsRect(layout.contentRect, rect))) { + if (CGRectIsEmpty(rect)) { return; } @@ -309,7 +308,8 @@ + (void)_measureElementsInRect:(CGRect)rect blockingRect:(CGRect)blockingRect la ASDisplayNodeAssert(! hasBlockingRect || CGRectContainsRect(rect, blockingRect), @"Blocking rect, if specified, must be within the other (outer) rect"); // Step 1: Clamp the specified rects between the bounds of content rect - CGRect contentRect = layout.contentRect; + CGSize contentSize = layout.contentSize; + CGRect contentRect = CGRectMake(0, 0, contentSize.width, contentSize.height); rect = CGRectIntersection(contentRect, rect); if (CGRectIsNull(rect)) { return; @@ -322,15 +322,17 @@ + (void)_measureElementsInRect:(CGRect)rect blockingRect:(CGRect)blockingRect la // Step 2: Get layout attributes of all unmeasured elements within the specified outer rect ASCollectionLayoutContext *context = layout.context; CGSize pageSize = context.viewportSize; - ASPageTable *unmeasuredAttrsTable = [layout.additionalInfo getAndRemoveUnmeasuredLayoutAttributesInRect:rect contentSize:layout.contentSize pageSize:pageSize]; + ASPageTable *unmeasuredAttrsTable = [layout.additionalInfo getAndRemoveUnmeasuredLayoutAttributesInRect:rect contentSize:contentSize pageSize:pageSize]; if (unmeasuredAttrsTable.count == 0) { // No unmeasured elements in this rect! Bail early return; } // Step 3: Split all those attributes into blocking and non-blocking buckets - NSMutableArray *blockingAttrs = hasBlockingRect ? [NSMutableArray array] : nil; - NSMutableArray *nonBlockingAttrs = [NSMutableArray array]; + // Use an ordered set here because some items may span multiple pages and they will be accessed by indexes. + NSMutableOrderedSet *blockingAttrs = hasBlockingRect ? [NSMutableOrderedSet orderedSet] : nil; + // Use a set here because some items may span multiple pages + NSMutableSet *nonBlockingAttrs = [NSMutableSet set]; for (id pagePtr in unmeasuredAttrsTable) { ASPageCoordinate page = (ASPageCoordinate)pagePtr; NSArray *attrsInPage = [[unmeasuredAttrsTable objectForPage:page] allObjects]; diff --git a/Source/Details/ASCollectionLayoutState.m b/Source/Details/ASCollectionLayoutState.m index 0ff0316bf..15452d9fd 100644 --- a/Source/Details/ASCollectionLayoutState.m +++ b/Source/Details/ASCollectionLayoutState.m @@ -93,7 +93,7 @@ - (instancetype)initWithContext:(ASCollectionLayoutContext *)context contentSize return @[]; } - // Use a mutable set here because some items may span multiple pages + // Use a set here because some items may span multiple pages NSMutableSet *result = [NSMutableSet set]; for (id pagePtr in pages) { ASPageCoordinate page = (ASPageCoordinate)pagePtr; From 7b0c9f88d78954e0a3ae36dceaac59ceef3f62e8 Mon Sep 17 00:00:00 2001 From: Huy Nguyen Date: Thu, 4 May 2017 13:34:17 +0100 Subject: [PATCH 03/25] Minor fixes --- AsyncDisplayKit.xcodeproj/project.pbxproj | 2 -- Source/Details/ASCollectionGalleryLayoutDelegate.mm | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/AsyncDisplayKit.xcodeproj/project.pbxproj b/AsyncDisplayKit.xcodeproj/project.pbxproj index 5647edee0..e2afb326c 100644 --- a/AsyncDisplayKit.xcodeproj/project.pbxproj +++ b/AsyncDisplayKit.xcodeproj/project.pbxproj @@ -1811,7 +1811,6 @@ B350624F1B010EFD0018CF92 /* ASDisplayNode+DebugTiming.h in Headers */, CC57EAF71E3939350034C595 /* ASCollectionView+Undeprecated.h in Headers */, B35062521B010EFD0018CF92 /* ASDisplayNodeInternal.h in Headers */, - E52F8AF21EAE6ADD00B5A912 /* (null) in Headers */, AC7A2C181BDE11DF0093FE1A /* ASTableViewInternal.h in Headers */, B35062531B010EFD0018CF92 /* ASImageNode+CGExtras.h in Headers */, E58E9E491E941DA5004CFC59 /* ASCollectionLayout.h in Headers */, @@ -2284,7 +2283,6 @@ E5ABAC7C1E8564EE007AC15C /* ASRectTable.m in Sources */, CCA282C91E9EB64B0037E8B7 /* ASDisplayNodeTipState.m in Sources */, 509E68601B3AED8E009B9150 /* ASScrollDirection.m in Sources */, - E52F8AFA1EAF600300B5A912 /* (null) in Sources */, B35062091B010EFD0018CF92 /* ASScrollNode.mm in Sources */, 69BCE3D91EC6513B007DCCAD /* ASDisplayNode+Layout.mm in Sources */, 8BDA5FC81CDBDF95007D13B2 /* ASVideoPlayerNode.mm in Sources */, diff --git a/Source/Details/ASCollectionGalleryLayoutDelegate.mm b/Source/Details/ASCollectionGalleryLayoutDelegate.mm index bea69dbc9..ca104778a 100644 --- a/Source/Details/ASCollectionGalleryLayoutDelegate.mm +++ b/Source/Details/ASCollectionGalleryLayoutDelegate.mm @@ -336,7 +336,7 @@ + (void)_measureElementsInRect:(CGRect)rect blockingRect:(CGRect)blockingRect la for (id pagePtr in unmeasuredAttrsTable) { ASPageCoordinate page = (ASPageCoordinate)pagePtr; NSArray *attrsInPage = [[unmeasuredAttrsTable objectForPage:page] allObjects]; - // Calcualte the page's rect but only if it's going to be used. + // 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)) { From a82a0be1f414147d6180c2763d031626787f3f57 Mon Sep 17 00:00:00 2001 From: Huy Nguyen Date: Sat, 8 Jul 2017 09:50:27 +0100 Subject: [PATCH 04/25] Fix failing tests --- .../Details/ASCollectionGalleryLayoutDelegate.mm | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/Source/Details/ASCollectionGalleryLayoutDelegate.mm b/Source/Details/ASCollectionGalleryLayoutDelegate.mm index ca104778a..e83b1860d 100644 --- a/Source/Details/ASCollectionGalleryLayoutDelegate.mm +++ b/Source/Details/ASCollectionGalleryLayoutDelegate.mm @@ -60,7 +60,7 @@ - (instancetype)init __unavailable; NS_ASSUME_NONNULL_END @implementation _ASGalleryLayoutItem { - ASPrimitiveTraitCollection _primitiveTraitCollection; + std::atomic _primitiveTraitCollection; } @synthesize style; @@ -107,6 +107,18 @@ - (ASLayout *)calculateLayoutThatFits:(ASSizeRange)constrainedSize return [ASLayout layoutWithLayoutElement:self size:_itemSize]; } +#pragma mark - ASLayoutElementAsciiArtProtocol + +- (NSString *)asciiArtString +{ + return [ASLayoutSpec asciiArtStringForChildren:@[] parentName:[self asciiArtName]]; +} + +- (NSString *)asciiArtName +{ + return [NSMutableString stringWithCString:object_getClassName(self) encoding:NSASCIIStringEncoding]; +} + @end #pragma mark - _ASGalleryLayoutStateAdditionInfo From abec0f4340233acf3891757d3d0ecbda700ef6e0 Mon Sep 17 00:00:00 2001 From: Huy Nguyen Date: Mon, 10 Jul 2017 15:16:05 +0100 Subject: [PATCH 05/25] Fix custom collection example --- .../Sample/MosaicCollectionLayoutDelegate.m | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/CustomCollectionView/Sample/MosaicCollectionLayoutDelegate.m b/examples/CustomCollectionView/Sample/MosaicCollectionLayoutDelegate.m index 4bbd53e6a..d82971a0b 100644 --- a/examples/CustomCollectionView/Sample/MosaicCollectionLayoutDelegate.m +++ b/examples/CustomCollectionView/Sample/MosaicCollectionLayoutDelegate.m @@ -108,7 +108,7 @@ - (ASCollectionLayoutState *)calculateLayoutWithContext:(ASCollectionLayoutConte CGFloat contentHeight = [[[columnHeights lastObject] firstObject] floatValue]; CGSize contentSize = CGSizeMake(layoutWidth, contentHeight); - return [[ASCollectionLayoutState alloc] initWithContext:context contentSize:contentSize elementToLayoutAttributesTable:attrsMap]; + return [[ASCollectionLayoutState alloc] initWithContext:context contentSize:contentSize additionalInfo:nil elementToLayoutAttributesTable:attrsMap]; } - (CGFloat)_widthForSection:(NSUInteger)section withLayoutWidth:(CGFloat)layoutWidth From 600f145c803ae2630a92260f32a141de9c5f69ac Mon Sep 17 00:00:00 2001 From: Huy Nguyen Date: Mon, 10 Jul 2017 15:28:03 +0100 Subject: [PATCH 06/25] Implement missing method in gallery layout delegate --- Source/Details/ASCollectionGalleryLayoutDelegate.mm | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Source/Details/ASCollectionGalleryLayoutDelegate.mm b/Source/Details/ASCollectionGalleryLayoutDelegate.mm index e83b1860d..46f0f350a 100644 --- a/Source/Details/ASCollectionGalleryLayoutDelegate.mm +++ b/Source/Details/ASCollectionGalleryLayoutDelegate.mm @@ -98,6 +98,11 @@ - (ASLayoutElementType)layoutElementType return nil; } +- (BOOL)implementsLayoutMethod +{ + return YES; +} + ASLayoutElementLayoutCalculationDefaults - (ASLayout *)calculateLayoutThatFits:(ASSizeRange)constrainedSize From a2d789e10f853a89d05606a8a691f50aa1f0cb36 Mon Sep 17 00:00:00 2001 From: Huy Nguyen Date: Mon, 10 Jul 2017 15:44:15 +0100 Subject: [PATCH 07/25] Fix warnings --- Source/Details/ASCollectionLayoutDelegate.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/Details/ASCollectionLayoutDelegate.h b/Source/Details/ASCollectionLayoutDelegate.h index 852604758..2bb9e6cd6 100644 --- a/Source/Details/ASCollectionLayoutDelegate.h +++ b/Source/Details/ASCollectionLayoutDelegate.h @@ -93,7 +93,7 @@ NS_ASSUME_NONNULL_BEGIN * * @discussion This method will be called on main thread and must block the thread until the layout attribues is ready. * - * @param elementKind A string that identifies the type of supplementary element. + * @param kind A string that identifies the type of supplementary element. * * @param indexPath The index path of the element. * From fbfcf764e426843535efe4732cf182b1e23afcdf Mon Sep 17 00:00:00 2001 From: Huy Nguyen Date: Mon, 10 Jul 2017 18:09:05 +0100 Subject: [PATCH 08/25] Some improvements - Collection layout delegates must have a crollable directions property. - Simplify gallery delegate by not storing unmeasured attributes since calling measure on already measured elements should be cache hits and super fast. - Abstact some code in gallery delegate to ASCollectionLayoutState+Private and _ASCollectionGalleryLayoutItem. - Other improvements in gallery delegate --- AsyncDisplayKit.xcodeproj/project.pbxproj | 20 +- .../Details/ASCollectionFlowLayoutDelegate.h | 1 - .../Details/ASCollectionFlowLayoutDelegate.m | 6 +- ...mm => ASCollectionGalleryLayoutDelegate.m} | 237 +++--------------- Source/Details/ASCollectionLayoutContext.h | 1 - Source/Details/ASCollectionLayoutDelegate.h | 7 +- Source/Details/ASCollectionLayoutState.h | 13 +- Source/Details/ASCollectionLayoutState.m | 91 +++++-- Source/Private/ASCollectionLayout.mm | 30 +-- .../Private/ASCollectionLayoutState+Private.h | 32 +++ .../Private/_ASCollectionGalleryLayoutItem.h | 38 +++ .../Private/_ASCollectionGalleryLayoutItem.mm | 86 +++++++ .../Sample/MosaicCollectionLayoutDelegate.m | 3 + 13 files changed, 315 insertions(+), 250 deletions(-) rename Source/Details/{ASCollectionGalleryLayoutDelegate.mm => ASCollectionGalleryLayoutDelegate.m} (53%) create mode 100644 Source/Private/ASCollectionLayoutState+Private.h create mode 100644 Source/Private/_ASCollectionGalleryLayoutItem.h create mode 100644 Source/Private/_ASCollectionGalleryLayoutItem.mm diff --git a/AsyncDisplayKit.xcodeproj/project.pbxproj b/AsyncDisplayKit.xcodeproj/project.pbxproj index e2afb326c..ae72f9f87 100644 --- a/AsyncDisplayKit.xcodeproj/project.pbxproj +++ b/AsyncDisplayKit.xcodeproj/project.pbxproj @@ -432,6 +432,9 @@ 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 */; }; + E5775AFC1F13CE9F00CAC9BC /* _ASCollectionGalleryLayoutItem.h in Headers */ = {isa = PBXBuildFile; fileRef = E5775AFB1F13CE9F00CAC9BC /* _ASCollectionGalleryLayoutItem.h */; }; + E5775AFE1F13CF7400CAC9BC /* _ASCollectionGalleryLayoutItem.mm in Sources */ = {isa = PBXBuildFile; fileRef = E5775AFD1F13CF7400CAC9BC /* _ASCollectionGalleryLayoutItem.mm */; }; + E5775B001F13D25400CAC9BC /* ASCollectionLayoutState+Private.h in Headers */ = {isa = PBXBuildFile; fileRef = E5775AFF1F13D25400CAC9BC /* ASCollectionLayoutState+Private.h */; }; E5855DEF1EBB4D83003639AE /* ASCollectionLayoutDefines.m in Sources */ = {isa = PBXBuildFile; fileRef = E5855DED1EBB4D83003639AE /* ASCollectionLayoutDefines.m */; }; E5855DF01EBB4D83003639AE /* ASCollectionLayoutDefines.h in Headers */ = {isa = PBXBuildFile; fileRef = E5855DEE1EBB4D83003639AE /* ASCollectionLayoutDefines.h */; }; E58E9E421E941D74004CFC59 /* ASCollectionFlowLayoutDelegate.h in Headers */ = {isa = PBXBuildFile; fileRef = E58E9E3D1E941D74004CFC59 /* ASCollectionFlowLayoutDelegate.h */; settings = {ATTRIBUTES = (Public, ); }; }; @@ -451,7 +454,7 @@ E5E281741E71C833006B67C2 /* ASCollectionLayoutState.h in Headers */ = {isa = PBXBuildFile; fileRef = E5E281731E71C833006B67C2 /* ASCollectionLayoutState.h */; settings = {ATTRIBUTES = (Public, ); }; }; E5E281761E71C845006B67C2 /* ASCollectionLayoutState.m in Sources */ = {isa = PBXBuildFile; fileRef = E5E281751E71C845006B67C2 /* ASCollectionLayoutState.m */; }; E5E2D72E1EA780C4005C24C6 /* ASCollectionGalleryLayoutDelegate.h in Headers */ = {isa = PBXBuildFile; fileRef = E5E2D72D1EA780C4005C24C6 /* ASCollectionGalleryLayoutDelegate.h */; settings = {ATTRIBUTES = (Public, ); }; }; - E5E2D7301EA780DF005C24C6 /* ASCollectionGalleryLayoutDelegate.mm in Sources */ = {isa = PBXBuildFile; fileRef = E5E2D72F1EA780DF005C24C6 /* ASCollectionGalleryLayoutDelegate.mm */; }; + E5E2D7301EA780DF005C24C6 /* ASCollectionGalleryLayoutDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = E5E2D72F1EA780DF005C24C6 /* ASCollectionGalleryLayoutDelegate.m */; }; F711994E1D20C21100568860 /* ASDisplayNodeExtrasTests.m in Sources */ = {isa = PBXBuildFile; fileRef = F711994D1D20C21100568860 /* ASDisplayNodeExtrasTests.m */; }; /* End PBXBuildFile section */ @@ -913,6 +916,9 @@ 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 = ""; }; + E5775AFB1F13CE9F00CAC9BC /* _ASCollectionGalleryLayoutItem.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = _ASCollectionGalleryLayoutItem.h; sourceTree = ""; }; + E5775AFD1F13CF7400CAC9BC /* _ASCollectionGalleryLayoutItem.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = _ASCollectionGalleryLayoutItem.mm; sourceTree = ""; }; + E5775AFF1F13D25400CAC9BC /* ASCollectionLayoutState+Private.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "ASCollectionLayoutState+Private.h"; sourceTree = ""; }; E5855DED1EBB4D83003639AE /* ASCollectionLayoutDefines.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASCollectionLayoutDefines.m; sourceTree = ""; }; E5855DEE1EBB4D83003639AE /* ASCollectionLayoutDefines.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASCollectionLayoutDefines.h; sourceTree = ""; }; E58E9E3D1E941D74004CFC59 /* ASCollectionFlowLayoutDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASCollectionFlowLayoutDelegate.h; sourceTree = ""; }; @@ -932,7 +938,7 @@ E5E281731E71C833006B67C2 /* ASCollectionLayoutState.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASCollectionLayoutState.h; sourceTree = ""; }; E5E281751E71C845006B67C2 /* ASCollectionLayoutState.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASCollectionLayoutState.m; sourceTree = ""; }; E5E2D72D1EA780C4005C24C6 /* ASCollectionGalleryLayoutDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASCollectionGalleryLayoutDelegate.h; sourceTree = ""; }; - E5E2D72F1EA780DF005C24C6 /* ASCollectionGalleryLayoutDelegate.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; lineEnding = 0; path = ASCollectionGalleryLayoutDelegate.mm; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.objcpp; }; + E5E2D72F1EA780DF005C24C6 /* ASCollectionGalleryLayoutDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; lineEnding = 0; path = ASCollectionGalleryLayoutDelegate.m; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.objcpp; }; EFA731F0396842FF8AB635EE /* libPods-AsyncDisplayKitTests.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-AsyncDisplayKitTests.a"; sourceTree = BUILT_PRODUCTS_DIR; }; F711994D1D20C21100568860 /* ASDisplayNodeExtrasTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASDisplayNodeExtrasTests.m; sourceTree = ""; }; FB07EABBCF28656C6297BC2D /* Pods-AsyncDisplayKitTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-AsyncDisplayKitTests.debug.xcconfig"; path = "Pods/Target Support Files/Pods-AsyncDisplayKitTests/Pods-AsyncDisplayKitTests.debug.xcconfig"; sourceTree = ""; }; @@ -1651,6 +1657,9 @@ E5B5B9D01E9BAD9800A6B726 /* ASCollectionLayoutContext+Private.h */, E5855DEE1EBB4D83003639AE /* ASCollectionLayoutDefines.h */, E5855DED1EBB4D83003639AE /* ASCollectionLayoutDefines.m */, + E5775AFF1F13D25400CAC9BC /* ASCollectionLayoutState+Private.h */, + E5775AFB1F13CE9F00CAC9BC /* _ASCollectionGalleryLayoutItem.h */, + E5775AFD1F13CF7400CAC9BC /* _ASCollectionGalleryLayoutItem.mm */, ); name = "Collection Layout"; sourceTree = ""; @@ -1666,7 +1675,7 @@ E58E9E3D1E941D74004CFC59 /* ASCollectionFlowLayoutDelegate.h */, E58E9E3E1E941D74004CFC59 /* ASCollectionFlowLayoutDelegate.m */, E5E2D72D1EA780C4005C24C6 /* ASCollectionGalleryLayoutDelegate.h */, - E5E2D72F1EA780DF005C24C6 /* ASCollectionGalleryLayoutDelegate.mm */, + E5E2D72F1EA780DF005C24C6 /* ASCollectionGalleryLayoutDelegate.m */, E54E81FA1EB357BD00FFE8E1 /* ASPageTable.h */, E54E81FB1EB357BD00FFE8E1 /* ASPageTable.m */, ); @@ -1692,6 +1701,7 @@ files = ( CC034A131E649F1300626263 /* AsyncDisplayKit+IGListKitMethods.h in Headers */, 693A1DCA1ECC944E00D0C9D2 /* IGListAdapter+AsyncDisplayKit.h in Headers */, + E5775B001F13D25400CAC9BC /* ASCollectionLayoutState+Private.h in Headers */, E5E2D72E1EA780C4005C24C6 /* ASCollectionGalleryLayoutDelegate.h in Headers */, E58E9E461E941D74004CFC59 /* ASCollectionLayoutDelegate.h in Headers */, E5E281741E71C833006B67C2 /* ASCollectionLayoutState.h in Headers */, @@ -1872,6 +1882,7 @@ 254C6B751BF94DF4003EC431 /* ASTextKitComponents.h in Headers */, B35062081B010EFD0018CF92 /* ASScrollNode.h in Headers */, CCA282CC1E9EB73E0037E8B7 /* ASTipNode.h in Headers */, + E5775AFC1F13CE9F00CAC9BC /* _ASCollectionGalleryLayoutItem.h in Headers */, 25E327571C16819500A2170C /* ASPagerNode.h in Headers */, CCCCCCDB1EC3EF060087FE10 /* ASTextLine.h in Headers */, 9C70F20E1CDBE9E5007D6C76 /* NSArray+Diffing.h in Headers */, @@ -2253,13 +2264,14 @@ 254C6B821BF94F8A003EC431 /* ASTextKitComponents.mm in Sources */, 34EFC7601B701C8B00AD841F /* ASInsetLayoutSpec.mm in Sources */, AC6145441D8AFD4F003D62A2 /* ASSection.m in Sources */, + E5775AFE1F13CF7400CAC9BC /* _ASCollectionGalleryLayoutItem.mm in Sources */, 34EFC75E1B701BF000AD841F /* ASInternalHelpers.m in Sources */, 34EFC7681B701CDE00AD841F /* ASLayout.mm in Sources */, DECBD6EA1BE56E1900CF4905 /* ASButtonNode.mm in Sources */, CCCCCCE01EC3EF060087FE10 /* ASTextRunDelegate.m in Sources */, CCCCCCDA1EC3EF060087FE10 /* ASTextLayout.m in Sources */, 254C6B841BF94F8A003EC431 /* ASTextNodeWordKerner.m in Sources */, - E5E2D7301EA780DF005C24C6 /* ASCollectionGalleryLayoutDelegate.mm in Sources */, + E5E2D7301EA780DF005C24C6 /* ASCollectionGalleryLayoutDelegate.m in Sources */, 34EFC76B1B701CEB00AD841F /* ASLayoutSpec.mm in Sources */, CC3B20861C3F76D600798563 /* ASPendingStateController.mm in Sources */, 254C6B8C1BF94F8A003EC431 /* ASTextKitTailTruncater.mm in Sources */, diff --git a/Source/Details/ASCollectionFlowLayoutDelegate.h b/Source/Details/ASCollectionFlowLayoutDelegate.h index a143eaf80..f68cc74fb 100644 --- a/Source/Details/ASCollectionFlowLayoutDelegate.h +++ b/Source/Details/ASCollectionFlowLayoutDelegate.h @@ -16,7 +16,6 @@ // #import -#import NS_ASSUME_NONNULL_BEGIN diff --git a/Source/Details/ASCollectionFlowLayoutDelegate.m b/Source/Details/ASCollectionFlowLayoutDelegate.m index 40c0e2e9d..68f5e297f 100644 --- a/Source/Details/ASCollectionFlowLayoutDelegate.m +++ b/Source/Details/ASCollectionFlowLayoutDelegate.m @@ -26,9 +26,9 @@ #import #import -@implementation ASCollectionFlowLayoutDelegate { - ASScrollDirection _scrollableDirections; -} +@implementation ASCollectionFlowLayoutDelegate + +@synthesize scrollableDirections = _scrollableDirections; - (instancetype)init { diff --git a/Source/Details/ASCollectionGalleryLayoutDelegate.mm b/Source/Details/ASCollectionGalleryLayoutDelegate.m similarity index 53% rename from Source/Details/ASCollectionGalleryLayoutDelegate.mm rename to Source/Details/ASCollectionGalleryLayoutDelegate.m index 46f0f350a..12452378d 100644 --- a/Source/Details/ASCollectionGalleryLayoutDelegate.mm +++ b/Source/Details/ASCollectionGalleryLayoutDelegate.m @@ -12,18 +12,17 @@ #import -#import +#import #import +#import #import #import #import #import -#import +#import #import #import #import -#import -#import #import #import #import @@ -36,198 +35,15 @@ static const ASScrollDirection kASStaticScrollDirection = (ASScrollDirectionRight | ASScrollDirectionDown); -#pragma mark - _ASGalleryLayoutItem - -NS_ASSUME_NONNULL_BEGIN - -/** - * A dummy item that represents a collection element to participate in the collection layout calculation process - * without triggering measurement on the actual node of the collection element. - * - * This item always has a fixed size that is the item size passed to it. - */ -AS_SUBCLASSING_RESTRICTED -@interface _ASGalleryLayoutItem : NSObject - -@property (nonatomic, assign, readonly) CGSize itemSize; -@property (nonatomic, weak, readonly) ASCollectionElement *collectionElement; - -- (instancetype)initWithItemSize:(CGSize)itemSize collectionElement:(ASCollectionElement *)collectionElement; -- (instancetype)init __unavailable; - -@end - -NS_ASSUME_NONNULL_END - -@implementation _ASGalleryLayoutItem { - std::atomic _primitiveTraitCollection; -} - -@synthesize style; - -- (instancetype)initWithItemSize:(CGSize)itemSize collectionElement:(ASCollectionElement *)collectionElement -{ - self = [super init]; - if (self) { - ASDisplayNodeAssert(! CGSizeEqualToSize(CGSizeZero, itemSize), @"Item size should not be zero"); - ASDisplayNodeAssertNotNil(collectionElement, @"Collection element should not be nil"); - _itemSize = itemSize; - _collectionElement = collectionElement; - } - return self; -} - -ASLayoutElementStyleExtensibilityForwarding -ASPrimitiveTraitCollectionDefaults -ASPrimitiveTraitCollectionDeprecatedImplementation - -- (ASTraitCollection *)asyncTraitCollection -{ - ASDisplayNodeAssertNotSupported(); - return nil; -} - -- (ASLayoutElementType)layoutElementType -{ - return ASLayoutElementTypeLayoutSpec; -} - -- (NSArray> *)sublayoutElements -{ - ASDisplayNodeAssertNotSupported(); - return nil; -} - -- (BOOL)implementsLayoutMethod -{ - return YES; -} - -ASLayoutElementLayoutCalculationDefaults - -- (ASLayout *)calculateLayoutThatFits:(ASSizeRange)constrainedSize -{ - ASDisplayNodeAssert(CGSizeEqualToSize(_itemSize, ASSizeRangeClamp(constrainedSize, _itemSize)), - @"Item size %@ can't fit within the bounds of constrained size %@", NSStringFromCGSize(_itemSize), NSStringFromASSizeRange(constrainedSize)); - return [ASLayout layoutWithLayoutElement:self size:_itemSize]; -} - -#pragma mark - ASLayoutElementAsciiArtProtocol - -- (NSString *)asciiArtString -{ - return [ASLayoutSpec asciiArtStringForChildren:@[] parentName:[self asciiArtName]]; -} - -- (NSString *)asciiArtName -{ - return [NSMutableString stringWithCString:object_getClassName(self) encoding:NSASCIIStringEncoding]; -} - -@end - -#pragma mark - _ASGalleryLayoutStateAdditionInfo - -NS_ASSUME_NONNULL_BEGIN - -/** - * A thread-safe object that contains additional information of a collection layout. - * - * It keeps track of layout attributes of all unmeasured elements in a layout and facilitates highly-optimized lookups - * for unmeasured elements within a given rect. - */ -AS_SUBCLASSING_RESTRICTED -@interface _ASGalleryLayoutStateAdditionInfo : NSObject - -/// Sets unmeasured layout attributes to this object. -- (void)setUnmeasuredLayoutAttributes:(NSArray *)layoutAttributes contentSize:(CGSize)contentSize pageSize:(CGSize)pageSize; - -/// Removes and returns unmeasured layout attributes that intersect the specified rect -- (nullable ASPageTable *> *)getAndRemoveUnmeasuredLayoutAttributesInRect:(CGRect)rect contentSize:(CGSize)contentSize pageSize:(CGSize)pageSize; - -@end - -NS_ASSUME_NONNULL_END - -@implementation _ASGalleryLayoutStateAdditionInfo { - ASDN::Mutex __instanceLock__; - ASPageTable *> *_pageToUnmeasuredLayoutAttributesTable; -} - -- (void)setUnmeasuredLayoutAttributes:(NSArray *)layoutAttributes contentSize:(CGSize)contentSize pageSize:(CGSize)pageSize -{ - ASDN::MutexLocker l(__instanceLock__); - _pageToUnmeasuredLayoutAttributesTable = [ASPageTable pageTableWithLayoutAttributes:layoutAttributes contentSize:contentSize pageSize:pageSize]; -} - -- (ASPageTable *> *)getAndRemoveUnmeasuredLayoutAttributesInRect:(CGRect)rect contentSize:(CGSize)contentSize pageSize:(CGSize)pageSize -{ - if (CGRectIsNull(rect) || CGRectIsEmpty(rect) || CGSizeEqualToSize(CGSizeZero, pageSize) || CGSizeEqualToSize(CGSizeZero, contentSize)) { - return nil; - } - - ASDN::MutexLocker l(__instanceLock__); - ASDisplayNodeAssertNotNil(_pageToUnmeasuredLayoutAttributesTable, @"Unmeasured page map hasn't been set"); - if (_pageToUnmeasuredLayoutAttributesTable.count == 0) { - return nil; - } - - // Step 1: Determine all the pages that intersect the specified rect - NSPointerArray *pagesInRect = ASPageCoordinatesForPagesThatIntersectRect(rect, contentSize, pageSize); - if (pagesInRect.count == 0) { - return nil; - } - - // Step 2: Filter out attributes in these pages that intersect the specified rect. Remove them from the internal table as we go - ASPageTable *result = [ASPageTable pageTableForStrongObjectPointers]; - for (id pagePtr in pagesInRect) { - ASPageCoordinate page = (ASPageCoordinate)pagePtr; - NSMutableArray *attrsInPage = [_pageToUnmeasuredLayoutAttributesTable objectForPage:page]; - - if (NSUInteger attrsCount = attrsInPage.count > 0) { - NSMutableArray *interesectingAttrsInPage = nil; - - CGRect pageRect = ASPageCoordinateGetPageRect(page, pageSize); - if (CGRectContainsRect(rect, pageRect)) { - // The page fits well within the specified rect. Simply return all attributes in this page. - // Don't need to make a copy of attrsInPage here because it will be removed from the page table soon anyway. - interesectingAttrsInPage = attrsInPage; - } else { - // The page intersects the specified rect. Some attributes in this page are to be returned, some are not. - for (UICollectionViewLayoutAttributes *attrs in attrsInPage) { - if (CGRectIntersectsRect(rect, attrs.frame)) { - if (interesectingAttrsInPage == nil) { - interesectingAttrsInPage = [NSMutableArray array]; - } - [interesectingAttrsInPage addObject:attrs]; - } - } - } - - if (NSUInteger interesectingAttrsCount = interesectingAttrsInPage.count > 0) { - [result setObject:interesectingAttrsInPage forPage:page]; - if (attrsCount == interesectingAttrsCount) { - // All attributes in this page intersect the specified rect. Remove the whole page. - [_pageToUnmeasuredLayoutAttributesTable removeObjectForPage:page]; - } else { - [attrsInPage removeObjectsInArray:interesectingAttrsInPage]; - } - } - } - } - - return result; -} - -@end - #pragma mark - ASCollectionGalleryLayoutDelegate @implementation ASCollectionGalleryLayoutDelegate { - ASScrollDirection _scrollableDirections; CGSize _itemSize; + ASSizeRange _itemSizeRange; } +@synthesize scrollableDirections = _scrollableDirections; + - (instancetype)initWithScrollableDirections:(ASScrollDirection)scrollableDirections itemSize:(CGSize)itemSize { self = [super init]; @@ -235,6 +51,7 @@ - (instancetype)initWithScrollableDirections:(ASScrollDirection)scrollableDirect ASDisplayNodeAssertFalse(CGSizeEqualToSize(CGSizeZero, itemSize)); _scrollableDirections = scrollableDirections; _itemSize = itemSize; + _itemSizeRange = ASSizeRangeMake(_itemSize); } return self; } @@ -263,7 +80,7 @@ - (ASCollectionLayoutState *)calculateLayoutWithContext:(ASCollectionLayoutConte flexWrap:ASStackLayoutFlexWrapWrap alignContent:ASStackLayoutAlignContentStart children:children]; - // TODO Profile to see if a concurrent stack helps here? + stackSpec.concurrent = YES; ASLayout *layout = [stackSpec layoutThatFits:ASSizeRangeForCollectionLayoutThatFitsViewportSize(pageSize, _scrollableDirections)]; // Step 2: Create neccessary objects to hold information extracted from the layout @@ -272,17 +89,14 @@ - (ASCollectionLayoutState *)calculateLayoutWithContext:(ASCollectionLayoutConte }; ASCollectionLayoutState *collectionLayout = [[ASCollectionLayoutState alloc] initWithContext:context layout:layout - additionalInfo:[[_ASGalleryLayoutStateAdditionInfo alloc] init] + additionalInfo:nil getElementBlock:getElementBlock]; CGSize contentSize = collectionLayout.contentSize; if (CGSizeEqualToSize(contentSize, CGSizeZero)) { return collectionLayout; } - // Step 3: Since _ASGalleryLayoutItem is a dummy layout object, register all elements as unmeasured. - [collectionLayout.additionalInfo setUnmeasuredLayoutAttributes:[collectionLayout allLayoutAttributes] contentSize:contentSize pageSize:pageSize]; - - // Step 4: Measure elements in the measure range ahead of time, block on the initial rect as it'll be visible shortly + // Step 3: Measure elements in the measure range ahead of time, block on the initial rect as it'll be visible shortly // TODO Consider content offset of the collection node CGRect initialRect = CGRectMake(0, 0, pageSize.width, pageSize.height); CGRect measureRect = CGRectExpandToRangeWithScrollableDirections(initialRect, kASDefaultMeasureRangeTuningParameters, _scrollableDirections, kASStaticScrollDirection); @@ -294,7 +108,7 @@ - (ASCollectionLayoutState *)calculateLayoutWithContext:(ASCollectionLayoutConte - (void)ensureLayoutAttributesForElementsInRect:(CGRect)rect withLayout:(ASCollectionLayoutState *)layout { ASDisplayNodeAssertMainThread(); - if (CGRectIsEmpty(rect)) { + if (CGRectIsEmpty(rect) || layout == nil) { return; } @@ -306,6 +120,10 @@ - (void)ensureLayoutAttributesForElementsInRect:(CGRect)rect withLayout:(ASColle - (void)ensureLayoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath withLayout:(ASCollectionLayoutState *)layout { ASDisplayNodeAssertMainThread(); + if (layout == nil) { + return; + } + ASCollectionElement *element = [layout.context.elements elementForItemAtIndexPath:indexPath]; ASCellNode *node = element.node; if (! CGSizeEqualToSize(_itemSize, node.calculatedSize)) { @@ -318,11 +136,14 @@ - (void)ensureLayoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath withLa */ + (void)_measureElementsInRect:(CGRect)rect blockingRect:(CGRect)blockingRect layout:(ASCollectionLayoutState *)layout elementSize:(CGSize)elementSize { - if (CGRectIsEmpty(rect)) { + if (CGRectIsEmpty(rect) || layout == nil || CGSizeEqualToSize(CGSizeZero, elementSize)) { return; } BOOL hasBlockingRect = !CGRectIsEmpty(blockingRect); - ASDisplayNodeAssert(! hasBlockingRect || CGRectContainsRect(rect, blockingRect), @"Blocking rect, if specified, must be within the other (outer) rect"); + if (hasBlockingRect && CGRectContainsRect(rect, blockingRect) == NO) { + ASDisplayNodeAssert(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; @@ -336,12 +157,12 @@ + (void)_measureElementsInRect:(CGRect)rect blockingRect:(CGRect)blockingRect la hasBlockingRect = !CGRectIsNull(blockingRect); } - // Step 2: Get layout attributes of all unmeasured elements within the specified outer rect + // Step 2: Get layout attributes of all elements within the specified outer rect ASCollectionLayoutContext *context = layout.context; CGSize pageSize = context.viewportSize; - ASPageTable *unmeasuredAttrsTable = [layout.additionalInfo getAndRemoveUnmeasuredLayoutAttributesInRect:rect contentSize:contentSize pageSize:pageSize]; - if (unmeasuredAttrsTable.count == 0) { - // No unmeasured elements in this rect! Bail early + ASPageTable *attrsTable = [layout pageToLayoutAttributesTableForElementsInRect:rect contentSize:contentSize pageSize:pageSize]; + if (attrsTable.count == 0) { + // No elements in this rect! Bail early return; } @@ -350,9 +171,9 @@ + (void)_measureElementsInRect:(CGRect)rect blockingRect:(CGRect)blockingRect la NSMutableOrderedSet *blockingAttrs = hasBlockingRect ? [NSMutableOrderedSet orderedSet] : nil; // Use a set here because some items may span multiple pages NSMutableSet *nonBlockingAttrs = [NSMutableSet set]; - for (id pagePtr in unmeasuredAttrsTable) { + for (id pagePtr in attrsTable) { ASPageCoordinate page = (ASPageCoordinate)pagePtr; - NSArray *attrsInPage = [[unmeasuredAttrsTable objectForPage:page] allObjects]; + NSArray *attrsInPage = [[attrsTable objectForPage:page] allObjects]; // Calculate the page's rect but only if it's going to be used. CGRect pageRect = hasBlockingRect ? ASPageCoordinateGetPageRect(page, pageSize) : CGRectZero; @@ -378,7 +199,8 @@ + (void)_measureElementsInRect:(CGRect)rect blockingRect:(CGRect)blockingRect la ASElementMap *elements = context.elements; ASSizeRange sizeRange = ASSizeRangeMake(elementSize); dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); - if (NSUInteger count = blockingAttrs.count > 0) { + NSUInteger count = blockingAttrs.count; + if (count > 0) { ASDispatchApply(count, queue, 0, ^(size_t i) { ASCollectionElement *element = [elements elementForItemAtIndexPath:blockingAttrs[i].indexPath]; ASCellNode *node = element.node; @@ -389,11 +211,12 @@ + (void)_measureElementsInRect:(CGRect)rect blockingRect:(CGRect)blockingRect la } // Step 5: Allocate and measure non-blocking ones + // TODO Limit the number of threads for (UICollectionViewLayoutAttributes *attrs in nonBlockingAttrs) { __weak ASCollectionElement *weakElement = [elements elementForItemAtIndexPath:attrs.indexPath]; dispatch_async(queue, ^{ __strong ASCollectionElement *strongElement = weakElement; - if (strongElement != nil) { + if (strongElement) { ASCellNode *node = strongElement.node; if (! CGSizeEqualToSize(elementSize, node.calculatedSize)) { [node layoutThatFits:sizeRange]; diff --git a/Source/Details/ASCollectionLayoutContext.h b/Source/Details/ASCollectionLayoutContext.h index 60c7c3929..c6ed3b84c 100644 --- a/Source/Details/ASCollectionLayoutContext.h +++ b/Source/Details/ASCollectionLayoutContext.h @@ -15,7 +15,6 @@ // http://www.apache.org/licenses/LICENSE-2.0 // -#import #import #import diff --git a/Source/Details/ASCollectionLayoutDelegate.h b/Source/Details/ASCollectionLayoutDelegate.h index 2bb9e6cd6..2ddd2e4fe 100644 --- a/Source/Details/ASCollectionLayoutDelegate.h +++ b/Source/Details/ASCollectionLayoutDelegate.h @@ -17,6 +17,7 @@ #import #import +#import @class ASElementMap, ASCollectionLayoutContext, ASCollectionLayoutState; @@ -24,6 +25,8 @@ NS_ASSUME_NONNULL_BEGIN @protocol ASCollectionLayoutDelegate +@property (nonatomic, assign, readonly) ASScrollDirection scrollableDirections; + // TODO Add delegate method to inform whether this delegate can allocate nodes itself /** @@ -99,7 +102,9 @@ NS_ASSUME_NONNULL_BEGIN * * @param layout The layout object previously returned by `-calculateLayoutWithContext:`. */ -- (void)ensureLayoutAttributesForSupplementaryElementOfKind:(NSString *)kind atIndexPath:(NSIndexPath *)indexPath withLayout:(ASCollectionLayoutState *)layout; +- (void)ensureLayoutAttributesForSupplementaryElementOfKind:(NSString *)kind + atIndexPath:(NSIndexPath *)indexPath + withLayout:(ASCollectionLayoutState *)layout; @end diff --git a/Source/Details/ASCollectionLayoutState.h b/Source/Details/ASCollectionLayoutState.h index 6cb314e58..ba1122723 100644 --- a/Source/Details/ASCollectionLayoutState.h +++ b/Source/Details/ASCollectionLayoutState.h @@ -55,7 +55,10 @@ AS_SUBCLASSING_RESTRICTED * @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 have NSMapTableObjectPointerPersonality and NSMapTableWeakMemory as key options. */ -- (instancetype)initWithContext:(ASCollectionLayoutContext *)context contentSize:(CGSize)contentSize additionalInfo:(nullable id)additionalInfo elementToLayoutAttributesTable:(NSMapTable *)table NS_DESIGNATED_INITIALIZER; +- (instancetype)initWithContext:(ASCollectionLayoutContext *)context + contentSize:(CGSize)contentSize + additionalInfo:(nullable id)additionalInfo + elementToLayoutAttributesTable:(NSMapTable *)table NS_DESIGNATED_INITIALIZER; /** * Convenience initializer. @@ -68,7 +71,10 @@ AS_SUBCLASSING_RESTRICTED * * @param getElementBlock A block that can retrieve the collection element from a direct sublayout of the root layout. */ -- (instancetype)initWithContext:(ASCollectionLayoutContext *)context layout:(ASLayout *)layout additionalInfo:(nullable id)additionalInfo getElementBlock:(ASCollectionElement *(^)(ASLayout *))getElementBlock; +- (instancetype)initWithContext:(ASCollectionLayoutContext *)context + layout:(ASLayout *)layout + additionalInfo:(nullable id)additionalInfo + getElementBlock:(ASCollectionElement *(^)(ASLayout *))getElementBlock; /** * Returns all layout attributes present in this object. @@ -96,7 +102,8 @@ AS_SUBCLASSING_RESTRICTED * * @param indexPath The index path of the element. */ -- (nullable UICollectionViewLayoutAttributes *)layoutAttributesForSupplementaryElementOfKind:(NSString *)kind atIndexPath:(NSIndexPath *)indexPath; +- (nullable UICollectionViewLayoutAttributes *)layoutAttributesForSupplementaryElementOfKind:(NSString *)kind + atIndexPath:(NSIndexPath *)indexPath; /** * Returns layout attributes of the specified element. diff --git a/Source/Details/ASCollectionLayoutState.m b/Source/Details/ASCollectionLayoutState.m index 15452d9fd..10ecca49d 100644 --- a/Source/Details/ASCollectionLayoutState.m +++ b/Source/Details/ASCollectionLayoutState.m @@ -16,6 +16,7 @@ // #import +#import #import #import @@ -38,7 +39,10 @@ @implementation ASCollectionLayoutState { ASPageTable *> *_pageToLayoutAttributesTable; } -- (instancetype)initWithContext:(ASCollectionLayoutContext *)context layout:(ASLayout *)layout additionalInfo:(nullable id)additionalInfo getElementBlock:(ASCollectionElement *(^)(ASLayout *))getElementBlock +- (instancetype)initWithContext:(ASCollectionLayoutContext *)context + layout:(ASLayout *)layout + additionalInfo:(nullable id)additionalInfo + getElementBlock:(ASCollectionElement *(^)(ASLayout *))getElementBlock { ASElementMap *elements = context.elements; NSMapTable *table = [NSMapTable elementToLayoutAttributesTable]; @@ -67,7 +71,10 @@ - (instancetype)initWithContext:(ASCollectionLayoutContext *)context layout:(ASL return [self initWithContext:context contentSize:layout.size additionalInfo:additionalInfo elementToLayoutAttributesTable:table]; } -- (instancetype)initWithContext:(ASCollectionLayoutContext *)context contentSize:(CGSize)contentSize additionalInfo:(id)additionalInfo elementToLayoutAttributesTable:(NSMapTable *)table +- (instancetype)initWithContext:(ASCollectionLayoutContext *)context + contentSize:(CGSize)contentSize + additionalInfo:(id)additionalInfo + elementToLayoutAttributesTable:(NSMapTable *)table { self = [super init]; if (self) { @@ -85,6 +92,24 @@ - (instancetype)initWithContext:(ASCollectionLayoutContext *)context contentSize return [_elementToLayoutAttributesTable.objectEnumerator 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]; +} + - (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect { CGSize pageSize = _context.viewportSize; @@ -92,7 +117,7 @@ - (instancetype)initWithContext:(ASCollectionLayoutContext *)context contentSize if (pages.count == 0) { return @[]; } - + // Use a set here because some items may span multiple pages NSMutableSet *result = [NSMutableSet set]; for (id pagePtr in pages) { @@ -100,7 +125,7 @@ - (instancetype)initWithContext:(ASCollectionLayoutContext *)context contentSize NSArray *allAttrs = [_pageToLayoutAttributesTable objectForPage:page]; if (allAttrs.count > 0) { CGRect pageRect = ASPageCoordinateGetPageRect(page, pageSize); - + if (CGRectContainsRect(rect, pageRect)) { [result addObjectsFromArray:allAttrs]; } else { @@ -115,21 +140,55 @@ - (instancetype)initWithContext:(ASCollectionLayoutContext *)context contentSize return [result allObjects]; } -- (UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath +- (ASPageTable *> *)pageToLayoutAttributesTableForElementsInRect:(CGRect)rect + contentSize:(CGSize)contentSize + pageSize:(CGSize)pageSize { - ASCollectionElement *element = [_context.elements elementForItemAtIndexPath:indexPath]; - return [_elementToLayoutAttributesTable objectForKey:element]; -} + if (_pageToLayoutAttributesTable.count == 0 || CGRectIsNull(rect) || CGRectIsEmpty(rect) || CGSizeEqualToSize(CGSizeZero, contentSize) || CGSizeEqualToSize(CGSizeZero, pageSize)) { + return nil; + } -- (UICollectionViewLayoutAttributes *)layoutAttributesForSupplementaryElementOfKind:(NSString *)elementKind atIndexPath:(NSIndexPath *)indexPath -{ - ASCollectionElement *element = [_context.elements supplementaryElementOfKind:elementKind atIndexPath:indexPath]; - return [_elementToLayoutAttributesTable objectForKey:element]; -} + // Step 1: Determine all the pages that intersect the specified rect + NSPointerArray *pagesInRect = ASPageCoordinatesForPagesThatIntersectRect(rect, contentSize, pageSize); + if (pagesInRect.count == 0) { + return nil; + } -- (UICollectionViewLayoutAttributes *)layoutAttributesForElement:(ASCollectionElement *)element -{ - return [_elementToLayoutAttributesTable objectForKey:element]; + // Step 2: Filter out attributes in these pages that intersect the specified rect. + ASPageTable *result = [ASPageTable pageTableForStrongObjectPointers]; + for (id pagePtr in pagesInRect) { + ASPageCoordinate page = (ASPageCoordinate)pagePtr; + NSMutableArray *attrsInPage = [_pageToLayoutAttributesTable objectForPage:page]; + + NSUInteger attrsCount = attrsInPage.count; + if (attrsCount > 0) { + NSMutableArray *interesectingAttrsInPage = nil; + + CGRect pageRect = ASPageCoordinateGetPageRect(page, pageSize); + if (CGRectContainsRect(rect, pageRect)) { + // The page fits well within the specified rect. Simply return all attributes in this page. + // Don't need to make a copy of attrsInPage here because it will be removed from the page table soon anyway. + interesectingAttrsInPage = attrsInPage; + } else { + // The page intersects the specified rect. Some attributes in this page are to be returned, some are not. + for (UICollectionViewLayoutAttributes *attrs in attrsInPage) { + if (CGRectIntersectsRect(rect, attrs.frame)) { + if (interesectingAttrsInPage == nil) { + interesectingAttrsInPage = [NSMutableArray array]; + } + [interesectingAttrsInPage addObject:attrs]; + } + } + } + + NSUInteger interesectingAttrsCount = interesectingAttrsInPage.count; + if (interesectingAttrsCount > 0) { + [result setObject:interesectingAttrsInPage forPage:page]; + } + } + } + + return result; } @end diff --git a/Source/Private/ASCollectionLayout.mm b/Source/Private/ASCollectionLayout.mm index d7a16063f..9fdb0013f 100644 --- a/Source/Private/ASCollectionLayout.mm +++ b/Source/Private/ASCollectionLayout.mm @@ -29,6 +29,13 @@ #import #import +struct ASCollectionLayoutDelegateFlags { + unsigned int implementsAdditionalInfoForLayoutWithElements:1; + unsigned int implementsEnsureLayoutAttributesForElementsInRect:1; + unsigned int implementsEnsureLayoutAttributesForItemAtIndexPath:1; + unsigned int implementsEnsureLayoutAttributesForSupplementaryElementOfKind:1; +}; + @interface ASCollectionLayout () { ASDN::Mutex __instanceLock__; // Non-recursive mutex, ftw! @@ -38,12 +45,7 @@ @interface ASCollectionLayout () { // The pending state calculated ahead of time, if any. ASCollectionLayoutState *_pendingLayout; - struct { - unsigned int additionalInfoForLayoutWithElements:1; - unsigned int ensureLayoutAttributesForElementsInRect:1; - unsigned int ensureLayoutAttributesForItemAtIndexPath:1; - unsigned int ensureLayoutAttributesForSupplementaryElementOfKind:1; - } _layoutDelegateFlags; + ASCollectionLayoutDelegateFlags _layoutDelegateFlags; } @end @@ -56,10 +58,10 @@ - (instancetype)initWithLayoutDelegate:(id)layoutDel if (self) { ASDisplayNodeAssertNotNil(layoutDelegate, @"Collection layout delegate cannot be nil"); _layoutDelegate = layoutDelegate; - _layoutDelegateFlags.additionalInfoForLayoutWithElements = [layoutDelegate respondsToSelector:@selector(additionalInfoForLayoutWithElements:)]; - _layoutDelegateFlags.ensureLayoutAttributesForElementsInRect = [layoutDelegate respondsToSelector:@selector(ensureLayoutAttributesForElementsInRect:withLayout:)]; - _layoutDelegateFlags.ensureLayoutAttributesForItemAtIndexPath = [layoutDelegate respondsToSelector:@selector(ensureLayoutAttributesForItemAtIndexPath:withLayout:)]; - _layoutDelegateFlags.ensureLayoutAttributesForSupplementaryElementOfKind = [layoutDelegate respondsToSelector:@selector(ensureLayoutAttributesForSupplementaryElementOfKind:atIndexPath:withLayout:)]; + _layoutDelegateFlags.implementsAdditionalInfoForLayoutWithElements = [layoutDelegate respondsToSelector:@selector(additionalInfoForLayoutWithElements:)]; + _layoutDelegateFlags.implementsEnsureLayoutAttributesForElementsInRect = [layoutDelegate respondsToSelector:@selector(ensureLayoutAttributesForElementsInRect:withLayout:)]; + _layoutDelegateFlags.implementsEnsureLayoutAttributesForItemAtIndexPath = [layoutDelegate respondsToSelector:@selector(ensureLayoutAttributesForItemAtIndexPath:withLayout:)]; + _layoutDelegateFlags.implementsEnsureLayoutAttributesForSupplementaryElementOfKind = [layoutDelegate respondsToSelector:@selector(ensureLayoutAttributesForSupplementaryElementOfKind:atIndexPath:withLayout:)]; } return self; } @@ -71,7 +73,7 @@ - (id)layoutContextWithElements:(ASElementMap *)elements ASDisplayNodeAssertMainThread(); CGSize viewportSize = [self viewportSize]; id additionalInfo = nil; - if (_layoutDelegateFlags.additionalInfoForLayoutWithElements) { + if (_layoutDelegateFlags.implementsAdditionalInfoForLayoutWithElements) { additionalInfo = [_layoutDelegate additionalInfoForLayoutWithElements:elements]; } return [[ASCollectionLayoutContext alloc] initWithViewportSize:viewportSize elements:elements additionalInfo:additionalInfo]; @@ -128,7 +130,7 @@ - (CGSize)collectionViewContentSize { ASDisplayNodeAssertMainThread(); - if (_layoutDelegate != nil && _layoutDelegateFlags.ensureLayoutAttributesForElementsInRect) { + if (_layoutDelegate != nil && _layoutDelegateFlags.implementsEnsureLayoutAttributesForElementsInRect) { [_layoutDelegate ensureLayoutAttributesForElementsInRect:rect withLayout:_layout]; } @@ -145,7 +147,7 @@ - (CGSize)collectionViewContentSize - (UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath { - if (_layoutDelegate != nil && _layoutDelegateFlags.ensureLayoutAttributesForItemAtIndexPath) { + if (_layoutDelegate != nil && _layoutDelegateFlags.implementsEnsureLayoutAttributesForItemAtIndexPath) { [_layoutDelegate ensureLayoutAttributesForItemAtIndexPath:indexPath withLayout:_layout]; } @@ -157,7 +159,7 @@ - (UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSInde - (UICollectionViewLayoutAttributes *)layoutAttributesForSupplementaryViewOfKind:(NSString *)elementKind atIndexPath:(NSIndexPath *)indexPath { - if (_layoutDelegate != nil && _layoutDelegateFlags.ensureLayoutAttributesForSupplementaryElementOfKind) { + if (_layoutDelegate != nil && _layoutDelegateFlags.implementsEnsureLayoutAttributesForSupplementaryElementOfKind) { [_layoutDelegate ensureLayoutAttributesForSupplementaryElementOfKind:elementKind atIndexPath:indexPath withLayout:_layout]; } diff --git a/Source/Private/ASCollectionLayoutState+Private.h b/Source/Private/ASCollectionLayoutState+Private.h new file mode 100644 index 000000000..e1e99edab --- /dev/null +++ b/Source/Private/ASCollectionLayoutState+Private.h @@ -0,0 +1,32 @@ +// +// ASCollectionLayoutState+Private.h +// 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 +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface ASCollectionLayoutState (Private) + +/// Returns layout attributes for elements that intersect the specified rect +- (nullable ASPageTable *> *)pageToLayoutAttributesTableForElementsInRect:(CGRect)rect + contentSize:(CGSize)contentSize + pageSize:(CGSize)pageSize; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Source/Private/_ASCollectionGalleryLayoutItem.h b/Source/Private/_ASCollectionGalleryLayoutItem.h new file mode 100644 index 000000000..07ba41d68 --- /dev/null +++ b/Source/Private/_ASCollectionGalleryLayoutItem.h @@ -0,0 +1,38 @@ +// +// _ASCollectionGalleryLayoutItem.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 + +@class ASCollectionElement; + +NS_ASSUME_NONNULL_BEGIN + +/** + * A dummy item that represents a collection element to participate in the collection layout calculation process + * without triggering measurement on the actual node of the collection element. + * + * This item always has a fixed size that is the item size passed to it. + */ +AS_SUBCLASSING_RESTRICTED +@interface _ASGalleryLayoutItem : NSObject + +@property (nonatomic, assign, readonly) CGSize itemSize; +@property (nonatomic, weak, readonly) ASCollectionElement *collectionElement; + +- (instancetype)initWithItemSize:(CGSize)itemSize collectionElement:(ASCollectionElement *)collectionElement; +- (instancetype)init __unavailable; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Source/Private/_ASCollectionGalleryLayoutItem.mm b/Source/Private/_ASCollectionGalleryLayoutItem.mm new file mode 100644 index 000000000..063ec79a0 --- /dev/null +++ b/Source/Private/_ASCollectionGalleryLayoutItem.mm @@ -0,0 +1,86 @@ +// +// _ASCollectionGalleryLayoutItem.m +// 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 +#import +#import + +@implementation _ASGalleryLayoutItem { + std::atomic _primitiveTraitCollection; +} + +@synthesize style; + +- (instancetype)initWithItemSize:(CGSize)itemSize collectionElement:(ASCollectionElement *)collectionElement +{ + self = [super init]; + if (self) { + ASDisplayNodeAssert(! CGSizeEqualToSize(CGSizeZero, itemSize), @"Item size should not be zero"); + ASDisplayNodeAssertNotNil(collectionElement, @"Collection element should not be nil"); + _itemSize = itemSize; + _collectionElement = collectionElement; + } + return self; +} + +ASLayoutElementStyleExtensibilityForwarding +ASPrimitiveTraitCollectionDefaults +ASPrimitiveTraitCollectionDeprecatedImplementation + +- (ASTraitCollection *)asyncTraitCollection +{ + ASDisplayNodeAssertNotSupported(); + return nil; +} + +- (ASLayoutElementType)layoutElementType +{ + return ASLayoutElementTypeLayoutSpec; +} + +- (NSArray> *)sublayoutElements +{ + ASDisplayNodeAssertNotSupported(); + return nil; +} + +- (BOOL)implementsLayoutMethod +{ + return YES; +} + +ASLayoutElementLayoutCalculationDefaults + +- (ASLayout *)calculateLayoutThatFits:(ASSizeRange)constrainedSize +{ + ASDisplayNodeAssert(CGSizeEqualToSize(_itemSize, ASSizeRangeClamp(constrainedSize, _itemSize)), + @"Item size %@ can't fit within the bounds of constrained size %@", NSStringFromCGSize(_itemSize), NSStringFromASSizeRange(constrainedSize)); + return [ASLayout layoutWithLayoutElement:self size:_itemSize]; +} + +#pragma mark - ASLayoutElementAsciiArtProtocol + +- (NSString *)asciiArtString +{ + return [ASLayoutSpec asciiArtStringForChildren:@[] parentName:[self asciiArtName]]; +} + +- (NSString *)asciiArtName +{ + return [NSMutableString stringWithCString:object_getClassName(self) encoding:NSASCIIStringEncoding]; +} + +@end diff --git a/examples/CustomCollectionView/Sample/MosaicCollectionLayoutDelegate.m b/examples/CustomCollectionView/Sample/MosaicCollectionLayoutDelegate.m index d82971a0b..63a3846b2 100644 --- a/examples/CustomCollectionView/Sample/MosaicCollectionLayoutDelegate.m +++ b/examples/CustomCollectionView/Sample/MosaicCollectionLayoutDelegate.m @@ -24,6 +24,8 @@ @implementation MosaicCollectionLayoutDelegate { UIEdgeInsets _interItemSpacing; } +@synthesize scrollableDirections = _scrollableDirections; + - (instancetype)initWithNumberOfColumns:(NSInteger)numberOfColumns headerHeight:(CGFloat)headerHeight { self = [super init]; @@ -33,6 +35,7 @@ - (instancetype)initWithNumberOfColumns:(NSInteger)numberOfColumns headerHeight: _columnSpacing = 10.0; _sectionInset = UIEdgeInsetsMake(10.0, 10.0, 10.0, 10.0); _interItemSpacing = UIEdgeInsetsMake(10.0, 0, 10.0, 0); + _scrollableDirections = ASScrollDirectionVerticalDirections; } return self; } From ab98d55cbea8dbd30d3eccc6c77c7cb04316d9f2 Mon Sep 17 00:00:00 2001 From: Huy Nguyen Date: Mon, 10 Jul 2017 19:44:25 +0100 Subject: [PATCH 09/25] Fix file licenses --- Source/Details/ASCollectionGalleryLayoutDelegate.m | 2 +- Source/Private/ASCollectionLayoutState+Private.h | 9 ++------- Source/Private/_ASCollectionGalleryLayoutItem.mm | 2 +- 3 files changed, 4 insertions(+), 9 deletions(-) diff --git a/Source/Details/ASCollectionGalleryLayoutDelegate.m b/Source/Details/ASCollectionGalleryLayoutDelegate.m index 12452378d..02a2bc228 100644 --- a/Source/Details/ASCollectionGalleryLayoutDelegate.m +++ b/Source/Details/ASCollectionGalleryLayoutDelegate.m @@ -1,5 +1,5 @@ // -// ASCollectionGalleryLayoutDelegate.mm +// ASCollectionGalleryLayoutDelegate.m // Texture // // Copyright (c) 2017-present, Pinterest, Inc. All rights reserved. diff --git a/Source/Private/ASCollectionLayoutState+Private.h b/Source/Private/ASCollectionLayoutState+Private.h index e1e99edab..8df2c222f 100644 --- a/Source/Private/ASCollectionLayoutState+Private.h +++ b/Source/Private/ASCollectionLayoutState+Private.h @@ -2,13 +2,8 @@ // ASCollectionLayoutState+Private.h // 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"); +// 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 // diff --git a/Source/Private/_ASCollectionGalleryLayoutItem.mm b/Source/Private/_ASCollectionGalleryLayoutItem.mm index 063ec79a0..688ef44d4 100644 --- a/Source/Private/_ASCollectionGalleryLayoutItem.mm +++ b/Source/Private/_ASCollectionGalleryLayoutItem.mm @@ -1,5 +1,5 @@ // -// _ASCollectionGalleryLayoutItem.m +// _ASCollectionGalleryLayoutItem.mm // Texture // // Copyright (c) 2017-present, Pinterest, Inc. All rights reserved. From 01eaba9cf1ce1e05e0539b737aa07c126d1037fe Mon Sep 17 00:00:00 2001 From: Huy Nguyen Date: Wed, 12 Jul 2017 13:30:19 +0100 Subject: [PATCH 10/25] Move measure range logic to ASCollectionLayout --- .../ASCollectionGalleryLayoutDelegate.m | 157 +------------ Source/Details/ASCollectionLayoutDelegate.h | 64 +---- Source/Details/ASCollectionLayoutState.h | 6 +- Source/Details/ASCollectionLayoutState.m | 20 +- Source/Details/ASDataController.h | 5 +- Source/Private/ASCollectionLayout.mm | 222 ++++++++++++++---- 6 files changed, 205 insertions(+), 269 deletions(-) diff --git a/Source/Details/ASCollectionGalleryLayoutDelegate.m b/Source/Details/ASCollectionGalleryLayoutDelegate.m index 02a2bc228..2faa3aeae 100644 --- a/Source/Details/ASCollectionGalleryLayoutDelegate.m +++ b/Source/Details/ASCollectionGalleryLayoutDelegate.m @@ -13,27 +13,16 @@ #import #import -#import #import #import #import #import #import -#import -#import +#import #import #import #import -#import #import -#import - -static const ASRangeTuningParameters kASDefaultMeasureRangeTuningParameters = { - .leadingBufferScreenfuls = 2.0, - .trailingBufferScreenfuls = 2.0 -}; - -static const ASScrollDirection kASStaticScrollDirection = (ASScrollDirectionRight | ASScrollDirectionDown); #pragma mark - ASCollectionGalleryLayoutDelegate @@ -72,7 +61,7 @@ - (ASCollectionLayoutState *)calculateLayoutWithContext:(ASCollectionLayoutConte return [[ASCollectionLayoutState alloc] initWithContext:context contentSize:CGSizeZero additionalInfo:nil elementToLayoutAttributesTable:[NSMapTable weakToStrongObjectsMapTable]]; } - // Step 1: Use a stack spec to calculate layout content size and frames of all elements without actually measuring each element + // Use a stack spec to calculate layout content size and frames of all elements without actually measuring each element ASStackLayoutSpec *stackSpec = [ASStackLayoutSpec stackLayoutSpecWithDirection:ASStackLayoutDirectionHorizontal spacing:0 justifyContent:ASStackLayoutJustifyContentStart @@ -83,147 +72,9 @@ - (ASCollectionLayoutState *)calculateLayoutWithContext:(ASCollectionLayoutConte stackSpec.concurrent = YES; ASLayout *layout = [stackSpec layoutThatFits:ASSizeRangeForCollectionLayoutThatFitsViewportSize(pageSize, _scrollableDirections)]; - // Step 2: Create neccessary objects to hold information extracted from the layout - ASCollectionElement * _Nonnull(^getElementBlock)(ASLayout * _Nonnull) = ^ASCollectionElement *(ASLayout *sublayout) { + return [[ASCollectionLayoutState alloc] initWithContext:context layout:layout additionalInfo:nil getElementBlock:^ASCollectionElement *(ASLayout *sublayout) { return ((_ASGalleryLayoutItem *)sublayout.layoutElement).collectionElement; - }; - ASCollectionLayoutState *collectionLayout = [[ASCollectionLayoutState alloc] initWithContext:context - layout:layout - additionalInfo:nil - getElementBlock:getElementBlock]; - CGSize contentSize = collectionLayout.contentSize; - if (CGSizeEqualToSize(contentSize, CGSizeZero)) { - return collectionLayout; - } - - // Step 3: Measure elements in the measure range ahead of time, block on the initial rect as it'll be visible shortly - // TODO Consider content offset of the collection node - CGRect initialRect = CGRectMake(0, 0, pageSize.width, pageSize.height); - CGRect measureRect = CGRectExpandToRangeWithScrollableDirections(initialRect, kASDefaultMeasureRangeTuningParameters, _scrollableDirections, kASStaticScrollDirection); - [ASCollectionGalleryLayoutDelegate _measureElementsInRect:measureRect blockingRect:initialRect layout:collectionLayout elementSize:_itemSize]; - - return collectionLayout; -} - -- (void)ensureLayoutAttributesForElementsInRect:(CGRect)rect withLayout:(ASCollectionLayoutState *)layout -{ - ASDisplayNodeAssertMainThread(); - if (CGRectIsEmpty(rect) || layout == nil) { - return; - } - - // Measure elements in the measure range, block on the requested rect - CGRect measureRect = CGRectExpandToRangeWithScrollableDirections(rect, kASDefaultMeasureRangeTuningParameters, _scrollableDirections, kASStaticScrollDirection); - [ASCollectionGalleryLayoutDelegate _measureElementsInRect:measureRect blockingRect:rect layout:layout elementSize:_itemSize]; -} - -- (void)ensureLayoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath withLayout:(ASCollectionLayoutState *)layout -{ - ASDisplayNodeAssertMainThread(); - if (layout == nil) { - return; - } - - ASCollectionElement *element = [layout.context.elements elementForItemAtIndexPath:indexPath]; - ASCellNode *node = element.node; - if (! CGSizeEqualToSize(_itemSize, node.calculatedSize)) { - [node layoutThatFits:ASSizeRangeMake(_itemSize, _itemSize)]; - } -} - -/** - * 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 elementSize:(CGSize)elementSize -{ - if (CGRectIsEmpty(rect) || layout == nil || CGSizeEqualToSize(CGSizeZero, elementSize)) { - return; - } - BOOL hasBlockingRect = !CGRectIsEmpty(blockingRect); - if (hasBlockingRect && CGRectContainsRect(rect, blockingRect) == NO) { - ASDisplayNodeAssert(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 - ASCollectionLayoutContext *context = layout.context; - CGSize pageSize = context.viewportSize; - ASPageTable *attrsTable = [layout pageToLayoutAttributesTableForElementsInRect:rect contentSize:contentSize pageSize:pageSize]; - 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 an ordered set here because some items may span multiple pages and they will be accessed by indexes. - NSMutableOrderedSet *blockingAttrs = hasBlockingRect ? [NSMutableOrderedSet orderedSet] : nil; - // Use a set here because some items may span multiple pages - NSMutableSet *nonBlockingAttrs = [NSMutableSet set]; - for (id pagePtr in attrsTable) { - ASPageCoordinate page = (ASPageCoordinate)pagePtr; - NSArray *attrsInPage = [[attrsTable objectForPage:page] allObjects]; - // 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; - ASSizeRange sizeRange = ASSizeRangeMake(elementSize); - dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); - NSUInteger count = blockingAttrs.count; - if (count > 0) { - ASDispatchApply(count, queue, 0, ^(size_t i) { - ASCollectionElement *element = [elements elementForItemAtIndexPath:blockingAttrs[i].indexPath]; - ASCellNode *node = element.node; - if (! CGSizeEqualToSize(elementSize, node.calculatedSize)) { - [node layoutThatFits:sizeRange]; - } - }); - } - - // Step 5: Allocate and measure non-blocking ones - // TODO Limit the number of threads - for (UICollectionViewLayoutAttributes *attrs in nonBlockingAttrs) { - __weak ASCollectionElement *weakElement = [elements elementForItemAtIndexPath:attrs.indexPath]; - dispatch_async(queue, ^{ - __strong ASCollectionElement *strongElement = weakElement; - if (strongElement) { - ASCellNode *node = strongElement.node; - if (! CGSizeEqualToSize(elementSize, node.calculatedSize)) { - [node layoutThatFits:sizeRange]; - } - } - }); - } + }]; } @end diff --git a/Source/Details/ASCollectionLayoutDelegate.h b/Source/Details/ASCollectionLayoutDelegate.h index 2ddd2e4fe..3136cde5c 100644 --- a/Source/Details/ASCollectionLayoutDelegate.h +++ b/Source/Details/ASCollectionLayoutDelegate.h @@ -27,16 +27,15 @@ NS_ASSUME_NONNULL_BEGIN @property (nonatomic, assign, readonly) ASScrollDirection scrollableDirections; -// TODO Add delegate method to inform whether this delegate can allocate nodes itself - /** - * @abstract Returns any additional information needed for a coming layout pass with the given elements. + * @abstract Returns any additional information needed for a coming layout pass (@see @c -calculateLayoutWithContext:) 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). + * It should contain all the information needed for the layout pass to perform. * - * @discussion This method will be called on main thread. + * This method will be called on main thread. */ - (nullable id)additionalInfoForLayoutWithElements:(ASElementMap *)elements; @@ -48,64 +47,13 @@ NS_ASSUME_NONNULL_BEGIN * @return The new layout calculated for the given context. * * @discussion This method is called ahead of time, i.e before the underlying collection/table view is aware of the provided elements. - * As a result, this method should rely solely on the given context and should not reach out to other objects for information not available in the context. - * - * @discussion This method will be called on background theads. It must be thread-safe and should not change any internal state of this object. + * As a result, clients must solely rely on the given context and should not reach out to other objects for information not available in the context. * - * @discussion This method must block its calling thread. It can dispatch to other theads to reduce blocking time. + * This method can be called on background theads. It must be thread-safe and should not change any internal state of this delegate. + * It must block the calling thread but can dispatch to other theads to reduce total blocking time. */ - (ASCollectionLayoutState *)calculateLayoutWithContext:(ASCollectionLayoutContext *)context; -@optional - -/** - * @abstract Ensures all elements within a given rect have valid and final layout attributes. - * - * @discussion This gives the layout delegate a chance to update the layout on the fly, as opposed to measuring everything - * in `-calculateLayoutWithContext:`. - * - * @discussion This method will be called on main thread, on every frame and must block the thread until all elements - * within the specified rect are ready. As a result, it's crucial to minimize the blocking time - * and do as much work ahead of time as possible. - * - * @param rect The rect in which elements should have valid and final layout attributes. - * - * @param layout The layout object previously returned by `-calculateLayoutWithContext:`. - */ -- (void)ensureLayoutAttributesForElementsInRect:(CGRect)rect withLayout:(ASCollectionLayoutState *)layout; - -/** - * @abstract Ensures the item at the index path has a valid and final layout attributes. - * - * @discussion This gives the layout delegate a chance to update the layout on the fly, as opposed to measuring everything - * in `-calculateLayoutWithContext:`. - * - * @discussion This method will be called on main thread and must block the thread until the layout attribues is ready. - * - * @param indexPath The index path of the item. - * - * @param layout The layout object previously returned by `-calculateLayoutWithContext:`. - */ -- (void)ensureLayoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath withLayout:(ASCollectionLayoutState *)layout; - -/** - * @abstract Ensures the supplementary element at the index path has a valid and final layout attributes. - * - * @discussion This gives the layout delegate a chance to update the layout on the fly, as opposed to measuring everything - * in `-calculateLayoutWithContext:`. - * - * @discussion This method will be called on main thread and must block the thread until the layout attribues is ready. - * - * @param kind A string that identifies the type of supplementary element. - * - * @param indexPath The index path of the element. - * - * @param layout The layout object previously returned by `-calculateLayoutWithContext:`. - */ -- (void)ensureLayoutAttributesForSupplementaryElementOfKind:(NSString *)kind - atIndexPath:(NSIndexPath *)indexPath - withLayout:(ASCollectionLayoutState *)layout; - @end NS_ASSUME_NONNULL_END diff --git a/Source/Details/ASCollectionLayoutState.h b/Source/Details/ASCollectionLayoutState.h index ba1122723..fe4b50c90 100644 --- a/Source/Details/ASCollectionLayoutState.h +++ b/Source/Details/ASCollectionLayoutState.h @@ -30,6 +30,8 @@ NS_ASSUME_NONNULL_BEGIN @end AS_SUBCLASSING_RESTRICTED + +/// An immutable state of the collection layout @interface ASCollectionLayoutState : NSObject /// The context used to calculate this object @@ -52,7 +54,7 @@ AS_SUBCLASSING_RESTRICTED * * @param additionalInfo Any additional information to be stored in this object. * - * @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. + * @param table A map between elements to their layout attributes. It must contain all elements. * It should have NSMapTableObjectPointerPersonality and NSMapTableWeakMemory as key options. */ - (instancetype)initWithContext:(ASCollectionLayoutContext *)context @@ -65,7 +67,7 @@ AS_SUBCLASSING_RESTRICTED * * @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. + * @param layout The layout describes size and position of all elements. * * @param additionalInfo Any additional information to be stored in this object. * diff --git a/Source/Details/ASCollectionLayoutState.m b/Source/Details/ASCollectionLayoutState.m index 10ecca49d..ddd1fc853 100644 --- a/Source/Details/ASCollectionLayoutState.m +++ b/Source/Details/ASCollectionLayoutState.m @@ -41,7 +41,7 @@ @implementation ASCollectionLayoutState { - (instancetype)initWithContext:(ASCollectionLayoutContext *)context layout:(ASLayout *)layout - additionalInfo:(nullable id)additionalInfo + additionalInfo:(id)additionalInfo getElementBlock:(ASCollectionElement *(^)(ASLayout *))getElementBlock { ASElementMap *elements = context.elements; @@ -74,13 +74,13 @@ - (instancetype)initWithContext:(ASCollectionLayoutContext *)context - (instancetype)initWithContext:(ASCollectionLayoutContext *)context contentSize:(CGSize)contentSize additionalInfo:(id)additionalInfo - elementToLayoutAttributesTable:(NSMapTable *)table + elementToLayoutAttributesTable:(NSMapTable *)table { self = [super init]; if (self) { _context = context; _contentSize = contentSize; - _elementToLayoutAttributesTable = table; + _elementToLayoutAttributesTable = [table copy]; // Copy the given table to make sure it won't be mutate by clients after this point. _pageToLayoutAttributesTable = [ASPageTable pageTableWithLayoutAttributes:_elementToLayoutAttributesTable.objectEnumerator contentSize:contentSize pageSize:context.viewportSize]; _additionalInfo = additionalInfo; } @@ -137,12 +137,13 @@ - (UICollectionViewLayoutAttributes *)layoutAttributesForElement:(ASCollectionEl } } } + return [result allObjects]; } -- (ASPageTable *> *)pageToLayoutAttributesTableForElementsInRect:(CGRect)rect - contentSize:(CGSize)contentSize - pageSize:(CGSize)pageSize +- (ASPageTable *> *)pageToLayoutAttributesTableForElementsInRect:(CGRect)rect + contentSize:(CGSize)contentSize + pageSize:(CGSize)pageSize { if (_pageToLayoutAttributesTable.count == 0 || CGRectIsNull(rect) || CGRectIsEmpty(rect) || CGSizeEqualToSize(CGSizeZero, contentSize) || CGSizeEqualToSize(CGSizeZero, pageSize)) { return nil; @@ -158,7 +159,7 @@ - (UICollectionViewLayoutAttributes *)layoutAttributesForElement:(ASCollectionEl ASPageTable *result = [ASPageTable pageTableForStrongObjectPointers]; for (id pagePtr in pagesInRect) { ASPageCoordinate page = (ASPageCoordinate)pagePtr; - NSMutableArray *attrsInPage = [_pageToLayoutAttributesTable objectForPage:page]; + NSArray *attrsInPage = [_pageToLayoutAttributesTable objectForPage:page]; NSUInteger attrsCount = attrsInPage.count; if (attrsCount > 0) { @@ -168,7 +169,7 @@ - (UICollectionViewLayoutAttributes *)layoutAttributesForElement:(ASCollectionEl if (CGRectContainsRect(rect, pageRect)) { // The page fits well within the specified rect. Simply return all attributes in this page. // Don't need to make a copy of attrsInPage here because it will be removed from the page table soon anyway. - interesectingAttrsInPage = attrsInPage; + interesectingAttrsInPage = [NSMutableArray arrayWithArray:attrsInPage]; } else { // The page intersects the specified rect. Some attributes in this page are to be returned, some are not. for (UICollectionViewLayoutAttributes *attrs in attrsInPage) { @@ -181,8 +182,7 @@ - (UICollectionViewLayoutAttributes *)layoutAttributesForElement:(ASCollectionEl } } - NSUInteger interesectingAttrsCount = interesectingAttrsInPage.count; - if (interesectingAttrsCount > 0) { + if (interesectingAttrsInPage.count > 0) { [result setObject:interesectingAttrsInPage forPage:page]; } } diff --git a/Source/Details/ASDataController.h b/Source/Details/ASDataController.h index 2bd56fe5e..adbe0b805 100644 --- a/Source/Details/ASDataController.h +++ b/Source/Details/ASDataController.h @@ -33,6 +33,7 @@ NS_ASSUME_NONNULL_BEGIN #define ASDataControllerLogEvent(dataController, ...) #endif +@class ASCollectionLayoutContext; @class ASCellNode; @class ASCollectionElement; @class ASDataController; @@ -136,7 +137,7 @@ extern NSString * const ASCollectionInvalidUpdateException; * * @discussion This method will be called on main thread. */ -- (id)layoutContextWithElements:(ASElementMap *)elements; +- (ASCollectionLayoutContext *)layoutContextWithElements:(ASElementMap *)elements; /** * @abstract Prepares in advance a new layout with the given context. @@ -151,7 +152,7 @@ extern NSString * const ASCollectionInvalidUpdateException; * * @discussion This method must block its calling thread. It can dispatch to other theads to reduce blocking time. */ -- (void)prepareLayoutWithContext:(id)context; +- (void)prepareLayoutWithContext:(ASCollectionLayoutContext *)context; @end diff --git a/Source/Private/ASCollectionLayout.mm b/Source/Private/ASCollectionLayout.mm index 9fdb0013f..6029f5b9a 100644 --- a/Source/Private/ASCollectionLayout.mm +++ b/Source/Private/ASCollectionLayout.mm @@ -18,24 +18,27 @@ #import #import +#import #import #import #import #import -#import +#import #import +#import #import #import #import +#import #import -struct ASCollectionLayoutDelegateFlags { - unsigned int implementsAdditionalInfoForLayoutWithElements:1; - unsigned int implementsEnsureLayoutAttributesForElementsInRect:1; - unsigned int implementsEnsureLayoutAttributesForItemAtIndexPath:1; - unsigned int implementsEnsureLayoutAttributesForSupplementaryElementOfKind:1; +static const ASRangeTuningParameters kASDefaultMeasureRangeTuningParameters = { + .leadingBufferScreenfuls = 2.0, + .trailingBufferScreenfuls = 2.0 }; +static const ASScrollDirection kASStaticScrollDirection = (ASScrollDirectionRight | ASScrollDirectionDown); + @interface ASCollectionLayout () { ASDN::Mutex __instanceLock__; // Non-recursive mutex, ftw! @@ -45,7 +48,9 @@ @interface ASCollectionLayout () { // The pending state calculated ahead of time, if any. ASCollectionLayoutState *_pendingLayout; - ASCollectionLayoutDelegateFlags _layoutDelegateFlags; + struct { + unsigned int implementsAdditionalInfoForLayoutWithElements:1; + } _layoutDelegateFlags; } @end @@ -59,19 +64,16 @@ - (instancetype)initWithLayoutDelegate:(id)layoutDel ASDisplayNodeAssertNotNil(layoutDelegate, @"Collection layout delegate cannot be nil"); _layoutDelegate = layoutDelegate; _layoutDelegateFlags.implementsAdditionalInfoForLayoutWithElements = [layoutDelegate respondsToSelector:@selector(additionalInfoForLayoutWithElements:)]; - _layoutDelegateFlags.implementsEnsureLayoutAttributesForElementsInRect = [layoutDelegate respondsToSelector:@selector(ensureLayoutAttributesForElementsInRect:withLayout:)]; - _layoutDelegateFlags.implementsEnsureLayoutAttributesForItemAtIndexPath = [layoutDelegate respondsToSelector:@selector(ensureLayoutAttributesForItemAtIndexPath:withLayout:)]; - _layoutDelegateFlags.implementsEnsureLayoutAttributesForSupplementaryElementOfKind = [layoutDelegate respondsToSelector:@selector(ensureLayoutAttributesForSupplementaryElementOfKind:atIndexPath:withLayout:)]; } return self; } #pragma mark - ASDataControllerLayoutDelegate -- (id)layoutContextWithElements:(ASElementMap *)elements +- (ASCollectionLayoutContext *)layoutContextWithElements:(ASElementMap *)elements { ASDisplayNodeAssertMainThread(); - CGSize viewportSize = [self viewportSize]; + CGSize viewportSize = [self _viewportSize]; id additionalInfo = nil; if (_layoutDelegateFlags.implementsAdditionalInfoForLayoutWithElements) { additionalInfo = [_layoutDelegate additionalInfoForLayoutWithElements:elements]; @@ -81,7 +83,7 @@ - (id)layoutContextWithElements:(ASElementMap *)elements - (void)prepareLayoutWithContext:(id)context { - ASCollectionLayoutState *layout = [_layoutDelegate calculateLayoutWithContext:context]; + ASCollectionLayoutState *layout = ASCollectionLayoutStateWithContext(context, _layoutDelegate); ASDN::MutexLocker l(__instanceLock__); _pendingLayout = layout; @@ -106,7 +108,7 @@ - (void)prepareLayout } if (layout == nil) { - layout = [_layoutDelegate calculateLayoutWithContext:context]; + layout = ASCollectionLayoutStateWithContext(context, _layoutDelegate); } _layout = layout; @@ -126,67 +128,68 @@ - (CGSize)collectionViewContentSize return _layout.contentSize; } -- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect +- (NSArray *)layoutAttributesForElementsInRect:(CGRect)blockingRect { ASDisplayNodeAssertMainThread(); - - if (_layoutDelegate != nil && _layoutDelegateFlags.implementsEnsureLayoutAttributesForElementsInRect) { - [_layoutDelegate ensureLayoutAttributesForElementsInRect:rect withLayout:_layout]; + if (CGRectIsEmpty(blockingRect)) { + return nil; } + + // Measure elements in the measure range, block on the requested rect + CGRect measureRect = CGRectExpandToRangeWithScrollableDirections(blockingRect, kASDefaultMeasureRangeTuningParameters, _layoutDelegate.scrollableDirections, kASStaticScrollDirection); + ASCollectionLayoutMeasureElementsInRects(measureRect, blockingRect, _layout); - NSArray *result = [_layout layoutAttributesForElementsInRect:rect]; - + NSArray *result = [_layout layoutAttributesForElementsInRect:blockingRect]; + ASElementMap *elements = _layout.context.elements; for (UICollectionViewLayoutAttributes *attrs in result) { ASCollectionElement *element = [elements elementForLayoutAttributes:attrs]; - [ASCollectionLayout setSize:attrs.frame.size toElement:element]; + ASCollectionLayoutSetSizeToElement(attrs.frame.size, element); } - + return result; } - (UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath { - if (_layoutDelegate != nil && _layoutDelegateFlags.implementsEnsureLayoutAttributesForItemAtIndexPath) { - [_layoutDelegate ensureLayoutAttributesForItemAtIndexPath:indexPath withLayout:_layout]; - } - + ASDisplayNodeAssertMainThread(); + ASCollectionElement *element = [_layout.context.elements elementForItemAtIndexPath:indexPath]; UICollectionViewLayoutAttributes *attrs = [_layout layoutAttributesForElement:element]; - [ASCollectionLayout setSize:attrs.frame.size toElement: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 { - if (_layoutDelegate != nil && _layoutDelegateFlags.implementsEnsureLayoutAttributesForSupplementaryElementOfKind) { - [_layoutDelegate ensureLayoutAttributesForSupplementaryElementOfKind:elementKind atIndexPath:indexPath withLayout:_layout]; - } - ASCollectionElement *element = [_layout.context.elements supplementaryElementOfKind:elementKind atIndexPath:indexPath]; UICollectionViewLayoutAttributes *attrs = [_layout layoutAttributesForElement:element]; - [ASCollectionLayout setSize:attrs.frame.size toElement: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)); + return (! CGSizeEqualToSize([self _viewportSize], newBounds.size)); } #pragma mark - Private methods -+ (void)setSize:(CGSize)size toElement:(ASCollectionElement *)element -{ - ASCellNode *node = element.node; - if (! CGSizeEqualToSize(size, node.frame.size)) { - CGRect nodeFrame = CGRectZero; - nodeFrame.size = size; - node.frame = nodeFrame; - } -} - -- (CGSize)viewportSize +- (CGSize)_viewportSize { ASCollectionNode *collectionNode = _collectionNode; if (collectionNode != nil && !collectionNode.isNodeLoaded) { @@ -198,4 +201,135 @@ - (CGSize)viewportSize } } +# pragma mark - Convenient inline functions + +ASDISPLAYNODE_INLINE ASCollectionLayoutState *ASCollectionLayoutStateWithContext(ASCollectionLayoutContext *context, id delegate) +{ + ASCollectionLayoutState *layout = [delegate calculateLayoutWithContext:context]; + + // Measure elements in the measure range ahead of time, block on the initial rect as it'll be visible shortly + CGSize viewportSize = context.viewportSize; + // TODO Consider content offset of the collection node + CGRect initialRect = CGRectMake(0, 0, viewportSize.width, viewportSize.height); + CGRect measureRect = CGRectExpandToRangeWithScrollableDirections(initialRect, kASDefaultMeasureRangeTuningParameters, delegate.scrollableDirections, kASStaticScrollDirection); + ASCollectionLayoutMeasureElementsInRects(measureRect, initialRect, layout); + + return layout; +} + +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; + } + } +} + +/** + * Measures all elements in the specified rect and blocks the calling thread while measuring those in the blocking rect. + */ +ASDISPLAYNODE_INLINE void ASCollectionLayoutMeasureElementsInRects(CGRect rect, CGRect blockingRect, ASCollectionLayoutState *layout) +{ + if (CGRectIsEmpty(rect) || layout == 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 + ASCollectionLayoutContext *context = layout.context; + CGSize pageSize = context.viewportSize; + ASPageTable *attrsTable = [layout pageToLayoutAttributesTableForElementsInRect:rect contentSize:contentSize pageSize:pageSize]; + 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 an ordered set here because some items may span multiple pages and they will be accessed by indexes. + NSMutableOrderedSet *blockingAttrs = hasBlockingRect ? [NSMutableOrderedSet orderedSet] : nil; + // Use a set here because some items may span multiple pages + NSMutableSet *nonBlockingAttrs = [NSMutableSet set]; + for (id pagePtr in attrsTable) { + ASPageCoordinate page = (ASPageCoordinate)pagePtr; + NSArray *attrsInPage = [[attrsTable objectForPage:page] allObjects]; + // 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); + NSUInteger count = blockingAttrs.count; + if (count > 0) { + ASDispatchApply(count, queue, 0, ^(size_t i) { + UICollectionViewLayoutAttributes *attrs = blockingAttrs[i]; + CGSize elementSize = attrs.frame.size; + ASCollectionElement *element = [elements elementForItemAtIndexPath:attrs.indexPath]; + ASCellNode *node = element.node; + if (! CGSizeEqualToSize(elementSize, node.calculatedSize)) { + [node layoutThatFits:ASCollectionLayoutElementSizeRangeFromSize(elementSize)]; + } + }); + } + + // Step 5: Allocate and measure non-blocking ones + // TODO Limit the number of threads + for (UICollectionViewLayoutAttributes *attrs in nonBlockingAttrs) { + CGSize elementSize = attrs.frame.size; + __weak ASCollectionElement *weakElement = [elements elementForItemAtIndexPath:attrs.indexPath]; + dispatch_async(queue, ^{ + __strong ASCollectionElement *strongElement = weakElement; + if (strongElement) { + ASCellNode *node = strongElement.node; + if (! CGSizeEqualToSize(elementSize, node.calculatedSize)) { + [node layoutThatFits:ASCollectionLayoutElementSizeRangeFromSize(elementSize)]; + } + } + }); + } +} + @end From a2b269fe17a2cba126c8f0cab18f9b4d0239de00 Mon Sep 17 00:00:00 2001 From: Huy Nguyen Date: Wed, 12 Jul 2017 14:45:18 +0100 Subject: [PATCH 11/25] Track unmeasured elements --- AsyncDisplayKit.xcodeproj/project.pbxproj | 8 +-- ...youtState.m => ASCollectionLayoutState.mm} | 72 ++++++++++++------- Source/Details/ASPageTable.h | 5 ++ Source/Details/ASPageTable.m | 15 +++- Source/Private/ASCollectionLayout.mm | 6 +- .../Private/ASCollectionLayoutState+Private.h | 12 ++-- 6 files changed, 81 insertions(+), 37 deletions(-) rename Source/Details/{ASCollectionLayoutState.m => ASCollectionLayoutState.mm} (71%) diff --git a/AsyncDisplayKit.xcodeproj/project.pbxproj b/AsyncDisplayKit.xcodeproj/project.pbxproj index ae72f9f87..692d233ea 100644 --- a/AsyncDisplayKit.xcodeproj/project.pbxproj +++ b/AsyncDisplayKit.xcodeproj/project.pbxproj @@ -452,7 +452,7 @@ E5C347B11ECB3D9200EC4BE4 /* ASBatchFetchingDelegate.h in Headers */ = {isa = PBXBuildFile; fileRef = E5C347B01ECB3D9200EC4BE4 /* ASBatchFetchingDelegate.h */; }; E5C347B31ECB40AA00EC4BE4 /* ASTableNode+Beta.h in Headers */ = {isa = PBXBuildFile; fileRef = E5C347B21ECB40AA00EC4BE4 /* ASTableNode+Beta.h */; }; E5E281741E71C833006B67C2 /* ASCollectionLayoutState.h in Headers */ = {isa = PBXBuildFile; fileRef = E5E281731E71C833006B67C2 /* ASCollectionLayoutState.h */; settings = {ATTRIBUTES = (Public, ); }; }; - E5E281761E71C845006B67C2 /* ASCollectionLayoutState.m in Sources */ = {isa = PBXBuildFile; fileRef = E5E281751E71C845006B67C2 /* ASCollectionLayoutState.m */; }; + E5E281761E71C845006B67C2 /* ASCollectionLayoutState.mm in Sources */ = {isa = PBXBuildFile; fileRef = E5E281751E71C845006B67C2 /* ASCollectionLayoutState.mm */; }; E5E2D72E1EA780C4005C24C6 /* ASCollectionGalleryLayoutDelegate.h in Headers */ = {isa = PBXBuildFile; fileRef = E5E2D72D1EA780C4005C24C6 /* ASCollectionGalleryLayoutDelegate.h */; settings = {ATTRIBUTES = (Public, ); }; }; E5E2D7301EA780DF005C24C6 /* ASCollectionGalleryLayoutDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = E5E2D72F1EA780DF005C24C6 /* ASCollectionGalleryLayoutDelegate.m */; }; F711994E1D20C21100568860 /* ASDisplayNodeExtrasTests.m in Sources */ = {isa = PBXBuildFile; fileRef = F711994D1D20C21100568860 /* ASDisplayNodeExtrasTests.m */; }; @@ -936,7 +936,7 @@ E5C347B01ECB3D9200EC4BE4 /* ASBatchFetchingDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASBatchFetchingDelegate.h; sourceTree = ""; }; E5C347B21ECB40AA00EC4BE4 /* ASTableNode+Beta.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "ASTableNode+Beta.h"; sourceTree = ""; }; E5E281731E71C833006B67C2 /* ASCollectionLayoutState.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASCollectionLayoutState.h; sourceTree = ""; }; - E5E281751E71C845006B67C2 /* ASCollectionLayoutState.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASCollectionLayoutState.m; sourceTree = ""; }; + E5E281751E71C845006B67C2 /* ASCollectionLayoutState.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASCollectionLayoutState.mm; sourceTree = ""; }; E5E2D72D1EA780C4005C24C6 /* ASCollectionGalleryLayoutDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASCollectionGalleryLayoutDelegate.h; sourceTree = ""; }; E5E2D72F1EA780DF005C24C6 /* ASCollectionGalleryLayoutDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; lineEnding = 0; path = ASCollectionGalleryLayoutDelegate.m; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.objcpp; }; EFA731F0396842FF8AB635EE /* libPods-AsyncDisplayKitTests.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-AsyncDisplayKitTests.a"; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -1670,7 +1670,7 @@ E58E9E3F1E941D74004CFC59 /* ASCollectionLayoutContext.h */, E58E9E401E941D74004CFC59 /* ASCollectionLayoutContext.mm */, E5E281731E71C833006B67C2 /* ASCollectionLayoutState.h */, - E5E281751E71C845006B67C2 /* ASCollectionLayoutState.m */, + E5E281751E71C845006B67C2 /* ASCollectionLayoutState.mm */, E58E9E411E941D74004CFC59 /* ASCollectionLayoutDelegate.h */, E58E9E3D1E941D74004CFC59 /* ASCollectionFlowLayoutDelegate.h */, E58E9E3E1E941D74004CFC59 /* ASCollectionFlowLayoutDelegate.m */, @@ -2244,7 +2244,7 @@ 68355B341CB579B9001D4E68 /* ASImageNode+AnimatedImage.mm in Sources */, E5711A301C840C96009619D4 /* ASCollectionElement.mm in Sources */, B35062511B010EFD0018CF92 /* ASDisplayNode+UIViewBridge.mm in Sources */, - E5E281761E71C845006B67C2 /* ASCollectionLayoutState.m in Sources */, + E5E281761E71C845006B67C2 /* ASCollectionLayoutState.mm in Sources */, B35061FC1B010EFD0018CF92 /* ASDisplayNode.mm in Sources */, B35061FF1B010EFD0018CF92 /* ASDisplayNodeExtras.mm in Sources */, B35062011B010EFD0018CF92 /* ASEditableTextNode.mm in Sources */, diff --git a/Source/Details/ASCollectionLayoutState.m b/Source/Details/ASCollectionLayoutState.mm similarity index 71% rename from Source/Details/ASCollectionLayoutState.m rename to Source/Details/ASCollectionLayoutState.mm index ddd1fc853..6223eb08e 100644 --- a/Source/Details/ASCollectionLayoutState.m +++ b/Source/Details/ASCollectionLayoutState.mm @@ -24,6 +24,7 @@ #import #import #import +#import @implementation NSMapTable (ASCollectionLayoutConvenience) @@ -35,8 +36,10 @@ @implementation NSMapTable (ASCollectionLayoutConvenience) @end @implementation ASCollectionLayoutState { + ASDN::Mutex __instanceLock__; NSMapTable *_elementToLayoutAttributesTable; - ASPageTable *> *_pageToLayoutAttributesTable; + ASPageTable *> *_pageToLayoutAttributesTable; + ASPageTable *> *_unmeasuredPageToLayoutAttributesTable; } - (instancetype)initWithContext:(ASCollectionLayoutContext *)context @@ -82,6 +85,8 @@ - (instancetype)initWithContext:(ASCollectionLayoutContext *)context _contentSize = contentSize; _elementToLayoutAttributesTable = [table copy]; // Copy the given table to make sure it won't be mutate by clients after this point. _pageToLayoutAttributesTable = [ASPageTable pageTableWithLayoutAttributes:_elementToLayoutAttributesTable.objectEnumerator contentSize:contentSize pageSize:context.viewportSize]; + // Assume that all elements are unmeasured, deep copy the original table + _unmeasuredPageToLayoutAttributesTable = [_pageToLayoutAttributesTable deepCopy]; _additionalInfo = additionalInfo; } return self; @@ -141,11 +146,12 @@ - (UICollectionViewLayoutAttributes *)layoutAttributesForElement:(ASCollectionEl return [result allObjects]; } -- (ASPageTable *> *)pageToLayoutAttributesTableForElementsInRect:(CGRect)rect - contentSize:(CGSize)contentSize - pageSize:(CGSize)pageSize +- (ASPageTable *> *)getAndRemoveUnmeasuredLayoutAttributesPageTableInRect:(CGRect)rect + contentSize:(CGSize)contentSize + pageSize:(CGSize)pageSize { - if (_pageToLayoutAttributesTable.count == 0 || CGRectIsNull(rect) || CGRectIsEmpty(rect) || CGSizeEqualToSize(CGSizeZero, contentSize) || CGSizeEqualToSize(CGSizeZero, pageSize)) { + ASDN::MutexLocker l(__instanceLock__); + if (_unmeasuredPageToLayoutAttributesTable.count == 0 || CGRectIsNull(rect) || CGRectIsEmpty(rect) || CGSizeEqualToSize(CGSizeZero, contentSize) || CGSizeEqualToSize(CGSizeZero, pageSize)) { return nil; } @@ -159,35 +165,49 @@ - (UICollectionViewLayoutAttributes *)layoutAttributesForElement:(ASCollectionEl ASPageTable *result = [ASPageTable pageTableForStrongObjectPointers]; for (id pagePtr in pagesInRect) { ASPageCoordinate page = (ASPageCoordinate)pagePtr; - NSArray *attrsInPage = [_pageToLayoutAttributesTable objectForPage:page]; + NSArray *attrsInPage = [_unmeasuredPageToLayoutAttributesTable objectForPage:page]; - NSUInteger attrsCount = attrsInPage.count; - if (attrsCount > 0) { - NSMutableArray *interesectingAttrsInPage = nil; + if (attrsInPage.count == 0) { + // Hm, this page should be removed before. + [_unmeasuredPageToLayoutAttributesTable removeObjectForPage:page]; + continue; + } - CGRect pageRect = ASPageCoordinateGetPageRect(page, pageSize); - if (CGRectContainsRect(rect, pageRect)) { - // The page fits well within the specified rect. Simply return all attributes in this page. - // Don't need to make a copy of attrsInPage here because it will be removed from the page table soon anyway. - interesectingAttrsInPage = [NSMutableArray arrayWithArray:attrsInPage]; - } else { - // The page intersects the specified rect. Some attributes in this page are to be returned, some are not. - for (UICollectionViewLayoutAttributes *attrs in attrsInPage) { - if (CGRectIntersectsRect(rect, attrs.frame)) { - if (interesectingAttrsInPage == nil) { - interesectingAttrsInPage = [NSMutableArray array]; - } - [interesectingAttrsInPage addObject:attrs]; - } + CGRect pageRect = ASPageCoordinateGetPageRect(page, pageSize); + if (CGRectContainsRect(rect, pageRect)) { + // This page fits well within the specified rect. Simply return all of its attributes. + [result setObject:attrsInPage forPage:page]; + [_unmeasuredPageToLayoutAttributesTable removeObjectForPage:page]; + continue; + } + + // The page intersects the specified rect. Some attributes in this page are returned, some are not. + NSMutableArray *intersectingAttrsInPage = nil; + NSMutableArray *unmeasuredAttrsInPage = nil; + BOOL hasIntersectingAttrs = NO; + + for (UICollectionViewLayoutAttributes *attrs in attrsInPage) { + if (CGRectIntersectsRect(rect, attrs.frame)) { + if (! hasIntersectingAttrs) { + intersectingAttrsInPage = [NSMutableArray array]; + unmeasuredAttrsInPage = [attrsInPage mutableCopy]; + hasIntersectingAttrs = YES; } + [intersectingAttrsInPage addObject:attrs]; + [unmeasuredAttrsInPage removeObject:attrs]; } + } - if (interesectingAttrsInPage.count > 0) { - [result setObject:interesectingAttrsInPage forPage:page]; + if (hasIntersectingAttrs) { + [result setObject:intersectingAttrsInPage forPage:page]; + if (unmeasuredAttrsInPage.count == 0) { + [_unmeasuredPageToLayoutAttributesTable removeObjectForPage:page]; + } else { + [_unmeasuredPageToLayoutAttributesTable setObject:unmeasuredAttrsInPage forPage:page]; } } } - + return result; } diff --git a/Source/Details/ASPageTable.h b/Source/Details/ASPageTable.h index d7136f8c7..d5f20f542 100644 --- a/Source/Details/ASPageTable.h +++ b/Source/Details/ASPageTable.h @@ -118,6 +118,11 @@ typedef NSMapTable ASPageTable; */ - (void)removeObjectForPage:(ASPageCoordinate)page; +/** + * Creates a new page table by deep copying a given page table + */ +- (NSMapTable *)deepCopy; + @end NS_ASSUME_NONNULL_END diff --git a/Source/Details/ASPageTable.m b/Source/Details/ASPageTable.m index 626ca975f..4d0d755a7 100644 --- a/Source/Details/ASPageTable.m +++ b/Source/Details/ASPageTable.m @@ -110,7 +110,7 @@ + (ASPageTable *)pageTableForWeakObjectPointers return [self pageTableWithValuePointerFunctions:weakObjectPointerFuncs]; } -+ (ASPageTable *> *)pageTableWithLayoutAttributes:(id)layoutAttributesEnumerator contentSize:(CGSize)contentSize pageSize:(CGSize)pageSize ++ (ASPageTable *> *)pageTableWithLayoutAttributes:(id)layoutAttributesEnumerator contentSize:(CGSize)contentSize pageSize:(CGSize)pageSize { ASPageTable *result = [ASPageTable pageTableForStrongObjectPointers]; for (UICollectionViewLayoutAttributes *attrs in layoutAttributesEnumerator) { @@ -148,4 +148,17 @@ - (void)removeObjectForPage:(ASPageCoordinate)page [self removeObjectForKey:key]; } +- (NSMapTable *)deepCopy +{ + NSMapTable *result = [[NSMapTable alloc] initWithKeyPointerFunctions:self.keyPointerFunctions + valuePointerFunctions:self.valuePointerFunctions + capacity:self.count]; + + for (id key in self) { + [result setObject:[[self objectForKey:key] copy] forKey:key]; + } + + return result; +} + @end diff --git a/Source/Private/ASCollectionLayout.mm b/Source/Private/ASCollectionLayout.mm index 6029f5b9a..7405f04ae 100644 --- a/Source/Private/ASCollectionLayout.mm +++ b/Source/Private/ASCollectionLayout.mm @@ -264,7 +264,9 @@ ASDISPLAYNODE_INLINE void ASCollectionLayoutMeasureElementsInRects(CGRect rect, // Step 2: Get layout attributes of all elements within the specified outer rect ASCollectionLayoutContext *context = layout.context; CGSize pageSize = context.viewportSize; - ASPageTable *attrsTable = [layout pageToLayoutAttributesTableForElementsInRect:rect contentSize:contentSize pageSize:pageSize]; + ASPageTable *attrsTable = [layout getAndRemoveUnmeasuredLayoutAttributesPageTableInRect:rect + contentSize:contentSize + pageSize:pageSize]; if (attrsTable.count == 0) { // No elements in this rect! Bail early return; @@ -277,7 +279,7 @@ ASDISPLAYNODE_INLINE void ASCollectionLayoutMeasureElementsInRects(CGRect rect, NSMutableSet *nonBlockingAttrs = [NSMutableSet set]; for (id pagePtr in attrsTable) { ASPageCoordinate page = (ASPageCoordinate)pagePtr; - NSArray *attrsInPage = [[attrsTable objectForPage:page] allObjects]; + NSArray *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; diff --git a/Source/Private/ASCollectionLayoutState+Private.h b/Source/Private/ASCollectionLayoutState+Private.h index 8df2c222f..a9fbb1fad 100644 --- a/Source/Private/ASCollectionLayoutState+Private.h +++ b/Source/Private/ASCollectionLayoutState+Private.h @@ -17,10 +17,14 @@ NS_ASSUME_NONNULL_BEGIN @interface ASCollectionLayoutState (Private) -/// Returns layout attributes for elements that intersect the specified rect -- (nullable ASPageTable *> *)pageToLayoutAttributesTableForElementsInRect:(CGRect)rect - contentSize:(CGSize)contentSize - pageSize:(CGSize)pageSize; +/** + * Remove and returns layout attributes for unmeasured elements that intersect the specified rect + * + * @discussion This method is atomic and thread-safe + */ +- (nullable ASPageTable *> *)getAndRemoveUnmeasuredLayoutAttributesPageTableInRect:(CGRect)rect + contentSize:(CGSize)contentSize + pageSize:(CGSize)pageSize; @end From f7ae61829c5c4cc371f392f782c83383f35cdd7e Mon Sep 17 00:00:00 2001 From: Huy Nguyen Date: Wed, 12 Jul 2017 15:20:16 +0100 Subject: [PATCH 12/25] Remove pending layout in ASCollectionLayout --- Source/Details/ASDataController.h | 24 +++++++--- Source/Details/ASDataController.mm | 8 ++-- Source/Private/ASCollectionLayout.mm | 65 +++++++++++----------------- 3 files changed, 46 insertions(+), 51 deletions(-) diff --git a/Source/Details/ASDataController.h b/Source/Details/ASDataController.h index adbe0b805..08081ec47 100644 --- a/Source/Details/ASDataController.h +++ b/Source/Details/ASDataController.h @@ -33,9 +33,10 @@ NS_ASSUME_NONNULL_BEGIN #define ASDataControllerLogEvent(dataController, ...) #endif -@class ASCollectionLayoutContext; @class ASCellNode; @class ASCollectionElement; +@class ASCollectionLayoutContext; +@class ASCollectionLayoutState; @class ASDataController; @class ASElementMap; @class ASLayout; @@ -140,19 +141,28 @@ extern NSString * const ASCollectionInvalidUpdateException; - (ASCollectionLayoutContext *)layoutContextWithElements:(ASElementMap *)elements; /** - * @abstract Prepares in advance a new layout with the given context. + * @abstract Prepares and returns a new layout for given context. * * @param context A context that was previously returned by `-layoutContextWithElements:`. * + * @return The new layout calculated for the given context. + * * @discussion This method is called ahead of time, i.e before the underlying collection/table view is aware of the provided elements. - * As a result, this method should rely solely on the given context and should not reach out to its collection/table view for information regarding items. + * As a result, clients must solely rely on the given context and should not reach out to other objects for information not available in the context. + * + * This method will be called on background theads. It must be thread-safe and should not change any internal state of the conforming object. + * It must block the calling thread but can dispatch to other theads to reduce total blocking time. + */ +- (ASCollectionLayoutState *)calculateLayoutWithContext:(ASCollectionLayoutContext *)context; + +/** + * @abstract Applies the given layout. * - * @discussion This method will be called on background theads. It must be thread-safe and should not change any internal state of the conforming object. - * It's recommended to put the resulting layouts of this method into a thread-safe cache that can be looked up later on. + * @param layout A layout that was previously returned by `-calculateLayoutWithContext:`. * - * @discussion This method must block its calling thread. It can dispatch to other theads to reduce blocking time. + * @discussion This method will be called on main thread. */ -- (void)prepareLayoutWithContext:(ASCollectionLayoutContext *)context; +- (void)applyLayout:(ASCollectionLayoutState *)layout; @end diff --git a/Source/Details/ASDataController.mm b/Source/Details/ASDataController.mm index 5516dbb7d..4be9200e7 100644 --- a/Source/Details/ASDataController.mm +++ b/Source/Details/ASDataController.mm @@ -600,13 +600,12 @@ - (void)updateWithChangeSet:(_ASHierarchyChangeSet *)changeSet ASCollectionElement *element, (element.nodeIfAllocated.calculatedLayout == nil ? element : nil)); } - + + // TODO Don't even allocate nodes here [self batchAllocateNodesFromElements:elementsToProcess andLayout:(! canDelegateLayout) batchSize:elementsToProcess.count batchCompletion:^(NSArray *elements, NSArray *nodes) { ASSERT_ON_EDITING_QUEUE; - if (canDelegateLayout) { - [_layoutDelegate prepareLayoutWithContext:layoutContext]; - } + ASCollectionLayoutState *layout = canDelegateLayout ? [_layoutDelegate calculateLayoutWithContext:layoutContext] : nil; [_mainSerialQueue performBlockOnMainThread:^{ as_activity_scope_leave(&preparationScope); @@ -623,6 +622,7 @@ - (void)updateWithChangeSet:(_ASHierarchyChangeSet *)changeSet // Thus, we can't just swap the new map immediately before step 4, but until this update block is executed. // (https://github.com/TextureGroup/Texture/issues/378) self.visibleMap = newMap; + [_layoutDelegate applyLayout:layout]; }]; }]; }]; diff --git a/Source/Private/ASCollectionLayout.mm b/Source/Private/ASCollectionLayout.mm index 7405f04ae..0a5ec34c4 100644 --- a/Source/Private/ASCollectionLayout.mm +++ b/Source/Private/ASCollectionLayout.mm @@ -30,7 +30,6 @@ #import #import #import -#import static const ASRangeTuningParameters kASDefaultMeasureRangeTuningParameters = { .leadingBufferScreenfuls = 2.0, @@ -40,14 +39,9 @@ static const ASScrollDirection kASStaticScrollDirection = (ASScrollDirectionRight | ASScrollDirectionDown); @interface ASCollectionLayout () { - ASDN::Mutex __instanceLock__; // Non-recursive mutex, ftw! - // Main thread only. ASCollectionLayoutState *_layout; - // The pending state calculated ahead of time, if any. - ASCollectionLayoutState *_pendingLayout; - struct { unsigned int implementsAdditionalInfoForLayoutWithElements:1; } _layoutDelegateFlags; @@ -81,12 +75,27 @@ - (ASCollectionLayoutContext *)layoutContextWithElements:(ASElementMap *)element return [[ASCollectionLayoutContext alloc] initWithViewportSize:viewportSize elements:elements additionalInfo:additionalInfo]; } -- (void)prepareLayoutWithContext:(id)context +- (ASCollectionLayoutState *)calculateLayoutWithContext:(ASCollectionLayoutContext *)context { - ASCollectionLayoutState *layout = ASCollectionLayoutStateWithContext(context, _layoutDelegate); - - ASDN::MutexLocker l(__instanceLock__); - _pendingLayout = layout; + ASCollectionLayoutState *layout = [_layoutDelegate calculateLayoutWithContext:context]; + + // Measure elements in the measure range ahead of time, block on the initial rect as it'll be visible shortly + CGSize viewportSize = context.viewportSize; + // TODO Consider content offset of the collection node + CGRect initialRect = CGRectMake(0, 0, viewportSize.width, viewportSize.height); + CGRect measureRect = CGRectExpandToRangeWithScrollableDirections(initialRect, + kASDefaultMeasureRangeTuningParameters, + _layoutDelegate.scrollableDirections, + kASStaticScrollDirection); + ASCollectionLayoutMeasureElementsInRects(measureRect, initialRect, layout); + + return layout; +} + +- (void)applyLayout:(ASCollectionLayoutState *)layout +{ + ASDisplayNodeAssertMainThread(); + _layout = layout; } #pragma mark - UICollectionViewLayout overrides @@ -95,23 +104,13 @@ - (void)prepareLayout { ASDisplayNodeAssertMainThread(); [super prepareLayout]; + ASCollectionLayoutContext *context = [self layoutContextWithElements:_collectionNode.visibleElements]; - - ASCollectionLayoutState *layout = nil; - { - ASDN::MutexLocker l(__instanceLock__); - if (_pendingLayout != nil && ASObjectIsEqual(_pendingLayout.context, context)) { - // Looks like we can use the pending layout. Great! - layout = _pendingLayout; - _pendingLayout = nil; - } + if (_layout == nil || ASObjectIsEqual(_layout.context, context) == NO) { + // Looks like the existing layout is either unavailable or no longer valid. + // Calculate a new layout and apply it immediately + [self applyLayout:[self calculateLayoutWithContext:context]]; } - - if (layout == nil) { - layout = ASCollectionLayoutStateWithContext(context, _layoutDelegate); - } - - _layout = layout; } - (void)invalidateLayout @@ -203,20 +202,6 @@ - (CGSize)_viewportSize # pragma mark - Convenient inline functions -ASDISPLAYNODE_INLINE ASCollectionLayoutState *ASCollectionLayoutStateWithContext(ASCollectionLayoutContext *context, id delegate) -{ - ASCollectionLayoutState *layout = [delegate calculateLayoutWithContext:context]; - - // Measure elements in the measure range ahead of time, block on the initial rect as it'll be visible shortly - CGSize viewportSize = context.viewportSize; - // TODO Consider content offset of the collection node - CGRect initialRect = CGRectMake(0, 0, viewportSize.width, viewportSize.height); - CGRect measureRect = CGRectExpandToRangeWithScrollableDirections(initialRect, kASDefaultMeasureRangeTuningParameters, delegate.scrollableDirections, kASStaticScrollDirection); - ASCollectionLayoutMeasureElementsInRects(measureRect, initialRect, layout); - - return layout; -} - ASDISPLAYNODE_INLINE ASSizeRange ASCollectionLayoutElementSizeRangeFromSize(CGSize size) { // The layout delegate consulted us that this element must fit within this size, From 8d550668f3fb2ead3c240772485b30731cc24db1 Mon Sep 17 00:00:00 2001 From: Huy Nguyen Date: Wed, 12 Jul 2017 15:37:21 +0100 Subject: [PATCH 13/25] Get back pending layout because the timing to latch new data is not ideal --- Source/ASCollectionNode.mm | 1 + Source/Details/ASDataController.mm | 2 +- Source/Private/ASCollectionLayout.mm | 24 ++++++++++++++++++------ 3 files changed, 20 insertions(+), 7 deletions(-) diff --git a/Source/ASCollectionNode.mm b/Source/ASCollectionNode.mm index a558193da..67aea1fc8 100644 --- a/Source/ASCollectionNode.mm +++ b/Source/ASCollectionNode.mm @@ -216,6 +216,7 @@ - (void)didEnterPreloadState [super didEnterPreloadState]; // Intentionally allocate the view here and trigger a layout pass on it, which in turn will trigger the intial data load. // We can get rid of this call later when ASDataController, ASRangeController and ASCollectionLayout can operate without the view. + // TODO (ASCL) If this node supports async layout, kick off the initial data load without allocating the view [[self view] layoutIfNeeded]; } diff --git a/Source/Details/ASDataController.mm b/Source/Details/ASDataController.mm index 4be9200e7..bde4fdf89 100644 --- a/Source/Details/ASDataController.mm +++ b/Source/Details/ASDataController.mm @@ -601,7 +601,7 @@ - (void)updateWithChangeSet:(_ASHierarchyChangeSet *)changeSet (element.nodeIfAllocated.calculatedLayout == nil ? element : nil)); } - // TODO Don't even allocate nodes here + // TODO (ASCL) Don't even allocate nodes here [self batchAllocateNodesFromElements:elementsToProcess andLayout:(! canDelegateLayout) batchSize:elementsToProcess.count batchCompletion:^(NSArray *elements, NSArray *nodes) { ASSERT_ON_EDITING_QUEUE; diff --git a/Source/Private/ASCollectionLayout.mm b/Source/Private/ASCollectionLayout.mm index 0a5ec34c4..66be17895 100644 --- a/Source/Private/ASCollectionLayout.mm +++ b/Source/Private/ASCollectionLayout.mm @@ -41,7 +41,10 @@ @interface ASCollectionLayout () { // Main thread only. ASCollectionLayoutState *_layout; - + + // The pending state calculated ahead of time, if any. Main thread only. + ASCollectionLayoutState *_pendingLayout; + struct { unsigned int implementsAdditionalInfoForLayoutWithElements:1; } _layoutDelegateFlags; @@ -95,7 +98,7 @@ - (ASCollectionLayoutState *)calculateLayoutWithContext:(ASCollectionLayoutConte - (void)applyLayout:(ASCollectionLayoutState *)layout { ASDisplayNodeAssertMainThread(); - _layout = layout; + _pendingLayout = layout; } #pragma mark - UICollectionViewLayout overrides @@ -106,11 +109,20 @@ - (void)prepareLayout [super prepareLayout]; ASCollectionLayoutContext *context = [self layoutContextWithElements:_collectionNode.visibleElements]; - if (_layout == nil || ASObjectIsEqual(_layout.context, context) == NO) { - // Looks like the existing layout is either unavailable or no longer valid. - // Calculate a new layout and apply it immediately - [self applyLayout:[self calculateLayoutWithContext:context]]; + if (_layout != nil && ASObjectIsEqual(_layout.context, context)) { + // The existing layout is still valid. No-op + return; } + + if (_pendingLayout != nil && ASObjectIsEqual(_pendingLayout.context, context)) { + // The existing can be used. Great! + _layout = _pendingLayout; + _pendingLayout = nil; + return; + } + + // A new layout is needed now. Calculate and apply it immediately + _layout = [self calculateLayoutWithContext:context]; } - (void)invalidateLayout From a4a0ae590e22cfdb962e485fc866a8147244a601 Mon Sep 17 00:00:00 2001 From: Huy Nguyen Date: Wed, 12 Jul 2017 17:19:39 +0100 Subject: [PATCH 14/25] Add ASCollectionLayoutCache --- Source/Details/ASCollectionLayoutContext.h | 2 +- Source/Details/ASDataController.h | 9 --- Source/Details/ASDataController.mm | 5 +- Source/Private/ASCollectionLayout.h | 10 +-- Source/Private/ASCollectionLayout.mm | 45 ++++------- Source/Private/ASCollectionLayoutCache.h | 34 ++++++++ Source/Private/ASCollectionLayoutCache.mm | 90 ++++++++++++++++++++++ 7 files changed, 147 insertions(+), 48 deletions(-) create mode 100644 Source/Private/ASCollectionLayoutCache.h create mode 100644 Source/Private/ASCollectionLayoutCache.mm diff --git a/Source/Details/ASCollectionLayoutContext.h b/Source/Details/ASCollectionLayoutContext.h index c6ed3b84c..00451584a 100644 --- a/Source/Details/ASCollectionLayoutContext.h +++ b/Source/Details/ASCollectionLayoutContext.h @@ -27,7 +27,7 @@ AS_SUBCLASSING_RESTRICTED @interface ASCollectionLayoutContext : NSObject @property (nonatomic, assign, readonly) CGSize viewportSize; -@property (nonatomic, strong, readonly) ASElementMap *elements; +@property (nonatomic, weak, readonly) ASElementMap *elements; @property (nonatomic, strong, readonly, nullable) id additionalInfo; - (instancetype)init __unavailable; diff --git a/Source/Details/ASDataController.h b/Source/Details/ASDataController.h index 08081ec47..fffbb804c 100644 --- a/Source/Details/ASDataController.h +++ b/Source/Details/ASDataController.h @@ -155,15 +155,6 @@ extern NSString * const ASCollectionInvalidUpdateException; */ - (ASCollectionLayoutState *)calculateLayoutWithContext:(ASCollectionLayoutContext *)context; -/** - * @abstract Applies the given layout. - * - * @param layout A layout that was previously returned by `-calculateLayoutWithContext:`. - * - * @discussion This method will be called on main thread. - */ -- (void)applyLayout:(ASCollectionLayoutState *)layout; - @end /** diff --git a/Source/Details/ASDataController.mm b/Source/Details/ASDataController.mm index bde4fdf89..a9d803091 100644 --- a/Source/Details/ASDataController.mm +++ b/Source/Details/ASDataController.mm @@ -605,7 +605,9 @@ - (void)updateWithChangeSet:(_ASHierarchyChangeSet *)changeSet [self batchAllocateNodesFromElements:elementsToProcess andLayout:(! canDelegateLayout) batchSize:elementsToProcess.count batchCompletion:^(NSArray *elements, NSArray *nodes) { ASSERT_ON_EDITING_QUEUE; - ASCollectionLayoutState *layout = canDelegateLayout ? [_layoutDelegate calculateLayoutWithContext:layoutContext] : nil; + if (canDelegateLayout) { + [_layoutDelegate calculateLayoutWithContext:layoutContext]; + } [_mainSerialQueue performBlockOnMainThread:^{ as_activity_scope_leave(&preparationScope); @@ -622,7 +624,6 @@ - (void)updateWithChangeSet:(_ASHierarchyChangeSet *)changeSet // Thus, we can't just swap the new map immediately before step 4, but until this update block is executed. // (https://github.com/TextureGroup/Texture/issues/378) self.visibleMap = newMap; - [_layoutDelegate applyLayout:layout]; }]; }]; }]; diff --git a/Source/Private/ASCollectionLayout.h b/Source/Private/ASCollectionLayout.h index f5bc713c5..45f12d707 100644 --- a/Source/Private/ASCollectionLayout.h +++ b/Source/Private/ASCollectionLayout.h @@ -2,13 +2,8 @@ // ASCollectionLayout.h // 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"); +// 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 // @@ -25,7 +20,6 @@ NS_ASSUME_NONNULL_BEGIN AS_SUBCLASSING_RESTRICTED - @interface ASCollectionLayout : UICollectionViewLayout /** diff --git a/Source/Private/ASCollectionLayout.mm b/Source/Private/ASCollectionLayout.mm index 66be17895..31747acf1 100644 --- a/Source/Private/ASCollectionLayout.mm +++ b/Source/Private/ASCollectionLayout.mm @@ -1,14 +1,9 @@ // -// ASCollectionLayout.mm +// ASCollectionLayout.m // 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"); +// 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 // @@ -21,6 +16,7 @@ #import #import #import +#import #import #import #import @@ -39,11 +35,8 @@ static const ASScrollDirection kASStaticScrollDirection = (ASScrollDirectionRight | ASScrollDirectionDown); @interface ASCollectionLayout () { - // Main thread only. - ASCollectionLayoutState *_layout; - - // The pending state calculated ahead of time, if any. Main thread only. - ASCollectionLayoutState *_pendingLayout; + ASCollectionLayoutCache *_layoutCache; + ASCollectionLayoutState *_layout; // Main thread only. struct { unsigned int implementsAdditionalInfoForLayoutWithElements:1; @@ -61,6 +54,7 @@ - (instancetype)initWithLayoutDelegate:(id)layoutDel ASDisplayNodeAssertNotNil(layoutDelegate, @"Collection layout delegate cannot be nil"); _layoutDelegate = layoutDelegate; _layoutDelegateFlags.implementsAdditionalInfoForLayoutWithElements = [layoutDelegate respondsToSelector:@selector(additionalInfoForLayoutWithElements:)]; + _layoutCache = [[ASCollectionLayoutCache alloc] init]; } return self; } @@ -81,6 +75,7 @@ - (ASCollectionLayoutContext *)layoutContextWithElements:(ASElementMap *)element - (ASCollectionLayoutState *)calculateLayoutWithContext:(ASCollectionLayoutContext *)context { ASCollectionLayoutState *layout = [_layoutDelegate calculateLayoutWithContext:context]; + [_layoutCache setLayout:layout forContext:context]; // Measure elements in the measure range ahead of time, block on the initial rect as it'll be visible shortly CGSize viewportSize = context.viewportSize; @@ -95,12 +90,6 @@ - (ASCollectionLayoutState *)calculateLayoutWithContext:(ASCollectionLayoutConte return layout; } -- (void)applyLayout:(ASCollectionLayoutState *)layout -{ - ASDisplayNodeAssertMainThread(); - _pendingLayout = layout; -} - #pragma mark - UICollectionViewLayout overrides - (void)prepareLayout @@ -114,22 +103,22 @@ - (void)prepareLayout return; } - if (_pendingLayout != nil && ASObjectIsEqual(_pendingLayout.context, context)) { - // The existing can be used. Great! - _layout = _pendingLayout; - _pendingLayout = nil; - return; + if (ASCollectionLayoutState *cachedLayout = [_layoutCache layoutForContext:context]) { + _layout = cachedLayout; + } else { + // A new layout is needed now. Calculate and apply it immediately + _layout = [self calculateLayoutWithContext:context]; } - - // A new layout is needed now. Calculate and apply it immediately - _layout = [self calculateLayoutWithContext:context]; } - (void)invalidateLayout { ASDisplayNodeAssertMainThread(); [super invalidateLayout]; - _layout = nil; + if (_layout != nil) { + [_layoutCache removeLayoutForContext:_layout.context]; + _layout = nil; + } } - (CGSize)collectionViewContentSize diff --git a/Source/Private/ASCollectionLayoutCache.h b/Source/Private/ASCollectionLayoutCache.h new file mode 100644 index 000000000..1bf336b61 --- /dev/null +++ b/Source/Private/ASCollectionLayoutCache.h @@ -0,0 +1,34 @@ +// +// ASCollectionLayoutCache.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 + +NS_ASSUME_NONNULL_BEGIN + +@class ASCollectionLayoutContext, ASCollectionLayoutState; + +/// A thread-safe cache for ASCollectionLayoutContext-ASCollectionLayoutState pairs +AS_SUBCLASSING_RESTRICTED +@interface ASCollectionLayoutCache : NSObject + +- (nullable ASCollectionLayoutState *)layoutForContext:(ASCollectionLayoutContext *)context; + +- (void)setLayout:(ASCollectionLayoutState *)layout forContext:(ASCollectionLayoutContext *)context; + +- (void)removeLayoutForContext:(ASCollectionLayoutContext *)context; + +- (void)removeAllLayouts; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Source/Private/ASCollectionLayoutCache.mm b/Source/Private/ASCollectionLayoutCache.mm new file mode 100644 index 000000000..2733ca4c5 --- /dev/null +++ b/Source/Private/ASCollectionLayoutCache.mm @@ -0,0 +1,90 @@ +// +// ASCollectionLayoutCache.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 +#import + +@implementation ASCollectionLayoutCache { + ASDN::Mutex __instanceLock__; + + /** + * The underlying data structure of this cache. + * + * The outer map table is a weak to strong table. That is because ASCollectionLayoutContext doesn't (and shouldn't) + * hold a strong reference on its element map. As a result, this cache should handle the case in which + * an element map no longer exists and all contexts and layouts associated with it should be cleared. + * + * The inner map table is a standard strong to strong map. + * Since different ASCollectionLayoutContext objects with the same content are considered equal, + * "object pointer personality" can't be used as a key option. + */ + NSMapTable *> *_map; +} + +- (instancetype)init +{ + self = [super init]; + if (self) { + _map = [NSMapTable mapTableWithKeyOptions:(NSMapTableWeakMemory | NSMapTableObjectPointerPersonality) valueOptions:NSMapTableStrongMemory]; + } + return self; +} + +- (ASCollectionLayoutState *)layoutForContext:(ASCollectionLayoutContext *)context +{ + ASElementMap *elements = context.elements; + if (elements == nil) { + return nil; + } + + ASDN::MutexLocker l(__instanceLock__); + return [[_map objectForKey:elements] objectForKey:context]; +} + +- (void)setLayout:(ASCollectionLayoutState *)layout forContext:(ASCollectionLayoutContext *)context +{ + ASElementMap *elements = context.elements; + if (layout == nil || elements == nil) { + return; + } + + ASDN::MutexLocker l(__instanceLock__); + auto innerMap = [_map objectForKey:elements]; + if (innerMap == nil) { + innerMap = [NSMapTable strongToStrongObjectsMapTable]; + [_map setObject:innerMap forKey:elements]; + } + [innerMap setObject:layout forKey:context]; +} + +- (void)removeLayoutForContext:(ASCollectionLayoutContext *)context +{ + ASElementMap *elements = context.elements; + if (elements == nil) { + return; + } + + ASDN::MutexLocker l(__instanceLock__); + [[_map objectForKey:elements] removeObjectForKey:context]; +} + +- (void)removeAllLayouts +{ + ASDN::MutexLocker l(__instanceLock__); + [_map removeAllObjects]; +} + +@end From 305a64151a542f51cea4c609dfe319eefcbe188c Mon Sep 17 00:00:00 2001 From: Huy Nguyen Date: Wed, 12 Jul 2017 17:23:29 +0100 Subject: [PATCH 15/25] Fix file licenses --- Source/Private/ASCollectionLayout.mm | 11 ++++++++--- Source/Private/ASCollectionLayoutCache.mm | 2 +- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/Source/Private/ASCollectionLayout.mm b/Source/Private/ASCollectionLayout.mm index 31747acf1..875ea08bb 100644 --- a/Source/Private/ASCollectionLayout.mm +++ b/Source/Private/ASCollectionLayout.mm @@ -1,9 +1,14 @@ // -// ASCollectionLayout.m +// ASCollectionLayout.mm // Texture // -// Copyright (c) 2017-present, Pinterest, Inc. All rights reserved. -// Licensed under the Apache License, Version 2.0 (the "License"); +// 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 // diff --git a/Source/Private/ASCollectionLayoutCache.mm b/Source/Private/ASCollectionLayoutCache.mm index 2733ca4c5..94b2bc18b 100644 --- a/Source/Private/ASCollectionLayoutCache.mm +++ b/Source/Private/ASCollectionLayoutCache.mm @@ -1,5 +1,5 @@ // -// ASCollectionLayoutCache.h +// ASCollectionLayoutCache.mm // Texture // // Copyright (c) 2017-present, Pinterest, Inc. All rights reserved. From 2c9912818724e43ea89ae2feb27bf47568866ade Mon Sep 17 00:00:00 2001 From: Huy Nguyen Date: Wed, 12 Jul 2017 17:27:36 +0100 Subject: [PATCH 16/25] Fix xcodeproj --- AsyncDisplayKit.xcodeproj/project.pbxproj | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/AsyncDisplayKit.xcodeproj/project.pbxproj b/AsyncDisplayKit.xcodeproj/project.pbxproj index 692d233ea..9d7c9cd40 100644 --- a/AsyncDisplayKit.xcodeproj/project.pbxproj +++ b/AsyncDisplayKit.xcodeproj/project.pbxproj @@ -434,9 +434,11 @@ E5711A301C840C96009619D4 /* ASCollectionElement.mm in Sources */ = {isa = PBXBuildFile; fileRef = E5711A2D1C840C96009619D4 /* ASCollectionElement.mm */; }; E5775AFC1F13CE9F00CAC9BC /* _ASCollectionGalleryLayoutItem.h in Headers */ = {isa = PBXBuildFile; fileRef = E5775AFB1F13CE9F00CAC9BC /* _ASCollectionGalleryLayoutItem.h */; }; E5775AFE1F13CF7400CAC9BC /* _ASCollectionGalleryLayoutItem.mm in Sources */ = {isa = PBXBuildFile; fileRef = E5775AFD1F13CF7400CAC9BC /* _ASCollectionGalleryLayoutItem.mm */; }; - E5775B001F13D25400CAC9BC /* ASCollectionLayoutState+Private.h in Headers */ = {isa = PBXBuildFile; fileRef = E5775AFF1F13D25400CAC9BC /* ASCollectionLayoutState+Private.h */; }; + E5775B001F13D25400CAC9BC /* ASCollectionLayoutState+Private.h in Headers */ = {isa = PBXBuildFile; fileRef = E5775AFF1F13D25400CAC9BC /* ASCollectionLayoutState+Private.h */; settings = {ATTRIBUTES = (Private, ); }; }; + E5775B021F16759300CAC9BC /* ASCollectionLayoutCache.h in Headers */ = {isa = PBXBuildFile; fileRef = E5775B011F16759300CAC9BC /* ASCollectionLayoutCache.h */; settings = {ATTRIBUTES = (Private, ); }; }; + E5775B041F16759F00CAC9BC /* ASCollectionLayoutCache.mm in Sources */ = {isa = PBXBuildFile; fileRef = E5775B031F16759F00CAC9BC /* ASCollectionLayoutCache.mm */; }; E5855DEF1EBB4D83003639AE /* ASCollectionLayoutDefines.m in Sources */ = {isa = PBXBuildFile; fileRef = E5855DED1EBB4D83003639AE /* ASCollectionLayoutDefines.m */; }; - E5855DF01EBB4D83003639AE /* ASCollectionLayoutDefines.h in Headers */ = {isa = PBXBuildFile; fileRef = E5855DEE1EBB4D83003639AE /* ASCollectionLayoutDefines.h */; }; + E5855DF01EBB4D83003639AE /* ASCollectionLayoutDefines.h in Headers */ = {isa = PBXBuildFile; fileRef = E5855DEE1EBB4D83003639AE /* ASCollectionLayoutDefines.h */; settings = {ATTRIBUTES = (Private, ); }; }; E58E9E421E941D74004CFC59 /* ASCollectionFlowLayoutDelegate.h in Headers */ = {isa = PBXBuildFile; fileRef = E58E9E3D1E941D74004CFC59 /* ASCollectionFlowLayoutDelegate.h */; settings = {ATTRIBUTES = (Public, ); }; }; E58E9E431E941D74004CFC59 /* ASCollectionFlowLayoutDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = E58E9E3E1E941D74004CFC59 /* ASCollectionFlowLayoutDelegate.m */; }; E58E9E441E941D74004CFC59 /* ASCollectionLayoutContext.h in Headers */ = {isa = PBXBuildFile; fileRef = E58E9E3F1E941D74004CFC59 /* ASCollectionLayoutContext.h */; settings = {ATTRIBUTES = (Public, ); }; }; @@ -448,7 +450,7 @@ E5ABAC7C1E8564EE007AC15C /* ASRectTable.m in Sources */ = {isa = PBXBuildFile; fileRef = E5ABAC7A1E8564EE007AC15C /* ASRectTable.m */; }; E5B077FF1E69F4EB00C24B5B /* ASElementMap.h in Headers */ = {isa = PBXBuildFile; fileRef = E5B077FD1E69F4EB00C24B5B /* ASElementMap.h */; settings = {ATTRIBUTES = (Public, ); }; }; E5B078001E69F4EB00C24B5B /* ASElementMap.m in Sources */ = {isa = PBXBuildFile; fileRef = E5B077FE1E69F4EB00C24B5B /* ASElementMap.m */; }; - E5B5B9D11E9BAD9800A6B726 /* ASCollectionLayoutContext+Private.h in Headers */ = {isa = PBXBuildFile; fileRef = E5B5B9D01E9BAD9800A6B726 /* ASCollectionLayoutContext+Private.h */; }; + E5B5B9D11E9BAD9800A6B726 /* ASCollectionLayoutContext+Private.h in Headers */ = {isa = PBXBuildFile; fileRef = E5B5B9D01E9BAD9800A6B726 /* ASCollectionLayoutContext+Private.h */; settings = {ATTRIBUTES = (Private, ); }; }; E5C347B11ECB3D9200EC4BE4 /* ASBatchFetchingDelegate.h in Headers */ = {isa = PBXBuildFile; fileRef = E5C347B01ECB3D9200EC4BE4 /* ASBatchFetchingDelegate.h */; }; E5C347B31ECB40AA00EC4BE4 /* ASTableNode+Beta.h in Headers */ = {isa = PBXBuildFile; fileRef = E5C347B21ECB40AA00EC4BE4 /* ASTableNode+Beta.h */; }; E5E281741E71C833006B67C2 /* ASCollectionLayoutState.h in Headers */ = {isa = PBXBuildFile; fileRef = E5E281731E71C833006B67C2 /* ASCollectionLayoutState.h */; settings = {ATTRIBUTES = (Public, ); }; }; @@ -919,6 +921,8 @@ E5775AFB1F13CE9F00CAC9BC /* _ASCollectionGalleryLayoutItem.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = _ASCollectionGalleryLayoutItem.h; sourceTree = ""; }; E5775AFD1F13CF7400CAC9BC /* _ASCollectionGalleryLayoutItem.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = _ASCollectionGalleryLayoutItem.mm; sourceTree = ""; }; E5775AFF1F13D25400CAC9BC /* ASCollectionLayoutState+Private.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "ASCollectionLayoutState+Private.h"; sourceTree = ""; }; + E5775B011F16759300CAC9BC /* ASCollectionLayoutCache.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASCollectionLayoutCache.h; sourceTree = ""; }; + E5775B031F16759F00CAC9BC /* ASCollectionLayoutCache.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASCollectionLayoutCache.mm; sourceTree = ""; }; E5855DED1EBB4D83003639AE /* ASCollectionLayoutDefines.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASCollectionLayoutDefines.m; sourceTree = ""; }; E5855DEE1EBB4D83003639AE /* ASCollectionLayoutDefines.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASCollectionLayoutDefines.h; sourceTree = ""; }; E58E9E3D1E941D74004CFC59 /* ASCollectionFlowLayoutDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASCollectionFlowLayoutDelegate.h; sourceTree = ""; }; @@ -1654,6 +1658,8 @@ children = ( E58E9E471E941DA5004CFC59 /* ASCollectionLayout.h */, E58E9E481E941DA5004CFC59 /* ASCollectionLayout.mm */, + E5775B011F16759300CAC9BC /* ASCollectionLayoutCache.h */, + E5775B031F16759F00CAC9BC /* ASCollectionLayoutCache.mm */, E5B5B9D01E9BAD9800A6B726 /* ASCollectionLayoutContext+Private.h */, E5855DEE1EBB4D83003639AE /* ASCollectionLayoutDefines.h */, E5855DED1EBB4D83003639AE /* ASCollectionLayoutDefines.m */, @@ -1701,7 +1707,6 @@ files = ( CC034A131E649F1300626263 /* AsyncDisplayKit+IGListKitMethods.h in Headers */, 693A1DCA1ECC944E00D0C9D2 /* IGListAdapter+AsyncDisplayKit.h in Headers */, - E5775B001F13D25400CAC9BC /* ASCollectionLayoutState+Private.h in Headers */, E5E2D72E1EA780C4005C24C6 /* ASCollectionGalleryLayoutDelegate.h in Headers */, E58E9E461E941D74004CFC59 /* ASCollectionLayoutDelegate.h in Headers */, E5E281741E71C833006B67C2 /* ASCollectionLayoutState.h in Headers */, @@ -1726,7 +1731,6 @@ B13CA1011C52004900E031AB /* ASCollectionNode+Beta.h in Headers */, 68C215581DE10D330019C4BC /* ASCollectionViewLayoutInspector.h in Headers */, B35062411B010EFD0018CF92 /* _ASAsyncTransactionGroup.h in Headers */, - E5855DF01EBB4D83003639AE /* ASCollectionLayoutDefines.h in Headers */, B350620F1B010EFD0018CF92 /* _ASDisplayLayer.h in Headers */, CCCCCCD71EC3EF060087FE10 /* ASTextInput.h in Headers */, B35062111B010EFD0018CF92 /* _ASDisplayView.h in Headers */, @@ -1810,6 +1814,10 @@ 044285081BAA63FE00D16268 /* ASBatchFetching.h in Headers */, AC026B701BD57DBF00BBC17E /* _ASHierarchyChangeSet.h in Headers */, CC87BB951DA8193C0090E380 /* ASCellNode+Internal.h in Headers */, + E5775B021F16759300CAC9BC /* ASCollectionLayoutCache.h in Headers */, + E5775B001F13D25400CAC9BC /* ASCollectionLayoutState+Private.h in Headers */, + E5855DF01EBB4D83003639AE /* ASCollectionLayoutDefines.h in Headers */, + E5B5B9D11E9BAD9800A6B726 /* ASCollectionLayoutContext+Private.h in Headers */, 9C8898BD1C738BB800D6B02E /* ASTextKitFontSizeAdjuster.h in Headers */, 254C6B791BF94DF4003EC431 /* ASTextKitEntityAttribute.h in Headers */, CC3B20841C3F76D600798563 /* ASPendingStateController.h in Headers */, @@ -1830,7 +1838,6 @@ 6977965F1D8AC8D3007E93D7 /* ASLayoutSpec+Subclasses.h in Headers */, 692BE8D71E36B65B00C86D87 /* ASLayoutSpecPrivate.h in Headers */, 34EFC75D1B701BE900AD841F /* ASInternalHelpers.h in Headers */, - E5B5B9D11E9BAD9800A6B726 /* ASCollectionLayoutContext+Private.h in Headers */, DEC146B71C37A16A004A0EE7 /* ASCollectionInternal.h in Headers */, 68B8A4E21CBDB958007E4543 /* ASWeakProxy.h in Headers */, 9F98C0271DBE29FC00476D92 /* ASControlTargetAction.h in Headers */, @@ -2223,6 +2230,7 @@ 68FC85EC1CE29C7D00EDD713 /* ASVisibilityProtocols.m in Sources */, CC55A7121E52A0F200594372 /* ASResponderChainEnumerator.m in Sources */, 68B8A4E41CBDB958007E4543 /* ASWeakProxy.m in Sources */, + E5775B041F16759F00CAC9BC /* ASCollectionLayoutCache.mm in Sources */, 9C70F20A1CDBE949007D6C76 /* ASTableNode.mm in Sources */, 69CB62AE1CB8165900024920 /* _ASDisplayViewAccessiblity.mm in Sources */, B35061F61B010EFD0018CF92 /* ASCollectionView.mm in Sources */, From 6b92d9ab43313b6146e1ed7f93011b435dc1662b Mon Sep 17 00:00:00 2001 From: Huy Nguyen Date: Thu, 13 Jul 2017 11:11:10 +0100 Subject: [PATCH 17/25] Add async collection layout to examples/ASCollectionView --- .../ASCollectionView/Sample/ViewController.m | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/examples/ASCollectionView/Sample/ViewController.m b/examples/ASCollectionView/Sample/ViewController.m index 270765802..13bfe64c7 100644 --- a/examples/ASCollectionView/Sample/ViewController.m +++ b/examples/ASCollectionView/Sample/ViewController.m @@ -21,6 +21,8 @@ #import "SupplementaryNode.h" #import "ItemNode.h" +#define ASYNC_COLLECTION_LAYOUT 0 + @interface ViewController () @property (nonatomic, strong) ASCollectionNode *collectionNode; @@ -43,11 +45,18 @@ - (void)dealloc - (void)viewDidLoad { [super viewDidLoad]; - - id layoutDelegate = [[ASCollectionGalleryLayoutDelegate alloc] initWithScrollableDirections:ASScrollDirectionVerticalDirections itemSize:CGSizeMake(180, 90)]; -// id layoutDelegate = [[ASCollectionFlowLayoutDelegate alloc] init]; - + +#if ASYNC_COLLECTION_LAYOUT + id layoutDelegate = [[ASCollectionGalleryLayoutDelegate alloc] initWithScrollableDirections:ASScrollDirectionVerticalDirections + itemSize:CGSizeMake(180, 90)]; self.collectionNode = [[ASCollectionNode alloc] initWithLayoutDelegate:layoutDelegate layoutFacilitator:nil]; +#else + UICollectionViewFlowLayout *layout = [[UICollectionViewFlowLayout alloc] init]; + layout.headerReferenceSize = CGSizeMake(50.0, 50.0); + layout.footerReferenceSize = CGSizeMake(50.0, 50.0); + self.collectionNode = [[ASCollectionNode alloc] initWithFrame:self.view.bounds collectionViewLayout:layout]; +#endif + self.collectionNode.dataSource = self; self.collectionNode.delegate = self; From 25afc72c899f72c10040bf7dadd3becaca1f949c Mon Sep 17 00:00:00 2001 From: Huy Nguyen Date: Thu, 13 Jul 2017 11:16:21 +0100 Subject: [PATCH 18/25] Measure method in ASCollectionLayout to be a class method --- Source/Private/ASCollectionLayout.mm | 46 ++++++++++++++-------------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/Source/Private/ASCollectionLayout.mm b/Source/Private/ASCollectionLayout.mm index 875ea08bb..3facba2ce 100644 --- a/Source/Private/ASCollectionLayout.mm +++ b/Source/Private/ASCollectionLayout.mm @@ -90,7 +90,7 @@ - (ASCollectionLayoutState *)calculateLayoutWithContext:(ASCollectionLayoutConte kASDefaultMeasureRangeTuningParameters, _layoutDelegate.scrollableDirections, kASStaticScrollDirection); - ASCollectionLayoutMeasureElementsInRects(measureRect, initialRect, layout); + [ASCollectionLayout _measureElementsInRect:measureRect blockingRect:initialRect layout:layout]; return layout; } @@ -142,7 +142,7 @@ - (CGSize)collectionViewContentSize // Measure elements in the measure range, block on the requested rect CGRect measureRect = CGRectExpandToRangeWithScrollableDirections(blockingRect, kASDefaultMeasureRangeTuningParameters, _layoutDelegate.scrollableDirections, kASStaticScrollDirection); - ASCollectionLayoutMeasureElementsInRects(measureRect, blockingRect, _layout); + [ASCollectionLayout _measureElementsInRect:measureRect blockingRect:blockingRect layout:_layout]; NSArray *result = [_layout layoutAttributesForElementsInRect:blockingRect]; @@ -206,30 +206,10 @@ - (CGSize)_viewportSize } } -# 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; - } - } -} - /** * Measures all elements in the specified rect and blocks the calling thread while measuring those in the blocking rect. */ -ASDISPLAYNODE_INLINE void ASCollectionLayoutMeasureElementsInRects(CGRect rect, CGRect blockingRect, ASCollectionLayoutState *layout) ++ (void)_measureElementsInRect:(CGRect)rect blockingRect:(CGRect)blockingRect layout:(ASCollectionLayoutState *)layout { if (CGRectIsEmpty(rect) || layout == nil) { return; @@ -325,4 +305,24 @@ ASDISPLAYNODE_INLINE void ASCollectionLayoutMeasureElementsInRects(CGRect rect, } } +# 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 From e997d30a78417fdf3f3927efc53b01c7d8a87ea7 Mon Sep 17 00:00:00 2001 From: Huy Nguyen Date: Thu, 13 Jul 2017 14:48:55 +0100 Subject: [PATCH 19/25] Encourage more immutable states - Make -calculateLayoutWithContext: to be class methods in ASDataControllerLayoutDelegate and ASCollectionLayoutDelegate. - Add layout delegate class and layout cache to ASCollectionLayoutContext+Private, to be use by ASCollectionLayout only. - ASDataController no longer allocates all nodes but lets ASCollectionLayout determine. - Add scrollableDirections to the layout context since it's often needed by the layout pass. Otherwise users have to wrap it in an info object. - Update built-in layout delegates and CustomCollectionView example. - Publish ASHashing. It might be helpful for clients that implement custom collection info objects. --- AsyncDisplayKit.xcodeproj/project.pbxproj | 24 ++-- .../Details/ASCollectionFlowLayoutDelegate.m | 18 ++- .../ASCollectionGalleryLayoutDelegate.m | 25 ++-- Source/Details/ASCollectionLayoutContext.h | 3 +- Source/Details/ASCollectionLayoutContext.m | 112 ++++++++++++++++++ Source/Details/ASCollectionLayoutContext.mm | 72 ----------- Source/Details/ASCollectionLayoutDelegate.h | 12 +- Source/Details/ASDataController.h | 2 +- Source/Details/ASDataController.mm | 91 +++++--------- Source/{Private => Details}/ASHashing.h | 0 Source/{Private => Details}/ASHashing.m | 0 Source/Private/ASCollectionLayout.mm | 29 +++-- .../ASCollectionLayoutContext+Private.h | 13 +- .../contents.xcworkspacedata | 10 ++ .../Sample.xcodeproj/project.pbxproj | 11 ++ .../Sample/MosaicCollectionLayoutDelegate.m | 82 ++++++------- .../Sample/MosaicCollectionLayoutInfo.h | 32 +++++ .../Sample/MosaicCollectionLayoutInfo.m | 78 ++++++++++++ 18 files changed, 402 insertions(+), 212 deletions(-) create mode 100644 Source/Details/ASCollectionLayoutContext.m delete mode 100644 Source/Details/ASCollectionLayoutContext.mm rename Source/{Private => Details}/ASHashing.h (100%) rename Source/{Private => Details}/ASHashing.m (100%) create mode 100644 examples/ASCollectionView/Sample.xcworkspace/contents.xcworkspacedata create mode 100644 examples/CustomCollectionView/Sample/MosaicCollectionLayoutInfo.h create mode 100644 examples/CustomCollectionView/Sample/MosaicCollectionLayoutInfo.m diff --git a/AsyncDisplayKit.xcodeproj/project.pbxproj b/AsyncDisplayKit.xcodeproj/project.pbxproj index 9d7c9cd40..65243e515 100644 --- a/AsyncDisplayKit.xcodeproj/project.pbxproj +++ b/AsyncDisplayKit.xcodeproj/project.pbxproj @@ -424,8 +424,6 @@ DECBD6E81BE56E1900CF4905 /* ASButtonNode.h in Headers */ = {isa = PBXBuildFile; fileRef = DECBD6E51BE56E1900CF4905 /* ASButtonNode.h */; settings = {ATTRIBUTES = (Public, ); }; }; DECBD6EA1BE56E1900CF4905 /* ASButtonNode.mm in Sources */ = {isa = PBXBuildFile; fileRef = DECBD6E61BE56E1900CF4905 /* ASButtonNode.mm */; }; 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 */; }; E51B78BF1F028ABF00E32604 /* ASLayoutFlatteningTests.m in Sources */ = {isa = PBXBuildFile; fileRef = E51B78BD1F01A0EE00E32604 /* ASLayoutFlatteningTests.m */; }; E54E81FC1EB357BD00FFE8E1 /* ASPageTable.h in Headers */ = {isa = PBXBuildFile; fileRef = E54E81FA1EB357BD00FFE8E1 /* ASPageTable.h */; }; E54E81FD1EB357BD00FFE8E1 /* ASPageTable.m in Sources */ = {isa = PBXBuildFile; fileRef = E54E81FB1EB357BD00FFE8E1 /* ASPageTable.m */; }; @@ -442,7 +440,7 @@ E58E9E421E941D74004CFC59 /* ASCollectionFlowLayoutDelegate.h in Headers */ = {isa = PBXBuildFile; fileRef = E58E9E3D1E941D74004CFC59 /* ASCollectionFlowLayoutDelegate.h */; settings = {ATTRIBUTES = (Public, ); }; }; E58E9E431E941D74004CFC59 /* ASCollectionFlowLayoutDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = E58E9E3E1E941D74004CFC59 /* ASCollectionFlowLayoutDelegate.m */; }; E58E9E441E941D74004CFC59 /* ASCollectionLayoutContext.h in Headers */ = {isa = PBXBuildFile; fileRef = E58E9E3F1E941D74004CFC59 /* ASCollectionLayoutContext.h */; settings = {ATTRIBUTES = (Public, ); }; }; - E58E9E451E941D74004CFC59 /* ASCollectionLayoutContext.mm in Sources */ = {isa = PBXBuildFile; fileRef = E58E9E401E941D74004CFC59 /* ASCollectionLayoutContext.mm */; }; + E58E9E451E941D74004CFC59 /* ASCollectionLayoutContext.m in Sources */ = {isa = PBXBuildFile; fileRef = E58E9E401E941D74004CFC59 /* ASCollectionLayoutContext.m */; }; E58E9E461E941D74004CFC59 /* ASCollectionLayoutDelegate.h in Headers */ = {isa = PBXBuildFile; fileRef = E58E9E411E941D74004CFC59 /* ASCollectionLayoutDelegate.h */; settings = {ATTRIBUTES = (Public, ); }; }; E58E9E491E941DA5004CFC59 /* ASCollectionLayout.h in Headers */ = {isa = PBXBuildFile; fileRef = E58E9E471E941DA5004CFC59 /* ASCollectionLayout.h */; settings = {ATTRIBUTES = (Private, ); }; }; E58E9E4A1E941DA5004CFC59 /* ASCollectionLayout.mm in Sources */ = {isa = PBXBuildFile; fileRef = E58E9E481E941DA5004CFC59 /* ASCollectionLayout.mm */; }; @@ -450,6 +448,8 @@ E5ABAC7C1E8564EE007AC15C /* ASRectTable.m in Sources */ = {isa = PBXBuildFile; fileRef = E5ABAC7A1E8564EE007AC15C /* ASRectTable.m */; }; E5B077FF1E69F4EB00C24B5B /* ASElementMap.h in Headers */ = {isa = PBXBuildFile; fileRef = E5B077FD1E69F4EB00C24B5B /* ASElementMap.h */; settings = {ATTRIBUTES = (Public, ); }; }; E5B078001E69F4EB00C24B5B /* ASElementMap.m in Sources */ = {isa = PBXBuildFile; fileRef = E5B077FE1E69F4EB00C24B5B /* ASElementMap.m */; }; + E5B225281F1790D6001E1431 /* ASHashing.h in Headers */ = {isa = PBXBuildFile; fileRef = E5B225271F1790B5001E1431 /* ASHashing.h */; settings = {ATTRIBUTES = (Public, ); }; }; + E5B225291F1790EE001E1431 /* ASHashing.m in Sources */ = {isa = PBXBuildFile; fileRef = E5B225261F1790B5001E1431 /* ASHashing.m */; }; E5B5B9D11E9BAD9800A6B726 /* ASCollectionLayoutContext+Private.h in Headers */ = {isa = PBXBuildFile; fileRef = E5B5B9D01E9BAD9800A6B726 /* ASCollectionLayoutContext+Private.h */; settings = {ATTRIBUTES = (Private, ); }; }; E5C347B11ECB3D9200EC4BE4 /* ASBatchFetchingDelegate.h in Headers */ = {isa = PBXBuildFile; fileRef = E5C347B01ECB3D9200EC4BE4 /* ASBatchFetchingDelegate.h */; }; E5C347B31ECB40AA00EC4BE4 /* ASTableNode+Beta.h in Headers */ = {isa = PBXBuildFile; fileRef = E5C347B21ECB40AA00EC4BE4 /* ASTableNode+Beta.h */; }; @@ -908,8 +908,6 @@ DEC146B51C37A16A004A0EE7 /* ASCollectionInternal.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = ASCollectionInternal.m; path = Details/ASCollectionInternal.m; sourceTree = ""; }; DECBD6E51BE56E1900CF4905 /* ASButtonNode.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASButtonNode.h; sourceTree = ""; }; DECBD6E61BE56E1900CF4905 /* ASButtonNode.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASButtonNode.mm; sourceTree = ""; }; - E516FC7D1E9FE24200714FF4 /* ASHashing.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASHashing.h; sourceTree = ""; }; - E516FC7E1E9FE24200714FF4 /* ASHashing.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASHashing.m; sourceTree = ""; }; E51B78BD1F01A0EE00E32604 /* ASLayoutFlatteningTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASLayoutFlatteningTests.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 = ""; }; @@ -928,7 +926,7 @@ E58E9E3D1E941D74004CFC59 /* ASCollectionFlowLayoutDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASCollectionFlowLayoutDelegate.h; sourceTree = ""; }; E58E9E3E1E941D74004CFC59 /* ASCollectionFlowLayoutDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASCollectionFlowLayoutDelegate.m; sourceTree = ""; }; E58E9E3F1E941D74004CFC59 /* ASCollectionLayoutContext.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASCollectionLayoutContext.h; sourceTree = ""; }; - E58E9E401E941D74004CFC59 /* ASCollectionLayoutContext.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASCollectionLayoutContext.mm; sourceTree = ""; }; + E58E9E401E941D74004CFC59 /* ASCollectionLayoutContext.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASCollectionLayoutContext.m; sourceTree = ""; }; E58E9E411E941D74004CFC59 /* ASCollectionLayoutDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASCollectionLayoutDelegate.h; sourceTree = ""; }; E58E9E471E941DA5004CFC59 /* ASCollectionLayout.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASCollectionLayout.h; sourceTree = ""; }; E58E9E481E941DA5004CFC59 /* ASCollectionLayout.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASCollectionLayout.mm; sourceTree = ""; }; @@ -936,6 +934,8 @@ E5ABAC7A1E8564EE007AC15C /* ASRectTable.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASRectTable.m; sourceTree = ""; }; E5B077FD1E69F4EB00C24B5B /* ASElementMap.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASElementMap.h; sourceTree = ""; }; E5B077FE1E69F4EB00C24B5B /* ASElementMap.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASElementMap.m; sourceTree = ""; }; + E5B225261F1790B5001E1431 /* ASHashing.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ASHashing.m; sourceTree = ""; }; + E5B225271F1790B5001E1431 /* ASHashing.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ASHashing.h; sourceTree = ""; }; E5B5B9D01E9BAD9800A6B726 /* ASCollectionLayoutContext+Private.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "ASCollectionLayoutContext+Private.h"; sourceTree = ""; }; E5C347B01ECB3D9200EC4BE4 /* ASBatchFetchingDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASBatchFetchingDelegate.h; sourceTree = ""; }; E5C347B21ECB40AA00EC4BE4 /* ASTableNode+Beta.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "ASTableNode+Beta.h"; sourceTree = ""; }; @@ -1260,6 +1260,8 @@ 205F0E1C1B373A2C007741D0 /* ASCollectionViewLayoutController.m */, 696F01EA1DD2AF450049FBD5 /* ASEventLog.h */, 696F01EB1DD2AF450049FBD5 /* ASEventLog.mm */, + E5B225271F1790B5001E1431 /* ASHashing.h */, + E5B225261F1790B5001E1431 /* ASHashing.m */, 4640521B1A3F83C40061C0BA /* ASTableLayoutController.h */, 4640521C1A3F83C40061C0BA /* ASTableLayoutController.m */, 058D09E6195D050800B7D73C /* ASHighlightOverlayLayer.h */, @@ -1385,8 +1387,6 @@ 058D0A0C195D050800B7D73C /* ASDisplayNodeInternal.h */, 6959433D1D70815300B0EE1F /* ASDisplayNodeLayout.h */, 6959433C1D70815300B0EE1F /* ASDisplayNodeLayout.mm */, - E516FC7D1E9FE24200714FF4 /* ASHashing.h */, - E516FC7E1E9FE24200714FF4 /* ASHashing.m */, 6900C5F31E8072DA00BCD75C /* ASImageNode+Private.h */, 68B8A4DB1CBD911D007E4543 /* ASImageNode+AnimatedImagePrivate.h */, 058D0A0D195D050800B7D73C /* ASImageNode+CGExtras.h */, @@ -1674,7 +1674,7 @@ isa = PBXGroup; children = ( E58E9E3F1E941D74004CFC59 /* ASCollectionLayoutContext.h */, - E58E9E401E941D74004CFC59 /* ASCollectionLayoutContext.mm */, + E58E9E401E941D74004CFC59 /* ASCollectionLayoutContext.m */, E5E281731E71C833006B67C2 /* ASCollectionLayoutState.h */, E5E281751E71C845006B67C2 /* ASCollectionLayoutState.mm */, E58E9E411E941D74004CFC59 /* ASCollectionLayoutDelegate.h */, @@ -1705,6 +1705,7 @@ isa = PBXHeadersBuildPhase; buildActionMask = 2147483647; files = ( + E5B225281F1790D6001E1431 /* ASHashing.h in Headers */, CC034A131E649F1300626263 /* AsyncDisplayKit+IGListKitMethods.h in Headers */, 693A1DCA1ECC944E00D0C9D2 /* IGListAdapter+AsyncDisplayKit.h in Headers */, E5E2D72E1EA780C4005C24C6 /* ASCollectionGalleryLayoutDelegate.h in Headers */, @@ -1822,7 +1823,6 @@ 254C6B791BF94DF4003EC431 /* ASTextKitEntityAttribute.h in Headers */, CC3B20841C3F76D600798563 /* ASPendingStateController.h in Headers */, DE6EA3231C14000600183B10 /* ASDisplayNode+FrameworkPrivate.h in Headers */, - E516FC7F1E9FE24200714FF4 /* ASHashing.h in Headers */, 9C70F20F1CDBE9FF007D6C76 /* ASLayoutManager.h in Headers */, 6947B0C31E36B5040007C478 /* ASStackPositionedLayout.h in Headers */, DBABFAFC1C6A8D2F0039EA4A /* _ASTransitionContext.h in Headers */, @@ -2180,6 +2180,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + E5B225291F1790EE001E1431 /* ASHashing.m in Sources */, DEB8ED7C1DD003D300DBDE55 /* ASLayoutTransition.mm in Sources */, CCA5F62E1EECC2A80060C137 /* ASAssert.m in Sources */, 9F98C0261DBE29E000476D92 /* ASControlTargetAction.m in Sources */, @@ -2194,7 +2195,6 @@ CCA282B91E9EA8E40037E8B7 /* AsyncDisplayKit+Tips.m in Sources */, 636EA1A51C7FF4EF00EE152F /* ASDefaultPlayButton.m in Sources */, B350623D1B010EFD0018CF92 /* _ASAsyncTransaction.mm in Sources */, - E516FC801E9FE24200714FF4 /* ASHashing.m in Sources */, 6947B0C51E36B5040007C478 /* ASStackPositionedLayout.mm in Sources */, B35062401B010EFD0018CF92 /* _ASAsyncTransactionContainer.m in Sources */, AC026B721BD57DBF00BBC17E /* _ASHierarchyChangeSet.mm in Sources */, @@ -2223,7 +2223,7 @@ B35062141B010EFD0018CF92 /* ASBasicImageDownloader.mm in Sources */, B35062161B010EFD0018CF92 /* ASBatchContext.mm in Sources */, AC47D9421B3B891B00AAEE9D /* ASCellNode.mm in Sources */, - E58E9E451E941D74004CFC59 /* ASCollectionLayoutContext.mm in Sources */, + E58E9E451E941D74004CFC59 /* ASCollectionLayoutContext.m in Sources */, 34EFC7641B701CC600AD841F /* ASCenterLayoutSpec.mm in Sources */, 18C2ED831B9B7DE800F627B3 /* ASCollectionNode.mm in Sources */, E55D86331CA8A14000A0C26F /* ASLayoutElement.mm in Sources */, diff --git a/Source/Details/ASCollectionFlowLayoutDelegate.m b/Source/Details/ASCollectionFlowLayoutDelegate.m index 68f5e297f..2dfc880da 100644 --- a/Source/Details/ASCollectionFlowLayoutDelegate.m +++ b/Source/Details/ASCollectionFlowLayoutDelegate.m @@ -26,9 +26,9 @@ #import #import -@implementation ASCollectionFlowLayoutDelegate - -@synthesize scrollableDirections = _scrollableDirections; +@implementation ASCollectionFlowLayoutDelegate { + ASScrollDirection _scrollableDirections; +} - (instancetype)init { @@ -44,12 +44,17 @@ - (instancetype)initWithScrollableDirections:(ASScrollDirection)scrollableDirect return self; } +- (ASScrollDirection)scrollableDirections +{ + return _scrollableDirections; +} + - (id)additionalInfoForLayoutWithElements:(ASElementMap *)elements { return nil; } -- (ASCollectionLayoutState *)calculateLayoutWithContext:(ASCollectionLayoutContext *)context ++ (ASCollectionLayoutState *)calculateLayoutWithContext:(ASCollectionLayoutContext *)context { ASElementMap *elements = context.elements; NSMutableArray *children = ASArrayByFlatMapping(elements.itemElements, ASCollectionElement *element, element.node); @@ -68,7 +73,10 @@ - (ASCollectionLayoutState *)calculateLayoutWithContext:(ASCollectionLayoutConte alignContent:ASStackLayoutAlignContentStart children:children]; stackSpec.concurrent = YES; - ASLayout *layout = [stackSpec layoutThatFits:ASSizeRangeForCollectionLayoutThatFitsViewportSize(context.viewportSize, _scrollableDirections)]; + + ASSizeRange sizeRange = ASSizeRangeForCollectionLayoutThatFitsViewportSize(context.viewportSize, context.scrollableDirections); + ASLayout *layout = [stackSpec layoutThatFits:sizeRange]; + return [[ASCollectionLayoutState alloc] initWithContext:context layout:layout additionalInfo:nil getElementBlock:^ASCollectionElement * _Nonnull(ASLayout * _Nonnull sublayout) { return ((ASCellNode *)sublayout.layoutElement).collectionElement; }]; diff --git a/Source/Details/ASCollectionGalleryLayoutDelegate.m b/Source/Details/ASCollectionGalleryLayoutDelegate.m index 2faa3aeae..58efc8f67 100644 --- a/Source/Details/ASCollectionGalleryLayoutDelegate.m +++ b/Source/Details/ASCollectionGalleryLayoutDelegate.m @@ -27,12 +27,10 @@ #pragma mark - ASCollectionGalleryLayoutDelegate @implementation ASCollectionGalleryLayoutDelegate { + ASScrollDirection _scrollableDirections; CGSize _itemSize; - ASSizeRange _itemSizeRange; } -@synthesize scrollableDirections = _scrollableDirections; - - (instancetype)initWithScrollableDirections:(ASScrollDirection)scrollableDirections itemSize:(CGSize)itemSize { self = [super init]; @@ -40,25 +38,34 @@ - (instancetype)initWithScrollableDirections:(ASScrollDirection)scrollableDirect ASDisplayNodeAssertFalse(CGSizeEqualToSize(CGSizeZero, itemSize)); _scrollableDirections = scrollableDirections; _itemSize = itemSize; - _itemSizeRange = ASSizeRangeMake(_itemSize); } return self; } +- (ASScrollDirection)scrollableDirections +{ + return _scrollableDirections; +} + - (id)additionalInfoForLayoutWithElements:(ASElementMap *)elements { - return nil; + return [NSValue valueWithCGSize:_itemSize]; } -- (ASCollectionLayoutState *)calculateLayoutWithContext:(ASCollectionLayoutContext *)context ++ (ASCollectionLayoutState *)calculateLayoutWithContext:(ASCollectionLayoutContext *)context { ASElementMap *elements = context.elements; CGSize pageSize = context.viewportSize; + CGSize itemSize = ((NSValue *)context.additionalInfo).CGSizeValue; + ASScrollDirection scrollableDirections = context.scrollableDirections; NSMutableArray<_ASGalleryLayoutItem *> *children = ASArrayByFlatMapping(elements.itemElements, ASCollectionElement *element, - [[_ASGalleryLayoutItem alloc] initWithItemSize:_itemSize collectionElement:element]); + [[_ASGalleryLayoutItem alloc] initWithItemSize:itemSize collectionElement:element]); if (children.count == 0) { - return [[ASCollectionLayoutState alloc] initWithContext:context contentSize:CGSizeZero additionalInfo:nil elementToLayoutAttributesTable:[NSMapTable weakToStrongObjectsMapTable]]; + return [[ASCollectionLayoutState alloc] initWithContext:context + contentSize:CGSizeZero + additionalInfo:nil + elementToLayoutAttributesTable:[NSMapTable weakToStrongObjectsMapTable]]; } // Use a stack spec to calculate layout content size and frames of all elements without actually measuring each element @@ -70,7 +77,7 @@ - (ASCollectionLayoutState *)calculateLayoutWithContext:(ASCollectionLayoutConte alignContent:ASStackLayoutAlignContentStart children:children]; stackSpec.concurrent = YES; - ASLayout *layout = [stackSpec layoutThatFits:ASSizeRangeForCollectionLayoutThatFitsViewportSize(pageSize, _scrollableDirections)]; + ASLayout *layout = [stackSpec layoutThatFits:ASSizeRangeForCollectionLayoutThatFitsViewportSize(pageSize, scrollableDirections)]; return [[ASCollectionLayoutState alloc] initWithContext:context layout:layout additionalInfo:nil getElementBlock:^ASCollectionElement *(ASLayout *sublayout) { return ((_ASGalleryLayoutItem *)sublayout.layoutElement).collectionElement; diff --git a/Source/Details/ASCollectionLayoutContext.h b/Source/Details/ASCollectionLayoutContext.h index 00451584a..a20873a81 100644 --- a/Source/Details/ASCollectionLayoutContext.h +++ b/Source/Details/ASCollectionLayoutContext.h @@ -17,16 +17,17 @@ #import #import +#import @class ASElementMap; NS_ASSUME_NONNULL_BEGIN AS_SUBCLASSING_RESTRICTED - @interface ASCollectionLayoutContext : NSObject @property (nonatomic, assign, readonly) CGSize viewportSize; +@property (nonatomic, assign, readonly) ASScrollDirection scrollableDirections; @property (nonatomic, weak, readonly) ASElementMap *elements; @property (nonatomic, strong, readonly, nullable) id additionalInfo; diff --git a/Source/Details/ASCollectionLayoutContext.m b/Source/Details/ASCollectionLayoutContext.m new file mode 100644 index 000000000..2ab3946f5 --- /dev/null +++ b/Source/Details/ASCollectionLayoutContext.m @@ -0,0 +1,112 @@ +// +// ASCollectionLayoutContext.m +// 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 +#import + +#import +#import +#import +#import +#import +#import + +@implementation ASCollectionLayoutContext { + Class _layoutDelegateClass; + + // This ivar doesn't directly involve in the layout calculation process, i.e contexts can be equal regardless of the layout caches. + // As a result, this ivar is ignored in -isEqualToContext: and -hash. + __weak ASCollectionLayoutCache *_layoutCache; +} + +- (instancetype)initWithViewportSize:(CGSize)viewportSize + scrollableDirections:(ASScrollDirection)scrollableDirections + elements:(ASElementMap *)elements + layoutDelegateClass:(Class)layoutDelegateClass + layoutCache:(ASCollectionLayoutCache *)layoutCache + additionalInfo:(id)additionalInfo +{ + self = [super init]; + if (self) { + ASDisplayNodeAssertTrue([layoutDelegateClass conformsToProtocol:@protocol(ASCollectionLayoutDelegate)]); + _viewportSize = viewportSize; + _scrollableDirections = scrollableDirections; + _elements = elements; + _layoutDelegateClass = layoutDelegateClass; + _layoutCache = layoutCache; + _additionalInfo = additionalInfo; + } + return self; +} + +- (Class)layoutDelegateClass +{ + return _layoutDelegateClass; +} + +- (ASCollectionLayoutCache *)layoutCache +{ + return _layoutCache; +} + +- (BOOL)isEqualToContext:(ASCollectionLayoutContext *)context +{ + if (context == nil) { + return NO; + } + + // NOTE: ASObjectIsEqual returns YES when both objects are nil. + // So don't use ASObjectIsEqual on _elements. + // It is a weak property and 2 layouts generated from different sets of elements + // should never be considered the same even if they are nil now. + return CGSizeEqualToSize(_viewportSize, context.viewportSize) + && _scrollableDirections == context.scrollableDirections + && [_elements isEqual:context.elements] + && _layoutDelegateClass == context.layoutDelegateClass + && ASObjectIsEqual(_additionalInfo, context.additionalInfo); +} + +- (BOOL)isEqual:(id)other +{ + if (self == other) { + return YES; + } + if (! [other isKindOfClass:[ASCollectionLayoutContext class]]) { + return NO; + } + return [self isEqualToContext:other]; +} + +- (NSUInteger)hash +{ + struct { + CGSize viewportSize; + ASScrollDirection scrollableDirections; + NSUInteger elementsHash; + NSUInteger layoutDelegateClassHash; + NSUInteger additionalInfoHash; + } data = { + _viewportSize, + _scrollableDirections, + _elements.hash, + _layoutDelegateClass.hash, + [_additionalInfo hash] + }; + return ASHashBytes(&data, sizeof(data)); +} + +@end diff --git a/Source/Details/ASCollectionLayoutContext.mm b/Source/Details/ASCollectionLayoutContext.mm deleted file mode 100644 index d84de9f67..000000000 --- a/Source/Details/ASCollectionLayoutContext.mm +++ /dev/null @@ -1,72 +0,0 @@ -// -// ASCollectionLayoutContext.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 -#import - -#import -#import -#import -#import - -@implementation ASCollectionLayoutContext - -- (instancetype)initWithViewportSize:(CGSize)viewportSize elements:(ASElementMap *)elements additionalInfo:(id)additionalInfo -{ - self = [super init]; - if (self) { - _viewportSize = viewportSize; - _elements = elements; - _additionalInfo = additionalInfo; - } - return self; -} - -- (BOOL)isEqualToContext:(ASCollectionLayoutContext *)context -{ - if (context == nil) { - return NO; - } - return CGSizeEqualToSize(_viewportSize, context.viewportSize) && ASObjectIsEqual(_elements, context.elements) && ASObjectIsEqual(_additionalInfo, context.additionalInfo); -} - -- (BOOL)isEqual:(id)other -{ - if (self == other) { - return YES; - } - if (! [other isKindOfClass:[ASCollectionLayoutContext class]]) { - return NO; - } - return [self isEqualToContext:other]; -} - -- (NSUInteger)hash -{ - struct { - CGSize viewportSize; - NSUInteger elementsHash; - NSUInteger addlInfoHash; - } data = { - _viewportSize, - _elements.hash, - [_additionalInfo hash] - }; - return ASHashBytes(&data, sizeof(data)); -} - -@end diff --git a/Source/Details/ASCollectionLayoutDelegate.h b/Source/Details/ASCollectionLayoutDelegate.h index 3136cde5c..9ef3da874 100644 --- a/Source/Details/ASCollectionLayoutDelegate.h +++ b/Source/Details/ASCollectionLayoutDelegate.h @@ -25,7 +25,13 @@ NS_ASSUME_NONNULL_BEGIN @protocol ASCollectionLayoutDelegate -@property (nonatomic, assign, readonly) ASScrollDirection scrollableDirections; +/** + * @abstract Returns the scrollable directions of the coming layout (@see @c -calculateLayoutWithContext:). + * It will be available in the context parameter in +calculateLayoutWithContext: + * + * @return The scrollable directions. + */ +- (ASScrollDirection)scrollableDirections; /** * @abstract Returns any additional information needed for a coming layout pass (@see @c -calculateLayoutWithContext:) with the given elements. @@ -33,7 +39,7 @@ NS_ASSUME_NONNULL_BEGIN * @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). - * It should contain all the information needed for the layout pass to perform. + * It should contain all the information needed for the layout pass to perform. It will be available in the context parameter in +calculateLayoutWithContext: * * This method will be called on main thread. */ @@ -52,7 +58,7 @@ NS_ASSUME_NONNULL_BEGIN * This method can be called on background theads. It must be thread-safe and should not change any internal state of this delegate. * It must block the calling thread but can dispatch to other theads to reduce total blocking time. */ -- (ASCollectionLayoutState *)calculateLayoutWithContext:(ASCollectionLayoutContext *)context; ++ (ASCollectionLayoutState *)calculateLayoutWithContext:(ASCollectionLayoutContext *)context; @end diff --git a/Source/Details/ASDataController.h b/Source/Details/ASDataController.h index fffbb804c..e0e7142e3 100644 --- a/Source/Details/ASDataController.h +++ b/Source/Details/ASDataController.h @@ -153,7 +153,7 @@ extern NSString * const ASCollectionInvalidUpdateException; * This method will be called on background theads. It must be thread-safe and should not change any internal state of the conforming object. * It must block the calling thread but can dispatch to other theads to reduce total blocking time. */ -- (ASCollectionLayoutState *)calculateLayoutWithContext:(ASCollectionLayoutContext *)context; ++ (ASCollectionLayoutState *)calculateLayoutWithContext:(ASCollectionLayoutContext *)context; @end diff --git a/Source/Details/ASDataController.mm b/Source/Details/ASDataController.mm index a9d803091..a83a94da2 100644 --- a/Source/Details/ASDataController.mm +++ b/Source/Details/ASDataController.mm @@ -54,7 +54,7 @@ NSString * const ASDataControllerRowNodeKind = @"_ASDataControllerRowNodeKind"; NSString * const ASCollectionInvalidUpdateException = @"ASCollectionInvalidUpdateException"; -typedef void (^ASDataControllerCompletionBlock)(NSArray *elements, NSArray *nodes); +typedef dispatch_block_t ASDataControllerCompletionBlock; @interface ASDataController () { id _layoutDelegate; @@ -151,12 +151,12 @@ - (void)setLayoutDelegate:(id)layoutDelegate #pragma mark - Cell Layout -- (void)batchAllocateNodesFromElements:(NSArray *)elements andLayout:(BOOL)shouldLayout batchSize:(NSInteger)batchSize batchCompletion:(ASDataControllerCompletionBlock)batchCompletionHandler +- (void)batchAllocateNodesFromElements:(NSArray *)elements batchSize:(NSInteger)batchSize batchCompletion:(ASDataControllerCompletionBlock)batchCompletionHandler { ASSERT_ON_EDITING_QUEUE; if (elements.count == 0 || _dataSource == nil) { - batchCompletionHandler(@[], @[]); + batchCompletionHandler(); return; } @@ -171,12 +171,11 @@ - (void)batchAllocateNodesFromElements:(NSArray *)element for (NSUInteger i = 0; i < count; i += batchSize) { NSRange batchedRange = NSMakeRange(i, MIN(count - i, batchSize)); NSArray *batchedElements = [elements subarrayWithRange:batchedRange]; - NSArray *nodes; { as_activity_create_for_scope("Data controller batch"); - nodes = [self _allocateNodesFromElements:batchedElements andLayout:shouldLayout]; + [self _allocateNodesFromElements:batchedElements]; } - batchCompletionHandler(batchedElements, nodes); + batchCompletionHandler(); } ASSignpostEndCustom(ASSignpostDataControllerBatch, self, 0, (_dataSource != nil ? ASSignpostColorDefault : ASSignpostColorRed)); @@ -195,17 +194,15 @@ - (void)_layoutNode:(ASCellNode *)node withConstrainedSize:(ASSizeRange)constrai } // TODO Is returned array still needed? Can it be removed? -- (NSArray *)_allocateNodesFromElements:(NSArray *)elements andLayout:(BOOL)shouldLayout +- (void)_allocateNodesFromElements:(NSArray *)elements { ASSERT_ON_EDITING_QUEUE; NSUInteger nodeCount = elements.count; if (!nodeCount || _dataSource == nil) { - return @[]; + return; } - __strong ASCellNode **allocatedNodeBuffer = (__strong ASCellNode **)calloc(nodeCount, sizeof(ASCellNode *)); - dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); ASDispatchApply(nodeCount, queue, 0, ^(size_t i) { RETURN_IF_NO_DATASOURCE(); @@ -218,29 +215,12 @@ - (void)_layoutNode:(ASCellNode *)node withConstrainedSize:(ASSizeRange)constrai node = [[ASCellNode alloc] init]; // Fallback to avoid crash for production apps. } - if (shouldLayout) { - // Layout the node if the size range is valid. - ASSizeRange sizeRange = context.constrainedSize; - if (ASSizeRangeHasSignificantArea(sizeRange)) { - [self _layoutNode:node withConstrainedSize:sizeRange]; - } + // Layout the node if the size range is valid. + ASSizeRange sizeRange = context.constrainedSize; + if (ASSizeRangeHasSignificantArea(sizeRange)) { + [self _layoutNode:node withConstrainedSize:sizeRange]; } - - allocatedNodeBuffer[i] = node; }); - - BOOL canceled = _dataSource == nil; - - // Create nodes array - NSArray *nodes = canceled ? nil : [NSArray arrayWithObjects:allocatedNodeBuffer count:nodeCount]; - - // Nil out buffer indexes to allow arc to free the stored cells. - for (int i = 0; i < nodeCount; i++) { - allocatedNodeBuffer[i] = nil; - } - free(allocatedNodeBuffer); - - return nodes; } #pragma mark - Data Source Access (Calling _dataSource) @@ -550,8 +530,8 @@ - (void)updateWithChangeSet:(_ASHierarchyChangeSet *)changeSet @throw e; } } - - BOOL canDelegateLayout = (_layoutDelegate != nil); + + BOOL canDelegate = (self.layoutDelegate != nil); ASElementMap *newMap; id layoutContext; { @@ -569,7 +549,7 @@ - (void)updateWithChangeSet:(_ASHierarchyChangeSet *)changeSet // Step 1.1: Update the mutable copies to match the data source's state [self _updateSectionContextsInMap:mutableMap changeSet:changeSet]; ASPrimitiveTraitCollection existingTraitCollection = [self.node primitiveTraitCollection]; - [self _updateElementsInMap:mutableMap changeSet:changeSet traitCollection:existingTraitCollection shouldFetchSizeRanges:(! canDelegateLayout) previousMap:previousMap]; + [self _updateElementsInMap:mutableMap changeSet:changeSet traitCollection:existingTraitCollection shouldFetchSizeRanges:(! canDelegate) previousMap:previousMap]; // Step 1.2: Clone the new data newMap = [mutableMap copy]; @@ -577,38 +557,19 @@ - (void)updateWithChangeSet:(_ASHierarchyChangeSet *)changeSet self.pendingMap = newMap; // Step 2: Ask layout delegate for contexts - if (canDelegateLayout) { - layoutContext = [_layoutDelegate layoutContextWithElements:newMap]; + if (canDelegate) { + layoutContext = [self.layoutDelegate layoutContextWithElements:newMap]; } } as_log_debug(ASCollectionLog(), "New content: %@", newMap.smallDescription); - + + Class layoutDelegateClass = [self.layoutDelegate class]; dispatch_group_async(_editingTransactionGroup, _editingTransactionQueue, ^{ __block __unused os_activity_scope_state_s preparationScope = {}; // unused if deployment target < iOS10 as_activity_scope_enter(as_activity_create("Prepare nodes for collection update", AS_ACTIVITY_CURRENT, OS_ACTIVITY_FLAG_DEFAULT), &preparationScope); - // Step 3: Allocate and layout elements if can't delegate - NSArray *elementsToProcess; - if (canDelegateLayout) { - // Allocate all nodes before handling them to the layout delegate. - // In the future, we may want to let the delegate drive allocation as well. - elementsToProcess = ASArrayByFlatMapping(newMap, - ASCollectionElement *element, - (element.nodeIfAllocated == nil ? element : nil)); - } else { - elementsToProcess = ASArrayByFlatMapping(newMap, - ASCollectionElement *element, - (element.nodeIfAllocated.calculatedLayout == nil ? element : nil)); - } - - // TODO (ASCL) Don't even allocate nodes here - [self batchAllocateNodesFromElements:elementsToProcess andLayout:(! canDelegateLayout) batchSize:elementsToProcess.count batchCompletion:^(NSArray *elements, NSArray *nodes) { - ASSERT_ON_EDITING_QUEUE; - - if (canDelegateLayout) { - [_layoutDelegate calculateLayoutWithContext:layoutContext]; - } + dispatch_block_t completion = ^() { [_mainSerialQueue performBlockOnMainThread:^{ as_activity_scope_leave(&preparationScope); // TODO Merge the two delegate methods below @@ -626,7 +587,18 @@ - (void)updateWithChangeSet:(_ASHierarchyChangeSet *)changeSet self.visibleMap = newMap; }]; }]; - }]; + }; + + // Step 3: Call the layout delegate if possible. Otherwise, allocate and layout all elements + if (canDelegate) { + [layoutDelegateClass calculateLayoutWithContext:layoutContext]; + completion(); + } else { + NSArray *elementsToProcess = ASArrayByFlatMapping(newMap, + ASCollectionElement *element, + (element.nodeIfAllocated.calculatedLayout == nil ? element : nil)); + [self batchAllocateNodesFromElements:elementsToProcess batchSize:elementsToProcess.count batchCompletion:completion]; + } }); if (_usesSynchronousDataLoading) { @@ -838,7 +810,6 @@ - (void)environmentDidChange // Can't update the trait collection right away because _visibleMap may not be up-to-date, // i.e there might be some elements that were allocated using the old trait collection but haven't been added to _visibleMap - [self _scheduleBlockOnMainSerialQueue:^{ ASPrimitiveTraitCollection newTraitCollection = [self.node primitiveTraitCollection]; for (ASCollectionElement *element in _visibleMap) { diff --git a/Source/Private/ASHashing.h b/Source/Details/ASHashing.h similarity index 100% rename from Source/Private/ASHashing.h rename to Source/Details/ASHashing.h diff --git a/Source/Private/ASHashing.m b/Source/Details/ASHashing.m similarity index 100% rename from Source/Private/ASHashing.m rename to Source/Details/ASHashing.m diff --git a/Source/Private/ASCollectionLayout.mm b/Source/Private/ASCollectionLayout.mm index 3facba2ce..05a409128 100644 --- a/Source/Private/ASCollectionLayout.mm +++ b/Source/Private/ASCollectionLayout.mm @@ -74,13 +74,26 @@ - (ASCollectionLayoutContext *)layoutContextWithElements:(ASElementMap *)element if (_layoutDelegateFlags.implementsAdditionalInfoForLayoutWithElements) { additionalInfo = [_layoutDelegate additionalInfoForLayoutWithElements:elements]; } - return [[ASCollectionLayoutContext alloc] initWithViewportSize:viewportSize elements:elements additionalInfo:additionalInfo]; + return [[ASCollectionLayoutContext alloc] initWithViewportSize:viewportSize + scrollableDirections:[_layoutDelegate scrollableDirections] + elements:elements + layoutDelegateClass:[_layoutDelegate class] + layoutCache:_layoutCache + additionalInfo:additionalInfo]; } -- (ASCollectionLayoutState *)calculateLayoutWithContext:(ASCollectionLayoutContext *)context ++ (ASCollectionLayoutState *)calculateLayoutWithContext:(ASCollectionLayoutContext *)context { - ASCollectionLayoutState *layout = [_layoutDelegate calculateLayoutWithContext:context]; - [_layoutCache setLayout:layout forContext:context]; + if (context.elements == nil) { + return [[ASCollectionLayoutState alloc] initWithContext:context + contentSize:CGSizeZero + additionalInfo:nil + elementToLayoutAttributesTable:[NSMapTable elementToLayoutAttributesTable]]; + } + + ASDisplayNodeAssertTrue([context.layoutDelegateClass conformsToProtocol:@protocol(ASCollectionLayoutDelegate)]); + ASCollectionLayoutState *layout = [context.layoutDelegateClass calculateLayoutWithContext:context]; + [context.layoutCache setLayout:layout forContext:context]; // Measure elements in the measure range ahead of time, block on the initial rect as it'll be visible shortly CGSize viewportSize = context.viewportSize; @@ -88,9 +101,9 @@ - (ASCollectionLayoutState *)calculateLayoutWithContext:(ASCollectionLayoutConte CGRect initialRect = CGRectMake(0, 0, viewportSize.width, viewportSize.height); CGRect measureRect = CGRectExpandToRangeWithScrollableDirections(initialRect, kASDefaultMeasureRangeTuningParameters, - _layoutDelegate.scrollableDirections, + context.scrollableDirections, kASStaticScrollDirection); - [ASCollectionLayout _measureElementsInRect:measureRect blockingRect:initialRect layout:layout]; + [self _measureElementsInRect:measureRect blockingRect:initialRect layout:layout]; return layout; } @@ -112,7 +125,7 @@ - (void)prepareLayout _layout = cachedLayout; } else { // A new layout is needed now. Calculate and apply it immediately - _layout = [self calculateLayoutWithContext:context]; + _layout = [ASCollectionLayout calculateLayoutWithContext:context]; } } @@ -211,7 +224,7 @@ - (CGSize)_viewportSize */ + (void)_measureElementsInRect:(CGRect)rect blockingRect:(CGRect)blockingRect layout:(ASCollectionLayoutState *)layout { - if (CGRectIsEmpty(rect) || layout == nil) { + if (CGRectIsEmpty(rect) || layout.context.elements == nil) { return; } BOOL hasBlockingRect = !CGRectIsEmpty(blockingRect); diff --git a/Source/Private/ASCollectionLayoutContext+Private.h b/Source/Private/ASCollectionLayoutContext+Private.h index f6827be8d..3c615aef2 100644 --- a/Source/Private/ASCollectionLayoutContext+Private.h +++ b/Source/Private/ASCollectionLayoutContext+Private.h @@ -17,11 +17,22 @@ #import +@class ASCollectionLayoutCache; +@protocol ASCollectionLayoutDelegate; + NS_ASSUME_NONNULL_BEGIN @interface ASCollectionLayoutContext (Private) -- (instancetype)initWithViewportSize:(CGSize)viewportSize elements:(ASElementMap *)elements additionalInfo:(nullable id)additionalInfo; +@property (nonatomic, strong, readonly) Class layoutDelegateClass; +@property (nonatomic, weak, readonly) ASCollectionLayoutCache *layoutCache; + +- (instancetype)initWithViewportSize:(CGSize)viewportSize + scrollableDirections:(ASScrollDirection)scrollableDirections + elements:(ASElementMap *)elements + layoutDelegateClass:(Class)layoutDelegateClass + layoutCache:(ASCollectionLayoutCache *)layoutCache + additionalInfo:(nullable id)additionalInfo; @end diff --git a/examples/ASCollectionView/Sample.xcworkspace/contents.xcworkspacedata b/examples/ASCollectionView/Sample.xcworkspace/contents.xcworkspacedata new file mode 100644 index 000000000..7b5a2f305 --- /dev/null +++ b/examples/ASCollectionView/Sample.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/examples/CustomCollectionView/Sample.xcodeproj/project.pbxproj b/examples/CustomCollectionView/Sample.xcodeproj/project.pbxproj index c1014c643..91818f8f8 100644 --- a/examples/CustomCollectionView/Sample.xcodeproj/project.pbxproj +++ b/examples/CustomCollectionView/Sample.xcodeproj/project.pbxproj @@ -16,6 +16,7 @@ AC3C4A671A11F47200143C57 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = AC3C4A661A11F47200143C57 /* AppDelegate.m */; }; AC3C4A6A1A11F47200143C57 /* ViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = AC3C4A691A11F47200143C57 /* ViewController.m */; }; AC3C4A8E1A11F80C00143C57 /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = AC3C4A8D1A11F80C00143C57 /* Images.xcassets */; }; + E5B2252C1F1791EA001E1431 /* MosaicCollectionLayoutInfo.m in Sources */ = {isa = PBXBuildFile; fileRef = E5B2252B1F1791EA001E1431 /* MosaicCollectionLayoutInfo.m */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ @@ -35,6 +36,8 @@ AC3C4A691A11F47200143C57 /* ViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ViewController.m; sourceTree = ""; }; AC3C4A8D1A11F80C00143C57 /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Images.xcassets; sourceTree = ""; }; E2F287D91FFDEA2A747630CE /* Pods-Sample.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Sample.release.xcconfig"; path = "Pods/Target Support Files/Pods-Sample/Pods-Sample.release.xcconfig"; sourceTree = ""; }; + E5B2252A1F1791DE001E1431 /* MosaicCollectionLayoutInfo.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MosaicCollectionLayoutInfo.h; sourceTree = ""; }; + E5B2252B1F1791EA001E1431 /* MosaicCollectionLayoutInfo.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MosaicCollectionLayoutInfo.m; sourceTree = ""; }; E5D73A3A1EA6766B006418A8 /* MosaicCollectionLayoutDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MosaicCollectionLayoutDelegate.h; sourceTree = ""; }; F36BCD8EBAF79797AB5C6708 /* Pods-Sample.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Sample.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Sample/Pods-Sample.debug.xcconfig"; sourceTree = ""; }; /* End PBXFileReference section */ @@ -83,6 +86,8 @@ children = ( E5D73A3A1EA6766B006418A8 /* MosaicCollectionLayoutDelegate.h */, 25A1FA841C02F7AC00193875 /* MosaicCollectionLayoutDelegate.m */, + E5B2252A1F1791DE001E1431 /* MosaicCollectionLayoutInfo.h */, + E5B2252B1F1791EA001E1431 /* MosaicCollectionLayoutInfo.m */, AC3C4A651A11F47200143C57 /* AppDelegate.h */, AC3C4A661A11F47200143C57 /* AppDelegate.m */, AC3C4A681A11F47200143C57 /* ViewController.h */, @@ -152,6 +157,7 @@ TargetAttributes = { AC3C4A5D1A11F47200143C57 = { CreatedOnToolsVersion = 6.1; + DevelopmentTeam = XSR3D45JSF; }; }; }; @@ -244,6 +250,7 @@ AC3C4A641A11F47200143C57 /* main.m in Sources */, 80364CCA1E3D95A90094400C /* ImageCollectionViewCell.m in Sources */, 25A1FA881C02FCB000193875 /* ImageCellNode.m in Sources */, + E5B2252C1F1791EA001E1431 /* MosaicCollectionLayoutInfo.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -333,9 +340,11 @@ baseConfigurationReference = F36BCD8EBAF79797AB5C6708 /* Pods-Sample.debug.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_LAUNCHIMAGE_NAME = LaunchImage; + DEVELOPMENT_TEAM = XSR3D45JSF; INFOPLIST_FILE = Sample/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 8.1; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = com.facebook.AsyncDisplayKit.Sample.CustomCollectionView; PRODUCT_NAME = "$(TARGET_NAME)"; TARGETED_DEVICE_FAMILY = 1; }; @@ -346,9 +355,11 @@ baseConfigurationReference = E2F287D91FFDEA2A747630CE /* Pods-Sample.release.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_LAUNCHIMAGE_NAME = LaunchImage; + DEVELOPMENT_TEAM = XSR3D45JSF; INFOPLIST_FILE = Sample/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 8.1; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = com.facebook.AsyncDisplayKit.Sample.CustomCollectionView; PRODUCT_NAME = "$(TARGET_NAME)"; TARGETED_DEVICE_FAMILY = 1; }; diff --git a/examples/CustomCollectionView/Sample/MosaicCollectionLayoutDelegate.m b/examples/CustomCollectionView/Sample/MosaicCollectionLayoutDelegate.m index 63a3846b2..6abce535a 100644 --- a/examples/CustomCollectionView/Sample/MosaicCollectionLayoutDelegate.m +++ b/examples/CustomCollectionView/Sample/MosaicCollectionLayoutDelegate.m @@ -11,66 +11,65 @@ // #import "MosaicCollectionLayoutDelegate.h" +#import "MosaicCollectionLayoutInfo.h" #import "ImageCellNode.h" #import @implementation MosaicCollectionLayoutDelegate { // Read-only properties - NSInteger _numberOfColumns; - CGFloat _headerHeight; - CGFloat _columnSpacing; - UIEdgeInsets _sectionInset; - UIEdgeInsets _interItemSpacing; + MosaicCollectionLayoutInfo *_info; } -@synthesize scrollableDirections = _scrollableDirections; - - (instancetype)initWithNumberOfColumns:(NSInteger)numberOfColumns headerHeight:(CGFloat)headerHeight { self = [super init]; if (self != nil) { - _numberOfColumns = numberOfColumns; - _headerHeight = headerHeight; - _columnSpacing = 10.0; - _sectionInset = UIEdgeInsetsMake(10.0, 10.0, 10.0, 10.0); - _interItemSpacing = UIEdgeInsetsMake(10.0, 0, 10.0, 0); - _scrollableDirections = ASScrollDirectionVerticalDirections; + _info = [[MosaicCollectionLayoutInfo alloc] initWithNumberOfColumns:numberOfColumns + headerHeight:headerHeight + columnSpacing:10.0 + sectionInsets:UIEdgeInsetsMake(10.0, 10.0, 10.0, 10.0) + interItemSpacing:UIEdgeInsetsMake(10.0, 0, 10.0, 0)]; } return self; } +- (ASScrollDirection)scrollableDirections +{ + return ASScrollDirectionVerticalDirections; +} + - (id)additionalInfoForLayoutWithElements:(ASElementMap *)elements { - return nil; + return _info; } -- (ASCollectionLayoutState *)calculateLayoutWithContext:(ASCollectionLayoutContext *)context ++ (ASCollectionLayoutState *)calculateLayoutWithContext:(ASCollectionLayoutContext *)context { CGFloat layoutWidth = context.viewportSize.width; ASElementMap *elements = context.elements; CGFloat top = 0; + MosaicCollectionLayoutInfo *info = (MosaicCollectionLayoutInfo *)context.additionalInfo; - // TODO use +[NSMapTable elementToLayoutAttributesTable] - NSMapTable *attrsMap = [NSMapTable mapTableWithKeyOptions:(NSMapTableObjectPointerPersonality | NSMapTableWeakMemory) valueOptions:NSMapTableStrongMemory]; + NSMapTable *attrsMap = [NSMapTable elementToLayoutAttributesTable]; NSMutableArray *columnHeights = [NSMutableArray array]; NSInteger numberOfSections = [elements numberOfSections]; for (NSUInteger section = 0; section < numberOfSections; section++) { NSInteger numberOfItems = [elements numberOfItemsInSection:section]; - top += _sectionInset.top; + top += info.sectionInsets.top; - if (_headerHeight > 0) { + if (info.headerHeight > 0) { NSIndexPath *indexPath = [NSIndexPath indexPathForItem:0 inSection:section]; ASCollectionElement *element = [elements supplementaryElementOfKind:UICollectionElementKindSectionHeader atIndexPath:indexPath]; UICollectionViewLayoutAttributes *attrs = [UICollectionViewLayoutAttributes layoutAttributesForSupplementaryViewOfKind:UICollectionElementKindSectionHeader - withIndexPath:indexPath]; + withIndexPath:indexPath]; - ASSizeRange sizeRange = [self sizeRangeForHeaderOfSection:section withLayoutWidth:layoutWidth]; + ASSizeRange sizeRange = [self _sizeRangeForHeaderOfSection:section withLayoutWidth:layoutWidth info:info]; CGSize size = [element.node layoutThatFits:sizeRange].size; - CGRect frame = CGRectMake(_sectionInset.left, top, size.width, size.height); + CGRect frame = CGRectMake(info.sectionInsets.left, top, size.width, size.height); attrs.frame = frame; [attrsMap setObject:attrs forKey:element]; @@ -78,31 +77,31 @@ - (ASCollectionLayoutState *)calculateLayoutWithContext:(ASCollectionLayoutConte } [columnHeights addObject:[NSMutableArray array]]; - for (NSUInteger idx = 0; idx < _numberOfColumns; idx++) { + for (NSUInteger idx = 0; idx < info.numberOfColumns; idx++) { [columnHeights[section] addObject:@(top)]; } - CGFloat columnWidth = [self _columnWidthForSection:section withLayoutWidth:layoutWidth]; + CGFloat columnWidth = [self _columnWidthForSection:section withLayoutWidth:layoutWidth info:info]; for (NSUInteger idx = 0; idx < numberOfItems; idx++) { NSUInteger columnIndex = [self _shortestColumnIndexInSection:section withColumnHeights:columnHeights]; NSIndexPath *indexPath = [NSIndexPath indexPathForItem:idx inSection:section]; ASCollectionElement *element = [elements elementForItemAtIndexPath:indexPath]; UICollectionViewLayoutAttributes *attrs = [UICollectionViewLayoutAttributes layoutAttributesForCellWithIndexPath:indexPath]; - ASSizeRange sizeRange = [self sizeRangeForItem:element.node atIndexPath:indexPath withLayoutWidth:layoutWidth]; + ASSizeRange sizeRange = [self _sizeRangeForItem:element.node atIndexPath:indexPath withLayoutWidth:layoutWidth info:info]; CGSize size = [element.node layoutThatFits:sizeRange].size; - CGPoint position = CGPointMake(_sectionInset.left + (columnWidth + _columnSpacing) * columnIndex, - [columnHeights[section][columnIndex] floatValue]); + CGPoint position = CGPointMake(info.sectionInsets.left + (columnWidth + info.columnSpacing) * columnIndex, + [columnHeights[section][columnIndex] floatValue]); CGRect frame = CGRectMake(position.x, position.y, size.width, size.height); attrs.frame = frame; [attrsMap setObject:attrs forKey:element]; // TODO Profile and avoid boxing if there are significant retain/release overheads - columnHeights[section][columnIndex] = @(CGRectGetMaxY(frame) + _interItemSpacing.bottom); + columnHeights[section][columnIndex] = @(CGRectGetMaxY(frame) + info.interItemSpacing.bottom); } NSUInteger columnIndex = [self _tallestColumnIndexInSection:section withColumnHeights:columnHeights]; - top = [columnHeights[section][columnIndex] floatValue] - _interItemSpacing.bottom + _sectionInset.bottom; + top = [columnHeights[section][columnIndex] floatValue] - info.interItemSpacing.bottom + info.sectionInsets.bottom; for (NSUInteger idx = 0; idx < [columnHeights[section] count]; idx++) { columnHeights[section][idx] = @(top); @@ -111,22 +110,25 @@ - (ASCollectionLayoutState *)calculateLayoutWithContext:(ASCollectionLayoutConte CGFloat contentHeight = [[[columnHeights lastObject] firstObject] floatValue]; CGSize contentSize = CGSizeMake(layoutWidth, contentHeight); - return [[ASCollectionLayoutState alloc] initWithContext:context contentSize:contentSize additionalInfo:nil elementToLayoutAttributesTable:attrsMap]; + return [[ASCollectionLayoutState alloc] initWithContext:context + contentSize:contentSize + additionalInfo:nil + elementToLayoutAttributesTable:attrsMap]; } -- (CGFloat)_widthForSection:(NSUInteger)section withLayoutWidth:(CGFloat)layoutWidth ++ (CGFloat)_columnWidthForSection:(NSUInteger)section withLayoutWidth:(CGFloat)layoutWidth info:(MosaicCollectionLayoutInfo *)info { - return layoutWidth - _sectionInset.left - _sectionInset.right; + return ([self _widthForSection:section withLayoutWidth:layoutWidth info:info] - ((info.numberOfColumns - 1) * info.columnSpacing)) / info.numberOfColumns; } -- (CGFloat)_columnWidthForSection:(NSUInteger)section withLayoutWidth:(CGFloat)layoutWidth ++ (CGFloat)_widthForSection:(NSUInteger)section withLayoutWidth:(CGFloat)layoutWidth info:(MosaicCollectionLayoutInfo *)info { - return ([self _widthForSection:section withLayoutWidth:layoutWidth] - ((_numberOfColumns - 1) * _columnSpacing)) / _numberOfColumns; + return layoutWidth - info.sectionInsets.left - info.sectionInsets.right; } -- (ASSizeRange)sizeRangeForItem:(ASCellNode *)item atIndexPath:(NSIndexPath *)indexPath withLayoutWidth:(CGFloat)layoutWidth; ++ (ASSizeRange)_sizeRangeForItem:(ASCellNode *)item atIndexPath:(NSIndexPath *)indexPath withLayoutWidth:(CGFloat)layoutWidth info:(MosaicCollectionLayoutInfo *)info { - CGFloat itemWidth = [self _columnWidthForSection:indexPath.section withLayoutWidth:layoutWidth]; + CGFloat itemWidth = [self _columnWidthForSection:indexPath.section withLayoutWidth:layoutWidth info:info]; if ([item isKindOfClass:[ImageCellNode class]]) { return ASSizeRangeMake(CGSizeMake(itemWidth, 0), CGSizeMake(itemWidth, CGFLOAT_MAX)); } else { @@ -134,12 +136,12 @@ - (ASSizeRange)sizeRangeForItem:(ASCellNode *)item atIndexPath:(NSIndexPath *)in } } -- (ASSizeRange)sizeRangeForHeaderOfSection:(NSInteger)section withLayoutWidth:(CGFloat)layoutWidth ++ (ASSizeRange)_sizeRangeForHeaderOfSection:(NSInteger)section withLayoutWidth:(CGFloat)layoutWidth info:(MosaicCollectionLayoutInfo *)info { - return ASSizeRangeMake(CGSizeMake(0, _headerHeight), CGSizeMake([self _widthForSection:section withLayoutWidth:layoutWidth], _headerHeight)); + return ASSizeRangeMake(CGSizeMake(0, info.headerHeight), CGSizeMake([self _widthForSection:section withLayoutWidth:layoutWidth info:info], info.headerHeight)); } -- (NSUInteger)_tallestColumnIndexInSection:(NSUInteger)section withColumnHeights:(NSArray *)columnHeights ++ (NSUInteger)_tallestColumnIndexInSection:(NSUInteger)section withColumnHeights:(NSArray *)columnHeights { __block NSUInteger index = 0; __block CGFloat tallestHeight = 0; @@ -152,7 +154,7 @@ - (NSUInteger)_tallestColumnIndexInSection:(NSUInteger)section withColumnHeights return index; } -- (NSUInteger)_shortestColumnIndexInSection:(NSUInteger)section withColumnHeights:(NSArray *)columnHeights ++ (NSUInteger)_shortestColumnIndexInSection:(NSUInteger)section withColumnHeights:(NSArray *)columnHeights { __block NSUInteger index = 0; __block CGFloat shortestHeight = CGFLOAT_MAX; diff --git a/examples/CustomCollectionView/Sample/MosaicCollectionLayoutInfo.h b/examples/CustomCollectionView/Sample/MosaicCollectionLayoutInfo.h new file mode 100644 index 000000000..1e7db5204 --- /dev/null +++ b/examples/CustomCollectionView/Sample/MosaicCollectionLayoutInfo.h @@ -0,0 +1,32 @@ +// +// MosaicCollectionLayoutInfo.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 + +@interface MosaicCollectionLayoutInfo : NSObject + +// Read-only properties +@property (nonatomic, assign, readonly) NSInteger numberOfColumns; +@property (nonatomic, assign, readonly) CGFloat headerHeight; +@property (nonatomic, assign, readonly) CGFloat columnSpacing; +@property (nonatomic, assign, readonly) UIEdgeInsets sectionInsets; +@property (nonatomic, assign, readonly) UIEdgeInsets interItemSpacing; + +- (instancetype)initWithNumberOfColumns:(NSInteger)numberOfColumns + headerHeight:(CGFloat)headerHeight + columnSpacing:(CGFloat)columnSpacing + sectionInsets:(UIEdgeInsets)sectionInsets + interItemSpacing:(UIEdgeInsets)interItemSpacing NS_DESIGNATED_INITIALIZER; + +- (instancetype)init __unavailable; + +@end diff --git a/examples/CustomCollectionView/Sample/MosaicCollectionLayoutInfo.m b/examples/CustomCollectionView/Sample/MosaicCollectionLayoutInfo.m new file mode 100644 index 000000000..f7a4224ab --- /dev/null +++ b/examples/CustomCollectionView/Sample/MosaicCollectionLayoutInfo.m @@ -0,0 +1,78 @@ +// +// MosaicCollectionLayoutInfo.m +// 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 "MosaicCollectionLayoutInfo.h" + +#import + +@implementation MosaicCollectionLayoutInfo + +- (instancetype)initWithNumberOfColumns:(NSInteger)numberOfColumns + headerHeight:(CGFloat)headerHeight + columnSpacing:(CGFloat)columnSpacing + sectionInsets:(UIEdgeInsets)sectionInsets + interItemSpacing:(UIEdgeInsets)interItemSpacing +{ + self = [super init]; + if (self) { + _numberOfColumns = numberOfColumns; + _headerHeight = headerHeight; + _columnSpacing = columnSpacing; + _sectionInsets = sectionInsets; + _interItemSpacing = interItemSpacing; + } + return self; +} + +- (BOOL)isEqualToInfo:(MosaicCollectionLayoutInfo *)info +{ + if (info == nil) { + return NO; + } + + return _numberOfColumns == info.numberOfColumns + && _headerHeight == info.headerHeight + && _columnSpacing == info.columnSpacing + && UIEdgeInsetsEqualToEdgeInsets(_sectionInsets, info.sectionInsets) + && UIEdgeInsetsEqualToEdgeInsets(_interItemSpacing, info.interItemSpacing); +} + +- (BOOL)isEqual:(id)other +{ + if (self == other) { + return YES; + } + if (! [other isKindOfClass:[MosaicCollectionLayoutInfo class]]) { + return NO; + } + return [self isEqualToInfo:other]; +} + +- (NSUInteger)hash +{ + struct { + NSInteger numberOfColumns; + CGFloat headerHeight; + CGFloat columnSpacing; + UIEdgeInsets sectionInsets; + UIEdgeInsets interItemSpacing; + } data = { + _numberOfColumns, + _headerHeight, + _columnSpacing, + _sectionInsets, + _interItemSpacing, + }; + return ASHashBytes(&data, sizeof(data)); +} + +@end From 7461c13d9bff6c0fe01231c9e3bf8b5ed3d071de Mon Sep 17 00:00:00 2001 From: Huy Nguyen Date: Thu, 13 Jul 2017 14:53:45 +0100 Subject: [PATCH 20/25] Remove additionalInfo property in ASCollectionLayoutState --- Source/Details/ASCollectionFlowLayoutDelegate.m | 3 +-- Source/Details/ASCollectionGalleryLayoutDelegate.m | 3 +-- Source/Details/ASCollectionLayoutState.h | 9 --------- Source/Details/ASCollectionLayoutState.mm | 5 +---- Source/Private/ASCollectionLayout.mm | 1 - .../Sample/MosaicCollectionLayoutDelegate.m | 1 - 6 files changed, 3 insertions(+), 19 deletions(-) diff --git a/Source/Details/ASCollectionFlowLayoutDelegate.m b/Source/Details/ASCollectionFlowLayoutDelegate.m index 2dfc880da..855f3791c 100644 --- a/Source/Details/ASCollectionFlowLayoutDelegate.m +++ b/Source/Details/ASCollectionFlowLayoutDelegate.m @@ -61,7 +61,6 @@ + (ASCollectionLayoutState *)calculateLayoutWithContext:(ASCollectionLayoutConte if (children.count == 0) { return [[ASCollectionLayoutState alloc] initWithContext:context contentSize:CGSizeZero - additionalInfo:nil elementToLayoutAttributesTable:[NSMapTable elementToLayoutAttributesTable]]; } @@ -77,7 +76,7 @@ + (ASCollectionLayoutState *)calculateLayoutWithContext:(ASCollectionLayoutConte ASSizeRange sizeRange = ASSizeRangeForCollectionLayoutThatFitsViewportSize(context.viewportSize, context.scrollableDirections); ASLayout *layout = [stackSpec layoutThatFits:sizeRange]; - return [[ASCollectionLayoutState alloc] initWithContext:context layout:layout additionalInfo:nil getElementBlock:^ASCollectionElement * _Nonnull(ASLayout * _Nonnull sublayout) { + return [[ASCollectionLayoutState alloc] initWithContext:context layout:layout getElementBlock:^ASCollectionElement * _Nonnull(ASLayout * _Nonnull sublayout) { return ((ASCellNode *)sublayout.layoutElement).collectionElement; }]; } diff --git a/Source/Details/ASCollectionGalleryLayoutDelegate.m b/Source/Details/ASCollectionGalleryLayoutDelegate.m index 58efc8f67..0c98f0fbf 100644 --- a/Source/Details/ASCollectionGalleryLayoutDelegate.m +++ b/Source/Details/ASCollectionGalleryLayoutDelegate.m @@ -64,7 +64,6 @@ + (ASCollectionLayoutState *)calculateLayoutWithContext:(ASCollectionLayoutConte if (children.count == 0) { return [[ASCollectionLayoutState alloc] initWithContext:context contentSize:CGSizeZero - additionalInfo:nil elementToLayoutAttributesTable:[NSMapTable weakToStrongObjectsMapTable]]; } @@ -79,7 +78,7 @@ + (ASCollectionLayoutState *)calculateLayoutWithContext:(ASCollectionLayoutConte stackSpec.concurrent = YES; ASLayout *layout = [stackSpec layoutThatFits:ASSizeRangeForCollectionLayoutThatFitsViewportSize(pageSize, scrollableDirections)]; - return [[ASCollectionLayoutState alloc] initWithContext:context layout:layout additionalInfo:nil getElementBlock:^ASCollectionElement *(ASLayout *sublayout) { + return [[ASCollectionLayoutState alloc] initWithContext:context layout:layout getElementBlock:^ASCollectionElement *(ASLayout *sublayout) { return ((_ASGalleryLayoutItem *)sublayout.layoutElement).collectionElement; }]; } diff --git a/Source/Details/ASCollectionLayoutState.h b/Source/Details/ASCollectionLayoutState.h index fe4b50c90..1804c58a3 100644 --- a/Source/Details/ASCollectionLayoutState.h +++ b/Source/Details/ASCollectionLayoutState.h @@ -40,9 +40,6 @@ AS_SUBCLASSING_RESTRICTED /// The final content size of the collection's layout @property (nonatomic, assign, readonly) CGSize contentSize; -/// Any additional information can be stored here. Clients are responsible for the thread-safety of this object. -@property (nonatomic, strong, readonly, nullable) id additionalInfo; - - (instancetype)init __unavailable; /** @@ -52,14 +49,11 @@ AS_SUBCLASSING_RESTRICTED * * @param contentSize The content size of the collection's layout * - * @param additionalInfo Any additional information to be stored in this object. - * * @param table A map between elements to their layout attributes. It must contain all elements. * It should have NSMapTableObjectPointerPersonality and NSMapTableWeakMemory as key options. */ - (instancetype)initWithContext:(ASCollectionLayoutContext *)context contentSize:(CGSize)contentSize - additionalInfo:(nullable id)additionalInfo elementToLayoutAttributesTable:(NSMapTable *)table NS_DESIGNATED_INITIALIZER; /** @@ -69,13 +63,10 @@ AS_SUBCLASSING_RESTRICTED * * @param layout The layout describes size and position of all elements. * - * @param additionalInfo Any additional information to be stored in this object. - * * @param getElementBlock A block that can retrieve the collection element from a direct sublayout of the root layout. */ - (instancetype)initWithContext:(ASCollectionLayoutContext *)context layout:(ASLayout *)layout - additionalInfo:(nullable id)additionalInfo getElementBlock:(ASCollectionElement *(^)(ASLayout *))getElementBlock; /** diff --git a/Source/Details/ASCollectionLayoutState.mm b/Source/Details/ASCollectionLayoutState.mm index 6223eb08e..58c2f92dc 100644 --- a/Source/Details/ASCollectionLayoutState.mm +++ b/Source/Details/ASCollectionLayoutState.mm @@ -44,7 +44,6 @@ @implementation ASCollectionLayoutState { - (instancetype)initWithContext:(ASCollectionLayoutContext *)context layout:(ASLayout *)layout - additionalInfo:(id)additionalInfo getElementBlock:(ASCollectionElement *(^)(ASLayout *))getElementBlock { ASElementMap *elements = context.elements; @@ -71,12 +70,11 @@ - (instancetype)initWithContext:(ASCollectionLayoutContext *)context [table setObject:attrs forKey:element]; } - return [self initWithContext:context contentSize:layout.size additionalInfo:additionalInfo elementToLayoutAttributesTable:table]; + return [self initWithContext:context contentSize:layout.size elementToLayoutAttributesTable:table]; } - (instancetype)initWithContext:(ASCollectionLayoutContext *)context contentSize:(CGSize)contentSize - additionalInfo:(id)additionalInfo elementToLayoutAttributesTable:(NSMapTable *)table { self = [super init]; @@ -87,7 +85,6 @@ - (instancetype)initWithContext:(ASCollectionLayoutContext *)context _pageToLayoutAttributesTable = [ASPageTable pageTableWithLayoutAttributes:_elementToLayoutAttributesTable.objectEnumerator contentSize:contentSize pageSize:context.viewportSize]; // Assume that all elements are unmeasured, deep copy the original table _unmeasuredPageToLayoutAttributesTable = [_pageToLayoutAttributesTable deepCopy]; - _additionalInfo = additionalInfo; } return self; } diff --git a/Source/Private/ASCollectionLayout.mm b/Source/Private/ASCollectionLayout.mm index 05a409128..a27ddf7aa 100644 --- a/Source/Private/ASCollectionLayout.mm +++ b/Source/Private/ASCollectionLayout.mm @@ -87,7 +87,6 @@ + (ASCollectionLayoutState *)calculateLayoutWithContext:(ASCollectionLayoutConte if (context.elements == nil) { return [[ASCollectionLayoutState alloc] initWithContext:context contentSize:CGSizeZero - additionalInfo:nil elementToLayoutAttributesTable:[NSMapTable elementToLayoutAttributesTable]]; } diff --git a/examples/CustomCollectionView/Sample/MosaicCollectionLayoutDelegate.m b/examples/CustomCollectionView/Sample/MosaicCollectionLayoutDelegate.m index 6abce535a..a7383f294 100644 --- a/examples/CustomCollectionView/Sample/MosaicCollectionLayoutDelegate.m +++ b/examples/CustomCollectionView/Sample/MosaicCollectionLayoutDelegate.m @@ -112,7 +112,6 @@ + (ASCollectionLayoutState *)calculateLayoutWithContext:(ASCollectionLayoutConte CGSize contentSize = CGSizeMake(layoutWidth, contentHeight); return [[ASCollectionLayoutState alloc] initWithContext:context contentSize:contentSize - additionalInfo:nil elementToLayoutAttributesTable:attrsMap]; } From b4df3c8f3de900e5cd09e9c770472f76918d827a Mon Sep 17 00:00:00 2001 From: Huy Nguyen Date: Thu, 13 Jul 2017 15:23:12 +0100 Subject: [PATCH 21/25] ASCollectionLayoutState to correctly filter unmeasured elements --- Source/Details/ASCollectionLayoutState.mm | 81 +++++++++++-------- Source/Details/ASPageTable.h | 12 +-- Source/Details/ASPageTable.m | 17 +--- Source/Private/ASCollectionLayout.mm | 11 ++- .../Private/ASCollectionLayoutState+Private.h | 6 +- 5 files changed, 64 insertions(+), 63 deletions(-) diff --git a/Source/Details/ASCollectionLayoutState.mm b/Source/Details/ASCollectionLayoutState.mm index 58c2f92dc..a272bdcbd 100644 --- a/Source/Details/ASCollectionLayoutState.mm +++ b/Source/Details/ASCollectionLayoutState.mm @@ -19,8 +19,10 @@ #import #import +#import #import #import +#import #import #import #import @@ -28,7 +30,7 @@ @implementation NSMapTable (ASCollectionLayoutConvenience) -+ (NSMapTable *)elementToLayoutAttributesTable ++ (NSMapTable *)elementToLayoutAttributesTable { return [NSMapTable mapTableWithKeyOptions:(NSMapTableWeakMemory | NSMapTableObjectPointerPersonality) valueOptions:NSMapTableStrongMemory]; } @@ -37,9 +39,9 @@ @implementation NSMapTable (ASCollectionLayoutConvenience) @implementation ASCollectionLayoutState { ASDN::Mutex __instanceLock__; - NSMapTable *_elementToLayoutAttributesTable; - ASPageTable *> *_pageToLayoutAttributesTable; - ASPageTable *> *_unmeasuredPageToLayoutAttributesTable; + NSMapTable *_elementToLayoutAttributesTable; + ASPageToLayoutAttributesTable *_pageToLayoutAttributesTable; + ASPageToLayoutAttributesTable *_unmeasuredPageToLayoutAttributesTable; } - (instancetype)initWithContext:(ASCollectionLayoutContext *)context @@ -82,9 +84,9 @@ - (instancetype)initWithContext:(ASCollectionLayoutContext *)context _context = context; _contentSize = contentSize; _elementToLayoutAttributesTable = [table copy]; // Copy the given table to make sure it won't be mutate by clients after this point. - _pageToLayoutAttributesTable = [ASPageTable pageTableWithLayoutAttributes:_elementToLayoutAttributesTable.objectEnumerator contentSize:contentSize pageSize:context.viewportSize]; - // Assume that all elements are unmeasured, deep copy the original table - _unmeasuredPageToLayoutAttributesTable = [_pageToLayoutAttributesTable deepCopy]; + CGSize pageSize = context.viewportSize; + _pageToLayoutAttributesTable = [ASPageTable pageTableWithLayoutAttributes:table.objectEnumerator contentSize:contentSize pageSize:pageSize]; + _unmeasuredPageToLayoutAttributesTable = [ASCollectionLayoutState _unmeasuredLayoutAttributesTableFromTable:table contentSize:contentSize pageSize:pageSize]; } return self; } @@ -143,9 +145,9 @@ - (UICollectionViewLayoutAttributes *)layoutAttributesForElement:(ASCollectionEl return [result allObjects]; } -- (ASPageTable *> *)getAndRemoveUnmeasuredLayoutAttributesPageTableInRect:(CGRect)rect - contentSize:(CGSize)contentSize - pageSize:(CGSize)pageSize +- (ASPageToLayoutAttributesTable *)getAndRemoveUnmeasuredLayoutAttributesPageTableInRect:(CGRect)rect + contentSize:(CGSize)contentSize + pageSize:(CGSize)pageSize { ASDN::MutexLocker l(__instanceLock__); if (_unmeasuredPageToLayoutAttributesTable.count == 0 || CGRectIsNull(rect) || CGRectIsEmpty(rect) || CGSizeEqualToSize(CGSizeZero, contentSize) || CGSizeEqualToSize(CGSizeZero, pageSize)) { @@ -159,53 +161,62 @@ - (UICollectionViewLayoutAttributes *)layoutAttributesForElement:(ASCollectionEl } // Step 2: Filter out attributes in these pages that intersect the specified rect. - ASPageTable *result = [ASPageTable pageTableForStrongObjectPointers]; + ASPageToLayoutAttributesTable *result = [ASPageTable pageTableForStrongObjectPointers]; for (id pagePtr in pagesInRect) { ASPageCoordinate page = (ASPageCoordinate)pagePtr; - NSArray *attrsInPage = [_unmeasuredPageToLayoutAttributesTable objectForPage:page]; + NSMutableArray *attrsInPage = [_unmeasuredPageToLayoutAttributesTable objectForPage:page]; if (attrsInPage.count == 0) { - // Hm, this page should be removed before. + // Hm, this page should have been removed. [_unmeasuredPageToLayoutAttributesTable removeObjectForPage:page]; continue; } + NSMutableArray *intersectingAttrsInPage = nil; CGRect pageRect = ASPageCoordinateGetPageRect(page, pageSize); if (CGRectContainsRect(rect, pageRect)) { // This page fits well within the specified rect. Simply return all of its attributes. - [result setObject:attrsInPage forPage:page]; - [_unmeasuredPageToLayoutAttributesTable removeObjectForPage:page]; - continue; - } - - // The page intersects the specified rect. Some attributes in this page are returned, some are not. - NSMutableArray *intersectingAttrsInPage = nil; - NSMutableArray *unmeasuredAttrsInPage = nil; - BOOL hasIntersectingAttrs = NO; - - for (UICollectionViewLayoutAttributes *attrs in attrsInPage) { - if (CGRectIntersectsRect(rect, attrs.frame)) { - if (! hasIntersectingAttrs) { - intersectingAttrsInPage = [NSMutableArray array]; - unmeasuredAttrsInPage = [attrsInPage mutableCopy]; - hasIntersectingAttrs = YES; + intersectingAttrsInPage = attrsInPage; + } else { + // The page intersects the specified rect. Some attributes in this page are returned, some are not. + for (UICollectionViewLayoutAttributes *attrs in attrsInPage) { + if (CGRectIntersectsRect(rect, attrs.frame)) { + if (intersectingAttrsInPage == nil) { + intersectingAttrsInPage = [NSMutableArray array]; + } + [intersectingAttrsInPage addObject:attrs]; } - [intersectingAttrsInPage addObject:attrs]; - [unmeasuredAttrsInPage removeObject:attrs]; } } - if (hasIntersectingAttrs) { - [result setObject:intersectingAttrsInPage forPage:page]; - if (unmeasuredAttrsInPage.count == 0) { + if (intersectingAttrsInPage.count > 0) { + if (attrsInPage.count == intersectingAttrsInPage.count) { [_unmeasuredPageToLayoutAttributesTable removeObjectForPage:page]; } else { - [_unmeasuredPageToLayoutAttributesTable setObject:unmeasuredAttrsInPage forPage:page]; + [attrsInPage removeObjectsInArray:intersectingAttrsInPage]; } + [result setObject:intersectingAttrsInPage forPage:page]; } } return result; } +#pragma mark - Private methods + ++ (ASPageToLayoutAttributesTable *)_unmeasuredLayoutAttributesTableFromTable:(NSMapTable *)table + contentSize:(CGSize)contentSize + pageSize:(CGSize)pageSize +{ + NSMutableArray *unmeasuredAttrs = [NSMutableArray array]; + for (ASCollectionElement *element in table) { + UICollectionViewLayoutAttributes *attrs = [table objectForKey:element]; + if (element.nodeIfAllocated == nil || CGSizeEqualToSize(element.nodeIfAllocated.calculatedSize, attrs.frame.size) == NO) { + [unmeasuredAttrs addObject:attrs]; + } + } + + return [ASPageTable pageTableWithLayoutAttributes:unmeasuredAttrs contentSize:contentSize pageSize:pageSize]; +} + @end diff --git a/Source/Details/ASPageTable.h b/Source/Details/ASPageTable.h index d5f20f542..e295e0256 100644 --- a/Source/Details/ASPageTable.h +++ b/Source/Details/ASPageTable.h @@ -69,6 +69,11 @@ ASDISPLAYNODE_EXTERN_C_END */ typedef NSMapTable ASPageTable; +/** + * A page to array of layout attributes table. + */ +typedef ASPageTable *> ASPageToLayoutAttributesTable; + /** * A category for creating & using map tables meant for storing objects using ASPage as keys. */ @@ -93,7 +98,7 @@ typedef NSMapTable ASPageTable; * * @param pageSize The size of each page. */ -+ (ASPageTable *> *)pageTableWithLayoutAttributes:(id)layoutAttributesEnumerator contentSize:(CGSize)contentSize pageSize:(CGSize)pageSize; ++ (ASPageToLayoutAttributesTable *)pageTableWithLayoutAttributes:(id)layoutAttributesEnumerator contentSize:(CGSize)contentSize pageSize:(CGSize)pageSize; /** * Retrieves the object for a given page, or nil if the page is not found. @@ -118,11 +123,6 @@ typedef NSMapTable ASPageTable; */ - (void)removeObjectForPage:(ASPageCoordinate)page; -/** - * Creates a new page table by deep copying a given page table - */ -- (NSMapTable *)deepCopy; - @end NS_ASSUME_NONNULL_END diff --git a/Source/Details/ASPageTable.m b/Source/Details/ASPageTable.m index 4d0d755a7..5cbf758dd 100644 --- a/Source/Details/ASPageTable.m +++ b/Source/Details/ASPageTable.m @@ -110,9 +110,9 @@ + (ASPageTable *)pageTableForWeakObjectPointers return [self pageTableWithValuePointerFunctions:weakObjectPointerFuncs]; } -+ (ASPageTable *> *)pageTableWithLayoutAttributes:(id)layoutAttributesEnumerator contentSize:(CGSize)contentSize pageSize:(CGSize)pageSize ++ (ASPageToLayoutAttributesTable *)pageTableWithLayoutAttributes:(id)layoutAttributesEnumerator contentSize:(CGSize)contentSize pageSize:(CGSize)pageSize { - ASPageTable *result = [ASPageTable pageTableForStrongObjectPointers]; + ASPageToLayoutAttributesTable *result = [ASPageTable pageTableForStrongObjectPointers]; for (UICollectionViewLayoutAttributes *attrs in layoutAttributesEnumerator) { // This attrs may span multiple pages. Make sure it's registered to all of them NSPointerArray *pages = ASPageCoordinatesForPagesThatIntersectRect(attrs.frame, contentSize, pageSize); @@ -148,17 +148,4 @@ - (void)removeObjectForPage:(ASPageCoordinate)page [self removeObjectForKey:key]; } -- (NSMapTable *)deepCopy -{ - NSMapTable *result = [[NSMapTable alloc] initWithKeyPointerFunctions:self.keyPointerFunctions - valuePointerFunctions:self.valuePointerFunctions - capacity:self.count]; - - for (id key in self) { - [result setObject:[[self objectForKey:key] copy] forKey:key]; - } - - return result; -} - @end diff --git a/Source/Private/ASCollectionLayout.mm b/Source/Private/ASCollectionLayout.mm index a27ddf7aa..db2b83f03 100644 --- a/Source/Private/ASCollectionLayout.mm +++ b/Source/Private/ASCollectionLayout.mm @@ -153,7 +153,10 @@ - (CGSize)collectionViewContentSize } // Measure elements in the measure range, block on the requested rect - CGRect measureRect = CGRectExpandToRangeWithScrollableDirections(blockingRect, kASDefaultMeasureRangeTuningParameters, _layoutDelegate.scrollableDirections, kASStaticScrollDirection); + CGRect measureRect = CGRectExpandToRangeWithScrollableDirections(blockingRect, + kASDefaultMeasureRangeTuningParameters, + _layout.context.scrollableDirections, + kASStaticScrollDirection); [ASCollectionLayout _measureElementsInRect:measureRect blockingRect:blockingRect layout:_layout]; NSArray *result = [_layout layoutAttributesForElementsInRect:blockingRect]; @@ -247,9 +250,9 @@ + (void)_measureElementsInRect:(CGRect)rect blockingRect:(CGRect)blockingRect la // Step 2: Get layout attributes of all elements within the specified outer rect ASCollectionLayoutContext *context = layout.context; CGSize pageSize = context.viewportSize; - ASPageTable *attrsTable = [layout getAndRemoveUnmeasuredLayoutAttributesPageTableInRect:rect - contentSize:contentSize - pageSize:pageSize]; + ASPageToLayoutAttributesTable *attrsTable = [layout getAndRemoveUnmeasuredLayoutAttributesPageTableInRect:rect + contentSize:contentSize + pageSize:pageSize]; if (attrsTable.count == 0) { // No elements in this rect! Bail early return; diff --git a/Source/Private/ASCollectionLayoutState+Private.h b/Source/Private/ASCollectionLayoutState+Private.h index a9fbb1fad..170e57acc 100644 --- a/Source/Private/ASCollectionLayoutState+Private.h +++ b/Source/Private/ASCollectionLayoutState+Private.h @@ -22,9 +22,9 @@ NS_ASSUME_NONNULL_BEGIN * * @discussion This method is atomic and thread-safe */ -- (nullable ASPageTable *> *)getAndRemoveUnmeasuredLayoutAttributesPageTableInRect:(CGRect)rect - contentSize:(CGSize)contentSize - pageSize:(CGSize)pageSize; +- (nullable ASPageToLayoutAttributesTable *)getAndRemoveUnmeasuredLayoutAttributesPageTableInRect:(CGRect)rect + contentSize:(CGSize)contentSize + pageSize:(CGSize)pageSize; @end From e7b14825f29240a4c439f11c2d8a31c6c5de1aca Mon Sep 17 00:00:00 2001 From: Huy Nguyen Date: Thu, 13 Jul 2017 16:33:42 +0100 Subject: [PATCH 22/25] Add ASHashing to umbrella header --- Source/AsyncDisplayKit.h | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Source/AsyncDisplayKit.h b/Source/AsyncDisplayKit.h index 665f9b422..ed61ea4c5 100644 --- a/Source/AsyncDisplayKit.h +++ b/Source/AsyncDisplayKit.h @@ -99,17 +99,18 @@ #import #import #import +#import +#import #import #import #import #import -#import #import #import +#import #import #import #import -#import #import #import From 1a643cbb9eeac9b182415795640e15665470bb72 Mon Sep 17 00:00:00 2001 From: Huy Nguyen Date: Thu, 13 Jul 2017 16:48:11 +0100 Subject: [PATCH 23/25] Fix file licenses --- Source/Details/ASCollectionLayoutContext.m | 9 ++------- Source/Details/ASCollectionLayoutState.mm | 11 +++-------- 2 files changed, 5 insertions(+), 15 deletions(-) diff --git a/Source/Details/ASCollectionLayoutContext.m b/Source/Details/ASCollectionLayoutContext.m index 2ab3946f5..6620e391e 100644 --- a/Source/Details/ASCollectionLayoutContext.m +++ b/Source/Details/ASCollectionLayoutContext.m @@ -2,13 +2,8 @@ // ASCollectionLayoutContext.m // 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"); +// 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 // diff --git a/Source/Details/ASCollectionLayoutState.mm b/Source/Details/ASCollectionLayoutState.mm index a272bdcbd..952fa7767 100644 --- a/Source/Details/ASCollectionLayoutState.mm +++ b/Source/Details/ASCollectionLayoutState.mm @@ -1,14 +1,9 @@ // -// ASCollectionLayoutState.m +// ASCollectionLayoutState.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"); +// 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 // From 5950a9f95c9f327ad352275a41e0a2002ba896b8 Mon Sep 17 00:00:00 2001 From: Huy Nguyen Date: Thu, 13 Jul 2017 19:02:53 +0100 Subject: [PATCH 24/25] Add ASDispatchAsync and use it in ASCollectionLayout --- AsyncDisplayKit.xcodeproj/project.pbxproj | 4 ++ Source/Details/ASCollectionLayoutState.mm | 8 +-- Source/Private/ASCollectionLayout.mm | 37 +++++++------- Source/Private/ASDispatch.h | 36 ++++++-------- Source/Private/ASDispatch.m | 59 +++++++++++++++++++++++ Tests/ASDispatchTests.m | 38 ++++++++++++++- 6 files changed, 134 insertions(+), 48 deletions(-) create mode 100644 Source/Private/ASDispatch.m diff --git a/AsyncDisplayKit.xcodeproj/project.pbxproj b/AsyncDisplayKit.xcodeproj/project.pbxproj index 65243e515..edcbb825e 100644 --- a/AsyncDisplayKit.xcodeproj/project.pbxproj +++ b/AsyncDisplayKit.xcodeproj/project.pbxproj @@ -450,6 +450,7 @@ E5B078001E69F4EB00C24B5B /* ASElementMap.m in Sources */ = {isa = PBXBuildFile; fileRef = E5B077FE1E69F4EB00C24B5B /* ASElementMap.m */; }; E5B225281F1790D6001E1431 /* ASHashing.h in Headers */ = {isa = PBXBuildFile; fileRef = E5B225271F1790B5001E1431 /* ASHashing.h */; settings = {ATTRIBUTES = (Public, ); }; }; E5B225291F1790EE001E1431 /* ASHashing.m in Sources */ = {isa = PBXBuildFile; fileRef = E5B225261F1790B5001E1431 /* ASHashing.m */; }; + E5B2252E1F17E521001E1431 /* ASDispatch.m in Sources */ = {isa = PBXBuildFile; fileRef = E5B2252D1F17E521001E1431 /* ASDispatch.m */; }; E5B5B9D11E9BAD9800A6B726 /* ASCollectionLayoutContext+Private.h in Headers */ = {isa = PBXBuildFile; fileRef = E5B5B9D01E9BAD9800A6B726 /* ASCollectionLayoutContext+Private.h */; settings = {ATTRIBUTES = (Private, ); }; }; E5C347B11ECB3D9200EC4BE4 /* ASBatchFetchingDelegate.h in Headers */ = {isa = PBXBuildFile; fileRef = E5C347B01ECB3D9200EC4BE4 /* ASBatchFetchingDelegate.h */; }; E5C347B31ECB40AA00EC4BE4 /* ASTableNode+Beta.h in Headers */ = {isa = PBXBuildFile; fileRef = E5C347B21ECB40AA00EC4BE4 /* ASTableNode+Beta.h */; }; @@ -936,6 +937,7 @@ E5B077FE1E69F4EB00C24B5B /* ASElementMap.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASElementMap.m; sourceTree = ""; }; E5B225261F1790B5001E1431 /* ASHashing.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ASHashing.m; sourceTree = ""; }; E5B225271F1790B5001E1431 /* ASHashing.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ASHashing.h; sourceTree = ""; }; + E5B2252D1F17E521001E1431 /* ASDispatch.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ASDispatch.m; sourceTree = ""; }; E5B5B9D01E9BAD9800A6B726 /* ASCollectionLayoutContext+Private.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "ASCollectionLayoutContext+Private.h"; sourceTree = ""; }; E5C347B01ECB3D9200EC4BE4 /* ASBatchFetchingDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASBatchFetchingDelegate.h; sourceTree = ""; }; E5C347B21ECB40AA00EC4BE4 /* ASTableNode+Beta.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "ASTableNode+Beta.h"; sourceTree = ""; }; @@ -1378,6 +1380,7 @@ AEB7B0181C5962EA00662EF4 /* ASDefaultPlayButton.h */, AEB7B0191C5962EA00662EF4 /* ASDefaultPlayButton.m */, CC54A81B1D70077A00296A24 /* ASDispatch.h */, + E5B2252D1F17E521001E1431 /* ASDispatch.m */, 058D0A08195D050800B7D73C /* ASDisplayNode+AsyncDisplay.mm */, 058D0A09195D050800B7D73C /* ASDisplayNode+DebugTiming.h */, 058D0A0A195D050800B7D73C /* ASDisplayNode+DebugTiming.mm */, @@ -2309,6 +2312,7 @@ E54E81FD1EB357BD00FFE8E1 /* ASPageTable.m in Sources */, 34EFC7721B701D0300AD841F /* ASStackLayoutSpec.mm in Sources */, 7AB338661C55B3420055FDE8 /* ASRelativeLayoutSpec.mm in Sources */, + E5B2252E1F17E521001E1431 /* ASDispatch.m in Sources */, 696F01EE1DD2AF450049FBD5 /* ASEventLog.mm in Sources */, 9C70F2051CDA4F06007D6C76 /* ASTraitCollection.m in Sources */, 83A7D95B1D44547700BF333E /* ASWeakMap.m in Sources */, diff --git a/Source/Details/ASCollectionLayoutState.mm b/Source/Details/ASCollectionLayoutState.mm index 952fa7767..3dfc27b8d 100644 --- a/Source/Details/ASCollectionLayoutState.mm +++ b/Source/Details/ASCollectionLayoutState.mm @@ -156,14 +156,11 @@ - (ASPageToLayoutAttributesTable *)getAndRemoveUnmeasuredLayoutAttributesPageTab } // Step 2: Filter out attributes in these pages that intersect the specified rect. - ASPageToLayoutAttributesTable *result = [ASPageTable pageTableForStrongObjectPointers]; + ASPageToLayoutAttributesTable *result = nil; for (id pagePtr in pagesInRect) { ASPageCoordinate page = (ASPageCoordinate)pagePtr; NSMutableArray *attrsInPage = [_unmeasuredPageToLayoutAttributesTable objectForPage:page]; - if (attrsInPage.count == 0) { - // Hm, this page should have been removed. - [_unmeasuredPageToLayoutAttributesTable removeObjectForPage:page]; continue; } @@ -190,6 +187,9 @@ - (ASPageToLayoutAttributesTable *)getAndRemoveUnmeasuredLayoutAttributesPageTab } else { [attrsInPage removeObjectsInArray:intersectingAttrsInPage]; } + if (result == nil) { + result = [ASPageTable pageTableForStrongObjectPointers]; + } [result setObject:intersectingAttrsInPage forPage:page]; } } diff --git a/Source/Private/ASCollectionLayout.mm b/Source/Private/ASCollectionLayout.mm index db2b83f03..935a3d680 100644 --- a/Source/Private/ASCollectionLayout.mm +++ b/Source/Private/ASCollectionLayout.mm @@ -259,10 +259,9 @@ + (void)_measureElementsInRect:(CGRect)rect blockingRect:(CGRect)blockingRect la } // Step 3: Split all those attributes into blocking and non-blocking buckets - // Use an ordered set here because some items may span multiple pages and they will be accessed by indexes. + // Use ordered sets here because some items may span multiple pages, and the sets will be accessed by indexes later on. NSMutableOrderedSet *blockingAttrs = hasBlockingRect ? [NSMutableOrderedSet orderedSet] : nil; - // Use a set here because some items may span multiple pages - NSMutableSet *nonBlockingAttrs = [NSMutableSet set]; + NSMutableOrderedSet *nonBlockingAttrs = [NSMutableOrderedSet orderedSet]; for (id pagePtr in attrsTable) { ASPageCoordinate page = (ASPageCoordinate)pagePtr; NSArray *attrsInPage = [attrsTable objectForPage:page]; @@ -290,30 +289,28 @@ + (void)_measureElementsInRect:(CGRect)rect blockingRect:(CGRect)blockingRect la // 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); - NSUInteger count = blockingAttrs.count; - if (count > 0) { + if (NSUInteger count = blockingAttrs.count) { ASDispatchApply(count, queue, 0, ^(size_t i) { UICollectionViewLayoutAttributes *attrs = blockingAttrs[i]; - CGSize elementSize = attrs.frame.size; - ASCollectionElement *element = [elements elementForItemAtIndexPath:attrs.indexPath]; - ASCellNode *node = element.node; - if (! CGSizeEqualToSize(elementSize, node.calculatedSize)) { - [node layoutThatFits:ASCollectionLayoutElementSizeRangeFromSize(elementSize)]; + 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 - // TODO Limit the number of threads - for (UICollectionViewLayoutAttributes *attrs in nonBlockingAttrs) { - CGSize elementSize = attrs.frame.size; - __weak ASCollectionElement *weakElement = [elements elementForItemAtIndexPath:attrs.indexPath]; - dispatch_async(queue, ^{ - __strong ASCollectionElement *strongElement = weakElement; - if (strongElement) { - ASCellNode *node = strongElement.node; - if (! CGSizeEqualToSize(elementSize, node.calculatedSize)) { - [node layoutThatFits:ASCollectionLayoutElementSizeRangeFromSize(elementSize)]; + 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)]; } } }); diff --git a/Source/Private/ASDispatch.h b/Source/Private/ASDispatch.h index c0a14c77d..4f2a1cda9 100644 --- a/Source/Private/ASDispatch.h +++ b/Source/Private/ASDispatch.h @@ -16,7 +16,9 @@ // #import -#import +#import + +ASDISPLAYNODE_EXTERN_C_BEGIN /** * Like dispatch_apply, but you can set the thread count. 0 means 2*active CPUs. @@ -24,24 +26,14 @@ * Note: The actual number of threads may be lower than threadCount, if libdispatch * decides the system can't handle it. In reality this rarely happens. */ -static void ASDispatchApply(size_t iterationCount, dispatch_queue_t queue, NSUInteger threadCount, NS_NOESCAPE void(^work)(size_t i)) { - if (threadCount == 0) { - threadCount = NSProcessInfo.processInfo.activeProcessorCount * 2; - } - dispatch_group_t group = dispatch_group_create(); - // HACK: This is a workaround for mm files that include this in Clang4.0 - // Omitting ATOMIC_VAR_INIT is okay in this case because the current - // expansion of that macro no-ops. - // TODO: Move this implementation into a m file so it's not compiled in C++ - // See: https://github.com/TextureGroup/Texture/pull/426 - __block atomic_size_t counter = 0; - for (NSUInteger t = 0; t < threadCount; t++) { - dispatch_group_async(group, queue, ^{ - size_t i; - while ((i = atomic_fetch_add(&counter, 1)) < iterationCount) { - work(i); - } - }); - } - dispatch_group_wait(group, DISPATCH_TIME_FOREVER); -}; +void ASDispatchApply(size_t iterationCount, dispatch_queue_t queue, NSUInteger threadCount, NS_NOESCAPE void(^work)(size_t i)); + +/** + * Like dispatch_async, but you can set the thread count. 0 means 2*active CPUs. + * + * Note: The actual number of threads may be lower than threadCount, if libdispatch + * decides the system can't handle it. In reality this rarely happens. + */ +void ASDispatchAsync(size_t iterationCount, dispatch_queue_t queue, NSUInteger threadCount, NS_NOESCAPE void(^work)(size_t i)); + +ASDISPLAYNODE_EXTERN_C_END diff --git a/Source/Private/ASDispatch.m b/Source/Private/ASDispatch.m new file mode 100644 index 000000000..14c60eb6d --- /dev/null +++ b/Source/Private/ASDispatch.m @@ -0,0 +1,59 @@ +// +// ASDispatch.m +// 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 + +/** + * Like dispatch_apply, but you can set the thread count. 0 means 2*active CPUs. + * + * Note: The actual number of threads may be lower than threadCount, if libdispatch + * decides the system can't handle it. In reality this rarely happens. + */ +void ASDispatchApply(size_t iterationCount, dispatch_queue_t queue, NSUInteger threadCount, NS_NOESCAPE void(^work)(size_t i)) { + if (threadCount == 0) { + threadCount = NSProcessInfo.processInfo.activeProcessorCount * 2; + } + dispatch_group_t group = dispatch_group_create(); + __block atomic_size_t counter = ATOMIC_VAR_INIT(0); + for (NSUInteger t = 0; t < threadCount; t++) { + dispatch_group_async(group, queue, ^{ + size_t i; + while ((i = atomic_fetch_add(&counter, 1)) < iterationCount) { + work(i); + } + }); + } + dispatch_group_wait(group, DISPATCH_TIME_FOREVER); +}; + +/** + * Like dispatch_async, but you can set the thread count. 0 means 2*active CPUs. + * + * Note: The actual number of threads may be lower than threadCount, if libdispatch + * decides the system can't handle it. In reality this rarely happens. + */ +void ASDispatchAsync(size_t iterationCount, dispatch_queue_t queue, NSUInteger threadCount, NS_NOESCAPE void(^work)(size_t i)) { + if (threadCount == 0) { + threadCount = NSProcessInfo.processInfo.activeProcessorCount * 2; + } + __block atomic_size_t counter = ATOMIC_VAR_INIT(0); + for (NSUInteger t = 0; t < threadCount; t++) { + dispatch_async(queue, ^{ + size_t i; + while ((i = atomic_fetch_add(&counter, 1)) < iterationCount) { + work(i); + } + }); + } +}; + diff --git a/Tests/ASDispatchTests.m b/Tests/ASDispatchTests.m index ba834b672..f159f5e9d 100644 --- a/Tests/ASDispatchTests.m +++ b/Tests/ASDispatchTests.m @@ -2,8 +2,17 @@ // ASDispatchTests.m // Texture // -// Created by Adlai Holler on 8/25/16. -// Copyright © 2016 Facebook. All rights reserved. +// 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 @@ -35,4 +44,29 @@ - (void)testDispatchApply XCTAssertEqualObjects(indices, [NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, iterations)]); } +- (void)testDispatchAsync +{ + dispatch_queue_t q = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); + NSInteger expectedThreadCount = [NSProcessInfo processInfo].activeProcessorCount * 2; + NSLock *lock = [NSLock new]; + NSMutableSet *threads = [NSMutableSet set]; + NSMutableIndexSet *indices = [NSMutableIndexSet indexSet]; + XCTestExpectation *expectation = [self expectationWithDescription:@"Executed all blocks"]; + + size_t const iterations = 1E5; + ASDispatchAsync(iterations, q, 0, ^(size_t i) { + [lock lock]; + [threads addObject:[NSThread currentThread]]; + XCTAssertFalse([indices containsIndex:i]); + [indices addIndex:i]; + if (indices.count == iterations) { + [expectation fulfill]; + } + [lock unlock]; + }); + [self waitForExpectationsWithTimeout:10 handler:nil]; + XCTAssertLessThanOrEqual(threads.count, expectedThreadCount); + XCTAssertEqualObjects(indices, [NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, iterations)]); +} + @end From bbca3f2d91e2219657a5b26ad199b3dc88672818 Mon Sep 17 00:00:00 2001 From: Huy Nguyen Date: Fri, 14 Jul 2017 19:21:45 +0100 Subject: [PATCH 25/25] Improve code comment in ASCollectionLayoutState --- Source/Details/ASCollectionLayoutState.mm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/Details/ASCollectionLayoutState.mm b/Source/Details/ASCollectionLayoutState.mm index 3dfc27b8d..0b2616a22 100644 --- a/Source/Details/ASCollectionLayoutState.mm +++ b/Source/Details/ASCollectionLayoutState.mm @@ -78,7 +78,7 @@ - (instancetype)initWithContext:(ASCollectionLayoutContext *)context if (self) { _context = context; _contentSize = contentSize; - _elementToLayoutAttributesTable = [table copy]; // Copy the given table to make sure it won't be mutate by clients after this point. + _elementToLayoutAttributesTable = [table copy]; // Copy the given table to make sure clients can't mutate it after this point. CGSize pageSize = context.viewportSize; _pageToLayoutAttributesTable = [ASPageTable pageTableWithLayoutAttributes:table.objectEnumerator contentSize:contentSize pageSize:pageSize]; _unmeasuredPageToLayoutAttributesTable = [ASCollectionLayoutState _unmeasuredLayoutAttributesTableFromTable:table contentSize:contentSize pageSize:pageSize];