From 1c6686e5d64577fe17dd0121ffa006933b61908d Mon Sep 17 00:00:00 2001 From: Michael Schneider Date: Wed, 27 Apr 2016 15:46:49 -0700 Subject: [PATCH] Improve rotation support for ASPagerNode --- AsyncDisplayKit/ASPagerFlowLayout.h | 13 +++- AsyncDisplayKit/ASPagerFlowLayout.m | 60 ++++++++------- AsyncDisplayKit/ASPagerNode.h | 5 +- AsyncDisplayKit/ASPagerNode.m | 93 ++++++++++++++++++----- AsyncDisplayKit/Details/ASDelegateProxy.m | 3 +- 5 files changed, 128 insertions(+), 46 deletions(-) diff --git a/AsyncDisplayKit/ASPagerFlowLayout.h b/AsyncDisplayKit/ASPagerFlowLayout.h index 9b784107a6..c689cad85d 100644 --- a/AsyncDisplayKit/ASPagerFlowLayout.h +++ b/AsyncDisplayKit/ASPagerFlowLayout.h @@ -8,6 +8,17 @@ #import -@interface ASPagerFlowLayout : UICollectionViewFlowLayout +@class ASPagerNode; + +@protocol ASPagerFlowLayoutPageProvider + +/// Provides the current page index to the ASPagerFlowLayout +- (NSInteger)currentPageIndex; + +@end + +@interface ASPagerFlowLayout : UICollectionViewFlowLayout + +- (instancetype)initWithPageProvider:(id)pageProvider; @end diff --git a/AsyncDisplayKit/ASPagerFlowLayout.m b/AsyncDisplayKit/ASPagerFlowLayout.m index dcb89a4008..5c359db0b9 100644 --- a/AsyncDisplayKit/ASPagerFlowLayout.m +++ b/AsyncDisplayKit/ASPagerFlowLayout.m @@ -7,53 +7,56 @@ // #import "ASPagerFlowLayout.h" +#import "ASPagerNode.h" @interface ASPagerFlowLayout () @property (strong, nonatomic) NSIndexPath *currentIndexPath; +@property (weak, nonatomic) id pageProvider; @end @implementation ASPagerFlowLayout -- (void)invalidateLayout +#pragma mark - Lifecycle + +- (instancetype)initWithPageProvider:(id)pageProvider { - self.currentIndexPath = [self _indexPathForVisiblyCenteredItem]; - [super invalidateLayout]; + self = [super init]; + if (self == nil) { return self; } + _pageProvider = pageProvider; + return self; +} + +#pragma mark - UICollectionViewFlowLayout + +- (BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds +{ + CGRect oldBounds = self.collectionView.bounds; + if (!CGSizeEqualToSize(oldBounds.size, newBounds.size)) { + return YES; + } + + return NO; } - (CGPoint)targetContentOffsetForProposedContentOffset:(CGPoint)proposedContentOffset -{ - if (self.currentIndexPath) { - return [self _targetContentOffsetForItemAtIndexPath:self.currentIndexPath - proposedContentOffset:proposedContentOffset]; - } - - return [super targetContentOffsetForProposedContentOffset:proposedContentOffset]; -} - -- (CGPoint)_targetContentOffsetForItemAtIndexPath:(NSIndexPath *)indexPath proposedContentOffset:(CGPoint)proposedContentOffset { if ([self _dataSourceIsEmpty]) { return proposedContentOffset; } + + if (_pageProvider == nil || [self _visibleRectIsInvalid]) { + return [super targetContentOffsetForProposedContentOffset:proposedContentOffset]; + } + + NSIndexPath *indexPath = [NSIndexPath indexPathForItem:_pageProvider.currentPageIndex inSection:0]; UICollectionViewLayoutAttributes *attributes = [self layoutAttributesForItemAtIndexPath:indexPath]; - CGFloat xOffset = (self.collectionView.bounds.size.width - attributes.frame.size.width) / 2; + CGFloat xOffset = (CGRectGetWidth(self.collectionView.bounds) - CGRectGetWidth(attributes.frame)) / 2; return CGPointMake(attributes.frame.origin.x - xOffset, proposedContentOffset.y); } -- (NSIndexPath *)_indexPathForVisiblyCenteredItem -{ - CGRect visibleRect = [self _visibleRect]; - CGFloat visibleXCenter = CGRectGetMidX(visibleRect); - NSArray *layoutAttributes = [self layoutAttributesForElementsInRect:visibleRect]; - for (UICollectionViewLayoutAttributes *attributes in layoutAttributes) { - if ([attributes representedElementCategory] == UICollectionElementCategoryCell && attributes.center.x == visibleXCenter) { - return attributes.indexPath; - } - } - return nil; -} +#pragma mark - Helper - (BOOL)_dataSourceIsEmpty { @@ -68,4 +71,9 @@ return visibleRect; } +- (BOOL)_visibleRectIsInvalid +{ + return CGRectEqualToRect([self _visibleRect], CGRectZero); +} + @end diff --git a/AsyncDisplayKit/ASPagerNode.h b/AsyncDisplayKit/ASPagerNode.h index 481ceaf1c6..4fae1d117e 100644 --- a/AsyncDisplayKit/ASPagerNode.h +++ b/AsyncDisplayKit/ASPagerNode.h @@ -89,7 +89,10 @@ /// The underlying ASCollectionView object. @property (nonatomic, readonly) ASCollectionView *view; -/// Scroll the contents of the receiver to ensure that the page is visible. +/// Returns the current page index +@property (nonatomic, assign, readonly) NSInteger currentPageIndex; + +/// Scroll the contents of the receiver to ensure that the page is visible - (void)scrollToPageAtIndex:(NSInteger)index animated:(BOOL)animated; @end diff --git a/AsyncDisplayKit/ASPagerNode.m b/AsyncDisplayKit/ASPagerNode.m index ac8cbc593f..52009ef607 100644 --- a/AsyncDisplayKit/ASPagerNode.m +++ b/AsyncDisplayKit/ASPagerNode.m @@ -12,15 +12,18 @@ #import "ASPagerFlowLayout.h" #import "UICollectionViewLayout+ASConvenience.h" -@interface ASPagerNode () +@interface ASPagerNode () { ASPagerFlowLayout *_flowLayout; - ASPagerNodeProxy *_proxy; - __weak id _pagerDataSource; + ASPagerNodeProxy *_dataSourceProxy; + ASPagerNodeProxy *_delegateProxy; + __weak id _pagerDataSource; BOOL _pagerDataSourceImplementsNodeBlockAtIndex; BOOL _pagerDataSourceImplementsConstrainedSizeForNode; } +@property (nonatomic, assign, readonly) NSInteger numberOfPages; + @end @implementation ASPagerNode @@ -28,7 +31,7 @@ - (instancetype)init { - ASPagerFlowLayout *flowLayout = [[ASPagerFlowLayout alloc] init]; + ASPagerFlowLayout *flowLayout = [[ASPagerFlowLayout alloc] initWithPageProvider:self]; flowLayout.scrollDirection = UICollectionViewScrollDirectionHorizontal; flowLayout.minimumInteritemSpacing = 0; flowLayout.minimumLineSpacing = 0; @@ -42,6 +45,7 @@ self = [super initWithCollectionViewLayout:flowLayout]; if (self != nil) { _flowLayout = flowLayout; + _currentPageIndex = 0; } return self; } @@ -63,6 +67,10 @@ // our view is only horizontally scrollable. This causes UICollectionViewFlowLayout to log a warning. // From here we cannot disable this directly (UIViewController's automaticallyAdjustsScrollViewInsets). cv.zeroContentInsets = YES; + + // Set the super delegate to the pager for now to inject the scroll delegate calls. If the API consumer + // set's the delegate on the ASPagerNode we add an ASPagerNodeProxy in between in setDelegate: + super.delegate = self; ASRangeTuningParameters minimumRenderParams = { .leadingBufferScreenfuls = 0.0, .trailingBufferScreenfuls = 0.0 }; ASRangeTuningParameters minimumPreloadParams = { .leadingBufferScreenfuls = 1.0, .trailingBufferScreenfuls = 1.0 }; @@ -75,16 +83,34 @@ [self setTuningParameters:fullPreloadParams forRangeMode:ASLayoutRangeModeFull rangeType:ASLayoutRangeTypeFetchData]; } +#pragma mark - Getter / Setter + +- (NSInteger)numberOfPages +{ + return [_pagerDataSource numberOfPagesInPagerNode:self]; +} + #pragma mark - Helpers - (void)scrollToPageAtIndex:(NSInteger)index animated:(BOOL)animated { - NSIndexPath *indexPath = [NSIndexPath indexPathForItem:index inSection:0]; - [self.view scrollToItemAtIndexPath:indexPath atScrollPosition:UICollectionViewScrollPositionLeft animated:animated]; + // Prevent an exception to scroll to an index path that is invalid + if (index >= 0 && index < self.numberOfPages) { + NSIndexPath *indexPath = [NSIndexPath indexPathForItem:index inSection:0]; + [self.view scrollToItemAtIndexPath:indexPath atScrollPosition:UICollectionViewScrollPositionLeft animated:animated]; + + _currentPageIndex = index; + } } #pragma mark - ASCollectionViewDataSource +- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section +{ + ASDisplayNodeAssert(_pagerDataSource != nil, @"ASPagerNode must have a data source to load nodes to display"); + return self.numberOfPages; +} + - (ASCellNodeBlock)collectionView:(ASCollectionView *)collectionView nodeBlockForItemAtIndexPath:(NSIndexPath *)indexPath { ASDisplayNodeAssert(_pagerDataSource != nil, @"ASPagerNode must have a data source to load nodes to display"); @@ -95,12 +121,6 @@ return [_pagerDataSource pagerNode:self nodeBlockAtIndex:indexPath.item]; } -- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section -{ - ASDisplayNodeAssert(_pagerDataSource != nil, @"ASPagerNode must have a data source to load nodes to display"); - return [_pagerDataSource numberOfPagesInPagerNode:self]; -} - - (ASSizeRange)collectionView:(ASCollectionView *)collectionView constrainedSizeForNodeAtIndexPath:(NSIndexPath *)indexPath { if (_pagerDataSourceImplementsConstrainedSizeForNode) { @@ -109,7 +129,7 @@ return ASSizeRangeMake(CGSizeZero, self.view.bounds.size); } -#pragma mark - Data Source Proxy +#pragma mark - Proxies - (id )dataSource { @@ -122,20 +142,59 @@ _pagerDataSource = pagerDataSource; _pagerDataSourceImplementsNodeBlockAtIndex = [_pagerDataSource respondsToSelector:@selector(pagerNode:nodeBlockAtIndex:)]; + _pagerDataSourceImplementsConstrainedSizeForNode = [_pagerDataSource respondsToSelector:@selector(pagerNode:constrainedSizeForNodeAtIndexPath:)]; + // Data source must implement pagerNode:nodeBlockAtIndex: or pagerNode:nodeAtIndex: ASDisplayNodeAssertTrue(_pagerDataSourceImplementsNodeBlockAtIndex || [_pagerDataSource respondsToSelector:@selector(pagerNode:nodeAtIndex:)]); - _pagerDataSourceImplementsConstrainedSizeForNode = [_pagerDataSource respondsToSelector:@selector(pagerNode:constrainedSizeForNodeAtIndexPath:)]; + _dataSourceProxy = pagerDataSource ? [[ASPagerNodeProxy alloc] initWithTarget:pagerDataSource interceptor:self] : nil; - _proxy = pagerDataSource ? [[ASPagerNodeProxy alloc] initWithTarget:pagerDataSource interceptor:self] : nil; - - super.dataSource = (id )_proxy; + super.dataSource = (id )_dataSourceProxy; } } +- (void)setDelegate:(id)delegate +{ + _delegateProxy = delegate ? [[ASPagerNodeProxy alloc] initWithTarget:delegate interceptor:self] : nil; + + super.delegate = (id )_delegateProxy; +} + - (void)proxyTargetHasDeallocated:(ASDelegateProxy *)proxy { [self setDataSource:nil]; + [self setDelegate:nil]; +} + +#pragma mark - + +- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView +{ + CGFloat pageWidth = CGRectGetWidth(self.view.frame); + _currentPageIndex = floor((self.view.contentOffset.x - pageWidth / 2) / pageWidth) + 1; +} + +- (void)scrollViewWillEndDragging:(UIScrollView*)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint*)targetContentOffset +{ + CGFloat pageWidth = CGRectGetWidth(self.view.frame); + NSInteger newPageIndex = _currentPageIndex; + + if (velocity.x == 0) { + // Handle slow dragging not lifting finger + newPageIndex = floor((targetContentOffset->x - pageWidth / 2) / pageWidth) + 1; + } else { + newPageIndex = velocity.x > 0 ? _currentPageIndex + 1 : _currentPageIndex - 1; + + if (newPageIndex < 0) { + newPageIndex = 0; + } + if (newPageIndex > self.view.contentSize.width / pageWidth) { + newPageIndex = ceil(self.view.contentSize.width / pageWidth) - 1.0; + } + } + _currentPageIndex = newPageIndex; + + *targetContentOffset = CGPointMake(newPageIndex * pageWidth, targetContentOffset->y); } @end diff --git a/AsyncDisplayKit/Details/ASDelegateProxy.m b/AsyncDisplayKit/Details/ASDelegateProxy.m index e70a51aa44..6a9e54e612 100644 --- a/AsyncDisplayKit/Details/ASDelegateProxy.m +++ b/AsyncDisplayKit/Details/ASDelegateProxy.m @@ -89,7 +89,8 @@ selector == @selector(collectionView:nodeForItemAtIndexPath:) || selector == @selector(collectionView:nodeBlockForItemAtIndexPath:) || selector == @selector(collectionView:numberOfItemsInSection:) || - selector == @selector(collectionView:constrainedSizeForNodeAtIndexPath:) + selector == @selector(collectionView:constrainedSizeForNodeAtIndexPath:) || + selector == @selector(scrollViewWillEndDragging:withVelocity:targetContentOffset:) ); }