|
@@ -0,0 +1,408 @@
|
|
|
+//
|
|
|
+// JXPagingView.swift
|
|
|
+// JXPagingView
|
|
|
+//
|
|
|
+// Created by jiaxin on 2018/5/22.
|
|
|
+// Copyright © 2018年 jiaxin. All rights reserved.
|
|
|
+//
|
|
|
+
|
|
|
+import UIKit
|
|
|
+
|
|
|
+@objc public protocol JXPagingViewListViewDelegate: NSObjectProtocol {
|
|
|
+
|
|
|
+ /// 返回listView
|
|
|
+ ///
|
|
|
+ /// - Returns: UIView
|
|
|
+ func listView() -> UIView
|
|
|
+
|
|
|
+ /// 返回listView内部持有的UIScrollView或UITableView或UICollectionView
|
|
|
+ /// 主要用于mainTableView已经显示了header,listView的contentOffset需要重置时,内部需要访问到外部传入进来的listView内的scrollView
|
|
|
+ ///
|
|
|
+ /// - Returns: listView内部持有的UIScrollView或UITableView或UICollectionView
|
|
|
+ func listScrollView() -> UIScrollView
|
|
|
+
|
|
|
+
|
|
|
+ /// 当listView内部持有的UIScrollView或UITableView或UICollectionView的代理方法`scrollViewDidScroll`回调时,需要调用该代理方法传入的callback
|
|
|
+ ///
|
|
|
+ /// - Parameter callback: `scrollViewDidScroll`回调时调用的callback
|
|
|
+ func listViewDidScrollCallback(callback: @escaping (UIScrollView)->())
|
|
|
+
|
|
|
+ /// 将要重置listScrollView的contentOffset
|
|
|
+ @objc optional func listScrollViewWillResetContentOffset()
|
|
|
+
|
|
|
+ /// 列表显示的时候调用
|
|
|
+ @objc optional func listDidAppear()
|
|
|
+
|
|
|
+ /// 列表消失的时候调用
|
|
|
+ @objc optional func listDidDisappear()
|
|
|
+}
|
|
|
+
|
|
|
+@objc public protocol JXPagingViewDelegate: NSObjectProtocol {
|
|
|
+
|
|
|
+
|
|
|
+ /// tableHeaderView的高度,因为内部需要比对判断,只能是整型数
|
|
|
+ ///
|
|
|
+ /// - Parameter pagingView: JXPagingViewView
|
|
|
+ /// - Returns: height
|
|
|
+ func tableHeaderViewHeight(in pagingView: JXPagingView) -> Int
|
|
|
+
|
|
|
+
|
|
|
+ /// 返回tableHeaderView
|
|
|
+ ///
|
|
|
+ /// - Parameter pagingView: JXPagingViewView
|
|
|
+ /// - Returns: view
|
|
|
+ func tableHeaderView(in pagingView: JXPagingView) -> UIView
|
|
|
+
|
|
|
+
|
|
|
+ /// 返回悬浮HeaderView的高度,因为内部需要比对判断,只能是整型数
|
|
|
+ ///
|
|
|
+ /// - Parameter pagingView: JXPagingViewView
|
|
|
+ /// - Returns: height
|
|
|
+ func heightForPinSectionHeader(in pagingView: JXPagingView) -> Int
|
|
|
+
|
|
|
+
|
|
|
+ /// 返回悬浮HeaderView。我用的是自己封装的JXCategoryView(Github:https://github.com/pujiaxin33/JXCategoryView),你也可以选择其他的三方库或者自己写
|
|
|
+ ///
|
|
|
+ /// - Parameter pagingView: JXPagingViewView
|
|
|
+ /// - Returns: view
|
|
|
+ func viewForPinSectionHeader(in pagingView: JXPagingView) -> UIView
|
|
|
+
|
|
|
+ /// 返回列表的数量
|
|
|
+ ///
|
|
|
+ /// - Parameter pagingView: pagingView description
|
|
|
+ /// - Returns: 列表的数量
|
|
|
+ func numberOfLists(in pagingView: JXPagingView) -> Int
|
|
|
+
|
|
|
+ /// 根据index初始化一个对应列表实例,需要是遵从`JXPagerViewListViewDelegate`协议的对象。
|
|
|
+ /// 如果列表是用自定义UIView封装的,就让自定义UIView遵从`JXPagerViewListViewDelegate`协议,该方法返回自定义UIView即可。
|
|
|
+ /// 如果列表是用自定义UIViewController封装的,就让自定义UIViewController遵从`JXPagerViewListViewDelegate`协议,该方法返回自定义UIViewController即可。
|
|
|
+ /// 注意:一定要是新生成的实例!!!
|
|
|
+ ///
|
|
|
+ /// - Parameters:
|
|
|
+ /// - pagingView: pagingView description
|
|
|
+ /// - index: 新生成的列表实例
|
|
|
+ func pagingView(_ pagingView: JXPagingView, initListAtIndex index: Int) -> JXPagingViewListViewDelegate
|
|
|
+
|
|
|
+ /// mainTableView的滚动回调,用于实现头图跟随缩放
|
|
|
+ ///
|
|
|
+ /// - Parameter scrollView: JXPagingViewMainTableView
|
|
|
+ @objc optional func mainTableViewDidScroll(_ scrollView: UIScrollView)
|
|
|
+}
|
|
|
+
|
|
|
+open class JXPagingView: UIView {
|
|
|
+ public unowned let delegate: JXPagingViewDelegate
|
|
|
+ open var mainTableView: JXPagingMainTableView!
|
|
|
+ open var listContainerView: JXPagingListContainerView!
|
|
|
+ public var validListDict = [Int:JXPagingViewListViewDelegate]() //当前已经加载过可用的列表字典,key就是index值,value是对应的列表。
|
|
|
+ open var pinSectionHeaderVerticalOffset: CGFloat = 0 //顶部固定sectionHeader的垂直偏移量。数值越大越往下沉。
|
|
|
+ public var isListHorizontalScrollEnabled = true {
|
|
|
+ didSet {
|
|
|
+ refreshListHorizontalScrollEnabledState()
|
|
|
+ }
|
|
|
+ }
|
|
|
+ open var automaticallyDisplayListVerticalScrollIndicator = true //是否允许当前列表自动显示或隐藏列表是垂直滚动指示器。true:悬浮的headerView滚动到顶部开始滚动列表时,就会显示,反之隐藏。false:内部不会处理列表的垂直滚动指示器。默认为:true。
|
|
|
+ public var currentScrollingListView: UIScrollView?
|
|
|
+ public var currentList: JXPagingViewListViewDelegate?
|
|
|
+ private var currentDeviceOrientation: UIDeviceOrientation?
|
|
|
+ private var currentIndex: Int = 0
|
|
|
+ private var retainedSelf: JXPagingView?
|
|
|
+ private var isWillRemoveFromWindow: Bool = false
|
|
|
+ private var isFirstMoveToWindow: Bool = true
|
|
|
+
|
|
|
+ deinit {
|
|
|
+ NotificationCenter.default.removeObserver(self)
|
|
|
+ }
|
|
|
+
|
|
|
+ public init(delegate: JXPagingViewDelegate) {
|
|
|
+ self.delegate = delegate
|
|
|
+ super.init(frame: CGRect.zero)
|
|
|
+
|
|
|
+ initializeViews()
|
|
|
+ }
|
|
|
+
|
|
|
+ @available(*, unavailable)
|
|
|
+ required public init?(coder aDecoder: NSCoder) {
|
|
|
+ fatalError("init(coder:) has not been implemented")
|
|
|
+ }
|
|
|
+
|
|
|
+ open func initializeViews(){
|
|
|
+ mainTableView = JXPagingMainTableView(frame: CGRect.zero, style: .plain)
|
|
|
+ mainTableView.showsVerticalScrollIndicator = false
|
|
|
+ mainTableView.showsHorizontalScrollIndicator = false
|
|
|
+ mainTableView.separatorStyle = .none
|
|
|
+ mainTableView.dataSource = self
|
|
|
+ mainTableView.delegate = self
|
|
|
+ mainTableView.scrollsToTop = false
|
|
|
+ mainTableView.tableHeaderView = self.delegate.tableHeaderView(in: self)
|
|
|
+ mainTableView.register(UITableViewCell.classForCoder(), forCellReuseIdentifier: "cell")
|
|
|
+ addSubview(mainTableView)
|
|
|
+
|
|
|
+ if #available(iOS 11.0, *) {
|
|
|
+ mainTableView.contentInsetAdjustmentBehavior = .never
|
|
|
+ }
|
|
|
+
|
|
|
+ listContainerView = JXPagingListContainerView(delegate: self)
|
|
|
+ listContainerView.mainTableView = mainTableView
|
|
|
+
|
|
|
+ refreshListHorizontalScrollEnabledState()
|
|
|
+
|
|
|
+ self.currentDeviceOrientation = UIDevice.current.orientation
|
|
|
+ NotificationCenter.default.addObserver(self, selector: #selector(deviceOrientationDidChange(notification:)), name: UIDevice.orientationDidChangeNotification, object: nil)
|
|
|
+ }
|
|
|
+
|
|
|
+ open override func willMove(toWindow newWindow: UIWindow?) {
|
|
|
+ if self.isFirstMoveToWindow {
|
|
|
+ //第一次调用过滤,因为第一次列表显示通知会从willDisplayCell方法通知
|
|
|
+ self.isFirstMoveToWindow = false
|
|
|
+ return
|
|
|
+ }
|
|
|
+ //当前页面push到一个新的页面时,willMoveToWindow会调用三次。第一次调用的newWindow为nil,第二次调用间隔1ms左右newWindow有值,第三次调用间隔400ms左右newWindow为nil。
|
|
|
+ //根据上述事实,第一次和第二次为无效调用,可以根据其间隔1ms左右过滤掉
|
|
|
+ if newWindow == nil {
|
|
|
+ self.isWillRemoveFromWindow = true
|
|
|
+ //当前页面被pop的时候,willMoveToWindow只会调用一次,而且整个页面会被销毁掉,所以需要循环引用自己,确保能延迟执行currentListDidDisappear方法,触发列表消失事件。由此可见,循环引用也不一定是个坏事。是天使还是魔鬼,就看你如何对待它了。
|
|
|
+ self.retainedSelf = self
|
|
|
+ self.perform(#selector(currentListDidDisappear), with: nil, afterDelay: 0.02)
|
|
|
+ }else {
|
|
|
+ if self.isWillRemoveFromWindow {
|
|
|
+ self.isWillRemoveFromWindow = false
|
|
|
+ NSObject.cancelPreviousPerformRequests(withTarget: self, selector: #selector(currentListDidDisappear), object: nil)
|
|
|
+ }else {
|
|
|
+ self.currentListDidAppear()
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ override open func layoutSubviews() {
|
|
|
+ super.layoutSubviews()
|
|
|
+
|
|
|
+ mainTableView.frame = self.bounds
|
|
|
+ }
|
|
|
+
|
|
|
+ open func reloadData() {
|
|
|
+ self.currentList = nil
|
|
|
+ self.currentScrollingListView = nil
|
|
|
+
|
|
|
+ for list in validListDict.values {
|
|
|
+ list.listView().removeFromSuperview()
|
|
|
+ }
|
|
|
+ validListDict.removeAll()
|
|
|
+
|
|
|
+ self.mainTableView.tableHeaderView = self.delegate.tableHeaderView(in: self)
|
|
|
+ self.mainTableView.reloadData()
|
|
|
+ self.listContainerView.reloadData()
|
|
|
+ }
|
|
|
+
|
|
|
+ open func preferredProcessListViewDidScroll(scrollView: UIScrollView) {
|
|
|
+ if (self.mainTableView.contentOffset.y < getMainTableViewMaxContentOffsetY()) {
|
|
|
+ //mainTableView的header还没有消失,让listScrollView一直为0
|
|
|
+ self.currentList?.listScrollViewWillResetContentOffset?()
|
|
|
+ currentScrollingListView!.contentOffset = CGPoint.zero
|
|
|
+ if automaticallyDisplayListVerticalScrollIndicator {
|
|
|
+ currentScrollingListView!.showsVerticalScrollIndicator = false
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ //mainTableView的header刚好消失,固定mainTableView的位置,显示listScrollView的滚动条
|
|
|
+ self.mainTableView.contentOffset = CGPoint(x: 0, y: getMainTableViewMaxContentOffsetY())
|
|
|
+ if automaticallyDisplayListVerticalScrollIndicator {
|
|
|
+ currentScrollingListView!.showsVerticalScrollIndicator = true
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ open func preferredProcessMainTableViewDidScroll(_ scrollView: UIScrollView) {
|
|
|
+ if (self.currentScrollingListView != nil && self.currentScrollingListView!.contentOffset.y > 0) {
|
|
|
+ //mainTableView的header已经滚动不见,开始滚动某一个listView,那么固定mainTableView的contentOffset,让其不动
|
|
|
+ self.mainTableView.contentOffset = CGPoint(x: 0, y: getMainTableViewMaxContentOffsetY())
|
|
|
+ }
|
|
|
+
|
|
|
+ if (self.mainTableView.contentOffset.y < getMainTableViewMaxContentOffsetY()) {
|
|
|
+ //mainTableView已经显示了header,listView的contentOffset需要重置
|
|
|
+ for list in self.validListDict.values {
|
|
|
+ list.listScrollViewWillResetContentOffset?()
|
|
|
+ list.listScrollView().contentOffset = CGPoint.zero
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if scrollView.contentOffset.y > getMainTableViewMaxContentOffsetY() && self.currentScrollingListView?.contentOffset.y == 0 {
|
|
|
+ //当往上滚动mainTableView的headerView时,滚动到底时,修复listView往上小幅度滚动
|
|
|
+ self.mainTableView.contentOffset = CGPoint(x: 0, y: getMainTableViewMaxContentOffsetY())
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ //MARK: - Private
|
|
|
+
|
|
|
+ func refreshListHorizontalScrollEnabledState() {
|
|
|
+ listContainerView.collectionView.isScrollEnabled = isListHorizontalScrollEnabled
|
|
|
+ }
|
|
|
+
|
|
|
+ func getMainTableViewMaxContentOffsetY() -> CGFloat {
|
|
|
+ return CGFloat(self.delegate.tableHeaderViewHeight(in: self)) - pinSectionHeaderVerticalOffset
|
|
|
+ }
|
|
|
+
|
|
|
+ func getPinSectionHeaderHeight() -> CGFloat {
|
|
|
+ return CGFloat(self.delegate.heightForPinSectionHeader(in: self))
|
|
|
+ }
|
|
|
+
|
|
|
+ /// 外部传入的listView,当其内部的scrollView滚动时,需要调用该方法
|
|
|
+ func listViewDidScroll(scrollView: UIScrollView) {
|
|
|
+ self.currentScrollingListView = scrollView
|
|
|
+
|
|
|
+ preferredProcessListViewDidScroll(scrollView: scrollView)
|
|
|
+ }
|
|
|
+
|
|
|
+ @objc func deviceOrientationDidChange(notification: Notification) {
|
|
|
+ if self.currentDeviceOrientation != UIDevice.current.orientation {
|
|
|
+ self.currentDeviceOrientation = UIDevice.current.orientation
|
|
|
+ //前后台切换也会触发该通知,所以不相同的时候才处理
|
|
|
+ mainTableView.reloadData()
|
|
|
+ listContainerView.deviceOrientationDidChanged()
|
|
|
+ listContainerView.reloadData()
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ @objc func currentListDidDisappear() {
|
|
|
+ let list = self.validListDict[self.currentIndex]
|
|
|
+ list?.listDidDisappear?()
|
|
|
+ self.isWillRemoveFromWindow = false
|
|
|
+ self.retainedSelf = nil
|
|
|
+ }
|
|
|
+
|
|
|
+ func currentListDidAppear() {
|
|
|
+ self.listDidAppear(index: self.currentIndex)
|
|
|
+ }
|
|
|
+
|
|
|
+ func listDidAppear(index: Int) {
|
|
|
+ let count = self.delegate.numberOfLists(in: self)
|
|
|
+ if count <= 0 || index >= count {
|
|
|
+ return
|
|
|
+ }
|
|
|
+ self.currentIndex = index
|
|
|
+ let list = self.validListDict[index]
|
|
|
+ list?.listDidAppear?()
|
|
|
+ }
|
|
|
+
|
|
|
+ func listDidDisappear(index: Int) {
|
|
|
+ let count = self.delegate.numberOfLists(in: self)
|
|
|
+ if count <= 0 || index >= count {
|
|
|
+ return
|
|
|
+ }
|
|
|
+ self.currentIndex = index
|
|
|
+ let list = self.validListDict[index]
|
|
|
+ list?.listDidDisappear?()
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+//MARK: - UITableViewDataSource, UITableViewDelegate
|
|
|
+extension JXPagingView: UITableViewDataSource, UITableViewDelegate {
|
|
|
+ public func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
|
|
+ return 1
|
|
|
+ }
|
|
|
+
|
|
|
+ public func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
|
|
|
+ return self.bounds.height - getPinSectionHeaderHeight() - pinSectionHeaderVerticalOffset
|
|
|
+ }
|
|
|
+
|
|
|
+ public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
|
|
+ let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
|
|
|
+ for subview in cell.contentView.subviews {
|
|
|
+ subview.removeFromSuperview()
|
|
|
+ }
|
|
|
+ listContainerView.frame = cell.contentView.bounds
|
|
|
+ cell.contentView.addSubview(listContainerView)
|
|
|
+ return cell
|
|
|
+ }
|
|
|
+
|
|
|
+ public func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
|
|
|
+ return getPinSectionHeaderHeight()
|
|
|
+ }
|
|
|
+
|
|
|
+ public func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
|
|
|
+ return self.delegate.viewForPinSectionHeader(in: self)
|
|
|
+ }
|
|
|
+
|
|
|
+ //加上footer之后,下滑滚动就变得丝般顺滑了
|
|
|
+ public func tableView(_ tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat {
|
|
|
+ return 1
|
|
|
+ }
|
|
|
+
|
|
|
+ public func tableView(_ tableView: UITableView, viewForFooterInSection section: Int) -> UIView? {
|
|
|
+ let footerView = UIView(frame: CGRect.zero)
|
|
|
+ footerView.backgroundColor = UIColor.clear
|
|
|
+ return footerView
|
|
|
+ }
|
|
|
+
|
|
|
+ public func scrollViewDidScroll(_ scrollView: UIScrollView) {
|
|
|
+ //用户正在上下滚动的时候,就不允许左右滚动
|
|
|
+ if scrollView.isTracking && isListHorizontalScrollEnabled {
|
|
|
+ self.listContainerView.collectionView.isScrollEnabled = false
|
|
|
+ }
|
|
|
+
|
|
|
+ if scrollView.contentOffset.y < pinSectionHeaderVerticalOffset && scrollView.contentOffset.y >= 0 {
|
|
|
+ //因为设置了contentInset.top,所以顶部会有对应高度的空白区间,所以需要设置负数抵消掉
|
|
|
+ scrollView.contentInset = UIEdgeInsets(top: -scrollView.contentOffset.y, left: 0, bottom: 0, right: 0)
|
|
|
+ }else if scrollView.contentOffset.y > pinSectionHeaderVerticalOffset {
|
|
|
+ //固定的位置就是contentInset.top
|
|
|
+ scrollView.contentInset = UIEdgeInsets(top: pinSectionHeaderVerticalOffset, left: 0, bottom: 0, right: 0)
|
|
|
+ }
|
|
|
+
|
|
|
+ preferredProcessMainTableViewDidScroll(scrollView)
|
|
|
+
|
|
|
+ self.delegate.mainTableViewDidScroll?(scrollView)
|
|
|
+ }
|
|
|
+
|
|
|
+ public func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
|
|
|
+ if isListHorizontalScrollEnabled {
|
|
|
+ self.listContainerView.collectionView.isScrollEnabled = true
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ public func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
|
|
|
+ if isListHorizontalScrollEnabled {
|
|
|
+ self.listContainerView.collectionView.isScrollEnabled = true
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ public func scrollViewDidEndScrollingAnimation(_ scrollView: UIScrollView) {
|
|
|
+ if isListHorizontalScrollEnabled {
|
|
|
+ self.listContainerView.collectionView.isScrollEnabled = true
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+extension JXPagingView: JXPagingListContainerViewDelegate {
|
|
|
+ public func numberOfRows(in listContainerView: JXPagingListContainerView) -> Int {
|
|
|
+ return self.delegate.numberOfLists(in: self)
|
|
|
+ }
|
|
|
+ public func listContainerView(_ listContainerView: JXPagingListContainerView, viewForListInRow row: Int) -> UIView {
|
|
|
+ var list = validListDict[row]
|
|
|
+ if list == nil {
|
|
|
+ list = self.delegate.pagingView(self, initListAtIndex: row)
|
|
|
+ list?.listViewDidScrollCallback {[weak self, weak list] (scrollView) in
|
|
|
+ self?.currentList = list
|
|
|
+ self?.listViewDidScroll(scrollView: scrollView)
|
|
|
+ }
|
|
|
+ validListDict[row] = list!
|
|
|
+ }
|
|
|
+ for listItem in validListDict.values {
|
|
|
+ if listItem === list {
|
|
|
+ listItem.listScrollView().scrollsToTop = true
|
|
|
+ }else {
|
|
|
+ listItem.listScrollView().scrollsToTop = false
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return list!.listView()
|
|
|
+ }
|
|
|
+
|
|
|
+ public func listContainerView(_ listContainerView: JXPagingListContainerView, willDisplayCellAt row: Int) {
|
|
|
+ self.listDidAppear(index: row)
|
|
|
+ self.currentScrollingListView = validListDict[row]?.listScrollView()
|
|
|
+ }
|
|
|
+
|
|
|
+ public func listContainerView(_ listContainerView: JXPagingListContainerView, didEndDisplayingCellAt row: Int) {
|
|
|
+ self.listDidDisappear(index: row)
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+
|
|
|
+
|