ZKCycleScrollView.m 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505
  1. //
  2. // ZKCycleScrollView.m
  3. // ZKCycleScrollViewDemo-OC
  4. //
  5. // Created by bestdew on 2019/3/9.
  6. // Copyright © 2019 bestdew. All rights reserved.
  7. //
  8. // d*##$.
  9. // zP"""""$e. $" $o
  10. //4$ '$ $" $
  11. //'$ '$ J$ $F
  12. // 'b $k $> $
  13. // $k $r J$ d$
  14. // '$ $ $" $~
  15. // '$ "$ '$E $
  16. // $ $L $" $F ...
  17. // $. 4B $ $$$*"""*b
  18. // '$ $. $$ $$ $F
  19. // "$ R$ $F $" $
  20. // $k ?$ u* dF .$
  21. // ^$. $$" z$ u$$$$e
  22. // #$b $E.dW@e$" ?$
  23. // #$ .o$$# d$$$$c ?F
  24. // $ .d$$#" . zo$> #$r .uF
  25. // $L .u$*" $&$$$k .$$d$$F
  26. // $$" ""^"$$$P"$P9$
  27. // JP .o$$$$u:$P $$
  28. // $ ..ue$" "" $"
  29. // d$ $F $
  30. // $$ ....udE 4B
  31. // #$ """"` $r @$
  32. // ^$L '$ $F
  33. // RN 4N $
  34. // *$b d$
  35. // $$k $F
  36. // $$b $F
  37. // $"" $F
  38. // '$ $
  39. // $L $
  40. // '$ $
  41. // $ $
  42. #import "ZKCycleScrollView.h"
  43. #import "ZKCycleScrollViewFlowLayout.h"
  44. @interface ZKCycleScrollView () <UICollectionViewDelegate, UICollectionViewDataSource>
  45. @property (nonatomic, strong) ZKCycleScrollViewFlowLayout *flowLayout;
  46. @property (nonatomic, strong) UICollectionView *collectionView;
  47. @property (nonatomic, strong) UIPageControl *pageControl;
  48. @property (nonatomic, strong) NSTimer *timer;
  49. @property (nonatomic, assign) NSInteger fromIndex;
  50. @property (nonatomic, assign) NSInteger numberOfItems;
  51. @property (nonatomic, assign) BOOL itemSizeFlag;
  52. @property (nonatomic, assign) NSInteger indexOffset;
  53. @property (nonatomic, assign) BOOL configuredFlag;
  54. @property (nonatomic, assign) NSInteger tempIndex;
  55. @end
  56. @implementation ZKCycleScrollView
  57. #pragma mark -- Init
  58. - (instancetype)initWithFrame:(CGRect)frame {
  59. return [self initWithFrame:frame shouldInfiniteLoop:YES];
  60. }
  61. - (instancetype)initWithFrame:(CGRect)frame shouldInfiniteLoop:(BOOL)infiniteLoop {
  62. if (self = [super initWithFrame:frame]) {
  63. _infiniteLoop = infiniteLoop;
  64. [self initialization];
  65. }
  66. return self;
  67. }
  68. - (instancetype)initWithCoder:(NSCoder *)aDecoder {
  69. if (self = [super initWithCoder:aDecoder]) {
  70. _infiniteLoop = YES;
  71. [self initialization];
  72. }
  73. return self;
  74. }
  75. - (void)initialization {
  76. _autoScroll = YES;
  77. _itemZoomScale = 1.f;
  78. _allowsDragging = YES;
  79. _autoScrollInterval = 3.f;
  80. _pageIndicatorTintColor = [UIColor grayColor];
  81. _currentPageIndicatorTintColor = [UIColor whiteColor];
  82. _flowLayout = [[ZKCycleScrollViewFlowLayout alloc] init];
  83. _flowLayout.minimumLineSpacing = 0.f;
  84. _flowLayout.minimumInteritemSpacing = 0.f;
  85. _flowLayout.scrollDirection = UICollectionViewScrollDirectionHorizontal;
  86. _collectionView = [[UICollectionView alloc] initWithFrame:CGRectZero collectionViewLayout:_flowLayout];
  87. _collectionView.backgroundColor = nil;
  88. _collectionView.delegate = self;
  89. _collectionView.dataSource = self;
  90. _collectionView.bounces = NO;
  91. _collectionView.scrollsToTop = NO;
  92. _collectionView.showsHorizontalScrollIndicator = NO;
  93. _collectionView.showsVerticalScrollIndicator = NO;
  94. [self addSubview:_collectionView];
  95. _pageControl = [[UIPageControl alloc] init];
  96. _pageControl.enabled = NO;
  97. _pageControl.hidesForSinglePage = YES;
  98. _pageControl.pageIndicatorTintColor = _pageIndicatorTintColor;
  99. _pageControl.currentPageIndicatorTintColor = _currentPageIndicatorTintColor;
  100. [self addSubview:_pageControl];
  101. dispatch_async(dispatch_get_main_queue(), ^{
  102. [self configuration];
  103. if (_loadCompletion) _loadCompletion();
  104. });
  105. }
  106. - (void)layoutSubviews {
  107. [super layoutSubviews];
  108. if (_itemSizeFlag) {
  109. _flowLayout.itemSize = _itemSize;
  110. _flowLayout.headerReferenceSize = CGSizeMake((self.bounds.size.width - _itemSize.width) / 2, (self.bounds.size.height - _itemSize.height) / 2);
  111. _flowLayout.footerReferenceSize = CGSizeMake((self.bounds.size.width - _itemSize.width) / 2, (self.bounds.size.height - _itemSize.height) / 2);
  112. } else {
  113. _flowLayout.itemSize = self.bounds.size;
  114. _flowLayout.headerReferenceSize = CGSizeZero;
  115. _flowLayout.footerReferenceSize = CGSizeZero;
  116. }
  117. _collectionView.frame = self.bounds;
  118. _pageControl.frame = CGRectMake(0.f, self.bounds.size.height - 15.f, self.bounds.size.width, 15.f);
  119. }
  120. - (void)willMoveToSuperview:(UIView *)newSuperview {
  121. if (newSuperview == nil) [self removeTimer];
  122. }
  123. - (void)dealloc {
  124. _collectionView.delegate = nil;
  125. _collectionView.dataSource = nil;
  126. }
  127. #pragma mark -- Public Methods
  128. - (void)registerCellClass:(nullable Class)cellClass forCellWithReuseIdentifier:(NSString *)identifier {
  129. [_collectionView registerClass:cellClass forCellWithReuseIdentifier:identifier];
  130. }
  131. - (void)registerCellNib:(nullable UINib *)nib forCellWithReuseIdentifier:(NSString *)identifier {
  132. [_collectionView registerNib:nib forCellWithReuseIdentifier:identifier];
  133. }
  134. - (__kindof ZKCycleScrollViewCell *)dequeueReusableCellWithReuseIdentifier:(NSString *)identifier forIndex:(NSInteger)index {
  135. index = [self changeIndex:index];
  136. NSIndexPath *indexPath = [NSIndexPath indexPathForItem:index inSection:0];
  137. return [_collectionView dequeueReusableCellWithReuseIdentifier:identifier forIndexPath:indexPath];
  138. }
  139. - (void)reloadData {
  140. [self removeTimer];
  141. [UIView performWithoutAnimation:^{
  142. [_collectionView reloadData];
  143. }];
  144. [_collectionView performBatchUpdates:nil completion:^(BOOL finished) {
  145. [self configuration];
  146. if (_loadCompletion) _loadCompletion();
  147. }];
  148. }
  149. - (void)scrollToItemAtIndex:(NSInteger)index animated:(BOOL)animated {
  150. NSInteger numberOfAddedCells = [self numberOfAddedCells];
  151. if (index < 0 || index > _numberOfItems - numberOfAddedCells - 1) {
  152. NSLog(@"⚠️attempt to scroll to invalid index:%zd", index);
  153. return;
  154. }
  155. [self removeTimer];
  156. index += numberOfAddedCells / 2;
  157. UICollectionViewScrollPosition position = [self scrollPosition];
  158. NSIndexPath *indexPath = [NSIndexPath indexPathForItem:index inSection:0];
  159. [_collectionView scrollToItemAtIndexPath:indexPath atScrollPosition:position animated:animated];
  160. [self addTimer];
  161. }
  162. - (__kindof ZKCycleScrollViewCell *)cellForItemAtIndex:(NSInteger)index {
  163. NSInteger numberOfAddedCells = [self numberOfAddedCells];
  164. if (index < 0 || index > _numberOfItems - numberOfAddedCells - 1) {
  165. return nil;
  166. }
  167. index += numberOfAddedCells / 2;
  168. NSIndexPath *indexPath = [NSIndexPath indexPathForItem:index inSection:0];
  169. ZKCycleScrollViewCell *cell = [_collectionView cellForItemAtIndexPath:indexPath];
  170. return cell;
  171. }
  172. - (void)beginUpdates {
  173. _tempIndex = [self pageIndex];
  174. [self removeTimer];
  175. }
  176. - (void)endUpdates {
  177. [_flowLayout invalidateLayout];
  178. [self scrollToItemAtIndex:_tempIndex animated:NO];
  179. [self addTimer];
  180. }
  181. #pragma mark -- Private Methods
  182. - (UICollectionViewScrollPosition)scrollPosition {
  183. switch (_scrollDirection) {
  184. case ZKScrollDirectionVertical:
  185. return UICollectionViewScrollPositionCenteredVertically;
  186. default:
  187. return UICollectionViewScrollPositionCenteredHorizontally;
  188. }
  189. }
  190. - (NSInteger)changeIndex:(NSInteger)index {
  191. if (_infiniteLoop && _numberOfItems > 1) {
  192. if (index == 0) {
  193. index = _numberOfItems - 6;
  194. } else if (index == 1) {
  195. index = _numberOfItems - 5;
  196. } else if (index == _numberOfItems - 2) {
  197. index = 0;
  198. } else if (index == _numberOfItems - 1) {
  199. index = 1;
  200. } else {
  201. index -= 2;
  202. }
  203. }
  204. return index;
  205. }
  206. - (void)configuration {
  207. _fromIndex = 0;
  208. _indexOffset = 0;
  209. _configuredFlag = NO;
  210. if (_numberOfItems < 2) return;
  211. if (_infiniteLoop) {
  212. UICollectionViewScrollPosition position = [self scrollPosition];
  213. NSIndexPath *indexPath = [NSIndexPath indexPathForItem:2 inSection:0];
  214. [_collectionView scrollToItemAtIndexPath:indexPath atScrollPosition:position animated:NO];
  215. }
  216. [self addTimer];
  217. [self updatePageControl];
  218. _configuredFlag = YES;
  219. }
  220. - (void)addTimer {
  221. [self removeTimer];
  222. if (_numberOfItems < 2 || !_autoScroll || _autoScrollInterval <= 0.f) return;
  223. _timer = [NSTimer scheduledTimerWithTimeInterval:_autoScrollInterval target:self selector:@selector(automaticScroll) userInfo:nil repeats:YES];
  224. [[NSRunLoop mainRunLoop] addTimer:_timer forMode:NSRunLoopCommonModes];
  225. }
  226. - (void)automaticScroll {
  227. NSInteger index = [self currentIndex] + 1;
  228. if (!_infiniteLoop && index >= _numberOfItems) index = 0;
  229. UICollectionViewScrollPosition position = [self scrollPosition];
  230. NSIndexPath *indexPath = [NSIndexPath indexPathForItem:index inSection:0];
  231. [_collectionView scrollToItemAtIndexPath:indexPath atScrollPosition:position animated:YES];
  232. }
  233. - (void)removeTimer {
  234. if (!_timer) return;
  235. [_timer invalidate];
  236. _timer = nil;
  237. }
  238. - (void)updatePageControl {
  239. _pageControl.currentPage = 0;
  240. _pageControl.numberOfPages = MAX(0, _numberOfItems - [self numberOfAddedCells]);
  241. _pageControl.hidden = (_hidesPageControl || _pageControl.numberOfPages < 2);
  242. }
  243. #pragma mark -- UICollectionView DataSource
  244. - (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section {
  245. if ([_dataSource respondsToSelector:@selector(numberOfItemsInCycleScrollView:)]) {
  246. _numberOfItems = [_dataSource numberOfItemsInCycleScrollView:self];
  247. if (_infiniteLoop && _numberOfItems > 1) {
  248. _numberOfItems += [self numberOfAddedCells];
  249. }
  250. }
  251. return _numberOfItems;
  252. }
  253. - (__kindof UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath {
  254. NSInteger index = [self changeIndex:indexPath.item];
  255. return [_dataSource cycleScrollView:self cellForItemAtIndex:index];
  256. }
  257. #pragma mark -- UICollectionView Delegate
  258. - (BOOL)collectionView:(UICollectionView *)collectionView shouldHighlightItemAtIndexPath:(NSIndexPath *)indexPath {
  259. return YES;
  260. }
  261. - (void)collectionView:(UICollectionView *)collectionView didHighlightItemAtIndexPath:(NSIndexPath *)indexPath {
  262. if ([_delegate respondsToSelector:@selector(cycleScrollView:didSelectItemAtIndex:)]) {
  263. [self removeTimer];
  264. }
  265. }
  266. - (void)collectionView:(UICollectionView *)collectionView didUnhighlightItemAtIndexPath:(NSIndexPath *)indexPath {
  267. if ([_delegate respondsToSelector:@selector(cycleScrollView:didSelectItemAtIndex:)]) {
  268. [self addTimer];
  269. }
  270. }
  271. - (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath {
  272. if ([_delegate respondsToSelector:@selector(cycleScrollView:didSelectItemAtIndex:)]) {
  273. NSInteger index = [self changeIndex:indexPath.item];
  274. [_delegate cycleScrollView:self didSelectItemAtIndex:index];
  275. }
  276. }
  277. - (void)scrollViewDidScroll:(UIScrollView *)scrollView {
  278. _pageControl.currentPage = [self pageIndex];
  279. if (_configuredFlag && [_delegate respondsToSelector:@selector(cycleScrollViewDidScroll:progress:)]) {
  280. CGFloat total = 0.f, offset = 0.f;
  281. NSInteger numberOfAddedCells = [self numberOfAddedCells];
  282. switch (_scrollDirection) {
  283. case ZKScrollDirectionVertical:
  284. total = (_numberOfItems - 1 - numberOfAddedCells) * (_flowLayout.itemSize.height + _flowLayout.minimumLineSpacing);
  285. offset = fmod([self contentOffset].y, (_flowLayout.itemSize.height + _flowLayout.minimumLineSpacing) * (_numberOfItems - numberOfAddedCells));
  286. break;
  287. default:
  288. total = (_numberOfItems - 1 - numberOfAddedCells) * (_flowLayout.itemSize.width + _flowLayout.minimumLineSpacing);
  289. offset = fmod([self contentOffset].x, (_flowLayout.itemSize.width + _flowLayout.minimumLineSpacing) * (_numberOfItems - numberOfAddedCells));
  290. break;
  291. }
  292. CGFloat percent = offset / total;
  293. CGFloat progress = percent * (_numberOfItems - 1 - numberOfAddedCells);
  294. [_delegate cycleScrollViewDidScroll:self progress:progress];
  295. }
  296. }
  297. - (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView {
  298. [self removeTimer];
  299. }
  300. - (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate {
  301. [self addTimer];
  302. }
  303. - (void)scrollViewDidEndScrollingAnimation:(UIScrollView *)scrollView {
  304. NSInteger index = [self currentIndex];
  305. if (_infiniteLoop) {
  306. UICollectionViewScrollPosition position = [self scrollPosition];
  307. if (index == 1) {
  308. NSIndexPath *indexPath = [NSIndexPath indexPathForItem:_numberOfItems - 3 inSection:0];
  309. [_collectionView scrollToItemAtIndexPath:indexPath atScrollPosition:position animated:NO];
  310. } else if (index == _numberOfItems - 2) {
  311. NSIndexPath *indexPath = [NSIndexPath indexPathForItem:2 inSection:0];
  312. [_collectionView scrollToItemAtIndexPath:indexPath atScrollPosition:position animated:NO];
  313. }
  314. }
  315. NSInteger toIndex = [self changeIndex:index];
  316. if ([_delegate respondsToSelector:@selector(cycleScrollView:didScrollFromIndex:toIndex:)]) {
  317. [_delegate cycleScrollView:self didScrollFromIndex:_fromIndex toIndex:toIndex];
  318. }
  319. _fromIndex = toIndex;
  320. }
  321. - (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset {
  322. if ([self pageIndex] != _fromIndex) return;
  323. CGFloat sum = velocity.x + velocity.y;
  324. if (sum > 0) {
  325. _indexOffset = 1;
  326. } else if (sum < 0) {
  327. _indexOffset = -1;
  328. } else {
  329. _indexOffset = 0;
  330. }
  331. }
  332. - (void)scrollViewWillBeginDecelerating:(UIScrollView *)scrollView {
  333. NSInteger index = [self currentIndex] + _indexOffset;
  334. index = MAX(0, index);
  335. index = MIN(_numberOfItems - 1, index);
  336. UICollectionViewScrollPosition position = [self scrollPosition];
  337. NSIndexPath *indexPath = [NSIndexPath indexPathForItem:index inSection:0];
  338. [_collectionView scrollToItemAtIndexPath:indexPath atScrollPosition:position animated:YES];
  339. _indexOffset = 0;
  340. }
  341. #pragma mark -- Getter & Setter
  342. - (NSInteger)currentIndex {
  343. if (_numberOfItems <= 0) {
  344. return -1;
  345. }
  346. NSInteger index = 0;
  347. NSInteger minimumIndex = 0;
  348. NSInteger maximumIndex = _numberOfItems - 1;
  349. if (_numberOfItems == 1) {
  350. return index;
  351. }
  352. if (_infiniteLoop) {
  353. minimumIndex = 1;
  354. maximumIndex = _numberOfItems - 2;
  355. }
  356. switch (_scrollDirection) {
  357. case ZKScrollDirectionVertical:
  358. index = (_collectionView.contentOffset.y + (_flowLayout.itemSize.height + _flowLayout.minimumLineSpacing) / 2) / (_flowLayout.itemSize.height + _flowLayout.minimumLineSpacing);
  359. break;
  360. default:
  361. index = (_collectionView.contentOffset.x + (_flowLayout.itemSize.width + _flowLayout.minimumLineSpacing) / 2) / (_flowLayout.itemSize.width + _flowLayout.minimumLineSpacing);
  362. break;
  363. }
  364. return MIN(maximumIndex, MAX(minimumIndex, index));
  365. }
  366. - (NSInteger)pageIndex {
  367. return [self changeIndex:[self currentIndex]];
  368. }
  369. - (CGPoint)contentOffset {
  370. NSInteger numberOfAddedCells = [self numberOfAddedCells];
  371. switch (_scrollDirection) {
  372. case ZKScrollDirectionVertical:
  373. return CGPointMake(0.f, MAX(0.f, _collectionView.contentOffset.y - (_flowLayout.itemSize.height + _flowLayout.minimumLineSpacing) * numberOfAddedCells / 2));
  374. default:
  375. return CGPointMake(MAX(0.f, (_collectionView.contentOffset.x - (_flowLayout.itemSize.width + _flowLayout.minimumLineSpacing) * numberOfAddedCells / 2)), 0.f);
  376. }
  377. }
  378. - (NSInteger)numberOfAddedCells {
  379. return _infiniteLoop ? 4 : 0;
  380. }
  381. - (void)setScrollDirection:(ZKScrollDirection)scrollDirection {
  382. _scrollDirection = scrollDirection;
  383. switch (scrollDirection) {
  384. case ZKScrollDirectionVertical:
  385. _flowLayout.scrollDirection = UICollectionViewScrollDirectionVertical;
  386. break;
  387. default:
  388. _flowLayout.scrollDirection = UICollectionViewScrollDirectionHorizontal;
  389. break;
  390. }
  391. }
  392. - (void)setAutoScroll:(BOOL)autoScroll {
  393. _autoScroll = autoScroll;
  394. [self addTimer];
  395. }
  396. - (void)setAllowsDragging:(BOOL)allowsDragging {
  397. _allowsDragging = allowsDragging;
  398. _collectionView.scrollEnabled = allowsDragging;
  399. }
  400. - (void)setAutoScrollInterval:(NSTimeInterval)autoScrollInterval {
  401. _autoScrollInterval = autoScrollInterval;
  402. [self addTimer];
  403. }
  404. - (void)setItemSize:(CGSize)itemSize {
  405. _itemSizeFlag = YES;
  406. _itemSize = itemSize;
  407. _flowLayout.itemSize = itemSize;
  408. _flowLayout.headerReferenceSize = CGSizeMake((self.bounds.size.width - itemSize.width) / 2, (self.bounds.size.height - itemSize.height) / 2);
  409. _flowLayout.footerReferenceSize = CGSizeMake((self.bounds.size.width - itemSize.width) / 2, (self.bounds.size.height - itemSize.height) / 2);
  410. }
  411. - (void)setItemSpacing:(CGFloat)itemSpacing {
  412. _itemSpacing = itemSpacing;
  413. _flowLayout.minimumLineSpacing = itemSpacing;
  414. }
  415. - (void)setItemZoomScale:(CGFloat)itemZoomScale {
  416. _itemZoomScale = itemZoomScale;
  417. _flowLayout.zoomScale = itemZoomScale;
  418. }
  419. - (void)setHidesPageControl:(BOOL)hidesPageControl {
  420. _hidesPageControl = hidesPageControl;
  421. _pageControl.hidden = hidesPageControl;
  422. }
  423. - (void)setPageIndicatorTintColor:(UIColor *)pageIndicatorTintColor {
  424. _pageIndicatorTintColor = pageIndicatorTintColor;
  425. _pageControl.pageIndicatorTintColor = pageIndicatorTintColor;
  426. }
  427. - (void)setCurrentPageIndicatorTintColor:(UIColor *)currentPageIndicatorTintColor {
  428. _currentPageIndicatorTintColor = currentPageIndicatorTintColor;
  429. _pageControl.currentPageIndicatorTintColor = currentPageIndicatorTintColor;
  430. }
  431. @end