JXSegmentedView.swift 33 KB

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