JXCategoryBaseView.m 30 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635
  1. //
  2. // JXCategoryBaseView.m
  3. // UI系列测试
  4. //
  5. // Created by jiaxin on 2018/3/15.
  6. // Copyright © 2018年 jiaxin. All rights reserved.
  7. //
  8. #import "JXCategoryBaseView.h"
  9. #import "JXCategoryFactory.h"
  10. #import "JXCategoryViewAnimator.h"
  11. struct DelegateFlags {
  12. unsigned int didSelectedItemAtIndexFlag : 1;
  13. unsigned int didClickSelectedItemAtIndexFlag : 1;
  14. unsigned int didScrollSelectedItemAtIndexFlag : 1;
  15. unsigned int didClickedItemContentScrollViewTransitionToIndexFlag : 1;
  16. unsigned int canClickItemAtIndexFlag : 1;
  17. unsigned int scrollingFromLeftIndexToRightIndexFlag : 1;
  18. };
  19. @interface JXCategoryBaseView () <UICollectionViewDataSource, UICollectionViewDelegate, UICollectionViewDelegateFlowLayout>
  20. @property (nonatomic, strong) JXCategoryCollectionView *collectionView;
  21. @property (nonatomic, assign) struct DelegateFlags delegateFlags;
  22. @property (nonatomic, assign) NSInteger selectedIndex;
  23. @property (nonatomic, assign) CGFloat innerCellSpacing;
  24. @property (nonatomic, assign) CGPoint lastContentViewContentOffset;
  25. @property (nonatomic, strong) JXCategoryViewAnimator *animator;
  26. // 正在滚动中的目标index。用于处理正在滚动列表的时候,立即点击item,会导致界面显示异常。
  27. @property (nonatomic, assign) NSInteger scrollingTargetIndex;
  28. @end
  29. @implementation JXCategoryBaseView
  30. - (void)dealloc
  31. {
  32. if (self.contentScrollView) {
  33. [self.contentScrollView removeObserver:self forKeyPath:@"contentOffset"];
  34. }
  35. [self.animator stop];
  36. }
  37. - (instancetype)initWithFrame:(CGRect)frame
  38. {
  39. self = [super initWithFrame:frame];
  40. if (self) {
  41. [self initializeData];
  42. [self initializeViews];
  43. }
  44. return self;
  45. }
  46. - (instancetype)initWithCoder:(NSCoder *)coder
  47. {
  48. self = [super initWithCoder:coder];
  49. if (self) {
  50. [self initializeData];
  51. [self initializeViews];
  52. }
  53. return self;
  54. }
  55. - (void)willMoveToSuperview:(UIView *)newSuperview {
  56. [super willMoveToSuperview:newSuperview];
  57. UIResponder *next = newSuperview;
  58. while (next != nil) {
  59. if ([next isKindOfClass:[UIViewController class]]) {
  60. ((UIViewController *)next).automaticallyAdjustsScrollViewInsets = NO;
  61. break;
  62. }
  63. next = next.nextResponder;
  64. }
  65. }
  66. - (void)reloadData {
  67. [self refreshDataSource];
  68. [self refreshState];
  69. [self.collectionView.collectionViewLayout invalidateLayout];
  70. [self.collectionView reloadData];
  71. }
  72. - (void)reloadCellAtIndex:(NSInteger)index {
  73. if (index < 0 || index >= self.dataSource.count) {
  74. return;
  75. }
  76. JXCategoryBaseCellModel *cellModel = self.dataSource[index];
  77. cellModel.selectedType = JXCategoryCellSelectedTypeUnknown;
  78. [self refreshCellModel:cellModel index:index];
  79. JXCategoryBaseCell *cell = (JXCategoryBaseCell *)[self.collectionView cellForItemAtIndexPath:[NSIndexPath indexPathForItem:index inSection:0]];
  80. [cell reloadData:cellModel];
  81. }
  82. - (void)selectItemAtIndex:(NSInteger)index {
  83. [self selectCellAtIndex:index selectedType:JXCategoryCellSelectedTypeCode];
  84. }
  85. - (void)layoutSubviews
  86. {
  87. [super layoutSubviews];
  88. //部分使用者为了适配不同的手机屏幕尺寸,JXCategoryView的宽高比要求保持一样,所以它的高度就会因为不同宽度的屏幕而不一样。计算出来的高度,有时候会是位数很长的浮点数,如果把这个高度设置给UICollectionView就会触发内部的一个错误。所以,为了规避这个问题,在这里对高度统一向下取整。
  89. //如果向下取整导致了你的页面异常,请自己重新设置JXCategoryView的高度,保证为整数即可。
  90. self.collectionView.frame = CGRectMake(0, 0, self.bounds.size.width, floor(self.bounds.size.height));
  91. [self reloadData];
  92. }
  93. #pragma mark - Setter
  94. - (void)setDelegate:(id<JXCategoryViewDelegate>)delegate {
  95. _delegate = delegate;
  96. _delegateFlags.didSelectedItemAtIndexFlag = [delegate respondsToSelector:@selector(categoryView:didSelectedItemAtIndex:)];
  97. _delegateFlags.didClickSelectedItemAtIndexFlag = [delegate respondsToSelector:@selector(categoryView:didClickSelectedItemAtIndex:)];
  98. _delegateFlags.didScrollSelectedItemAtIndexFlag = [delegate respondsToSelector:@selector(categoryView:didScrollSelectedItemAtIndex:)];
  99. _delegateFlags.didClickedItemContentScrollViewTransitionToIndexFlag = [delegate respondsToSelector:@selector(categoryView:didClickedItemContentScrollViewTransitionToIndex:)];
  100. _delegateFlags.canClickItemAtIndexFlag = [delegate respondsToSelector:@selector(categoryView:canClickItemAtIndex:)];
  101. _delegateFlags.scrollingFromLeftIndexToRightIndexFlag = [delegate respondsToSelector:@selector(categoryView:scrollingFromLeftIndex:toRightIndex:ratio:)];
  102. }
  103. - (void)setDefaultSelectedIndex:(NSInteger)defaultSelectedIndex
  104. {
  105. _defaultSelectedIndex = defaultSelectedIndex;
  106. self.selectedIndex = defaultSelectedIndex;
  107. }
  108. - (void)setContentScrollView:(UIScrollView *)contentScrollView
  109. {
  110. if (_contentScrollView != nil) {
  111. [_contentScrollView removeObserver:self forKeyPath:@"contentOffset"];
  112. }
  113. _contentScrollView = contentScrollView;
  114. self.contentScrollView.scrollsToTop = NO;
  115. [self.contentScrollView addObserver:self forKeyPath:@"contentOffset" options:NSKeyValueObservingOptionNew context:nil];
  116. }
  117. #pragma mark - <UICollectionViewDataSource, UICollectionViewDelegate>
  118. - (NSInteger)numberOfSectionsInCollectionView:(UICollectionView *)collectionView {
  119. return 1;
  120. }
  121. - (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section {
  122. return self.dataSource.count;
  123. }
  124. - (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath {
  125. return [collectionView dequeueReusableCellWithReuseIdentifier:NSStringFromClass([self preferredCellClass]) forIndexPath:indexPath];
  126. }
  127. - (void)collectionView:(UICollectionView *)collectionView willDisplayCell:(UICollectionViewCell *)cell forItemAtIndexPath:(NSIndexPath *)indexPath {
  128. JXCategoryBaseCellModel *cellModel = self.dataSource[indexPath.item];
  129. cellModel.selectedType = JXCategoryCellSelectedTypeUnknown;
  130. [(JXCategoryBaseCell *)cell reloadData:cellModel];
  131. }
  132. - (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath {
  133. BOOL isTransitionAnimating = NO;
  134. for (JXCategoryBaseCellModel *cellModel in self.dataSource) {
  135. if (cellModel.isTransitionAnimating) {
  136. isTransitionAnimating = YES;
  137. break;
  138. }
  139. }
  140. if (!isTransitionAnimating) {
  141. //当前没有正在过渡的item,才允许点击选中
  142. [self clickSelectItemAtIndex:indexPath.row];
  143. }
  144. }
  145. #pragma mark - <UICollectionViewDelegateFlowLayout>
  146. - (UIEdgeInsets)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout insetForSectionAtIndex:(NSInteger)section {
  147. return UIEdgeInsetsMake(0, [self getContentEdgeInsetLeft], 0, [self getContentEdgeInsetRight]);
  148. }
  149. - (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath {
  150. return CGSizeMake(self.dataSource[indexPath.item].cellWidth, self.collectionView.bounds.size.height);
  151. }
  152. - (CGFloat)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout minimumLineSpacingForSectionAtIndex:(NSInteger)section {
  153. return self.innerCellSpacing;
  154. }
  155. - (CGFloat)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout minimumInteritemSpacingForSectionAtIndex:(NSInteger)section {
  156. return self.innerCellSpacing;
  157. }
  158. #pragma mark - KVO
  159. - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
  160. if ([keyPath isEqualToString:@"contentOffset"]) {
  161. CGPoint contentOffset = [change[NSKeyValueChangeNewKey] CGPointValue];
  162. if ((self.contentScrollView.isTracking || self.contentScrollView.isDecelerating)) {
  163. //只处理用户滚动的情况
  164. [self contentOffsetOfContentScrollViewDidChanged:contentOffset];
  165. }
  166. self.lastContentViewContentOffset = contentOffset;
  167. }
  168. }
  169. #pragma mark - Private
  170. - (CGFloat)getContentEdgeInsetLeft {
  171. if (self.contentEdgeInsetLeft == JXCategoryViewAutomaticDimension) {
  172. return self.innerCellSpacing;
  173. }
  174. return self.contentEdgeInsetLeft;
  175. }
  176. - (CGFloat)getContentEdgeInsetRight {
  177. if (self.contentEdgeInsetRight == JXCategoryViewAutomaticDimension) {
  178. return self.innerCellSpacing;
  179. }
  180. return self.contentEdgeInsetRight;
  181. }
  182. - (CGFloat)getCellWidthAtIndex:(NSInteger)index {
  183. return [self preferredCellWidthAtIndex:index] + self.cellWidthIncrement;
  184. }
  185. - (void)clickSelectItemAtIndex:(NSInteger)index {
  186. if (self.delegateFlags.canClickItemAtIndexFlag && ![self.delegate categoryView:self canClickItemAtIndex:index]) {
  187. return;
  188. }
  189. [self selectCellAtIndex:index selectedType:JXCategoryCellSelectedTypeClick];
  190. }
  191. - (void)scrollSelectItemAtIndex:(NSInteger)index {
  192. [self selectCellAtIndex:index selectedType:JXCategoryCellSelectedTypeScroll];
  193. }
  194. @end
  195. @implementation JXCategoryBaseView (UISubclassingBaseHooks)
  196. - (CGRect)getTargetCellFrame:(NSInteger)targetIndex
  197. {
  198. CGFloat x = [self getContentEdgeInsetLeft];
  199. for (int i = 0; i < targetIndex; i ++) {
  200. JXCategoryBaseCellModel *cellModel = self.dataSource[i];
  201. CGFloat cellWidth;
  202. if (cellModel.isTransitionAnimating && cellModel.isCellWidthZoomEnabled) {
  203. //正在进行动画的时候,cellWidthCurrentZoomScale是随着动画渐变的,而没有立即更新到目标值
  204. if (cellModel.isSelected) {
  205. cellWidth = [self getCellWidthAtIndex:cellModel.index]*cellModel.cellWidthSelectedZoomScale;
  206. }else {
  207. cellWidth = [self getCellWidthAtIndex:cellModel.index]*cellModel.cellWidthNormalZoomScale;
  208. }
  209. }else {
  210. cellWidth = cellModel.cellWidth;
  211. }
  212. x += cellWidth + self.innerCellSpacing;
  213. }
  214. CGFloat width;
  215. JXCategoryBaseCellModel *selectedCellModel = self.dataSource[targetIndex];
  216. if (selectedCellModel.isTransitionAnimating && selectedCellModel.isCellWidthZoomEnabled) {
  217. width = [self getCellWidthAtIndex:selectedCellModel.index]*selectedCellModel.cellWidthSelectedZoomScale;
  218. }else {
  219. width = selectedCellModel.cellWidth;
  220. }
  221. return CGRectMake(x, 0, width, self.bounds.size.height);
  222. }
  223. - (CGRect)getTargetSelectedCellFrame:(NSInteger)targetIndex selectedType:(JXCategoryCellSelectedType)selectedType
  224. {
  225. CGFloat x = [self getContentEdgeInsetLeft];
  226. for (int i = 0; i < targetIndex; i ++) {
  227. JXCategoryBaseCellModel *cellModel = self.dataSource[i];
  228. x += [self getCellWidthAtIndex:cellModel.index] + self.innerCellSpacing;
  229. }
  230. CGFloat cellWidth = 0;
  231. JXCategoryBaseCellModel *selectedCellModel = self.dataSource[targetIndex];
  232. if (selectedCellModel.cellWidthZoomEnabled) {
  233. cellWidth = [self getCellWidthAtIndex:targetIndex]*selectedCellModel.cellWidthSelectedZoomScale;
  234. }else {
  235. cellWidth = [self getCellWidthAtIndex:targetIndex];
  236. }
  237. return CGRectMake(x, 0, cellWidth, self.bounds.size.height);
  238. }
  239. - (void)initializeData
  240. {
  241. _dataSource = [NSMutableArray array];
  242. _selectedIndex = 0;
  243. _cellWidth = JXCategoryViewAutomaticDimension;
  244. _cellWidthIncrement = 0;
  245. _cellSpacing = 20;
  246. _averageCellSpacingEnabled = YES;
  247. _cellWidthZoomEnabled = NO;
  248. _cellWidthZoomScale = 1.2;
  249. _cellWidthZoomScrollGradientEnabled = YES;
  250. _contentEdgeInsetLeft = JXCategoryViewAutomaticDimension;
  251. _contentEdgeInsetRight = JXCategoryViewAutomaticDimension;
  252. _lastContentViewContentOffset = CGPointZero;
  253. _selectedAnimationEnabled = NO;
  254. _selectedAnimationDuration = 0.25;
  255. _scrollingTargetIndex = -1;
  256. _contentScrollViewClickTransitionAnimationEnabled = YES;
  257. }
  258. - (void)initializeViews
  259. {
  260. UICollectionViewFlowLayout *layout = [[UICollectionViewFlowLayout alloc] init];
  261. layout.scrollDirection = UICollectionViewScrollDirectionHorizontal;
  262. _collectionView = [[JXCategoryCollectionView alloc] initWithFrame:self.bounds collectionViewLayout:layout];
  263. self.collectionView.backgroundColor = [UIColor clearColor];
  264. self.collectionView.showsHorizontalScrollIndicator = NO;
  265. self.collectionView.showsVerticalScrollIndicator = NO;
  266. self.collectionView.scrollsToTop = NO;
  267. self.collectionView.dataSource = self;
  268. self.collectionView.delegate = self;
  269. [self.collectionView registerClass:[self preferredCellClass] forCellWithReuseIdentifier:NSStringFromClass([self preferredCellClass])];
  270. if (@available(iOS 10.0, *)) {
  271. self.collectionView.prefetchingEnabled = NO;
  272. }
  273. if (@available(iOS 11.0, *)) {
  274. self.collectionView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever;
  275. }
  276. [self addSubview:self.collectionView];
  277. }
  278. - (void)refreshDataSource {
  279. }
  280. - (void)refreshState {
  281. if (self.selectedIndex < 0 || self.selectedIndex >= self.dataSource.count) {
  282. self.selectedIndex = 0;
  283. }
  284. self.innerCellSpacing = self.cellSpacing;
  285. //总的内容宽度(左边距+cell总宽度+总cellSpacing+右边距)
  286. __block CGFloat totalItemWidth = [self getContentEdgeInsetLeft];
  287. //总的cell宽度
  288. CGFloat totalCellWidth = 0;
  289. for (int i = 0; i < self.dataSource.count; i++) {
  290. JXCategoryBaseCellModel *cellModel = self.dataSource[i];
  291. cellModel.index = i;
  292. cellModel.cellWidthZoomEnabled = self.cellWidthZoomEnabled;
  293. cellModel.cellWidthNormalZoomScale = 1;
  294. cellModel.cellWidthSelectedZoomScale = self.cellWidthZoomScale;
  295. cellModel.selectedAnimationEnabled = self.selectedAnimationEnabled;
  296. cellModel.selectedAnimationDuration = self.selectedAnimationDuration;
  297. cellModel.cellSpacing = self.innerCellSpacing;
  298. if (i == self.selectedIndex) {
  299. cellModel.selected = YES;
  300. cellModel.cellWidthCurrentZoomScale = cellModel.cellWidthSelectedZoomScale;
  301. }else {
  302. cellModel.selected = NO;
  303. cellModel.cellWidthCurrentZoomScale = cellModel.cellWidthNormalZoomScale;
  304. }
  305. if (self.isCellWidthZoomEnabled) {
  306. cellModel.cellWidth = [self getCellWidthAtIndex:i]*cellModel.cellWidthCurrentZoomScale;
  307. }else {
  308. cellModel.cellWidth = [self getCellWidthAtIndex:i];
  309. }
  310. totalCellWidth += cellModel.cellWidth;
  311. if (i == self.dataSource.count - 1) {
  312. totalItemWidth += cellModel.cellWidth + [self getContentEdgeInsetRight];
  313. }else {
  314. totalItemWidth += cellModel.cellWidth + self.innerCellSpacing;
  315. }
  316. [self refreshCellModel:cellModel index:i];
  317. }
  318. if (self.isAverageCellSpacingEnabled && totalItemWidth < self.bounds.size.width) {
  319. //如果总的内容宽度都没有超过视图宽度,就将cellSpacing等分
  320. NSInteger cellSpacingItemCount = self.dataSource.count - 1;
  321. CGFloat totalCellSpacingWidth = self.bounds.size.width - totalCellWidth;
  322. //如果内容左边距是Automatic,就加1
  323. if (self.contentEdgeInsetLeft == JXCategoryViewAutomaticDimension) {
  324. cellSpacingItemCount += 1;
  325. }else {
  326. totalCellSpacingWidth -= self.contentEdgeInsetLeft;
  327. }
  328. //如果内容右边距是Automatic,就加1
  329. if (self.contentEdgeInsetRight == JXCategoryViewAutomaticDimension) {
  330. cellSpacingItemCount += 1;
  331. }else {
  332. totalCellSpacingWidth -= self.contentEdgeInsetRight;
  333. }
  334. CGFloat cellSpacing = 0;
  335. if (cellSpacingItemCount > 0) {
  336. cellSpacing = totalCellSpacingWidth/cellSpacingItemCount;
  337. }
  338. self.innerCellSpacing = cellSpacing;
  339. [self.dataSource enumerateObjectsUsingBlock:^(JXCategoryBaseCellModel * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
  340. obj.cellSpacing = self.innerCellSpacing;
  341. }];
  342. }
  343. //---------------------定位collectionView到当前选中的位置----------------------
  344. //因为初始化的时候,collectionView并没有初始化完,cell都没有被加载出来。只有自己手动计算当前选中的index的位置,然后更新到contentOffset
  345. __block CGFloat frameXOfSelectedCell = self.innerCellSpacing;
  346. __block CGFloat selectedCellWidth = 0;
  347. totalItemWidth = [self getContentEdgeInsetLeft];
  348. [self.dataSource enumerateObjectsUsingBlock:^(JXCategoryBaseCellModel * cellModel, NSUInteger idx, BOOL * _Nonnull stop) {
  349. if (idx < self.selectedIndex) {
  350. frameXOfSelectedCell += cellModel.cellWidth + self.innerCellSpacing;
  351. }else if (idx == self.selectedIndex) {
  352. selectedCellWidth = cellModel.cellWidth;
  353. }
  354. if (idx == self.dataSource.count - 1) {
  355. totalItemWidth += cellModel.cellWidth + [self getContentEdgeInsetRight];
  356. }else {
  357. totalItemWidth += cellModel.cellWidth + self.innerCellSpacing;
  358. }
  359. }];
  360. CGFloat minX = 0;
  361. CGFloat maxX = totalItemWidth - self.bounds.size.width;
  362. CGFloat targetX = frameXOfSelectedCell - self.bounds.size.width/2.0 + selectedCellWidth/2.0;
  363. [self.collectionView setContentOffset:CGPointMake(MAX(MIN(maxX, targetX), minX), 0) animated:NO];
  364. //---------------------定位collectionView到当前选中的位置----------------------
  365. if (CGRectEqualToRect(self.contentScrollView.frame, CGRectZero) && self.contentScrollView.superview != nil) {
  366. //某些情况系统会出现JXCategoryView先布局,contentScrollView后布局。就会导致下面指定defaultSelectedIndex失效,所以发现contentScrollView的frame为zero时,强行触发其父视图链里面已经有frame的一个父视图的layoutSubviews方法。
  367. //比如JXSegmentedListContainerView会将contentScrollView包裹起来使用,该情况需要JXSegmentedListContainerView.superView触发布局更新
  368. UIView *parentView = self.contentScrollView.superview;
  369. while (parentView != nil && CGRectEqualToRect(parentView.frame, CGRectZero)) {
  370. parentView = parentView.superview;
  371. }
  372. [parentView setNeedsLayout];
  373. [parentView layoutIfNeeded];
  374. }
  375. //将contentScrollView的contentOffset定位到当前选中index的位置
  376. [self.contentScrollView setContentOffset:CGPointMake(self.selectedIndex*self.contentScrollView.bounds.size.width, 0) animated:NO];
  377. }
  378. - (BOOL)selectCellAtIndex:(NSInteger)targetIndex selectedType:(JXCategoryCellSelectedType)selectedType {
  379. if (targetIndex < 0 || targetIndex >= self.dataSource.count) {
  380. return NO;
  381. }
  382. if (self.selectedIndex == targetIndex) {
  383. //目标index和当前选中的index相等,就不需要处理后续的选中更新逻辑,只需要回调代理方法即可。
  384. if (selectedType == JXCategoryCellSelectedTypeClick) {
  385. if (self.delegateFlags.didClickSelectedItemAtIndexFlag) {
  386. [self.delegate categoryView:self didClickSelectedItemAtIndex:targetIndex];
  387. }
  388. }else if (selectedType == JXCategoryCellSelectedTypeScroll) {
  389. if (self.delegateFlags.didScrollSelectedItemAtIndexFlag) {
  390. [self.delegate categoryView:self didScrollSelectedItemAtIndex:targetIndex];
  391. }
  392. }
  393. if (self.delegateFlags.didSelectedItemAtIndexFlag) {
  394. [self.delegate categoryView:self didSelectedItemAtIndex:targetIndex];
  395. }
  396. self.scrollingTargetIndex = -1;
  397. return NO;
  398. }
  399. //通知子类刷新当前选中的和将要选中的cellModel
  400. JXCategoryBaseCellModel *lastCellModel = self.dataSource[self.selectedIndex];
  401. lastCellModel.selectedType = selectedType;
  402. JXCategoryBaseCellModel *selectedCellModel = self.dataSource[targetIndex];
  403. selectedCellModel.selectedType = selectedType;
  404. [self refreshSelectedCellModel:selectedCellModel unselectedCellModel:lastCellModel];
  405. //刷新当前选中的和将要选中的cell
  406. JXCategoryBaseCell *lastCell = (JXCategoryBaseCell *)[self.collectionView cellForItemAtIndexPath:[NSIndexPath indexPathForItem:self.selectedIndex inSection:0]];
  407. [lastCell reloadData:lastCellModel];
  408. JXCategoryBaseCell *selectedCell = (JXCategoryBaseCell *)[self.collectionView cellForItemAtIndexPath:[NSIndexPath indexPathForItem:targetIndex inSection:0]];
  409. [selectedCell reloadData:selectedCellModel];
  410. if (self.scrollingTargetIndex != -1 && self.scrollingTargetIndex != targetIndex) {
  411. JXCategoryBaseCellModel *scrollingTargetCellModel = self.dataSource[self.scrollingTargetIndex];
  412. scrollingTargetCellModel.selected = NO;
  413. scrollingTargetCellModel.selectedType = selectedType;
  414. [self refreshSelectedCellModel:selectedCellModel unselectedCellModel:scrollingTargetCellModel];
  415. JXCategoryBaseCell *scrollingTargetCell = (JXCategoryBaseCell *)[self.collectionView cellForItemAtIndexPath:[NSIndexPath indexPathForItem:self.scrollingTargetIndex inSection:0]];
  416. [scrollingTargetCell reloadData:scrollingTargetCellModel];
  417. }
  418. if (self.isCellWidthZoomEnabled) {
  419. [self.collectionView.collectionViewLayout invalidateLayout];
  420. //延时为了解决cellwidth变化,点击最后几个cell,scrollToItem会出现位置偏移bu。需要等cellWidth动画渐变结束后再滚动到index的cell位置。
  421. dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(self.selectedAnimationDuration * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
  422. [self.collectionView scrollToItemAtIndexPath:[NSIndexPath indexPathForItem:targetIndex inSection:0] atScrollPosition:UICollectionViewScrollPositionCenteredHorizontally animated:YES];
  423. });
  424. }else {
  425. [self.collectionView scrollToItemAtIndexPath:[NSIndexPath indexPathForItem:targetIndex inSection:0] atScrollPosition:UICollectionViewScrollPositionCenteredHorizontally animated:YES];
  426. }
  427. if (selectedType == JXCategoryCellSelectedTypeClick ||
  428. selectedType == JXCategoryCellSelectedTypeCode) {
  429. if (self.delegateFlags.didClickedItemContentScrollViewTransitionToIndexFlag) {
  430. [self.delegate categoryView:self didClickedItemContentScrollViewTransitionToIndex:targetIndex];
  431. }else {
  432. [self.contentScrollView setContentOffset:CGPointMake(targetIndex*self.contentScrollView.bounds.size.width, 0) animated:self.isContentScrollViewClickTransitionAnimationEnabled];
  433. }
  434. }
  435. self.selectedIndex = targetIndex;
  436. if (selectedType == JXCategoryCellSelectedTypeClick) {
  437. if (self.delegateFlags.didClickSelectedItemAtIndexFlag) {
  438. [self.delegate categoryView:self didClickSelectedItemAtIndex:targetIndex];
  439. }
  440. }else if(selectedType == JXCategoryCellSelectedTypeScroll) {
  441. if (self.delegateFlags.didScrollSelectedItemAtIndexFlag) {
  442. [self.delegate categoryView:self didScrollSelectedItemAtIndex:targetIndex];
  443. }
  444. }
  445. if (self.delegateFlags.didSelectedItemAtIndexFlag) {
  446. [self.delegate categoryView:self didSelectedItemAtIndex:targetIndex];
  447. }
  448. self.scrollingTargetIndex = -1;
  449. return YES;
  450. }
  451. - (void)refreshSelectedCellModel:(JXCategoryBaseCellModel *)selectedCellModel unselectedCellModel:(JXCategoryBaseCellModel *)unselectedCellModel {
  452. selectedCellModel.selected = YES;
  453. unselectedCellModel.selected = NO;
  454. if (self.isCellWidthZoomEnabled) {
  455. if (selectedCellModel.selectedType == JXCategoryCellSelectedTypeCode ||
  456. selectedCellModel.selectedType == JXCategoryCellSelectedTypeClick) {
  457. self.animator = [[JXCategoryViewAnimator alloc] init];
  458. self.animator.duration = self.selectedAnimationDuration;
  459. __weak typeof(self) weakSelf = self;
  460. self.animator.progressCallback = ^(CGFloat percent) {
  461. selectedCellModel.transitionAnimating = YES;
  462. unselectedCellModel.transitionAnimating = YES;
  463. selectedCellModel.cellWidthCurrentZoomScale = [JXCategoryFactory interpolationFrom:selectedCellModel.cellWidthNormalZoomScale to:selectedCellModel.cellWidthSelectedZoomScale percent:percent];
  464. selectedCellModel.cellWidth = [weakSelf getCellWidthAtIndex:selectedCellModel.index] * selectedCellModel.cellWidthCurrentZoomScale;
  465. unselectedCellModel.cellWidthCurrentZoomScale = [JXCategoryFactory interpolationFrom:unselectedCellModel.cellWidthSelectedZoomScale to:unselectedCellModel.cellWidthNormalZoomScale percent:percent];
  466. unselectedCellModel.cellWidth = [weakSelf getCellWidthAtIndex:unselectedCellModel.index] * unselectedCellModel.cellWidthCurrentZoomScale;
  467. [weakSelf.collectionView.collectionViewLayout invalidateLayout];
  468. };
  469. self.animator.completeCallback = ^{
  470. selectedCellModel.transitionAnimating = NO;
  471. unselectedCellModel.transitionAnimating = NO;
  472. };
  473. [self.animator start];
  474. }else {
  475. selectedCellModel.cellWidthCurrentZoomScale = selectedCellModel.cellWidthSelectedZoomScale;
  476. selectedCellModel.cellWidth = [self getCellWidthAtIndex:selectedCellModel.index] * selectedCellModel.cellWidthCurrentZoomScale;
  477. unselectedCellModel.cellWidthCurrentZoomScale = unselectedCellModel.cellWidthNormalZoomScale;
  478. unselectedCellModel.cellWidth = [self getCellWidthAtIndex:unselectedCellModel.index] * unselectedCellModel.cellWidthCurrentZoomScale;
  479. }
  480. }
  481. }
  482. - (void)contentOffsetOfContentScrollViewDidChanged:(CGPoint)contentOffset {
  483. CGFloat ratio = contentOffset.x/self.contentScrollView.bounds.size.width;
  484. if (ratio > self.dataSource.count - 1 || ratio < 0) {
  485. //超过了边界,不需要处理
  486. return;
  487. }
  488. if (contentOffset.x == 0 && self.selectedIndex == 0 && self.lastContentViewContentOffset.x == 0) {
  489. //滚动到了最左边,且已经选中了第一个,且之前的contentOffset.x为0
  490. return;
  491. }
  492. CGFloat maxContentOffsetX = self.contentScrollView.contentSize.width - self.contentScrollView.bounds.size.width;
  493. if (contentOffset.x == maxContentOffsetX && self.selectedIndex == self.dataSource.count - 1 && self.lastContentViewContentOffset.x == maxContentOffsetX) {
  494. //滚动到了最右边,且已经选中了最后一个,且之前的contentOffset.x为maxContentOffsetX
  495. return;
  496. }
  497. ratio = MAX(0, MIN(self.dataSource.count - 1, ratio));
  498. NSInteger baseIndex = floorf(ratio);
  499. CGFloat remainderRatio = ratio - baseIndex;
  500. if (remainderRatio == 0) {
  501. //快速滑动翻页,用户一直在拖拽contentScrollView,需要更新选中状态
  502. //滑动一小段距离,然后放开回到原位,contentOffset同样的值会回调多次。例如在index为1的情况,滑动放开回到原位,contentOffset会多次回调CGPoint(width, 0)
  503. if (!(self.lastContentViewContentOffset.x == contentOffset.x && self.selectedIndex == baseIndex)) {
  504. [self scrollSelectItemAtIndex:baseIndex];
  505. }
  506. }else {
  507. if (self.animator.isExecuting) {
  508. [self.animator invalid];
  509. //需要重置之前animator.progessCallback为处理完的状态
  510. for (JXCategoryBaseCellModel *model in self.dataSource) {
  511. if (model.isSelected) {
  512. model.cellWidthCurrentZoomScale = model.cellWidthSelectedZoomScale;
  513. model.cellWidth = [self getCellWidthAtIndex:model.index] * model.cellWidthCurrentZoomScale;
  514. }else {
  515. model.cellWidthCurrentZoomScale = model.cellWidthNormalZoomScale;
  516. model.cellWidth = [self getCellWidthAtIndex:model.index] * model.cellWidthCurrentZoomScale;
  517. }
  518. }
  519. }
  520. //快速滑动翻页,当remainderRatio没有变成0,但是已经翻页了,需要通过下面的判断,触发选中
  521. if (fabs(ratio - self.selectedIndex) > 1) {
  522. NSInteger targetIndex = baseIndex;
  523. if (ratio < self.selectedIndex) {
  524. targetIndex = baseIndex + 1;
  525. }
  526. [self scrollSelectItemAtIndex:targetIndex];
  527. }
  528. if (self.selectedIndex == baseIndex) {
  529. self.scrollingTargetIndex = baseIndex + 1;
  530. }else {
  531. self.scrollingTargetIndex = baseIndex;
  532. }
  533. if (self.isCellWidthZoomEnabled && self.isCellWidthZoomScrollGradientEnabled) {
  534. JXCategoryBaseCellModel *leftCellModel = (JXCategoryBaseCellModel *)self.dataSource[baseIndex];
  535. JXCategoryBaseCellModel *rightCellModel = (JXCategoryBaseCellModel *)self.dataSource[baseIndex + 1];
  536. leftCellModel.cellWidthCurrentZoomScale = [JXCategoryFactory interpolationFrom:leftCellModel.cellWidthSelectedZoomScale to:leftCellModel.cellWidthNormalZoomScale percent:remainderRatio];
  537. leftCellModel.cellWidth = [self getCellWidthAtIndex:leftCellModel.index] * leftCellModel.cellWidthCurrentZoomScale;
  538. rightCellModel.cellWidthCurrentZoomScale = [JXCategoryFactory interpolationFrom:rightCellModel.cellWidthNormalZoomScale to:rightCellModel.cellWidthSelectedZoomScale percent:remainderRatio];
  539. rightCellModel.cellWidth = [self getCellWidthAtIndex:rightCellModel.index] * rightCellModel.cellWidthCurrentZoomScale;
  540. [self.collectionView.collectionViewLayout invalidateLayout];
  541. }
  542. if (self.delegateFlags.scrollingFromLeftIndexToRightIndexFlag) {
  543. [self.delegate categoryView:self scrollingFromLeftIndex:baseIndex toRightIndex:baseIndex + 1 ratio:remainderRatio];
  544. }
  545. }
  546. }
  547. - (CGFloat)preferredCellWidthAtIndex:(NSInteger)index {
  548. return 0;
  549. }
  550. - (Class)preferredCellClass {
  551. return JXCategoryBaseCell.class;
  552. }
  553. - (void)refreshCellModel:(JXCategoryBaseCellModel *)cellModel index:(NSInteger)index {
  554. }
  555. @end