JXPagerView.m 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410
  1. //
  2. // JXPagerView.m
  3. // JXPagerView
  4. //
  5. // Created by jiaxin on 2018/8/27.
  6. // Copyright © 2018年 jiaxin. All rights reserved.
  7. //
  8. #import "JXPagerView.h"
  9. @class JXPagerListContainerScrollView;
  10. @class JXPagerListContainerCollectionView;
  11. @interface JXPagerView () <UITableViewDataSource, UITableViewDelegate, JXPagerListContainerViewDelegate>
  12. @property (nonatomic, weak) id<JXPagerViewDelegate> delegate;
  13. @property (nonatomic, strong) JXPagerMainTableView *mainTableView;
  14. @property (nonatomic, strong) JXPagerListContainerView *listContainerView;
  15. @property (nonatomic, strong) UIScrollView *currentScrollingListView;
  16. @property (nonatomic, strong) id<JXPagerViewListViewDelegate> currentList;
  17. @property (nonatomic, strong) NSMutableDictionary <NSNumber *, id<JXPagerViewListViewDelegate>> *validListDict;
  18. @property (nonatomic, strong) UIView *tableHeaderContainerView;
  19. @property (nonatomic, strong) NSMutableDictionary<NSString *, id<JXPagerViewListViewDelegate>> *listCache;
  20. @end
  21. @implementation JXPagerView
  22. - (instancetype)initWithDelegate:(id<JXPagerViewDelegate>)delegate {
  23. return [self initWithDelegate:delegate listContainerType:JXPagerListContainerType_CollectionView];
  24. }
  25. - (instancetype)initWithDelegate:(id<JXPagerViewDelegate>)delegate listContainerType:(JXPagerListContainerType)type {
  26. self = [super initWithFrame:CGRectZero];
  27. if (self) {
  28. _delegate = delegate;
  29. _validListDict = [NSMutableDictionary dictionary];
  30. _automaticallyDisplayListVerticalScrollIndicator = YES;
  31. _isListHorizontalScrollEnabled = YES;
  32. _mainTableView = [[JXPagerMainTableView alloc] initWithFrame:CGRectZero style:UITableViewStylePlain];
  33. self.mainTableView.showsVerticalScrollIndicator = NO;
  34. self.mainTableView.showsHorizontalScrollIndicator = NO;
  35. self.mainTableView.separatorStyle = UITableViewCellSeparatorStyleNone;
  36. self.mainTableView.scrollsToTop = NO;
  37. self.mainTableView.dataSource = self;
  38. self.mainTableView.delegate = self;
  39. [self refreshTableHeaderView];
  40. [self.mainTableView registerClass:[UITableViewCell class] forCellReuseIdentifier:@"cell"];
  41. if (@available(iOS 11.0, *)) {
  42. self.mainTableView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever;
  43. }
  44. #if __IPHONE_OS_VERSION_MAX_ALLOWED >= 150000
  45. if (@available(iOS 15.0, *)) {
  46. self.mainTableView.sectionHeaderTopPadding = 0;
  47. }
  48. #endif
  49. [self addSubview:self.mainTableView];
  50. _listContainerView = [[JXPagerListContainerView alloc] initWithType:type delegate:self];
  51. }
  52. return self;
  53. }
  54. - (void)layoutSubviews {
  55. [super layoutSubviews];
  56. if (!CGRectEqualToRect(self.bounds, self.mainTableView.frame)) {
  57. self.mainTableView.frame = self.bounds;
  58. [self.mainTableView reloadData];
  59. }
  60. }
  61. - (void)setDefaultSelectedIndex:(NSInteger)defaultSelectedIndex {
  62. _defaultSelectedIndex = defaultSelectedIndex;
  63. self.listContainerView.defaultSelectedIndex = defaultSelectedIndex;
  64. }
  65. - (void)setIsListHorizontalScrollEnabled:(BOOL)isListHorizontalScrollEnabled {
  66. _isListHorizontalScrollEnabled = isListHorizontalScrollEnabled;
  67. self.listContainerView.scrollView.scrollEnabled = isListHorizontalScrollEnabled;
  68. }
  69. - (void)reloadData {
  70. self.currentList = nil;
  71. self.currentScrollingListView = nil;
  72. [_validListDict removeAllObjects];
  73. //根据新数据删除不需要的list
  74. if (self.allowsCacheList) {
  75. NSMutableArray *newListIdentifierArray = [NSMutableArray array];
  76. if (self.delegate && [self.delegate respondsToSelector:@selector(numberOfListsInPagerView:)]) {
  77. NSInteger listCount = [self.delegate numberOfListsInPagerView:self];
  78. for (NSInteger index = 0; index < listCount; index ++) {
  79. if (self.delegate && [self.delegate respondsToSelector:@selector(pagerView:listIdentifierAtIndex:)]) {
  80. NSString *listIdentifier = [self.delegate pagerView:self listIdentifierAtIndex:index];
  81. [newListIdentifierArray addObject:listIdentifier];
  82. }
  83. }
  84. }
  85. NSArray *existedKeys = self.listCache.allKeys;
  86. for (NSString *listIdentifier in existedKeys) {
  87. if (![newListIdentifierArray containsObject:listIdentifier]) {
  88. [self.listCache removeObjectForKey:listIdentifier];
  89. }
  90. }
  91. }
  92. [self refreshTableHeaderView];
  93. if (self.pinSectionHeaderVerticalOffset != 0 && self.mainTableView.contentOffset.y > self.pinSectionHeaderVerticalOffset) {
  94. self.mainTableView.contentOffset = CGPointZero;
  95. }
  96. [self.mainTableView reloadData];
  97. [self.listContainerView reloadData];
  98. }
  99. - (void)resizeTableHeaderViewHeightWithAnimatable:(BOOL)animatable duration:(NSTimeInterval)duration curve:(UIViewAnimationCurve)curve {
  100. if (animatable) {
  101. UIViewAnimationOptions options = UIViewAnimationOptionCurveLinear;
  102. switch (curve) {
  103. case UIViewAnimationCurveEaseIn: options = UIViewAnimationOptionCurveEaseIn; break;
  104. case UIViewAnimationCurveEaseOut: options = UIViewAnimationOptionCurveEaseOut; break;
  105. case UIViewAnimationCurveEaseInOut: options = UIViewAnimationOptionCurveEaseInOut; break;
  106. default: break;
  107. }
  108. [UIView animateWithDuration:duration delay:0 options:options animations:^{
  109. CGRect frame = self.tableHeaderContainerView.bounds;
  110. frame.size.height = [self.delegate tableHeaderViewHeightInPagerView:self];
  111. self.tableHeaderContainerView.frame = frame;
  112. self.mainTableView.tableHeaderView = self.tableHeaderContainerView;
  113. [self.mainTableView setNeedsLayout];
  114. [self.mainTableView layoutIfNeeded];
  115. } completion:^(BOOL finished) { }];
  116. }else {
  117. CGRect frame = self.tableHeaderContainerView.bounds;
  118. frame.size.height = [self.delegate tableHeaderViewHeightInPagerView:self];
  119. self.tableHeaderContainerView.frame = frame;
  120. self.mainTableView.tableHeaderView = self.tableHeaderContainerView;
  121. }
  122. }
  123. #pragma mark - Private
  124. - (void)refreshTableHeaderView {
  125. UIView *tableHeaderView = [self.delegate tableHeaderViewInPagerView:self];
  126. UIView *containerView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 0, [self.delegate tableHeaderViewHeightInPagerView:self])];
  127. [containerView addSubview:tableHeaderView];
  128. tableHeaderView.translatesAutoresizingMaskIntoConstraints = NO;
  129. NSLayoutConstraint *top = [NSLayoutConstraint constraintWithItem:tableHeaderView attribute:NSLayoutAttributeTop relatedBy:NSLayoutRelationEqual toItem:containerView attribute:NSLayoutAttributeTop multiplier:1 constant:0];
  130. NSLayoutConstraint *leading = [NSLayoutConstraint constraintWithItem:tableHeaderView attribute:NSLayoutAttributeLeading relatedBy:NSLayoutRelationEqual toItem:containerView attribute:NSLayoutAttributeLeading multiplier:1 constant:0];
  131. NSLayoutConstraint *bottom = [NSLayoutConstraint constraintWithItem:tableHeaderView attribute:NSLayoutAttributeBottom relatedBy:NSLayoutRelationEqual toItem:containerView attribute:NSLayoutAttributeBottom multiplier:1 constant:0];
  132. NSLayoutConstraint *trailing = [NSLayoutConstraint constraintWithItem:tableHeaderView attribute:NSLayoutAttributeTrailing relatedBy:NSLayoutRelationEqual toItem:containerView attribute:NSLayoutAttributeTrailing multiplier:1 constant:0];
  133. [containerView addConstraints:@[top, leading, bottom, trailing]];
  134. self.tableHeaderContainerView = containerView;
  135. self.mainTableView.tableHeaderView = containerView;
  136. }
  137. - (void)adjustMainScrollViewToTargetContentInsetIfNeeded:(UIEdgeInsets)insets {
  138. if (UIEdgeInsetsEqualToEdgeInsets(insets, self.mainTableView.contentInset) == NO) {
  139. self.mainTableView.delegate = nil;
  140. self.mainTableView.contentInset = insets;
  141. self.mainTableView.delegate = self;
  142. }
  143. }
  144. - (void)listViewDidScroll:(UIScrollView *)scrollView {
  145. self.currentScrollingListView = scrollView;
  146. [self preferredProcessListViewDidScroll:scrollView];
  147. }
  148. //仅用于处理设置了pinSectionHeaderVerticalOffset,又添加了MJRefresh的下拉刷新。这种情况会导致JXPagingView和MJRefresh来回设置contentInset值。针对这种及其特殊的情况,就内部特殊处理了。通过下面的判断条件,来判定当前是否处于下拉刷新中。请勿让pinSectionHeaderVerticalOffset和下拉刷新设置的contentInset.top值相同。
  149. //具体原因参考:https://github.com/pujiaxin33/JXPagingView/issues/203
  150. - (BOOL)isSetMainScrollViewContentInsetToZeroEnabled:(UIScrollView *)scrollView {
  151. //scrollView.contentInset.top不为0,且scrollView.contentInset.top不等于pinSectionHeaderVerticalOffset,即可认为列表正在刷新。所以这里必须要保证pinSectionHeaderVerticalOffset和MJRefresh的mj_insetT的值不相等。
  152. BOOL isRefreshing = scrollView.contentInset.top != 0 && scrollView.contentInset.top != self.pinSectionHeaderVerticalOffset;
  153. return !isRefreshing;
  154. }
  155. #pragma mark - UITableViewDataSource, UITableViewDelegate
  156. - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
  157. return 1;
  158. }
  159. - (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
  160. return MAX(self.bounds.size.height - [self.delegate heightForPinSectionHeaderInPagerView:self] - self.pinSectionHeaderVerticalOffset, 0);
  161. }
  162. - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
  163. UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"cell" forIndexPath:indexPath];
  164. cell.selectionStyle = UITableViewCellSelectionStyleNone;
  165. cell.backgroundColor = [UIColor clearColor];
  166. if (self.listContainerView.superview != cell.contentView) {
  167. [cell.contentView addSubview:self.listContainerView];
  168. }
  169. if (!CGRectEqualToRect(self.listContainerView.frame, cell.bounds)) {
  170. self.listContainerView.frame = cell.bounds;
  171. }
  172. return cell;
  173. }
  174. - (CGFloat)tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)section {
  175. return [self.delegate heightForPinSectionHeaderInPagerView:self];
  176. }
  177. - (UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section {
  178. return [self.delegate viewForPinSectionHeaderInPagerView:self];
  179. }
  180. - (CGFloat)tableView:(UITableView *)tableView heightForFooterInSection:(NSInteger)section {
  181. return 1;
  182. }
  183. - (UIView *)tableView:(UITableView *)tableView viewForFooterInSection:(NSInteger)section {
  184. UIView *footer = [[UIView alloc] initWithFrame:CGRectZero];
  185. footer.backgroundColor = [UIColor clearColor];
  186. return footer;
  187. }
  188. - (void)scrollViewDidScroll:(UIScrollView *)scrollView {
  189. if (self.pinSectionHeaderVerticalOffset != 0) {
  190. if (!(self.currentScrollingListView != nil && self.currentScrollingListView.contentOffset.y > [self minContentOffsetYInListScrollView:self.currentScrollingListView])) {
  191. //没有处于滚动某一个listView的状态
  192. if (scrollView.contentOffset.y >= self.pinSectionHeaderVerticalOffset) {
  193. //固定的位置就是contentInset.top
  194. [self adjustMainScrollViewToTargetContentInsetIfNeeded:UIEdgeInsetsMake(self.pinSectionHeaderVerticalOffset, 0, 0, 0)];
  195. }else {
  196. if ([self isSetMainScrollViewContentInsetToZeroEnabled:scrollView]) {
  197. [self adjustMainScrollViewToTargetContentInsetIfNeeded:UIEdgeInsetsZero];
  198. }
  199. }
  200. }
  201. }
  202. [self preferredProcessMainTableViewDidScroll:scrollView];
  203. if (self.delegate && [self.delegate respondsToSelector:@selector(mainTableViewDidScroll:)]) {
  204. #pragma GCC diagnostic push
  205. #pragma GCC diagnostic ignored "-Wdeprecated-declarations"
  206. [self.delegate mainTableViewDidScroll:scrollView];
  207. #pragma GCC diagnostic pop
  208. }
  209. if (self.delegate && [self.delegate respondsToSelector:@selector(pagerView:mainTableViewDidScroll:)]) {
  210. [self.delegate pagerView:self mainTableViewDidScroll:scrollView];
  211. }
  212. }
  213. - (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView {
  214. self.listContainerView.scrollView.scrollEnabled = NO;
  215. if (self.delegate && [self.delegate respondsToSelector:@selector(pagerView:mainTableViewWillBeginDragging:)]) {
  216. [self.delegate pagerView:self mainTableViewWillBeginDragging:scrollView];
  217. }
  218. }
  219. - (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate {
  220. if (self.isListHorizontalScrollEnabled && !decelerate) {
  221. self.listContainerView.scrollView.scrollEnabled = YES;
  222. }
  223. if (self.delegate && [self.delegate respondsToSelector:@selector(pagerView:mainTableViewDidEndDragging:willDecelerate:)]) {
  224. [self.delegate pagerView:self mainTableViewDidEndDragging:scrollView willDecelerate:decelerate];
  225. }
  226. }
  227. - (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView {
  228. if (self.isListHorizontalScrollEnabled) {
  229. self.listContainerView.scrollView.scrollEnabled = YES;
  230. }
  231. if ([self isSetMainScrollViewContentInsetToZeroEnabled:scrollView]) {
  232. if (self.mainTableView.contentInset.top != 0 && self.pinSectionHeaderVerticalOffset != 0) {
  233. [self adjustMainScrollViewToTargetContentInsetIfNeeded:UIEdgeInsetsZero];
  234. }
  235. }
  236. if (self.delegate && [self.delegate respondsToSelector:@selector(pagerView:mainTableViewDidEndDecelerating:)]) {
  237. [self.delegate pagerView:self mainTableViewDidEndDecelerating:scrollView];
  238. }
  239. }
  240. - (void)scrollViewDidEndScrollingAnimation:(UIScrollView *)scrollView {
  241. if (self.isListHorizontalScrollEnabled) {
  242. self.listContainerView.scrollView.scrollEnabled = YES;
  243. }
  244. if (self.delegate && [self.delegate respondsToSelector:@selector(pagerView:mainTableViewDidEndScrollingAnimation:)]) {
  245. [self.delegate pagerView:self mainTableViewDidEndScrollingAnimation:scrollView];
  246. }
  247. }
  248. #pragma mark - JXPagerListContainerViewDelegate
  249. - (NSInteger)numberOfListsInlistContainerView:(JXPagerListContainerView *)listContainerView {
  250. return [self.delegate numberOfListsInPagerView:self];
  251. }
  252. - (id<JXPagerViewListViewDelegate>)listContainerView:(JXPagerListContainerView *)listContainerView initListForIndex:(NSInteger)index {
  253. id<JXPagerViewListViewDelegate> list = self.validListDict[@(index)];
  254. if (list == nil) {
  255. if (self.allowsCacheList && self.delegate && [self.delegate respondsToSelector:@selector(pagerView:listIdentifierAtIndex:)]) {
  256. NSString *listIdentifier = [self.delegate pagerView:self listIdentifierAtIndex:index];
  257. list = self.listCache[listIdentifier];
  258. }
  259. }
  260. if (list == nil) {
  261. list = [self.delegate pagerView:self initListAtIndex:index];
  262. __weak typeof(self)weakSelf = self;
  263. __weak typeof(id<JXPagerViewListViewDelegate>) weakList = list;
  264. [list listViewDidScrollCallback:^(UIScrollView *scrollView) {
  265. weakSelf.currentList = weakList;
  266. [weakSelf listViewDidScroll:scrollView];
  267. }];
  268. _validListDict[@(index)] = list;
  269. if (self.allowsCacheList && self.delegate && [self.delegate respondsToSelector:@selector(pagerView:listIdentifierAtIndex:)]) {
  270. NSString *listIdentifier = [self.delegate pagerView:self listIdentifierAtIndex:index];
  271. self.listCache[listIdentifier] = list;
  272. }
  273. }
  274. return list;
  275. }
  276. - (void)listContainerViewWillBeginDragging:(JXPagerListContainerView *)listContainerView {
  277. self.mainTableView.scrollEnabled = NO;
  278. }
  279. - (void)listContainerViewWDidEndScroll:(JXPagerListContainerView *)listContainerView {
  280. self.mainTableView.scrollEnabled = YES;
  281. }
  282. - (void)listContainerView:(JXPagerListContainerView *)listContainerView listDidAppearAtIndex:(NSInteger)index {
  283. self.currentScrollingListView = [self.validListDict[@(index)] listScrollView];
  284. for (id<JXPagerViewListViewDelegate> listItem in self.validListDict.allValues) {
  285. if (listItem == self.validListDict[@(index)]) {
  286. [listItem listScrollView].scrollsToTop = YES;
  287. }else {
  288. [listItem listScrollView].scrollsToTop = NO;
  289. }
  290. }
  291. }
  292. - (Class)scrollViewClassInlistContainerView:(JXPagerListContainerView *)listContainerView {
  293. if (self.delegate && [self.delegate respondsToSelector:@selector(scrollViewClassInlistContainerViewInPagerView:)]) {
  294. return [self.delegate scrollViewClassInlistContainerViewInPagerView:self];
  295. }
  296. return nil;
  297. }
  298. @end
  299. @implementation JXPagerView (UISubclassingGet)
  300. - (CGFloat)mainTableViewMaxContentOffsetY {
  301. return [self.delegate tableHeaderViewHeightInPagerView:self] - self.pinSectionHeaderVerticalOffset;
  302. }
  303. @end
  304. @implementation JXPagerView (UISubclassingHooks)
  305. - (void)preferredProcessListViewDidScroll:(UIScrollView *)scrollView {
  306. if (self.mainTableView.contentOffset.y < self.mainTableViewMaxContentOffsetY) {
  307. //mainTableView的header还没有消失,让listScrollView一直为0
  308. if (self.currentList && [self.currentList respondsToSelector:@selector(listScrollViewWillResetContentOffset)]) {
  309. [self.currentList listScrollViewWillResetContentOffset];
  310. }
  311. [self setListScrollViewToMinContentOffsetY:scrollView];
  312. if (self.automaticallyDisplayListVerticalScrollIndicator) {
  313. scrollView.showsVerticalScrollIndicator = NO;
  314. }
  315. }else {
  316. //mainTableView的header刚好消失,固定mainTableView的位置,显示listScrollView的滚动条
  317. self.mainTableView.contentOffset = CGPointMake(0, self.mainTableViewMaxContentOffsetY);
  318. if (self.automaticallyDisplayListVerticalScrollIndicator) {
  319. scrollView.showsVerticalScrollIndicator = YES;
  320. }
  321. }
  322. }
  323. - (void)preferredProcessMainTableViewDidScroll:(UIScrollView *)scrollView {
  324. if (self.currentScrollingListView != nil && self.currentScrollingListView.contentOffset.y > [self minContentOffsetYInListScrollView:self.currentScrollingListView]) {
  325. //mainTableView的header已经滚动不见,开始滚动某一个listView,那么固定mainTableView的contentOffset,让其不动
  326. [self setMainTableViewToMaxContentOffsetY];
  327. }
  328. if (scrollView.contentOffset.y < self.mainTableViewMaxContentOffsetY) {
  329. //mainTableView已经显示了header,listView的contentOffset需要重置
  330. for (id<JXPagerViewListViewDelegate> list in self.validListDict.allValues) {
  331. if ([list respondsToSelector:@selector(listScrollViewWillResetContentOffset)]) {
  332. [list listScrollViewWillResetContentOffset];
  333. }
  334. [self setListScrollViewToMinContentOffsetY:[list listScrollView]];
  335. }
  336. }
  337. if (scrollView.contentOffset.y > self.mainTableViewMaxContentOffsetY && self.currentScrollingListView.contentOffset.y == [self minContentOffsetYInListScrollView:self.currentScrollingListView]) {
  338. //当往上滚动mainTableView的headerView时,滚动到底时,修复listView往上小幅度滚动
  339. [self setMainTableViewToMaxContentOffsetY];
  340. }
  341. }
  342. - (void)setMainTableViewToMaxContentOffsetY {
  343. self.mainTableView.contentOffset = CGPointMake(0, self.mainTableViewMaxContentOffsetY);
  344. }
  345. - (void)setListScrollViewToMinContentOffsetY:(UIScrollView *)scrollView {
  346. scrollView.contentOffset = CGPointMake(scrollView.contentOffset.x, [self minContentOffsetYInListScrollView:scrollView]);
  347. }
  348. - (CGFloat)minContentOffsetYInListScrollView:(UIScrollView *)scrollView {
  349. if (@available(iOS 11.0, *)) {
  350. return -scrollView.adjustedContentInset.top;
  351. }
  352. return -scrollView.contentInset.top;
  353. }
  354. @end