From fdc1f0468cbeaa984bfbd8168e2d3f23e5b9a667 Mon Sep 17 00:00:00 2001 From: Huy Nguyen Date: Tue, 8 Aug 2017 19:11:40 +0100 Subject: [PATCH] Improvements in ASCollectionGalleryLayoutDelegate (#496) * Improvements in ASCollectionGalleryLayoutDelegate - It now can handle section inset, as well as interitem and line spacings - Other small changes * Fix build failure and update file licenses * Update CHANGELOG * Minor change * Another assertion on scrollable directions of gallery layout delegate --- AsyncDisplayKit.xcodeproj/project.pbxproj | 20 ++- CHANGELOG.md | 2 +- Source/ASPagerNode.m | 6 +- .../Details/ASCollectionFlowLayoutDelegate.m | 5 +- .../ASCollectionGalleryLayoutDelegate.h | 54 ++++++- .../ASCollectionGalleryLayoutDelegate.m | 93 ------------ .../ASCollectionGalleryLayoutDelegate.mm | 140 ++++++++++++++++++ Source/Details/ASCollectionLayoutState.h | 6 +- Source/Details/ASCollectionLayoutState.mm | 60 +++++--- Source/Layout/ASLayout.mm | 2 +- .../Private/_ASCollectionGalleryLayoutInfo.h | 30 ++++ .../Private/_ASCollectionGalleryLayoutInfo.m | 72 +++++++++ .../ASCollectionView/Sample/ViewController.m | 6 +- 13 files changed, 364 insertions(+), 132 deletions(-) delete mode 100644 Source/Details/ASCollectionGalleryLayoutDelegate.m create mode 100644 Source/Details/ASCollectionGalleryLayoutDelegate.mm create mode 100644 Source/Private/_ASCollectionGalleryLayoutInfo.h create mode 100644 Source/Private/_ASCollectionGalleryLayoutInfo.m diff --git a/AsyncDisplayKit.xcodeproj/project.pbxproj b/AsyncDisplayKit.xcodeproj/project.pbxproj index ba9df64e5..c02b9fd52 100644 --- a/AsyncDisplayKit.xcodeproj/project.pbxproj +++ b/AsyncDisplayKit.xcodeproj/project.pbxproj @@ -429,9 +429,11 @@ 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 */; }; + E5667E8C1F33871300FA6FC0 /* _ASCollectionGalleryLayoutInfo.h in Headers */ = {isa = PBXBuildFile; fileRef = E5667E8B1F33871300FA6FC0 /* _ASCollectionGalleryLayoutInfo.h */; settings = {ATTRIBUTES = (Private, ); }; }; + E5667E8E1F33872700FA6FC0 /* _ASCollectionGalleryLayoutInfo.m in Sources */ = {isa = PBXBuildFile; fileRef = E5667E8D1F33872700FA6FC0 /* _ASCollectionGalleryLayoutInfo.m */; }; 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 */; }; + E5775AFC1F13CE9F00CAC9BC /* _ASCollectionGalleryLayoutItem.h in Headers */ = {isa = PBXBuildFile; fileRef = E5775AFB1F13CE9F00CAC9BC /* _ASCollectionGalleryLayoutItem.h */; settings = {ATTRIBUTES = (Private, ); }; }; 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, ); }; }; @@ -458,7 +460,7 @@ E5E281741E71C833006B67C2 /* ASCollectionLayoutState.h in Headers */ = {isa = PBXBuildFile; fileRef = E5E281731E71C833006B67C2 /* ASCollectionLayoutState.h */; settings = {ATTRIBUTES = (Public, ); }; }; 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 */; }; + E5E2D7301EA780DF005C24C6 /* ASCollectionGalleryLayoutDelegate.mm in Sources */ = {isa = PBXBuildFile; fileRef = E5E2D72F1EA780DF005C24C6 /* ASCollectionGalleryLayoutDelegate.mm */; }; F711994E1D20C21100568860 /* ASDisplayNodeExtrasTests.m in Sources */ = {isa = PBXBuildFile; fileRef = F711994D1D20C21100568860 /* ASDisplayNodeExtrasTests.m */; }; /* End PBXBuildFile section */ @@ -917,6 +919,8 @@ E54E81FA1EB357BD00FFE8E1 /* ASPageTable.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASPageTable.h; sourceTree = ""; }; E54E81FB1EB357BD00FFE8E1 /* ASPageTable.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASPageTable.m; sourceTree = ""; }; E55D86311CA8A14000A0C26F /* ASLayoutElement.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASLayoutElement.mm; sourceTree = ""; }; + E5667E8B1F33871300FA6FC0 /* _ASCollectionGalleryLayoutInfo.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = _ASCollectionGalleryLayoutInfo.h; sourceTree = ""; }; + E5667E8D1F33872700FA6FC0 /* _ASCollectionGalleryLayoutInfo.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = _ASCollectionGalleryLayoutInfo.m; 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 = ""; }; @@ -946,7 +950,7 @@ E5E281731E71C833006B67C2 /* ASCollectionLayoutState.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASCollectionLayoutState.h; 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; }; + E5E2D72F1EA780DF005C24C6 /* ASCollectionGalleryLayoutDelegate.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; lineEnding = 0; path = ASCollectionGalleryLayoutDelegate.mm; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.objcpp; }; EFA731F0396842FF8AB635EE /* libPods-AsyncDisplayKitTests.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-AsyncDisplayKitTests.a"; sourceTree = BUILT_PRODUCTS_DIR; }; F711994D1D20C21100568860 /* ASDisplayNodeExtrasTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASDisplayNodeExtrasTests.m; sourceTree = ""; }; FB07EABBCF28656C6297BC2D /* Pods-AsyncDisplayKitTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-AsyncDisplayKitTests.debug.xcconfig"; path = "Pods/Target Support Files/Pods-AsyncDisplayKitTests/Pods-AsyncDisplayKitTests.debug.xcconfig"; sourceTree = ""; }; @@ -1670,6 +1674,8 @@ E5855DEE1EBB4D83003639AE /* ASCollectionLayoutDefines.h */, E5855DED1EBB4D83003639AE /* ASCollectionLayoutDefines.m */, E5775AFF1F13D25400CAC9BC /* ASCollectionLayoutState+Private.h */, + E5667E8B1F33871300FA6FC0 /* _ASCollectionGalleryLayoutInfo.h */, + E5667E8D1F33872700FA6FC0 /* _ASCollectionGalleryLayoutInfo.m */, E5775AFB1F13CE9F00CAC9BC /* _ASCollectionGalleryLayoutItem.h */, E5775AFD1F13CF7400CAC9BC /* _ASCollectionGalleryLayoutItem.mm */, ); @@ -1687,7 +1693,7 @@ E58E9E3D1E941D74004CFC59 /* ASCollectionFlowLayoutDelegate.h */, E58E9E3E1E941D74004CFC59 /* ASCollectionFlowLayoutDelegate.m */, E5E2D72D1EA780C4005C24C6 /* ASCollectionGalleryLayoutDelegate.h */, - E5E2D72F1EA780DF005C24C6 /* ASCollectionGalleryLayoutDelegate.m */, + E5E2D72F1EA780DF005C24C6 /* ASCollectionGalleryLayoutDelegate.mm */, E54E81FA1EB357BD00FFE8E1 /* ASPageTable.h */, E54E81FB1EB357BD00FFE8E1 /* ASPageTable.m */, ); @@ -1824,6 +1830,8 @@ CC87BB951DA8193C0090E380 /* ASCellNode+Internal.h in Headers */, E5775B021F16759300CAC9BC /* ASCollectionLayoutCache.h in Headers */, E5775B001F13D25400CAC9BC /* ASCollectionLayoutState+Private.h in Headers */, + E5667E8C1F33871300FA6FC0 /* _ASCollectionGalleryLayoutInfo.h in Headers */, + E5775AFC1F13CE9F00CAC9BC /* _ASCollectionGalleryLayoutItem.h in Headers */, E5855DF01EBB4D83003639AE /* ASCollectionLayoutDefines.h in Headers */, E5B5B9D11E9BAD9800A6B726 /* ASCollectionLayoutContext+Private.h in Headers */, 9C8898BD1C738BB800D6B02E /* ASTextKitFontSizeAdjuster.h in Headers */, @@ -1896,7 +1904,6 @@ 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 */, @@ -2251,6 +2258,7 @@ CCCCCCD61EC3EF060087FE10 /* ASTextDebugOption.m in Sources */, 34EFC75C1B701BD200AD841F /* ASDimension.mm in Sources */, B350624E1B010EFD0018CF92 /* ASDisplayNode+AsyncDisplay.mm in Sources */, + E5667E8E1F33872700FA6FC0 /* _ASCollectionGalleryLayoutInfo.m in Sources */, 25E327591C16819500A2170C /* ASPagerNode.m in Sources */, 636EA1A41C7FF4EC00EE152F /* NSArray+Diffing.m in Sources */, B35062501B010EFD0018CF92 /* ASDisplayNode+DebugTiming.mm in Sources */, @@ -2286,7 +2294,7 @@ CCCCCCE01EC3EF060087FE10 /* ASTextRunDelegate.m in Sources */, CCCCCCDA1EC3EF060087FE10 /* ASTextLayout.m in Sources */, 254C6B841BF94F8A003EC431 /* ASTextNodeWordKerner.m in Sources */, - E5E2D7301EA780DF005C24C6 /* ASCollectionGalleryLayoutDelegate.m in Sources */, + E5E2D7301EA780DF005C24C6 /* ASCollectionGalleryLayoutDelegate.mm in Sources */, 34EFC76B1B701CEB00AD841F /* ASLayoutSpec.mm in Sources */, CC3B20861C3F76D600798563 /* ASPendingStateController.mm in Sources */, 254C6B8C1BF94F8A003EC431 /* ASTextKitTailTruncater.mm in Sources */, diff --git a/CHANGELOG.md b/CHANGELOG.md index 94b88bd11..f4bf0db81 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,7 @@ - [ASCollectionView] Add delegate bridging and index space translation for missing UICollectionViewLayout properties. [Scott Goodson](https://github.com/appleguy) - [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) [#451](https://github.com/TextureGroup/Texture/pull/451) +- 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) [#451](https://github.com/TextureGroup/Texture/pull/451) [#496](https://github.com/TextureGroup/Texture/pull/496) - Fix an issue that causes infinite layout loop in ASDisplayNode after [#428](https://github.com/TextureGroup/Texture/pull/428) [Huy Nguyen](https://github.com/nguyenhuy) [#455](https://github.com/TextureGroup/Texture/pull/455) - Fix an issue in layout transition that causes it to unexpectedly use the old layout [Huy Nguyen](https://github.com/nguyenhuy) [#464](https://github.com/TextureGroup/Texture/pull/464) - Add -[ASDisplayNode detailedLayoutDescription] property to aid debugging. [Adlai Holler](https://github.com/Adlai-Holler) [#476](https://github.com/TextureGroup/Texture/pull/476) diff --git a/Source/ASPagerNode.m b/Source/ASPagerNode.m index a5f84178c..02666993e 100644 --- a/Source/ASPagerNode.m +++ b/Source/ASPagerNode.m @@ -29,7 +29,7 @@ #import #import -@interface ASPagerNode () +@interface ASPagerNode () { __weak id _pagerDataSource; ASPagerNodeProxy *_proxyDataSource; @@ -75,7 +75,7 @@ - (instancetype)initUsingAsyncCollectionLayout ASCollectionGalleryLayoutDelegate *layoutDelegate = [[ASCollectionGalleryLayoutDelegate alloc] initWithScrollableDirections:ASScrollDirectionHorizontalDirections]; self = [super initWithLayoutDelegate:layoutDelegate layoutFacilitator:nil]; if (self) { - layoutDelegate.sizeProvider = self; + layoutDelegate.propertiesProvider = self; } return self; } @@ -137,7 +137,7 @@ - (NSInteger)indexOfPageWithNode:(ASCellNode *)node return indexPath.row; } -#pragma mark - ASCollectionGalleryLayoutSizeProviding +#pragma mark - ASCollectionGalleryLayoutPropertiesProviding - (CGSize)sizeForElements:(ASElementMap *)elements { diff --git a/Source/Details/ASCollectionFlowLayoutDelegate.m b/Source/Details/ASCollectionFlowLayoutDelegate.m index 79287a594..548fa0181 100644 --- a/Source/Details/ASCollectionFlowLayoutDelegate.m +++ b/Source/Details/ASCollectionFlowLayoutDelegate.m @@ -76,8 +76,9 @@ + (ASCollectionLayoutState *)calculateLayoutWithContext:(ASCollectionLayoutConte 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; + return [[ASCollectionLayoutState alloc] initWithContext:context layout:layout getElementBlock:^ASCollectionElement * _Nullable(ASLayout * _Nonnull sublayout) { + ASCellNode *node = ASDynamicCast(sublayout.layoutElement, ASCellNode); + return node ? node.collectionElement : nil; }]; } diff --git a/Source/Details/ASCollectionGalleryLayoutDelegate.h b/Source/Details/ASCollectionGalleryLayoutDelegate.h index d2d41b788..dfd0f5c87 100644 --- a/Source/Details/ASCollectionGalleryLayoutDelegate.h +++ b/Source/Details/ASCollectionGalleryLayoutDelegate.h @@ -17,7 +17,7 @@ NS_ASSUME_NONNULL_BEGIN -@protocol ASCollectionGalleryLayoutSizeProviding +@protocol ASCollectionGalleryLayoutPropertiesProviding /** * Returns the fixed size of each and every element. @@ -30,6 +30,51 @@ NS_ASSUME_NONNULL_BEGIN */ - (CGSize)sizeForElements:(ASElementMap *)elements; +@optional + +/** + * Returns the minumum spacing to use between lines of items. + * + * @discussion This method will only be called on main thread. + * + * @discussion For a vertically scrolling layout, this value represents the minimum spacing between rows. + * For a horizontally scrolling one, it represents the minimum spacing between columns. + * It is not applied between the first line and the header, or between the last line and the footer. + * This is the same behavior as UICollectionViewFlowLayout's minimumLineSpacing. + * + * @param elements All elements in the layout. + * + * @return The interitem spacing + */ +- (CGFloat)minimumLineSpacingForElements:(ASElementMap *)elements; + +/** + * Returns the minumum spacing to use between items in the same row or column, depending on the scroll directions. + * + * @discussion This method will only be called on main thread. + * + * @discussion For a vertically scrolling layout, this value represents the minimum spacing between items in the same row. + * For a horizontally scrolling one, it represents the minimum spacing between items in the same column. + * It is considered while fitting items into lines, but the actual final spacing between some items might be larger. + * This is the same behavior as UICollectionViewFlowLayout's minimumInteritemSpacing. + * + * @param elements All elements in the layout. + * + * @return The interitem spacing + */ +- (CGFloat)minimumInteritemSpacingForElements:(ASElementMap *)elements; + +/** + * Returns the margins of each section. + * + * @discussion This method will only be called on main thread. + * + * @param elements All elements in the layout. + * + * @return The margins used to layout content in a section + */ +- (UIEdgeInsets)sectionInsetForElements:(ASElementMap *)elements; + @end /** @@ -40,8 +85,13 @@ NS_ASSUME_NONNULL_BEGIN AS_SUBCLASSING_RESTRICTED @interface ASCollectionGalleryLayoutDelegate : NSObject -@property (nonatomic, weak) id sizeProvider; +@property (nonatomic, weak) id propertiesProvider; +/** + * Designated initializer. + * + * @param scrollableDirections The scrollable directions of this layout. Must be either vertical or horizontal directions. + */ - (instancetype)initWithScrollableDirections:(ASScrollDirection)scrollableDirections NS_DESIGNATED_INITIALIZER; - (instancetype)init __unavailable; diff --git a/Source/Details/ASCollectionGalleryLayoutDelegate.m b/Source/Details/ASCollectionGalleryLayoutDelegate.m deleted file mode 100644 index 5ab5c23d2..000000000 --- a/Source/Details/ASCollectionGalleryLayoutDelegate.m +++ /dev/null @@ -1,93 +0,0 @@ -// -// 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 -{ - self = [super init]; - if (self) { - _scrollableDirections = scrollableDirections; - } - return self; -} - -- (ASScrollDirection)scrollableDirections -{ - ASDisplayNodeAssertMainThread(); - return _scrollableDirections; -} - -- (id)additionalInfoForLayoutWithElements:(ASElementMap *)elements -{ - ASDisplayNodeAssertMainThread(); - if (_sizeProvider == nil) { - return nil; - } - - return [NSValue valueWithCGSize:[_sizeProvider sizeForElements:elements]]; -} - -+ (ASCollectionLayoutState *)calculateLayoutWithContext:(ASCollectionLayoutContext *)context -{ - ASElementMap *elements = context.elements; - CGSize pageSize = context.viewportSize; - ASScrollDirection scrollableDirections = context.scrollableDirections; - - CGSize itemSize = context.additionalInfo ? ((NSValue *)context.additionalInfo).CGSizeValue : CGSizeZero; - if (CGSizeEqualToSize(CGSizeZero, itemSize)) { - return [[ASCollectionLayoutState alloc] initWithContext:context]; - } - - NSMutableArray<_ASGalleryLayoutItem *> *children = ASArrayByFlatMapping(elements.itemElements, - ASCollectionElement *element, - [[_ASGalleryLayoutItem alloc] initWithItemSize:itemSize collectionElement:element]); - if (children.count == 0) { - return [[ASCollectionLayoutState alloc] initWithContext:context]; - } - - // 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/ASCollectionGalleryLayoutDelegate.mm b/Source/Details/ASCollectionGalleryLayoutDelegate.mm new file mode 100644 index 000000000..a01fdf7f7 --- /dev/null +++ b/Source/Details/ASCollectionGalleryLayoutDelegate.mm @@ -0,0 +1,140 @@ +// +// ASCollectionGalleryLayoutDelegate.mm +// Texture +// +// Copyright (c) 2017-present, Pinterest, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import + +#pragma mark - ASCollectionGalleryLayoutDelegate + +@implementation ASCollectionGalleryLayoutDelegate { + ASScrollDirection _scrollableDirections; + + struct { + unsigned int minimumLineSpacingForElements:1; + unsigned int minimumInteritemSpacingForElements:1; + unsigned int sectionInsetForElements:1; + } _propertiesProviderFlags; +} + +- (instancetype)initWithScrollableDirections:(ASScrollDirection)scrollableDirections +{ + self = [super init]; + if (self) { + // Scrollable directions must be either vertical or horizontal, but not both + ASDisplayNodeAssertTrue(ASScrollDirectionContainsVerticalDirection(scrollableDirections) + || ASScrollDirectionContainsHorizontalDirection(scrollableDirections)); + ASDisplayNodeAssertFalse(ASScrollDirectionContainsVerticalDirection(scrollableDirections) + && ASScrollDirectionContainsHorizontalDirection(scrollableDirections)); + _scrollableDirections = scrollableDirections; + } + return self; +} + +- (ASScrollDirection)scrollableDirections +{ + ASDisplayNodeAssertMainThread(); + return _scrollableDirections; +} + +- (void)setPropertiesProvider:(id)propertiesProvider +{ + ASDisplayNodeAssertMainThread(); + if (propertiesProvider == nil) { + _propertiesProvider = nil; + _propertiesProviderFlags = {}; + } else { + _propertiesProvider = propertiesProvider; + _propertiesProviderFlags.minimumLineSpacingForElements = [_propertiesProvider respondsToSelector:@selector(minimumLineSpacingForElements:)]; + _propertiesProviderFlags.minimumInteritemSpacingForElements = [_propertiesProvider respondsToSelector:@selector(minimumInteritemSpacingForElements:)]; + _propertiesProviderFlags.sectionInsetForElements = [_propertiesProvider respondsToSelector:@selector(sectionInsetForElements:)]; + } +} + +- (id)additionalInfoForLayoutWithElements:(ASElementMap *)elements +{ + ASDisplayNodeAssertMainThread(); + id propertiesProvider = _propertiesProvider; + if (propertiesProvider == nil) { + return nil; + } + + CGSize itemSize = [propertiesProvider sizeForElements:elements]; + UIEdgeInsets sectionInset = _propertiesProviderFlags.sectionInsetForElements ? [propertiesProvider sectionInsetForElements:elements] : UIEdgeInsetsZero; + CGFloat lineSpacing = _propertiesProviderFlags.minimumLineSpacingForElements ? [propertiesProvider minimumLineSpacingForElements:elements] : 0.0; + CGFloat interitemSpacing = _propertiesProviderFlags.minimumInteritemSpacingForElements ? [propertiesProvider minimumInteritemSpacingForElements:elements] : 0.0; + return [[_ASCollectionGalleryLayoutInfo alloc] initWithItemSize:itemSize + minimumLineSpacing:lineSpacing + minimumInteritemSpacing:interitemSpacing + sectionInset:sectionInset]; +} + ++ (ASCollectionLayoutState *)calculateLayoutWithContext:(ASCollectionLayoutContext *)context +{ + ASElementMap *elements = context.elements; + CGSize pageSize = context.viewportSize; + ASScrollDirection scrollableDirections = context.scrollableDirections; + + _ASCollectionGalleryLayoutInfo *info = ASDynamicCast(context.additionalInfo, _ASCollectionGalleryLayoutInfo); + CGSize itemSize = info.itemSize; + if (info == nil || CGSizeEqualToSize(CGSizeZero, itemSize)) { + return [[ASCollectionLayoutState alloc] initWithContext:context]; + } + + NSMutableArray<_ASGalleryLayoutItem *> *children = ASArrayByFlatMapping(elements.itemElements, + ASCollectionElement *element, + [[_ASGalleryLayoutItem alloc] initWithItemSize:itemSize collectionElement:element]); + if (children.count == 0) { + return [[ASCollectionLayoutState alloc] initWithContext:context]; + } + + // Use a stack spec to calculate layout content size and frames of all elements without actually measuring each element + ASStackLayoutDirection stackDirection = ASScrollDirectionContainsVerticalDirection(scrollableDirections) + ? ASStackLayoutDirectionHorizontal + : ASStackLayoutDirectionVertical; + ASStackLayoutSpec *stackSpec = [ASStackLayoutSpec stackLayoutSpecWithDirection:stackDirection + spacing:info.minimumInteritemSpacing + justifyContent:ASStackLayoutJustifyContentStart + alignItems:ASStackLayoutAlignItemsStart + flexWrap:ASStackLayoutFlexWrapWrap + alignContent:ASStackLayoutAlignContentStart + lineSpacing:info.minimumLineSpacing + children:children]; + stackSpec.concurrent = YES; + + ASLayoutSpec *finalSpec = stackSpec; + UIEdgeInsets sectionInset = info.sectionInset; + if (UIEdgeInsetsEqualToEdgeInsets(sectionInset, UIEdgeInsetsZero) == NO) { + finalSpec = [ASInsetLayoutSpec insetLayoutSpecWithInsets:sectionInset child:stackSpec]; + } + + ASLayout *layout = [finalSpec layoutThatFits:ASSizeRangeForCollectionLayoutThatFitsViewportSize(pageSize, scrollableDirections)]; + + return [[ASCollectionLayoutState alloc] initWithContext:context layout:layout getElementBlock:^ASCollectionElement * _Nullable(ASLayout * _Nonnull sublayout) { + _ASGalleryLayoutItem *item = ASDynamicCast(sublayout.layoutElement, _ASGalleryLayoutItem); + return item ? item.collectionElement : nil; + }]; +} + +@end diff --git a/Source/Details/ASCollectionLayoutState.h b/Source/Details/ASCollectionLayoutState.h index c21a37dff..88eafc4da 100644 --- a/Source/Details/ASCollectionLayoutState.h +++ b/Source/Details/ASCollectionLayoutState.h @@ -23,6 +23,8 @@ NS_ASSUME_NONNULL_BEGIN +typedef ASCollectionElement * _Nullable (^ASCollectionLayoutStateGetElementBlock)(ASLayout *); + @interface NSMapTable (ASCollectionLayoutConvenience) + (NSMapTable *)elementToLayoutAttributesTable; @@ -70,11 +72,11 @@ AS_SUBCLASSING_RESTRICTED * * @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. + * @param getElementBlock A block that can retrieve the collection element from a sublayout of the root layout. */ - (instancetype)initWithContext:(ASCollectionLayoutContext *)context layout:(ASLayout *)layout - getElementBlock:(ASCollectionElement *(^)(ASLayout *))getElementBlock; + getElementBlock:(ASCollectionLayoutStateGetElementBlock)getElementBlock; /** * Returns all layout attributes present in this object. diff --git a/Source/Details/ASCollectionLayoutState.mm b/Source/Details/ASCollectionLayoutState.mm index 81f003ab4..eb59edf0c 100644 --- a/Source/Details/ASCollectionLayoutState.mm +++ b/Source/Details/ASCollectionLayoutState.mm @@ -20,9 +20,12 @@ #import #import #import +#import #import #import +#import + @implementation NSMapTable (ASCollectionLayoutConvenience) + (NSMapTable *)elementToLayoutAttributesTable @@ -48,30 +51,49 @@ - (instancetype)initWithContext:(ASCollectionLayoutContext *)context - (instancetype)initWithContext:(ASCollectionLayoutContext *)context layout:(ASLayout *)layout - getElementBlock:(ASCollectionElement *(^)(ASLayout *))getElementBlock + getElementBlock:(ASCollectionLayoutStateGetElementBlock)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; + + // Traverse the given layout tree in breadth first fashion. Generate layout attributes for all included elements along the way. + struct Context { + ASLayout *layout; + CGPoint absolutePosition; + }; + + std::queue queue; + queue.push({layout, CGPointZero}); + + while (!queue.empty()) { + Context context = queue.front(); + queue.pop(); + + ASLayout *layout = context.layout; + const CGPoint absolutePosition = context.absolutePosition; + + ASCollectionElement *element = getElementBlock(layout); + if (element != nil) { + 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]; + } + + CGRect frame = layout.frame; + frame.origin = absolutePosition; + attrs.frame = frame; + [table setObject:attrs forKey:element]; } - - 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]; + + // Add all sublayouts to process in next step + for (ASLayout *sublayout in layout.sublayouts) { + queue.push({sublayout, absolutePosition + sublayout.position}); } - - attrs.frame = sublayout.frame; - [table setObject:attrs forKey:element]; } return [self initWithContext:context contentSize:layout.size elementToLayoutAttributesTable:table]; diff --git a/Source/Layout/ASLayout.mm b/Source/Layout/ASLayout.mm index 82eb62a87..22904b8b6 100644 --- a/Source/Layout/ASLayout.mm +++ b/Source/Layout/ASLayout.mm @@ -258,7 +258,7 @@ - (ASLayout *)filteredNodeLayoutTree } else if (sublayoutsCount > 0){ std::vector sublayoutContexts; for (ASLayout *sublayout in sublayouts) { - sublayoutContexts.push_back({sublayout, context.absolutePosition + sublayout.position}); + sublayoutContexts.push_back({sublayout, absolutePosition + sublayout.position}); } queue.insert(queue.cbegin(), sublayoutContexts.begin(), sublayoutContexts.end()); } diff --git a/Source/Private/_ASCollectionGalleryLayoutInfo.h b/Source/Private/_ASCollectionGalleryLayoutInfo.h new file mode 100644 index 000000000..9fcb08c15 --- /dev/null +++ b/Source/Private/_ASCollectionGalleryLayoutInfo.h @@ -0,0 +1,30 @@ +// +// _ASCollectionGalleryLayoutInfo.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 _ASCollectionGalleryLayoutInfo : NSObject + +// Read-only properties +@property (nonatomic, assign, readonly) CGSize itemSize; +@property (nonatomic, assign, readonly) CGFloat minimumLineSpacing; +@property (nonatomic, assign, readonly) CGFloat minimumInteritemSpacing; +@property (nonatomic, assign, readonly) UIEdgeInsets sectionInset; + +- (instancetype)initWithItemSize:(CGSize)itemSize + minimumLineSpacing:(CGFloat)minimumLineSpacing + minimumInteritemSpacing:(CGFloat)minimumInteritemSpacing + sectionInset:(UIEdgeInsets)sectionInset NS_DESIGNATED_INITIALIZER; + +- (instancetype)init __unavailable; + +@end diff --git a/Source/Private/_ASCollectionGalleryLayoutInfo.m b/Source/Private/_ASCollectionGalleryLayoutInfo.m new file mode 100644 index 000000000..4dc9209d0 --- /dev/null +++ b/Source/Private/_ASCollectionGalleryLayoutInfo.m @@ -0,0 +1,72 @@ +// +// _ASCollectionGalleryLayoutInfo.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 + +@implementation _ASCollectionGalleryLayoutInfo + +- (instancetype)initWithItemSize:(CGSize)itemSize + minimumLineSpacing:(CGFloat)minimumLineSpacing + minimumInteritemSpacing:(CGFloat)minimumInteritemSpacing + sectionInset:(UIEdgeInsets)sectionInset +{ + self = [super init]; + if (self) { + _itemSize = itemSize; + _minimumLineSpacing = minimumLineSpacing; + _minimumInteritemSpacing = minimumInteritemSpacing; + _sectionInset = sectionInset; + } + return self; +} + +- (BOOL)isEqualToInfo:(_ASCollectionGalleryLayoutInfo *)info +{ + if (info == nil) { + return NO; + } + + return CGSizeEqualToSize(_itemSize, info.itemSize) + && _minimumLineSpacing == info.minimumLineSpacing + && _minimumInteritemSpacing == info.minimumInteritemSpacing + && UIEdgeInsetsEqualToEdgeInsets(_sectionInset, info.sectionInset); +} + +- (BOOL)isEqual:(id)other +{ + if (self == other) { + return YES; + } + if (! [other isKindOfClass:[_ASCollectionGalleryLayoutInfo class]]) { + return NO; + } + return [self isEqualToInfo:other]; +} + +- (NSUInteger)hash +{ + struct { + CGSize itemSize; + CGFloat minimumLineSpacing; + CGFloat minimumInteritemSpacing; + UIEdgeInsets sectionInset; + } data = { + _itemSize, + _minimumLineSpacing, + _minimumInteritemSpacing, + _sectionInset, + }; + return ASHashBytes(&data, sizeof(data)); +} + +@end diff --git a/examples/ASCollectionView/Sample/ViewController.m b/examples/ASCollectionView/Sample/ViewController.m index 36391e8f3..b895e9c9b 100644 --- a/examples/ASCollectionView/Sample/ViewController.m +++ b/examples/ASCollectionView/Sample/ViewController.m @@ -23,7 +23,7 @@ #define ASYNC_COLLECTION_LAYOUT 0 -@interface ViewController () +@interface ViewController () @property (nonatomic, strong) ASCollectionNode *collectionNode; @property (nonatomic, strong) NSArray *data; @@ -48,7 +48,7 @@ - (void)viewDidLoad #if ASYNC_COLLECTION_LAYOUT ASCollectionGalleryLayoutDelegate *layoutDelegate = [[ASCollectionGalleryLayoutDelegate alloc] initWithScrollableDirections:ASScrollDirectionVerticalDirections]; - layoutDelegate.sizeProvider = self; + layoutDelegate.propertiesProvider = self; self.collectionNode = [[ASCollectionNode alloc] initWithLayoutDelegate:layoutDelegate layoutFacilitator:nil]; #else UICollectionViewFlowLayout *layout = [[UICollectionViewFlowLayout alloc] init]; @@ -108,7 +108,7 @@ - (void)reloadTapped [self.collectionNode reloadData]; } -#pragma mark - ASCollectionGalleryLayoutSizeProviding +#pragma mark - ASCollectionGalleryLayoutPropertiesProviding - (CGSize)sizeForElements:(ASElementMap *)elements {