JXCategoryBaseView.m 31 KB

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