From 14f8178f280ebb0c0790f3fc7151349c7f32d730 Mon Sep 17 00:00:00 2001 From: Huy Nguyen Date: Fri, 14 Jul 2017 18:50:26 +0000 Subject: [PATCH] Introduce ASCollectionGalleryLayoutDelegate (#76) * 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. * Handle items that span multiple pages and other improvements in gallery delegate * Minor fixes * Fix failing tests * Fix custom collection example * Implement missing method in gallery layout delegate * Fix warnings * 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 * Fix file licenses * Move measure range logic to ASCollectionLayout * Track unmeasured elements * Remove pending layout in ASCollectionLayout * Get back pending layout because the timing to latch new data is not ideal * Add ASCollectionLayoutCache * Fix file licenses * Fix xcodeproj * Add async collection layout to examples/ASCollectionView * Measure method in ASCollectionLayout to be a class method * 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. * Remove additionalInfo property in ASCollectionLayoutState * ASCollectionLayoutState to correctly filter unmeasured elements * Add ASHashing to umbrella header * Fix file licenses * Add ASDispatchAsync and use it in ASCollectionLayout * Improve code comment in ASCollectionLayoutState --- AsyncDisplayKit.xcodeproj/project.pbxproj | 90 ++++-- CHANGELOG.md | 1 + Source/ASCollectionNode.mm | 1 + Source/AsyncDisplayKit.h | 6 +- .../Details/ASCollectionFlowLayoutDelegate.h | 1 - .../Details/ASCollectionFlowLayoutDelegate.m | 27 +- .../ASCollectionGalleryLayoutDelegate.h | 32 +++ .../ASCollectionGalleryLayoutDelegate.m | 86 ++++++ Source/Details/ASCollectionLayoutContext.h | 6 +- Source/Details/ASCollectionLayoutContext.m | 107 +++++++ Source/Details/ASCollectionLayoutContext.mm | 72 ----- Source/Details/ASCollectionLayoutDelegate.h | 23 +- Source/Details/ASCollectionLayoutState.h | 20 +- Source/Details/ASCollectionLayoutState.m | 135 --------- Source/Details/ASCollectionLayoutState.mm | 217 ++++++++++++++ Source/Details/ASDataController.h | 18 +- Source/Details/ASDataController.mm | 90 ++---- Source/{Private => Details}/ASHashing.h | 0 Source/{Private => Details}/ASHashing.m | 0 Source/Details/ASPageTable.h | 7 +- Source/Details/ASPageTable.m | 4 +- Source/Private/ASCollectionLayout.h | 10 +- Source/Private/ASCollectionLayout.mm | 270 ++++++++++++++---- Source/Private/ASCollectionLayoutCache.h | 34 +++ Source/Private/ASCollectionLayoutCache.mm | 90 ++++++ .../ASCollectionLayoutContext+Private.h | 13 +- Source/Private/ASCollectionLayoutDefines.h | 27 ++ Source/Private/ASCollectionLayoutDefines.m | 27 ++ .../Private/ASCollectionLayoutState+Private.h | 31 ++ Source/Private/ASDispatch.h | 36 +-- Source/Private/ASDispatch.m | 59 ++++ .../Private/_ASCollectionGalleryLayoutItem.h | 38 +++ .../Private/_ASCollectionGalleryLayoutItem.mm | 86 ++++++ Tests/ASDispatchTests.m | 38 ++- .../contents.xcworkspacedata | 10 + .../ASCollectionView/Sample/ViewController.m | 32 ++- .../Sample.xcodeproj/project.pbxproj | 11 + .../Sample/MosaicCollectionLayoutDelegate.m | 78 ++--- .../Sample/MosaicCollectionLayoutInfo.h | 32 +++ .../Sample/MosaicCollectionLayoutInfo.m | 78 +++++ 40 files changed, 1477 insertions(+), 466 deletions(-) create mode 100644 Source/Details/ASCollectionGalleryLayoutDelegate.h create mode 100644 Source/Details/ASCollectionGalleryLayoutDelegate.m create mode 100644 Source/Details/ASCollectionLayoutContext.m delete mode 100644 Source/Details/ASCollectionLayoutContext.mm delete mode 100644 Source/Details/ASCollectionLayoutState.m create mode 100644 Source/Details/ASCollectionLayoutState.mm rename Source/{Private => Details}/ASHashing.h (100%) rename Source/{Private => Details}/ASHashing.m (100%) create mode 100644 Source/Private/ASCollectionLayoutCache.h create mode 100644 Source/Private/ASCollectionLayoutCache.mm create mode 100644 Source/Private/ASCollectionLayoutDefines.h create mode 100644 Source/Private/ASCollectionLayoutDefines.m create mode 100644 Source/Private/ASCollectionLayoutState+Private.h create mode 100644 Source/Private/ASDispatch.m create mode 100644 Source/Private/_ASCollectionGalleryLayoutItem.h create mode 100644 Source/Private/_ASCollectionGalleryLayoutItem.mm 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 58ac2eec4..edcbb825e 100644 --- a/AsyncDisplayKit.xcodeproj/project.pbxproj +++ b/AsyncDisplayKit.xcodeproj/project.pbxproj @@ -424,18 +424,23 @@ 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 */; }; 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 */; 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 */; 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, ); }; }; - 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 */; }; @@ -443,11 +448,16 @@ 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 */; }; + 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 */; }; 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 */; }; /* End PBXBuildFile section */ @@ -899,8 +909,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 = ""; }; @@ -909,10 +917,17 @@ 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 = ""; }; + 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 = ""; }; 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 = ""; }; @@ -920,11 +935,16 @@ 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 = ""; }; + 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 = ""; }; 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; }; 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 = ""; }; @@ -1242,6 +1262,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 */, @@ -1334,6 +1356,7 @@ CC55A7101E52A0F200594372 /* ASResponderChainEnumerator.m */, 6947B0BB1E36B4E30007C478 /* Layout */, CCE04B2A1E313EDA006AEBBB /* Collection Data Adapter */, + E52F8AEE1EAE659600B5A912 /* Collection Layout */, 058D0A03195D050800B7D73C /* _ASCoreAnimationExtras.h */, 058D0A04195D050800B7D73C /* _ASCoreAnimationExtras.mm */, AC026B6D1BD57DBF00BBC17E /* _ASHierarchyChangeSet.h */, @@ -1347,9 +1370,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 */, @@ -1360,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 */, @@ -1369,8 +1390,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 */, @@ -1637,16 +1656,35 @@ path = Debug; sourceTree = ""; }; + E52F8AEE1EAE659600B5A912 /* Collection Layout */ = { + isa = PBXGroup; + children = ( + E58E9E471E941DA5004CFC59 /* ASCollectionLayout.h */, + E58E9E481E941DA5004CFC59 /* ASCollectionLayout.mm */, + E5775B011F16759300CAC9BC /* ASCollectionLayoutCache.h */, + E5775B031F16759F00CAC9BC /* ASCollectionLayoutCache.mm */, + E5B5B9D01E9BAD9800A6B726 /* ASCollectionLayoutContext+Private.h */, + E5855DEE1EBB4D83003639AE /* ASCollectionLayoutDefines.h */, + E5855DED1EBB4D83003639AE /* ASCollectionLayoutDefines.m */, + E5775AFF1F13D25400CAC9BC /* ASCollectionLayoutState+Private.h */, + E5775AFB1F13CE9F00CAC9BC /* _ASCollectionGalleryLayoutItem.h */, + E5775AFD1F13CF7400CAC9BC /* _ASCollectionGalleryLayoutItem.mm */, + ); + name = "Collection Layout"; + sourceTree = ""; + }; E5B077EB1E6843AF00C24B5B /* Collection Layout */ = { isa = PBXGroup; children = ( E58E9E3F1E941D74004CFC59 /* ASCollectionLayoutContext.h */, - E58E9E401E941D74004CFC59 /* ASCollectionLayoutContext.mm */, + E58E9E401E941D74004CFC59 /* ASCollectionLayoutContext.m */, E5E281731E71C833006B67C2 /* ASCollectionLayoutState.h */, - E5E281751E71C845006B67C2 /* ASCollectionLayoutState.m */, + E5E281751E71C845006B67C2 /* ASCollectionLayoutState.mm */, E58E9E411E941D74004CFC59 /* ASCollectionLayoutDelegate.h */, E58E9E3D1E941D74004CFC59 /* ASCollectionFlowLayoutDelegate.h */, E58E9E3E1E941D74004CFC59 /* ASCollectionFlowLayoutDelegate.m */, + E5E2D72D1EA780C4005C24C6 /* ASCollectionGalleryLayoutDelegate.h */, + E5E2D72F1EA780DF005C24C6 /* ASCollectionGalleryLayoutDelegate.m */, E54E81FA1EB357BD00FFE8E1 /* ASPageTable.h */, E54E81FB1EB357BD00FFE8E1 /* ASPageTable.m */, ); @@ -1670,8 +1708,10 @@ 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 */, E58E9E461E941D74004CFC59 /* ASCollectionLayoutDelegate.h in Headers */, E5E281741E71C833006B67C2 /* ASCollectionLayoutState.h in Headers */, E5B077FF1E69F4EB00C24B5B /* ASElementMap.h in Headers */, @@ -1778,11 +1818,14 @@ 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 */, 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 */, @@ -1798,7 +1841,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 */, @@ -1850,6 +1892,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 */, @@ -2140,6 +2183,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 */, @@ -2154,7 +2198,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 */, @@ -2183,13 +2226,14 @@ 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 */, 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 */, @@ -2211,7 +2255,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 */, @@ -2226,16 +2270,19 @@ 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 */, 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.m in Sources */, 34EFC76B1B701CEB00AD841F /* ASLayoutSpec.mm in Sources */, CC3B20861C3F76D600798563 /* ASPendingStateController.mm in Sources */, 254C6B8C1BF94F8A003EC431 /* ASTextKitTailTruncater.mm in Sources */, @@ -2265,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/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/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/AsyncDisplayKit.h b/Source/AsyncDisplayKit.h index 2c319b0f2..ed61ea4c5 100644 --- a/Source/AsyncDisplayKit.h +++ b/Source/AsyncDisplayKit.h @@ -52,6 +52,7 @@ #import #import #import +#import #import #import @@ -98,17 +99,18 @@ #import #import #import +#import +#import #import #import #import #import -#import #import #import +#import #import #import #import -#import #import #import 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 009ba2123..855f3791c 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,18 +44,9 @@ - (instancetype)initWithScrollableDirections:(ASScrollDirection)scrollableDirect return self; } -- (ASSizeRange)sizeRangeThatFits:(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; + return _scrollableDirections; } - (id)additionalInfoForLayoutWithElements:(ASElementMap *)elements @@ -62,7 +54,7 @@ - (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); @@ -80,8 +72,13 @@ - (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]; + + ASSizeRange sizeRange = ASSizeRangeForCollectionLayoutThatFitsViewportSize(context.viewportSize, context.scrollableDirections); + ASLayout *layout = [stackSpec layoutThatFits:sizeRange]; + + return [[ASCollectionLayoutState alloc] initWithContext:context layout:layout 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.m b/Source/Details/ASCollectionGalleryLayoutDelegate.m new file mode 100644 index 000000000..0c98f0fbf --- /dev/null +++ b/Source/Details/ASCollectionGalleryLayoutDelegate.m @@ -0,0 +1,86 @@ +// +// ASCollectionGalleryLayoutDelegate.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 +#import +#import +#import +#import +#import +#import + +#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; +} + +- (ASScrollDirection)scrollableDirections +{ + return _scrollableDirections; +} + +- (id)additionalInfoForLayoutWithElements:(ASElementMap *)elements +{ + return [NSValue valueWithCGSize:_itemSize]; +} + ++ (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]); + if (children.count == 0) { + return [[ASCollectionLayoutState alloc] initWithContext:context + contentSize:CGSizeZero + elementToLayoutAttributesTable:[NSMapTable weakToStrongObjectsMapTable]]; + } + + // 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]; + stackSpec.concurrent = YES; + ASLayout *layout = [stackSpec layoutThatFits:ASSizeRangeForCollectionLayoutThatFitsViewportSize(pageSize, scrollableDirections)]; + + return [[ASCollectionLayoutState alloc] initWithContext:context layout:layout getElementBlock:^ASCollectionElement *(ASLayout *sublayout) { + return ((_ASGalleryLayoutItem *)sublayout.layoutElement).collectionElement; + }]; +} + +@end diff --git a/Source/Details/ASCollectionLayoutContext.h b/Source/Details/ASCollectionLayoutContext.h index 60c7c3929..a20873a81 100644 --- a/Source/Details/ASCollectionLayoutContext.h +++ b/Source/Details/ASCollectionLayoutContext.h @@ -15,20 +15,20 @@ // http://www.apache.org/licenses/LICENSE-2.0 // -#import #import #import +#import @class ASElementMap; NS_ASSUME_NONNULL_BEGIN AS_SUBCLASSING_RESTRICTED - @interface ASCollectionLayoutContext : NSObject @property (nonatomic, assign, readonly) CGSize viewportSize; -@property (nonatomic, strong, readonly) ASElementMap *elements; +@property (nonatomic, assign, readonly) ASScrollDirection scrollableDirections; +@property (nonatomic, weak, readonly) ASElementMap *elements; @property (nonatomic, strong, readonly, nullable) id additionalInfo; - (instancetype)init __unavailable; diff --git a/Source/Details/ASCollectionLayoutContext.m b/Source/Details/ASCollectionLayoutContext.m new file mode 100644 index 000000000..6620e391e --- /dev/null +++ b/Source/Details/ASCollectionLayoutContext.m @@ -0,0 +1,107 @@ +// +// ASCollectionLayoutContext.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 +#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 7fd17467a..9ef3da874 100644 --- a/Source/Details/ASCollectionLayoutDelegate.h +++ b/Source/Details/ASCollectionLayoutDelegate.h @@ -17,6 +17,7 @@ #import #import +#import @class ASElementMap, ASCollectionLayoutContext, ASCollectionLayoutState; @@ -25,13 +26,22 @@ NS_ASSUME_NONNULL_BEGIN @protocol ASCollectionLayoutDelegate /** - * @abstract Returns any additional information needed for a coming layout pass with the given elements. + * @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. * * @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 will be available in the context parameter in +calculateLayoutWithContext: * - * @discussion This method will be called on main thread. + * This method will be called on main thread. */ - (nullable id)additionalInfoForLayoutWithElements:(ASElementMap *)elements; @@ -43,13 +53,12 @@ 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; ++ (ASCollectionLayoutState *)calculateLayoutWithContext:(ASCollectionLayoutContext *)context; @end diff --git a/Source/Details/ASCollectionLayoutState.h b/Source/Details/ASCollectionLayoutState.h index e70b951aa..1804c58a3 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 @@ -47,20 +49,25 @@ 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 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 elementToLayoutAttributesTable:(NSMapTable *)table NS_DESIGNATED_INITIALIZER; +- (instancetype)initWithContext:(ASCollectionLayoutContext *)context + contentSize:(CGSize)contentSize + elementToLayoutAttributesTable:(NSMapTable *)table NS_DESIGNATED_INITIALIZER; /** * Convenience initializer. * * @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 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 + getElementBlock:(ASCollectionElement *(^)(ASLayout *))getElementBlock; /** * Returns all layout attributes present in this object. @@ -88,7 +95,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 deleted file mode 100644 index ecb7e1503..000000000 --- a/Source/Details/ASCollectionLayoutState.m +++ /dev/null @@ -1,135 +0,0 @@ -// -// ASCollectionLayoutState.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 NSMapTable (ASCollectionLayoutConvenience) - -+ (NSMapTable *)elementToLayoutAttributesTable -{ - return [NSMapTable mapTableWithKeyOptions:(NSMapTableWeakMemory | NSMapTableObjectPointerPersonality) valueOptions:NSMapTableStrongMemory]; -} - -@end - -@implementation ASCollectionLayoutState { - NSMapTable *_elementToLayoutAttributesTable; - ASPageTable *> *_pageToLayoutAttributesTable; -} - -- (instancetype)initWithContext:(ASCollectionLayoutContext *)context layout:(ASLayout *)layout -{ - ASElementMap *elements = context.elements; - NSMapTable *table = [NSMapTable elementToLayoutAttributesTable]; - - for (ASLayout *sublayout in layout.sublayouts) { - ASCollectionElement *element = ((ASCellNode *)sublayout.layoutElement).collectionElement; - if (element == nil) { - ASDisplayNodeFailAssert(@"Element not found!"); - continue; - } - - NSIndexPath *indexPath = [elements indexPathForElement:element]; - NSString *supplementaryElementKind = element.supplementaryElementKind; - - UICollectionViewLayoutAttributes *attrs; - if (supplementaryElementKind == nil) { - attrs = [UICollectionViewLayoutAttributes layoutAttributesForCellWithIndexPath:indexPath]; - } else { - attrs = [UICollectionViewLayoutAttributes layoutAttributesForSupplementaryViewOfKind:supplementaryElementKind withIndexPath:indexPath]; - } - - attrs.frame = sublayout.frame; - [table setObject:attrs forKey:element]; - } - - return [self initWithContext:context contentSize:layout.size elementToLayoutAttributesTable:table]; -} - -- (instancetype)initWithContext:(ASCollectionLayoutContext *)context contentSize:(CGSize)contentSize elementToLayoutAttributesTable:(NSMapTable *)table -{ - self = [super init]; - if (self) { - _context = context; - _contentSize = contentSize; - _elementToLayoutAttributesTable = table; - _pageToLayoutAttributesTable = [ASPageTable pageTableWithLayoutAttributes:_elementToLayoutAttributesTable.objectEnumerator contentSize:contentSize pageSize:context.viewportSize]; - } - return self; -} - -- (NSArray *)allLayoutAttributes -{ - return [_elementToLayoutAttributesTable.objectEnumerator allObjects]; -} - -- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect -{ - CGSize pageSize = _context.viewportSize; - NSPointerArray *pages = ASPageCoordinatesForPagesThatIntersectRect(rect, _contentSize, pageSize); - if (pages.count == 0) { - return @[]; - } - - // Use a mutable set here because some items may span multiple pages - NSMutableSet *result = [NSMutableSet set]; - for (id pagePtr in pages) { - ASPageCoordinate page = (ASPageCoordinate)pagePtr; - NSArray *allAttrs = [_pageToLayoutAttributesTable objectForPage:page]; - if (allAttrs.count > 0) { - CGRect pageRect = ASPageCoordinateGetPageRect(page, pageSize); - - if (CGRectContainsRect(rect, pageRect)) { - [result addObjectsFromArray:allAttrs]; - } else { - for (UICollectionViewLayoutAttributes *attrs in allAttrs) { - if (CGRectIntersectsRect(rect, attrs.frame)) { - [result addObject:attrs]; - } - } - } - } - } - return [result allObjects]; -} - -- (UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath -{ - ASCollectionElement *element = [_context.elements elementForItemAtIndexPath:indexPath]; - return [_elementToLayoutAttributesTable objectForKey:element]; -} - -- (UICollectionViewLayoutAttributes *)layoutAttributesForSupplementaryElementOfKind:(NSString *)elementKind atIndexPath:(NSIndexPath *)indexPath -{ - ASCollectionElement *element = [_context.elements supplementaryElementOfKind:elementKind atIndexPath:indexPath]; - return [_elementToLayoutAttributesTable objectForKey:element]; -} - -- (UICollectionViewLayoutAttributes *)layoutAttributesForElement:(ASCollectionElement *)element -{ - return [_elementToLayoutAttributesTable objectForKey:element]; -} - -@end diff --git a/Source/Details/ASCollectionLayoutState.mm b/Source/Details/ASCollectionLayoutState.mm new file mode 100644 index 000000000..0b2616a22 --- /dev/null +++ b/Source/Details/ASCollectionLayoutState.mm @@ -0,0 +1,217 @@ +// +// ASCollectionLayoutState.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 + +@implementation NSMapTable (ASCollectionLayoutConvenience) + ++ (NSMapTable *)elementToLayoutAttributesTable +{ + return [NSMapTable mapTableWithKeyOptions:(NSMapTableWeakMemory | NSMapTableObjectPointerPersonality) valueOptions:NSMapTableStrongMemory]; +} + +@end + +@implementation ASCollectionLayoutState { + ASDN::Mutex __instanceLock__; + NSMapTable *_elementToLayoutAttributesTable; + ASPageToLayoutAttributesTable *_pageToLayoutAttributesTable; + ASPageToLayoutAttributesTable *_unmeasuredPageToLayoutAttributesTable; +} + +- (instancetype)initWithContext:(ASCollectionLayoutContext *)context + layout:(ASLayout *)layout + getElementBlock:(ASCollectionElement *(^)(ASLayout *))getElementBlock +{ + ASElementMap *elements = context.elements; + NSMapTable *table = [NSMapTable elementToLayoutAttributesTable]; + + for (ASLayout *sublayout in layout.sublayouts) { + ASCollectionElement *element = getElementBlock(sublayout); + if (element == nil) { + ASDisplayNodeFailAssert(@"Element not found!"); + continue; + } + + NSIndexPath *indexPath = [elements indexPathForElement:element]; + NSString *supplementaryElementKind = element.supplementaryElementKind; + + UICollectionViewLayoutAttributes *attrs; + if (supplementaryElementKind == nil) { + attrs = [UICollectionViewLayoutAttributes layoutAttributesForCellWithIndexPath:indexPath]; + } else { + attrs = [UICollectionViewLayoutAttributes layoutAttributesForSupplementaryViewOfKind:supplementaryElementKind withIndexPath:indexPath]; + } + + attrs.frame = sublayout.frame; + [table setObject:attrs forKey:element]; + } + + return [self initWithContext:context contentSize:layout.size elementToLayoutAttributesTable:table]; +} + +- (instancetype)initWithContext:(ASCollectionLayoutContext *)context + contentSize:(CGSize)contentSize + elementToLayoutAttributesTable:(NSMapTable *)table +{ + self = [super init]; + if (self) { + _context = context; + _contentSize = contentSize; + _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]; + } + return self; +} + +- (NSArray *)allLayoutAttributes +{ + 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; + NSPointerArray *pages = ASPageCoordinatesForPagesThatIntersectRect(rect, _contentSize, pageSize); + 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) { + ASPageCoordinate page = (ASPageCoordinate)pagePtr; + NSArray *allAttrs = [_pageToLayoutAttributesTable objectForPage:page]; + if (allAttrs.count > 0) { + CGRect pageRect = ASPageCoordinateGetPageRect(page, pageSize); + + if (CGRectContainsRect(rect, pageRect)) { + [result addObjectsFromArray:allAttrs]; + } else { + for (UICollectionViewLayoutAttributes *attrs in allAttrs) { + if (CGRectIntersectsRect(rect, attrs.frame)) { + [result addObject:attrs]; + } + } + } + } + } + + return [result allObjects]; +} + +- (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)) { + 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. + ASPageToLayoutAttributesTable *result = nil; + for (id pagePtr in pagesInRect) { + ASPageCoordinate page = (ASPageCoordinate)pagePtr; + NSMutableArray *attrsInPage = [_unmeasuredPageToLayoutAttributesTable objectForPage:page]; + if (attrsInPage.count == 0) { + 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. + 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]; + } + } + } + + if (intersectingAttrsInPage.count > 0) { + if (attrsInPage.count == intersectingAttrsInPage.count) { + [_unmeasuredPageToLayoutAttributesTable removeObjectForPage:page]; + } else { + [attrsInPage removeObjectsInArray:intersectingAttrsInPage]; + } + if (result == nil) { + result = [ASPageTable pageTableForStrongObjectPointers]; + } + [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/ASDataController.h b/Source/Details/ASDataController.h index 2bd56fe5e..e0e7142e3 100644 --- a/Source/Details/ASDataController.h +++ b/Source/Details/ASDataController.h @@ -35,6 +35,8 @@ NS_ASSUME_NONNULL_BEGIN @class ASCellNode; @class ASCollectionElement; +@class ASCollectionLayoutContext; +@class ASCollectionLayoutState; @class ASDataController; @class ASElementMap; @class ASLayout; @@ -136,22 +138,22 @@ 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. + * @abstract Prepares and returns a new layout for given context. * * @param context A context that was previously returned by `-layoutContextWithElements:`. * - * @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. + * @return The new layout calculated for the given context. * - * @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. + * @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, 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 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. */ -- (void)prepareLayoutWithContext:(id)context; ++ (ASCollectionLayoutState *)calculateLayoutWithContext:(ASCollectionLayoutContext *)context; @end diff --git a/Source/Details/ASDataController.mm b/Source/Details/ASDataController.mm index 5516dbb7d..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,37 +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)); - } - - [self batchAllocateNodesFromElements:elementsToProcess andLayout:(! canDelegateLayout) batchSize:elementsToProcess.count batchCompletion:^(NSArray *elements, NSArray *nodes) { - ASSERT_ON_EDITING_QUEUE; - - if (canDelegateLayout) { - [_layoutDelegate prepareLayoutWithContext:layoutContext]; - } + dispatch_block_t completion = ^() { [_mainSerialQueue performBlockOnMainThread:^{ as_activity_scope_leave(&preparationScope); // TODO Merge the two delegate methods below @@ -625,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) { @@ -837,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/Details/ASPageTable.h b/Source/Details/ASPageTable.h index d7136f8c7..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. diff --git a/Source/Details/ASPageTable.m b/Source/Details/ASPageTable.m index 626ca975f..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); 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 2ca86f478..935a3d680 100644 --- a/Source/Private/ASCollectionLayout.mm +++ b/Source/Private/ASCollectionLayout.mm @@ -18,27 +18,34 @@ #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); @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; - - BOOL _layoutDelegateImplementsAdditionalInfoForLayoutWithElements; + ASCollectionLayoutCache *_layoutCache; + ASCollectionLayoutState *_layout; // Main thread only. + + struct { + unsigned int implementsAdditionalInfoForLayoutWithElements:1; + } _layoutDelegateFlags; } @end @@ -51,30 +58,53 @@ - (instancetype)initWithLayoutDelegate:(id)layoutDel if (self) { ASDisplayNodeAssertNotNil(layoutDelegate, @"Collection layout delegate cannot be nil"); _layoutDelegate = layoutDelegate; - _layoutDelegateImplementsAdditionalInfoForLayoutWithElements = [layoutDelegate respondsToSelector:@selector(additionalInfoForLayoutWithElements:)]; + _layoutDelegateFlags.implementsAdditionalInfoForLayoutWithElements = [layoutDelegate respondsToSelector:@selector(additionalInfoForLayoutWithElements:)]; + _layoutCache = [[ASCollectionLayoutCache alloc] init]; } 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 (_layoutDelegateImplementsAdditionalInfoForLayoutWithElements) { + 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]; } -- (void)prepareLayoutWithContext:(id)context ++ (ASCollectionLayoutState *)calculateLayoutWithContext:(ASCollectionLayoutContext *)context { - ASCollectionLayoutState *layout = [_layoutDelegate calculateLayoutWithContext:context]; - - ASDN::MutexLocker l(__instanceLock__); - _pendingLayout = layout; + if (context.elements == nil) { + return [[ASCollectionLayoutState alloc] initWithContext:context + contentSize:CGSizeZero + 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; + // TODO Consider content offset of the collection node + CGRect initialRect = CGRectMake(0, 0, viewportSize.width, viewportSize.height); + CGRect measureRect = CGRectExpandToRangeWithScrollableDirections(initialRect, + kASDefaultMeasureRangeTuningParameters, + context.scrollableDirections, + kASStaticScrollDirection); + [self _measureElementsInRect:measureRect blockingRect:initialRect layout:layout]; + + return layout; } #pragma mark - UICollectionViewLayout overrides @@ -83,30 +113,29 @@ - (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)) { + // The existing layout is still valid. No-op + return; } - - if (layout == nil) { - layout = [_layoutDelegate calculateLayoutWithContext:context]; + + if (ASCollectionLayoutState *cachedLayout = [_layoutCache layoutForContext:context]) { + _layout = cachedLayout; + } else { + // A new layout is needed now. Calculate and apply it immediately + _layout = [ASCollectionLayout calculateLayoutWithContext:context]; } - - _layout = layout; } - (void)invalidateLayout { ASDisplayNodeAssertMainThread(); [super invalidateLayout]; - _layout = nil; + if (_layout != nil) { + [_layoutCache removeLayoutForContext:_layout.context]; + _layout = nil; + } } - (CGSize)collectionViewContentSize @@ -116,25 +145,45 @@ - (CGSize)collectionViewContentSize return _layout.contentSize; } -- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect +- (NSArray *)layoutAttributesForElementsInRect:(CGRect)blockingRect { ASDisplayNodeAssertMainThread(); - NSArray *result = [_layout layoutAttributesForElementsInRect:rect]; + if (CGRectIsEmpty(blockingRect)) { + return nil; + } + + // Measure elements in the measure range, block on the requested rect + CGRect measureRect = CGRectExpandToRangeWithScrollableDirections(blockingRect, + kASDefaultMeasureRangeTuningParameters, + _layout.context.scrollableDirections, + kASStaticScrollDirection); + [ASCollectionLayout _measureElementsInRect:measureRect blockingRect:blockingRect layout:_layout]; + NSArray *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 { + 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; } @@ -142,28 +191,25 @@ - (UICollectionViewLayoutAttributes *)layoutAttributesForSupplementaryViewOfKind { 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) { @@ -175,4 +221,120 @@ - (CGSize)viewportSize } } +/** + * Measures all elements in the specified rect and blocks the calling thread while measuring those in the blocking rect. + */ ++ (void)_measureElementsInRect:(CGRect)rect blockingRect:(CGRect)blockingRect layout:(ASCollectionLayoutState *)layout +{ + if (CGRectIsEmpty(rect) || layout.context.elements == nil) { + return; + } + BOOL hasBlockingRect = !CGRectIsEmpty(blockingRect); + if (hasBlockingRect && CGRectContainsRect(rect, blockingRect) == NO) { + ASDisplayNodeCAssert(NO, @"Blocking rect, if specified, must be within the other (outer) rect"); + return; + } + + // Step 1: Clamp the specified rects between the bounds of content rect + CGSize contentSize = layout.contentSize; + CGRect contentRect = CGRectMake(0, 0, contentSize.width, contentSize.height); + rect = CGRectIntersection(contentRect, rect); + if (CGRectIsNull(rect)) { + return; + } + if (hasBlockingRect) { + blockingRect = CGRectIntersection(contentRect, blockingRect); + hasBlockingRect = !CGRectIsNull(blockingRect); + } + + // Step 2: Get layout attributes of all elements within the specified outer rect + ASCollectionLayoutContext *context = layout.context; + CGSize pageSize = context.viewportSize; + ASPageToLayoutAttributesTable *attrsTable = [layout getAndRemoveUnmeasuredLayoutAttributesPageTableInRect: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 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; + NSMutableOrderedSet *nonBlockingAttrs = [NSMutableOrderedSet orderedSet]; + for (id pagePtr in attrsTable) { + ASPageCoordinate page = (ASPageCoordinate)pagePtr; + 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; + + if (hasBlockingRect && CGRectContainsRect(blockingRect, pageRect)) { + // The page fits well within the blocking rect. All attributes in this page are blocking. + [blockingAttrs addObjectsFromArray:attrsInPage]; + } else if (hasBlockingRect && CGRectIntersectsRect(blockingRect, pageRect)) { + // The page intersects the blocking rect. Some elements in this page are blocking, some are not. + for (UICollectionViewLayoutAttributes *attrs in attrsInPage) { + if (CGRectIntersectsRect(blockingRect, attrs.frame)) { + [blockingAttrs addObject:attrs]; + } else { + [nonBlockingAttrs addObject:attrs]; + } + } + } else { + // The page doesn't intersect the blocking rect. All elements in this page are non-blocking. + [nonBlockingAttrs addObjectsFromArray:attrsInPage]; + } + } + + // Step 4: Allocate and measure blocking elements' node + ASElementMap *elements = context.elements; + dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); + if (NSUInteger count = blockingAttrs.count) { + ASDispatchApply(count, queue, 0, ^(size_t i) { + UICollectionViewLayoutAttributes *attrs = blockingAttrs[i]; + ASCellNode *node = [elements elementForItemAtIndexPath:attrs.indexPath].node; + CGSize expectedSize = attrs.frame.size; + if (! CGSizeEqualToSize(expectedSize, node.calculatedSize)) { + [node layoutThatFits:ASCollectionLayoutElementSizeRangeFromSize(expectedSize)]; + } + }); + } + + // Step 5: Allocate and measure non-blocking ones + if (NSUInteger count = nonBlockingAttrs.count) { + __weak ASElementMap *weakElements = elements; + ASDispatchAsync(count, queue, 0, ^(size_t i) { + __strong ASElementMap *strongElements = weakElements; + if (strongElements) { + UICollectionViewLayoutAttributes *attrs = nonBlockingAttrs[i]; + ASCellNode *node = [elements elementForItemAtIndexPath:attrs.indexPath].node; + CGSize expectedSize = attrs.frame.size; + if (! CGSizeEqualToSize(expectedSize, node.calculatedSize)) { + [node layoutThatFits:ASCollectionLayoutElementSizeRangeFromSize(expectedSize)]; + } + } + }); + } +} + +# pragma mark - Convenient inline functions + +ASDISPLAYNODE_INLINE ASSizeRange ASCollectionLayoutElementSizeRangeFromSize(CGSize size) +{ + // The layout delegate consulted us that this element must fit within this size, + // and the only way to achieve that without asking it again is to use an exact size range here. + return ASSizeRangeMake(size); +} + +ASDISPLAYNODE_INLINE void ASCollectionLayoutSetSizeToElement(CGSize size, ASCollectionElement *element) +{ + if (ASCellNode *node = element.node) { + if (! CGSizeEqualToSize(size, node.frame.size)) { + CGRect frame = CGRectZero; + frame.size = size; + node.frame = frame; + } + } +} + @end 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..94b2bc18b --- /dev/null +++ b/Source/Private/ASCollectionLayoutCache.mm @@ -0,0 +1,90 @@ +// +// ASCollectionLayoutCache.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 + +@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 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/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/Source/Private/ASCollectionLayoutState+Private.h b/Source/Private/ASCollectionLayoutState+Private.h new file mode 100644 index 000000000..170e57acc --- /dev/null +++ b/Source/Private/ASCollectionLayoutState+Private.h @@ -0,0 +1,31 @@ +// +// ASCollectionLayoutState+Private.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 + +@interface ASCollectionLayoutState (Private) + +/** + * Remove and returns layout attributes for unmeasured elements that intersect the specified rect + * + * @discussion This method is atomic and thread-safe + */ +- (nullable ASPageToLayoutAttributesTable *)getAndRemoveUnmeasuredLayoutAttributesPageTableInRect:(CGRect)rect + contentSize:(CGSize)contentSize + pageSize:(CGSize)pageSize; + +@end + +NS_ASSUME_NONNULL_END 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/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..688ef44d4 --- /dev/null +++ b/Source/Private/_ASCollectionGalleryLayoutItem.mm @@ -0,0 +1,86 @@ +// +// _ASCollectionGalleryLayoutItem.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 + +@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/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 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/ASCollectionView/Sample/ViewController.m b/examples/ASCollectionView/Sample/ViewController.m index d00ea60df..13bfe64c7 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" @@ -21,6 +21,8 @@ #import "SupplementaryNode.h" #import "ItemNode.h" +#define ASYNC_COLLECTION_LAYOUT 0 + @interface ViewController () @property (nonatomic, strong) ASCollectionNode *collectionNode; @@ -43,8 +45,18 @@ - (void)dealloc - (void)viewDidLoad { [super viewDidLoad]; + +#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 = [[ASCollectionNode alloc] initWithLayoutDelegate:[[ASCollectionFlowLayoutDelegate alloc] init] layoutFacilitator:nil]; self.collectionNode.dataSource = self; self.collectionNode.delegate = self; 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 4bbd53e6a..a7383f294 100644 --- a/examples/CustomCollectionView/Sample/MosaicCollectionLayoutDelegate.m +++ b/examples/CustomCollectionView/Sample/MosaicCollectionLayoutDelegate.m @@ -11,63 +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; } - (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); + _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]; @@ -75,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); @@ -108,22 +110,24 @@ - (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 + 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 { @@ -131,12 +135,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; @@ -149,7 +153,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