JXSegmentedView.swift 35 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730
  1. //
  2. // JXSegmentedView.swift
  3. // JXSegmentedView
  4. //
  5. // Created by jiaxin on 2018/12/26.
  6. // Copyright © 2018 jiaxin. All rights reserved.
  7. //
  8. import UIKit
  9. public let JXSegmentedViewAutomaticDimension: CGFloat = -1
  10. /// 选中item时的类型
  11. ///
  12. /// - unknown: 不是选中
  13. /// - code: 通过代码调用方法`func selectItemAt(index: Int)`选中
  14. /// - click: 通过点击item选中
  15. /// - scroll: 通过滚动到item选中
  16. public enum JXSegmentedViewItemSelectedType {
  17. case unknown
  18. case code
  19. case click
  20. case scroll
  21. }
  22. public protocol JXSegmentedViewListContainer {
  23. var defaultSelectedIndex: Int { set get }
  24. func contentScrollView() -> UIScrollView
  25. func reloadData()
  26. func didClickSelectedItem(at index: Int)
  27. }
  28. public protocol JXSegmentedViewDataSource: AnyObject {
  29. var isItemWidthZoomEnabled: Bool { get }
  30. var selectedAnimationDuration: TimeInterval { get }
  31. var itemSpacing: CGFloat { get }
  32. var isItemSpacingAverageEnabled: Bool { get }
  33. func reloadData(selectedIndex: Int)
  34. /// 返回数据源数组,数组元素必须是JXSegmentedBaseItemModel及其子类
  35. ///
  36. /// - Parameter segmentedView: JXSegmentedView
  37. /// - Returns: 数据源数组
  38. func itemDataSource(in segmentedView: JXSegmentedView) -> [JXSegmentedBaseItemModel]
  39. /// 返回index对应item的宽度,等同于cell的宽度。
  40. ///
  41. /// - Parameters:
  42. /// - segmentedView: JXSegmentedView
  43. /// - index: 目标index
  44. /// - Returns: item的宽度
  45. func segmentedView(_ segmentedView: JXSegmentedView, widthForItemAt index: Int) -> CGFloat
  46. /// 返回index对应item的content宽度,等同于cell上面内容的宽度。与上面的代理方法不同,需要注意辨别。部分使用场景下,cell的宽度比较大,但是内容的宽度比较小。这个时候指示器又需要和item的content等宽。所以,添加了此代理方法。
  47. /// - Parameters:
  48. /// - segmentedView: JXSegmentedView
  49. /// - index: 目标index
  50. func segmentedView(_ segmentedView: JXSegmentedView, widthForItemContentAt index: Int) -> CGFloat
  51. /// 注册cell class
  52. ///
  53. /// - Parameter segmentedView: JXSegmentedView
  54. func registerCellClass(in segmentedView: JXSegmentedView)
  55. /// 返回index对应的cell
  56. ///
  57. /// - Parameters:
  58. /// - segmentedView: JXSegmentedView
  59. /// - index: 目标index
  60. /// - Returns: JXSegmentedBaseCell及其子类
  61. func segmentedView(_ segmentedView: JXSegmentedView, cellForItemAt index: Int) -> JXSegmentedBaseCell
  62. /// 根据当前选中的selectedIndex,刷新目标index的itemModel
  63. ///
  64. /// - Parameters:
  65. /// - itemModel: JXSegmentedBaseItemModel
  66. /// - index: 目标index
  67. /// - selectedIndex: 当前选中的index
  68. func refreshItemModel(_ segmentedView: JXSegmentedView, _ itemModel: JXSegmentedBaseItemModel, at index: Int, selectedIndex: Int)
  69. /// item选中的时候调用。当前选中的currentSelectedItemModel状态需要更新为未选中;将要选中的willSelectedItemModel状态需要更新为选中。
  70. ///
  71. /// - Parameters:
  72. /// - currentSelectedItemModel: 当前选中的itemModel
  73. /// - willSelectedItemModel: 将要选中的itemModel
  74. /// - selectedType: 选中的类型
  75. func refreshItemModel(_ segmentedView: JXSegmentedView, currentSelectedItemModel: JXSegmentedBaseItemModel, willSelectedItemModel: JXSegmentedBaseItemModel, selectedType: JXSegmentedViewItemSelectedType)
  76. /// 左右滚动过渡时调用。根据当前的从左到右的百分比,刷新leftItemModel和rightItemModel
  77. ///
  78. /// - Parameters:
  79. /// - leftItemModel: 相对位置在左边的itemModel
  80. /// - rightItemModel: 相对位置在右边的itemModel
  81. /// - percent: 从左到右的百分比
  82. func refreshItemModel(_ segmentedView: JXSegmentedView, leftItemModel: JXSegmentedBaseItemModel, rightItemModel: JXSegmentedBaseItemModel, percent: CGFloat)
  83. }
  84. /// 为什么会把选中代理分为三个,因为有时候只关心点击选中的,有时候只关心滚动选中的,有时候只关心选中。所以具体情况,使用对应方法。
  85. public protocol JXSegmentedViewDelegate: AnyObject {
  86. /// 点击选中或者滚动选中都会调用该方法。适用于只关心选中事件,而不关心具体是点击还是滚动选中的情况。
  87. ///
  88. /// - Parameters:
  89. /// - segmentedView: JXSegmentedView
  90. /// - index: 选中的index
  91. func segmentedView(_ segmentedView: JXSegmentedView, didSelectedItemAt index: Int)
  92. /// 点击选中的情况才会调用该方法
  93. ///
  94. /// - Parameters:
  95. /// - segmentedView: JXSegmentedView
  96. /// - index: 选中的index
  97. func segmentedView(_ segmentedView: JXSegmentedView, didClickSelectedItemAt index: Int)
  98. /// 滚动选中的情况才会调用该方法
  99. ///
  100. /// - Parameters:
  101. /// - segmentedView: JXSegmentedView
  102. /// - index: 选中的index
  103. func segmentedView(_ segmentedView: JXSegmentedView, didScrollSelectedItemAt index: Int)
  104. /// 正在滚动中的回调
  105. ///
  106. /// - Parameters:
  107. /// - segmentedView: JXSegmentedView
  108. /// - leftIndex: 正在滚动中,相对位置处于左边的index
  109. /// - rightIndex: 正在滚动中,相对位置处于右边的index
  110. /// - percent: 从左往右计算的百分比
  111. func segmentedView(_ segmentedView: JXSegmentedView, scrollingFrom leftIndex: Int, to rightIndex: Int, percent: CGFloat)
  112. /// 是否允许点击选中目标index的item
  113. ///
  114. /// - Parameters:
  115. /// - segmentedView: JXSegmentedView
  116. /// - index: 目标index
  117. func segmentedView(_ segmentedView: JXSegmentedView, canClickItemAt index: Int) -> Bool
  118. }
  119. /// 提供JXSegmentedViewDelegate的默认实现,这样对于遵从JXSegmentedViewDelegate的类来说,所有代理方法都是可选实现的。
  120. public extension JXSegmentedViewDelegate {
  121. func segmentedView(_ segmentedView: JXSegmentedView, didSelectedItemAt index: Int) { }
  122. func segmentedView(_ segmentedView: JXSegmentedView, didClickSelectedItemAt index: Int) { }
  123. func segmentedView(_ segmentedView: JXSegmentedView, didScrollSelectedItemAt index: Int) { }
  124. func segmentedView(_ segmentedView: JXSegmentedView, scrollingFrom leftIndex: Int, to rightIndex: Int, percent: CGFloat) { }
  125. func segmentedView(_ segmentedView: JXSegmentedView, canClickItemAt index: Int) -> Bool { return true }
  126. }
  127. /// 内部会自己找到父UIViewController,然后将其automaticallyAdjustsScrollViewInsets设置为false,这一点请知晓。
  128. open class JXSegmentedView: UIView, JXSegmentedViewRTLCompatible {
  129. open weak var dataSource: JXSegmentedViewDataSource? {
  130. didSet {
  131. dataSource?.registerCellClass(in: self)
  132. dataSource?.reloadData(selectedIndex: selectedIndex)
  133. }
  134. }
  135. open weak var delegate: JXSegmentedViewDelegate?
  136. open private(set) var collectionView: JXSegmentedCollectionView!
  137. open var contentScrollView: UIScrollView? {
  138. willSet {
  139. contentScrollView?.removeObserver(self, forKeyPath: "contentOffset")
  140. }
  141. didSet {
  142. contentScrollView?.scrollsToTop = false
  143. contentScrollView?.addObserver(self, forKeyPath: "contentOffset", options: .new, context: nil)
  144. }
  145. }
  146. public var listContainer: JXSegmentedViewListContainer? = nil {
  147. didSet {
  148. listContainer?.defaultSelectedIndex = defaultSelectedIndex
  149. contentScrollView = listContainer?.contentScrollView()
  150. }
  151. }
  152. /// indicators的元素必须是遵从JXSegmentedIndicatorProtocol协议的UIView及其子类
  153. open var indicators = [JXSegmentedIndicatorProtocol]() {
  154. didSet {
  155. collectionView.indicators = indicators
  156. }
  157. }
  158. /// 初始化或者reloadData之前设置,用于指定默认的index
  159. open var defaultSelectedIndex: Int = 0 {
  160. didSet {
  161. selectedIndex = defaultSelectedIndex
  162. if listContainer != nil {
  163. listContainer?.defaultSelectedIndex = defaultSelectedIndex
  164. }
  165. }
  166. }
  167. open private(set) var selectedIndex: Int = 0
  168. /// 整体内容的左边距,默认JXSegmentedViewAutomaticDimension(等于itemSpacing)
  169. open var contentEdgeInsetLeft: CGFloat = JXSegmentedViewAutomaticDimension
  170. /// 整体内容的右边距,默认JXSegmentedViewAutomaticDimension(等于itemSpacing)
  171. open var contentEdgeInsetRight: CGFloat = JXSegmentedViewAutomaticDimension
  172. /// 点击切换的时候,contentScrollView的切换是否需要动画
  173. open var isContentScrollViewClickTransitionAnimationEnabled: Bool = true
  174. private var itemDataSource = [JXSegmentedBaseItemModel]()
  175. private var innerItemSpacing: CGFloat = 0
  176. private var lastContentOffset: CGPoint = CGPoint.zero
  177. /// 正在滚动中的目标index。用于处理正在滚动列表的时候,立即点击item,会导致界面显示异常。
  178. private var scrollingTargetIndex: Int = -1
  179. private var isFirstLayoutSubviews = true
  180. deinit {
  181. contentScrollView?.removeObserver(self, forKeyPath: "contentOffset")
  182. }
  183. public override init(frame: CGRect) {
  184. super.init(frame: frame)
  185. commonInit()
  186. }
  187. required public init?(coder aDecoder: NSCoder) {
  188. super.init(coder: aDecoder)
  189. commonInit()
  190. }
  191. private func commonInit() {
  192. let layout = UICollectionViewFlowLayout()
  193. layout.scrollDirection = .horizontal
  194. collectionView = JXSegmentedCollectionView(frame: CGRect.zero, collectionViewLayout: layout)
  195. collectionView.backgroundColor = .clear
  196. collectionView.showsVerticalScrollIndicator = false
  197. collectionView.showsHorizontalScrollIndicator = false
  198. collectionView.scrollsToTop = false
  199. collectionView.register(UICollectionViewCell.self, forCellWithReuseIdentifier: "JXSegmentedViewInnerEmptyCell")
  200. collectionView.dataSource = self
  201. collectionView.delegate = self
  202. if #available(iOS 10.0, *) {
  203. collectionView.isPrefetchingEnabled = false
  204. }
  205. if #available(iOS 11.0, *) {
  206. collectionView.contentInsetAdjustmentBehavior = .never
  207. }
  208. if segmentedViewShouldRTLLayout() {
  209. collectionView.semanticContentAttribute = .forceLeftToRight
  210. segmentedView(horizontalFlipForView: collectionView)
  211. }
  212. addSubview(collectionView)
  213. }
  214. open override func willMove(toSuperview newSuperview: UIView?) {
  215. super.willMove(toSuperview: newSuperview)
  216. var nextResponder: UIResponder? = newSuperview
  217. while nextResponder != nil {
  218. if let parentVC = nextResponder as? UIViewController {
  219. parentVC.automaticallyAdjustsScrollViewInsets = false
  220. break
  221. }
  222. nextResponder = nextResponder?.next
  223. }
  224. }
  225. open override func layoutSubviews() {
  226. super.layoutSubviews()
  227. //部分使用者为了适配不同的手机屏幕尺寸,JXSegmentedView的宽高比要求保持一样。所以它的高度就会因为不同宽度的屏幕而不一样。计算出来的高度,有时候会是位数很长的浮点数,如果把这个高度设置给UICollectionView就会触发内部的一个错误。所以,为了规避这个问题,在这里对高度统一向下取整。
  228. //如果向下取整导致了你的页面异常,请自己重新设置JXSegmentedView的高度,保证为整数即可。
  229. let targetFrame = CGRect(x: 0, y: 0, width: bounds.size.width, height: floor(bounds.size.height))
  230. if isFirstLayoutSubviews {
  231. isFirstLayoutSubviews = false
  232. collectionView.frame = targetFrame
  233. reloadDataWithoutListContainer()
  234. }else {
  235. if collectionView.frame != targetFrame {
  236. collectionView.frame = targetFrame
  237. collectionView.collectionViewLayout.invalidateLayout()
  238. collectionView.reloadData()
  239. }
  240. }
  241. }
  242. //MARK: - Public
  243. public final func dequeueReusableCell(withReuseIdentifier identifier: String, at index: Int) -> JXSegmentedBaseCell {
  244. let indexPath = IndexPath(item: index, section: 0)
  245. let cell = collectionView.dequeueReusableCell(withReuseIdentifier: identifier, for: indexPath)
  246. guard cell.isKind(of: JXSegmentedBaseCell.self) else {
  247. fatalError("Cell class must be subclass of JXSegmentedBaseCell")
  248. }
  249. return cell as! JXSegmentedBaseCell
  250. }
  251. open func reloadData() {
  252. reloadDataWithoutListContainer()
  253. listContainer?.reloadData()
  254. }
  255. open func reloadDataWithoutListContainer() {
  256. dataSource?.reloadData(selectedIndex: selectedIndex)
  257. if let itemSource = dataSource?.itemDataSource(in: self) {
  258. itemDataSource = itemSource
  259. }
  260. if selectedIndex < 0 || selectedIndex >= itemDataSource.count {
  261. defaultSelectedIndex = 0
  262. selectedIndex = 0
  263. }
  264. innerItemSpacing = dataSource?.itemSpacing ?? 0
  265. var totalItemWidth: CGFloat = 0
  266. var totalContentWidth: CGFloat = getContentEdgeInsetLeft()
  267. for (index, itemModel) in itemDataSource.enumerated() {
  268. itemModel.index = index
  269. itemModel.itemWidth = (dataSource?.segmentedView(self, widthForItemAt: index) ?? 0)
  270. if dataSource?.isItemWidthZoomEnabled == true {
  271. itemModel.itemWidth *= itemModel.itemWidthCurrentZoomScale
  272. }
  273. itemModel.isSelected = (index == selectedIndex)
  274. totalItemWidth += itemModel.itemWidth
  275. if index == itemDataSource.count - 1 {
  276. totalContentWidth += itemModel.itemWidth + getContentEdgeInsetRight()
  277. }else {
  278. totalContentWidth += itemModel.itemWidth + innerItemSpacing
  279. }
  280. }
  281. if dataSource?.isItemSpacingAverageEnabled == true && totalContentWidth < bounds.size.width {
  282. var itemSpacingCount = itemDataSource.count - 1
  283. var totalItemSpacingWidth = bounds.size.width - totalItemWidth
  284. if contentEdgeInsetLeft == JXSegmentedViewAutomaticDimension {
  285. itemSpacingCount += 1
  286. }else {
  287. totalItemSpacingWidth -= contentEdgeInsetLeft
  288. }
  289. if contentEdgeInsetRight == JXSegmentedViewAutomaticDimension {
  290. itemSpacingCount += 1
  291. }else {
  292. totalItemSpacingWidth -= contentEdgeInsetRight
  293. }
  294. if itemSpacingCount > 0 {
  295. innerItemSpacing = totalItemSpacingWidth / CGFloat(itemSpacingCount)
  296. }
  297. }
  298. var selectedItemFrameX = innerItemSpacing
  299. var selectedItemWidth: CGFloat = 0
  300. totalContentWidth = getContentEdgeInsetLeft()
  301. for (index, itemModel) in itemDataSource.enumerated() {
  302. if index < selectedIndex {
  303. selectedItemFrameX += itemModel.itemWidth + innerItemSpacing
  304. }else if index == selectedIndex {
  305. selectedItemWidth = itemModel.itemWidth
  306. }
  307. if index == itemDataSource.count - 1 {
  308. totalContentWidth += itemModel.itemWidth + getContentEdgeInsetRight()
  309. }else {
  310. totalContentWidth += itemModel.itemWidth + innerItemSpacing
  311. }
  312. }
  313. let minX: CGFloat = 0
  314. let maxX = totalContentWidth - bounds.size.width
  315. let targetX = selectedItemFrameX - bounds.size.width/2 + selectedItemWidth/2
  316. collectionView.setContentOffset(CGPoint(x: max(min(maxX, targetX), minX), y: 0), animated: false)
  317. if contentScrollView != nil {
  318. if contentScrollView!.frame.equalTo(CGRect.zero) &&
  319. contentScrollView!.superview != nil {
  320. //某些情况系统会出现JXSegmentedView先布局,contentScrollView后布局。就会导致下面指定defaultSelectedIndex失效,所以发现contentScrollView的frame为zero时,强行触发其父视图链里面已经有frame的一个父视图的layoutSubviews方法。
  321. //比如JXSegmentedListContainerView会将contentScrollView包裹起来使用,该情况需要JXSegmentedListContainerView.superView触发布局更新
  322. var parentView = contentScrollView?.superview
  323. while parentView != nil && parentView?.frame.equalTo(CGRect.zero) == true {
  324. parentView = parentView?.superview
  325. }
  326. parentView?.setNeedsLayout()
  327. parentView?.layoutIfNeeded()
  328. }
  329. contentScrollView!.setContentOffset(CGPoint(x: CGFloat(selectedIndex) * contentScrollView!.bounds.size.width
  330. , y: 0), animated: false)
  331. }
  332. for indicator in indicators {
  333. if itemDataSource.isEmpty {
  334. indicator.isHidden = true
  335. }else {
  336. indicator.isHidden = false
  337. let selectedItemFrame = getItemFrameAt(index: selectedIndex)
  338. let indicatorParams = JXSegmentedIndicatorSelectedParams(currentSelectedIndex: selectedIndex,
  339. currentSelectedItemFrame: selectedItemFrame,
  340. selectedType: .unknown,
  341. currentItemContentWidth: dataSource?.segmentedView(self, widthForItemContentAt: selectedIndex) ?? 0,
  342. collectionViewContentSize: CGSize(width: totalContentWidth, height: bounds.size.height))
  343. indicator.refreshIndicatorState(model: indicatorParams)
  344. if indicator.isIndicatorConvertToItemFrameEnabled {
  345. var indicatorConvertToItemFrame = indicator.frame
  346. indicatorConvertToItemFrame.origin.x -= selectedItemFrame.origin.x
  347. itemDataSource[selectedIndex].indicatorConvertToItemFrame = indicatorConvertToItemFrame
  348. }
  349. }
  350. }
  351. collectionView.reloadData()
  352. collectionView.collectionViewLayout.invalidateLayout()
  353. }
  354. open func reloadItem(at index: Int) {
  355. guard index >= 0 && index < itemDataSource.count else {
  356. return
  357. }
  358. dataSource?.refreshItemModel(self, itemDataSource[index], at: index, selectedIndex: selectedIndex)
  359. let cell = collectionView.cellForItem(at: IndexPath(item: index, section: 0)) as? JXSegmentedBaseCell
  360. cell?.reloadData(itemModel: itemDataSource[index], selectedType: .unknown)
  361. }
  362. /// 代码选中指定index
  363. /// 如果要同时触发列表容器对应index的列表加载,请再调用`listContainerView.didClickSelectedItem(at: index)`方法
  364. ///
  365. /// - Parameter index: 目标index
  366. open func selectItemAt(index: Int) {
  367. selectItemAt(index: index, selectedType: .code)
  368. }
  369. //MARK: - KVO
  370. open override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
  371. if keyPath == "contentOffset" {
  372. let contentOffset = change?[NSKeyValueChangeKey.newKey] as! CGPoint
  373. if contentScrollView?.isTracking == true || contentScrollView?.isDecelerating == true {
  374. //用户滚动引起的contentOffset变化,才处理。
  375. if contentScrollView?.bounds.size.width == 0 {
  376. // 如果contentScrollView Frame为零,直接忽略
  377. return
  378. }
  379. var progress = contentOffset.x/contentScrollView!.bounds.size.width
  380. if Int(progress) > itemDataSource.count - 1 || progress < 0 {
  381. //超过了边界,不需要处理
  382. return
  383. }
  384. if contentOffset.x == 0 && selectedIndex == 0 && lastContentOffset.x == 0 {
  385. //滚动到了最左边,且已经选中了第一个,且之前的contentOffset.x为0
  386. return
  387. }
  388. let maxContentOffsetX = contentScrollView!.contentSize.width - contentScrollView!.bounds.size.width
  389. if contentOffset.x == maxContentOffsetX && selectedIndex == itemDataSource.count - 1 && lastContentOffset.x == maxContentOffsetX {
  390. //滚动到了最右边,且已经选中了最后一个,且之前的contentOffset.x为maxContentOffsetX
  391. return
  392. }
  393. progress = max(0, min(CGFloat(itemDataSource.count - 1), progress))
  394. let baseIndex = Int(floor(progress))
  395. let remainderProgress = progress - CGFloat(baseIndex)
  396. let leftItemFrame = getItemFrameAt(index: baseIndex)
  397. let rightItemFrame = getItemFrameAt(index: baseIndex + 1)
  398. var rightItemContentWidth: CGFloat = 0
  399. if baseIndex + 1 < itemDataSource.count {
  400. rightItemContentWidth = dataSource?.segmentedView(self, widthForItemContentAt: baseIndex + 1) ?? 0
  401. }
  402. let indicatorParams = JXSegmentedIndicatorTransitionParams(currentSelectedIndex: selectedIndex,
  403. leftIndex: baseIndex,
  404. leftItemFrame: leftItemFrame,
  405. leftItemContentWidth: dataSource?.segmentedView(self, widthForItemContentAt: baseIndex) ?? 0,
  406. rightIndex: baseIndex + 1,
  407. rightItemFrame: rightItemFrame,
  408. rightItemContentWidth: rightItemContentWidth,
  409. percent: remainderProgress)
  410. if remainderProgress == 0 {
  411. //滑动翻页,需要更新选中状态
  412. //滑动一小段距离,然后放开回到原位,contentOffset同样的值会回调多次。例如在index为1的情况,滑动放开回到原位,contentOffset会多次回调CGPoint(width, 0)
  413. if !(lastContentOffset.x == contentOffset.x && selectedIndex == baseIndex) {
  414. scrollSelectItemAt(index: baseIndex)
  415. }
  416. }else {
  417. //快速滑动翻页,当remainderRatio没有变成0,但是已经翻页了,需要通过下面的判断,触发选中
  418. if abs(progress - CGFloat(selectedIndex)) > 1 {
  419. var targetIndex = baseIndex
  420. if progress < CGFloat(selectedIndex) {
  421. targetIndex = baseIndex + 1
  422. }
  423. scrollSelectItemAt(index: targetIndex)
  424. }
  425. if selectedIndex == baseIndex {
  426. scrollingTargetIndex = baseIndex + 1
  427. }else {
  428. scrollingTargetIndex = baseIndex
  429. }
  430. dataSource?.refreshItemModel(self, leftItemModel: itemDataSource[baseIndex], rightItemModel: itemDataSource[baseIndex + 1], percent: remainderProgress)
  431. for indicator in indicators {
  432. indicator.contentScrollViewDidScroll(model: indicatorParams)
  433. if indicator.isIndicatorConvertToItemFrameEnabled {
  434. var leftIndicatorConvertToItemFrame = indicator.frame
  435. leftIndicatorConvertToItemFrame.origin.x -= leftItemFrame.origin.x
  436. itemDataSource[baseIndex].indicatorConvertToItemFrame = leftIndicatorConvertToItemFrame
  437. var rightIndicatorConvertToItemFrame = indicator.frame
  438. rightIndicatorConvertToItemFrame.origin.x -= rightItemFrame.origin.x
  439. itemDataSource[baseIndex + 1].indicatorConvertToItemFrame = rightIndicatorConvertToItemFrame
  440. }
  441. }
  442. let leftCell = collectionView.cellForItem(at: IndexPath(item: baseIndex, section: 0)) as? JXSegmentedBaseCell
  443. leftCell?.reloadData(itemModel: itemDataSource[baseIndex], selectedType: .unknown)
  444. let rightCell = collectionView.cellForItem(at: IndexPath(item: baseIndex + 1, section: 0)) as? JXSegmentedBaseCell
  445. rightCell?.reloadData(itemModel: itemDataSource[baseIndex + 1], selectedType: .unknown)
  446. delegate?.segmentedView(self, scrollingFrom: baseIndex, to: baseIndex + 1, percent: remainderProgress)
  447. }
  448. }
  449. lastContentOffset = contentOffset
  450. }
  451. }
  452. //MARK: - Private
  453. private func clickSelectItemAt(index: Int) {
  454. guard delegate?.segmentedView(self, canClickItemAt: index) != false else {
  455. return
  456. }
  457. selectItemAt(index: index, selectedType: .click)
  458. }
  459. private func scrollSelectItemAt(index: Int) {
  460. selectItemAt(index: index, selectedType: .scroll)
  461. }
  462. private func selectItemAt(index: Int, selectedType: JXSegmentedViewItemSelectedType) {
  463. guard index >= 0 && index < itemDataSource.count else {
  464. return
  465. }
  466. if index == selectedIndex {
  467. if selectedType == .code {
  468. listContainer?.didClickSelectedItem(at: index)
  469. }else if selectedType == .click {
  470. delegate?.segmentedView(self, didClickSelectedItemAt: index)
  471. listContainer?.didClickSelectedItem(at: index)
  472. }else if selectedType == .scroll {
  473. delegate?.segmentedView(self, didScrollSelectedItemAt: index)
  474. }
  475. delegate?.segmentedView(self, didSelectedItemAt: index)
  476. scrollingTargetIndex = -1
  477. return
  478. }
  479. let currentSelectedItemModel = itemDataSource[selectedIndex]
  480. let willSelectedItemModel = itemDataSource[index]
  481. dataSource?.refreshItemModel(self, currentSelectedItemModel: currentSelectedItemModel, willSelectedItemModel: willSelectedItemModel, selectedType: selectedType)
  482. let currentSelectedCell = collectionView.cellForItem(at: IndexPath(item: selectedIndex, section: 0)) as? JXSegmentedBaseCell
  483. currentSelectedCell?.reloadData(itemModel: currentSelectedItemModel, selectedType: selectedType)
  484. let willSelectedCell = collectionView.cellForItem(at: IndexPath(item: index, section: 0)) as? JXSegmentedBaseCell
  485. willSelectedCell?.reloadData(itemModel: willSelectedItemModel, selectedType: selectedType)
  486. if scrollingTargetIndex != -1 && scrollingTargetIndex != index {
  487. let scrollingTargetItemModel = itemDataSource[scrollingTargetIndex]
  488. scrollingTargetItemModel.isSelected = false
  489. dataSource?.refreshItemModel(self, currentSelectedItemModel: scrollingTargetItemModel, willSelectedItemModel: willSelectedItemModel, selectedType: selectedType)
  490. let scrollingTargetCell = collectionView.cellForItem(at: IndexPath(item: scrollingTargetIndex, section: 0)) as? JXSegmentedBaseCell
  491. scrollingTargetCell?.reloadData(itemModel: scrollingTargetItemModel, selectedType: selectedType)
  492. }
  493. if dataSource?.isItemWidthZoomEnabled == true {
  494. if selectedType == .click || selectedType == .code {
  495. //延时为了解决cellwidth变化,点击最后几个cell,scrollToItem会出现位置偏移bu。需要等cellWidth动画渐变结束后再滚动到index的cell位置。
  496. let selectedAnimationDurationInMilliseconds = Int((dataSource?.selectedAnimationDuration ?? 0)*1000)
  497. DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + DispatchTimeInterval.milliseconds(selectedAnimationDurationInMilliseconds)) {
  498. self.collectionView.scrollToItem(at: IndexPath(item: index, section: 0), at: .centeredHorizontally, animated: true)
  499. }
  500. }else if selectedType == .scroll {
  501. //滚动选中的直接处理
  502. collectionView.scrollToItem(at: IndexPath(item: index, section: 0), at: .centeredHorizontally, animated: true)
  503. }
  504. }else {
  505. collectionView.scrollToItem(at: IndexPath(item: index, section: 0), at: .centeredHorizontally, animated: true)
  506. }
  507. if contentScrollView != nil && (selectedType == .click || selectedType == .code) {
  508. contentScrollView!.setContentOffset(CGPoint(x: contentScrollView!.bounds.size.width*CGFloat(index), y: 0), animated: isContentScrollViewClickTransitionAnimationEnabled)
  509. }
  510. selectedIndex = index
  511. let currentSelectedItemFrame = getSelectedItemFrameAt(index: selectedIndex)
  512. for indicator in indicators {
  513. let indicatorParams = JXSegmentedIndicatorSelectedParams(currentSelectedIndex: selectedIndex,
  514. currentSelectedItemFrame: currentSelectedItemFrame,
  515. selectedType: selectedType,
  516. currentItemContentWidth: dataSource?.segmentedView(self, widthForItemContentAt: selectedIndex) ?? 0,
  517. collectionViewContentSize: nil)
  518. indicator.selectItem(model: indicatorParams)
  519. if indicator.isIndicatorConvertToItemFrameEnabled {
  520. var indicatorConvertToItemFrame = indicator.frame
  521. indicatorConvertToItemFrame.origin.x -= currentSelectedItemFrame.origin.x
  522. itemDataSource[selectedIndex].indicatorConvertToItemFrame = indicatorConvertToItemFrame
  523. willSelectedCell?.reloadData(itemModel: willSelectedItemModel, selectedType: selectedType)
  524. }
  525. }
  526. scrollingTargetIndex = -1
  527. if selectedType == .code {
  528. listContainer?.didClickSelectedItem(at: index)
  529. }else if selectedType == .click {
  530. delegate?.segmentedView(self, didClickSelectedItemAt: index)
  531. listContainer?.didClickSelectedItem(at: index)
  532. }else if selectedType == .scroll {
  533. delegate?.segmentedView(self, didScrollSelectedItemAt: index)
  534. }
  535. delegate?.segmentedView(self, didSelectedItemAt: index)
  536. }
  537. private func getItemFrameAt(index: Int) -> CGRect {
  538. guard index < itemDataSource.count else {
  539. return CGRect.zero
  540. }
  541. var x = getContentEdgeInsetLeft()
  542. for i in 0..<index {
  543. let itemModel = itemDataSource[i]
  544. var itemWidth: CGFloat = 0
  545. if itemModel.isTransitionAnimating && itemModel.isItemWidthZoomEnabled {
  546. //正在进行动画的时候,itemWidthCurrentZoomScale是随着动画渐变的,而没有立即更新到目标值
  547. if itemModel.isSelected {
  548. itemWidth = (dataSource?.segmentedView(self, widthForItemAt: itemModel.index) ?? 0) * itemModel.itemWidthSelectedZoomScale
  549. }else {
  550. itemWidth = (dataSource?.segmentedView(self, widthForItemAt: itemModel.index) ?? 0) * itemModel.itemWidthNormalZoomScale
  551. }
  552. }else {
  553. itemWidth = itemModel.itemWidth
  554. }
  555. x += itemWidth + innerItemSpacing
  556. }
  557. var width: CGFloat = 0
  558. let selectedItemModel = itemDataSource[index]
  559. if selectedItemModel.isTransitionAnimating && selectedItemModel.isItemWidthZoomEnabled {
  560. width = (dataSource?.segmentedView(self, widthForItemAt: selectedItemModel.index) ?? 0) * selectedItemModel.itemWidthSelectedZoomScale
  561. }else {
  562. width = selectedItemModel.itemWidth
  563. }
  564. return CGRect(x: x, y: 0, width: width, height: bounds.size.height)
  565. }
  566. private func getSelectedItemFrameAt(index: Int) -> CGRect {
  567. guard index < itemDataSource.count else {
  568. return CGRect.zero
  569. }
  570. var x = getContentEdgeInsetLeft()
  571. for i in 0..<index {
  572. let itemWidth = (dataSource?.segmentedView(self, widthForItemAt: i) ?? 0)
  573. x += itemWidth + innerItemSpacing
  574. }
  575. var width: CGFloat = 0
  576. let selectedItemModel = itemDataSource[index]
  577. if selectedItemModel.isItemWidthZoomEnabled {
  578. width = (dataSource?.segmentedView(self, widthForItemAt: selectedItemModel.index) ?? 0) * selectedItemModel.itemWidthSelectedZoomScale
  579. }else {
  580. width = selectedItemModel.itemWidth
  581. }
  582. return CGRect(x: x, y: 0, width: width, height: bounds.size.height)
  583. }
  584. private func getContentEdgeInsetLeft() -> CGFloat {
  585. if contentEdgeInsetLeft == JXSegmentedViewAutomaticDimension {
  586. return innerItemSpacing
  587. }else {
  588. return contentEdgeInsetLeft
  589. }
  590. }
  591. private func getContentEdgeInsetRight() -> CGFloat {
  592. if contentEdgeInsetRight == JXSegmentedViewAutomaticDimension {
  593. return innerItemSpacing
  594. }else {
  595. return contentEdgeInsetRight
  596. }
  597. }
  598. }
  599. extension JXSegmentedView: UICollectionViewDataSource {
  600. public func numberOfSections(in collectionView: UICollectionView) -> Int {
  601. return 1
  602. }
  603. public func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
  604. return itemDataSource.count
  605. }
  606. public func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
  607. if let cell = dataSource?.segmentedView(self, cellForItemAt: indexPath.item) {
  608. cell.reloadData(itemModel: itemDataSource[indexPath.item], selectedType: .unknown)
  609. return cell
  610. }else {
  611. return collectionView.dequeueReusableCell(withReuseIdentifier: "JXSegmentedViewInnerEmptyCell", for: indexPath)
  612. }
  613. }
  614. }
  615. extension JXSegmentedView: UICollectionViewDelegate {
  616. public func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
  617. var isTransitionAnimating = false
  618. for itemModel in itemDataSource {
  619. if itemModel.isTransitionAnimating {
  620. isTransitionAnimating = true
  621. break
  622. }
  623. }
  624. if !isTransitionAnimating {
  625. //当前没有正在过渡的item,才允许点击选中
  626. clickSelectItemAt(index: indexPath.item)
  627. }
  628. }
  629. }
  630. extension JXSegmentedView: UICollectionViewDelegateFlowLayout {
  631. public func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets {
  632. return UIEdgeInsets(top: 0, left: getContentEdgeInsetLeft(), bottom: 0, right: getContentEdgeInsetRight())
  633. }
  634. public func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
  635. if indexPath.item >= 0, indexPath.item < itemDataSource.count {
  636. return CGSize(width: itemDataSource[indexPath.item].itemWidth, height: collectionView.bounds.size.height)
  637. } else {
  638. return .zero
  639. }
  640. }
  641. public func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAt section: Int) -> CGFloat {
  642. return innerItemSpacing
  643. }
  644. public func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumInteritemSpacingForSectionAt section: Int) -> CGFloat {
  645. return innerItemSpacing
  646. }
  647. }