JXCategoryBaseView.m 33 KB

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