731 lines
36 KiB
Swift
731 lines
36 KiB
Swift
//
|
||
// JXSegmentedView.swift
|
||
// JXSegmentedView
|
||
//
|
||
// Created by jiaxin on 2018/12/26.
|
||
// Copyright © 2018 jiaxin. All rights reserved.
|
||
//
|
||
|
||
import UIKit
|
||
|
||
public let JXSegmentedViewAutomaticDimension: CGFloat = -1
|
||
|
||
/// 选中item时的类型
|
||
///
|
||
/// - unknown: 不是选中
|
||
/// - code: 通过代码调用方法`func selectItemAt(index: Int)`选中
|
||
/// - click: 通过点击item选中
|
||
/// - scroll: 通过滚动到item选中
|
||
public enum JXSegmentedViewItemSelectedType {
|
||
case unknown
|
||
case code
|
||
case click
|
||
case scroll
|
||
}
|
||
|
||
public protocol JXSegmentedViewListContainer {
|
||
var defaultSelectedIndex: Int { set get }
|
||
func contentScrollView() -> UIScrollView
|
||
func reloadData()
|
||
func didClickSelectedItem(at index: Int)
|
||
}
|
||
|
||
public protocol JXSegmentedViewDataSource: AnyObject {
|
||
var isItemWidthZoomEnabled: Bool { get }
|
||
var selectedAnimationDuration: TimeInterval { get }
|
||
var itemSpacing: CGFloat { get }
|
||
var isItemSpacingAverageEnabled: Bool { get }
|
||
|
||
func reloadData(selectedIndex: Int)
|
||
|
||
/// 返回数据源数组,数组元素必须是JXSegmentedBaseItemModel及其子类
|
||
///
|
||
/// - Parameter segmentedView: JXSegmentedView
|
||
/// - Returns: 数据源数组
|
||
func itemDataSource(in segmentedView: JXSegmentedView) -> [JXSegmentedBaseItemModel]
|
||
|
||
/// 返回index对应item的宽度,等同于cell的宽度。
|
||
///
|
||
/// - Parameters:
|
||
/// - segmentedView: JXSegmentedView
|
||
/// - index: 目标index
|
||
/// - Returns: item的宽度
|
||
func segmentedView(_ segmentedView: JXSegmentedView, widthForItemAt index: Int) -> CGFloat
|
||
|
||
/// 返回index对应item的content宽度,等同于cell上面内容的宽度。与上面的代理方法不同,需要注意辨别。部分使用场景下,cell的宽度比较大,但是内容的宽度比较小。这个时候指示器又需要和item的content等宽。所以,添加了此代理方法。
|
||
/// - Parameters:
|
||
/// - segmentedView: JXSegmentedView
|
||
/// - index: 目标index
|
||
func segmentedView(_ segmentedView: JXSegmentedView, widthForItemContentAt index: Int) -> CGFloat
|
||
|
||
/// 注册cell class
|
||
///
|
||
/// - Parameter segmentedView: JXSegmentedView
|
||
func registerCellClass(in segmentedView: JXSegmentedView)
|
||
|
||
/// 返回index对应的cell
|
||
///
|
||
/// - Parameters:
|
||
/// - segmentedView: JXSegmentedView
|
||
/// - index: 目标index
|
||
/// - Returns: JXSegmentedBaseCell及其子类
|
||
func segmentedView(_ segmentedView: JXSegmentedView, cellForItemAt index: Int) -> JXSegmentedBaseCell
|
||
|
||
/// 根据当前选中的selectedIndex,刷新目标index的itemModel
|
||
///
|
||
/// - Parameters:
|
||
/// - itemModel: JXSegmentedBaseItemModel
|
||
/// - index: 目标index
|
||
/// - selectedIndex: 当前选中的index
|
||
func refreshItemModel(_ segmentedView: JXSegmentedView, _ itemModel: JXSegmentedBaseItemModel, at index: Int, selectedIndex: Int)
|
||
|
||
/// item选中的时候调用。当前选中的currentSelectedItemModel状态需要更新为未选中;将要选中的willSelectedItemModel状态需要更新为选中。
|
||
///
|
||
/// - Parameters:
|
||
/// - currentSelectedItemModel: 当前选中的itemModel
|
||
/// - willSelectedItemModel: 将要选中的itemModel
|
||
/// - selectedType: 选中的类型
|
||
func refreshItemModel(_ segmentedView: JXSegmentedView, currentSelectedItemModel: JXSegmentedBaseItemModel, willSelectedItemModel: JXSegmentedBaseItemModel, selectedType: JXSegmentedViewItemSelectedType)
|
||
|
||
/// 左右滚动过渡时调用。根据当前的从左到右的百分比,刷新leftItemModel和rightItemModel
|
||
///
|
||
/// - Parameters:
|
||
/// - leftItemModel: 相对位置在左边的itemModel
|
||
/// - rightItemModel: 相对位置在右边的itemModel
|
||
/// - percent: 从左到右的百分比
|
||
func refreshItemModel(_ segmentedView: JXSegmentedView, leftItemModel: JXSegmentedBaseItemModel, rightItemModel: JXSegmentedBaseItemModel, percent: CGFloat)
|
||
}
|
||
|
||
/// 为什么会把选中代理分为三个,因为有时候只关心点击选中的,有时候只关心滚动选中的,有时候只关心选中。所以具体情况,使用对应方法。
|
||
public protocol JXSegmentedViewDelegate: AnyObject {
|
||
/// 点击选中或者滚动选中都会调用该方法。适用于只关心选中事件,而不关心具体是点击还是滚动选中的情况。
|
||
///
|
||
/// - Parameters:
|
||
/// - segmentedView: JXSegmentedView
|
||
/// - index: 选中的index
|
||
func segmentedView(_ segmentedView: JXSegmentedView, didSelectedItemAt index: Int)
|
||
|
||
/// 点击选中的情况才会调用该方法
|
||
///
|
||
/// - Parameters:
|
||
/// - segmentedView: JXSegmentedView
|
||
/// - index: 选中的index
|
||
func segmentedView(_ segmentedView: JXSegmentedView, didClickSelectedItemAt index: Int)
|
||
|
||
/// 滚动选中的情况才会调用该方法
|
||
///
|
||
/// - Parameters:
|
||
/// - segmentedView: JXSegmentedView
|
||
/// - index: 选中的index
|
||
func segmentedView(_ segmentedView: JXSegmentedView, didScrollSelectedItemAt index: Int)
|
||
|
||
/// 正在滚动中的回调
|
||
///
|
||
/// - Parameters:
|
||
/// - segmentedView: JXSegmentedView
|
||
/// - leftIndex: 正在滚动中,相对位置处于左边的index
|
||
/// - rightIndex: 正在滚动中,相对位置处于右边的index
|
||
/// - percent: 从左往右计算的百分比
|
||
func segmentedView(_ segmentedView: JXSegmentedView, scrollingFrom leftIndex: Int, to rightIndex: Int, percent: CGFloat)
|
||
|
||
|
||
/// 是否允许点击选中目标index的item
|
||
///
|
||
/// - Parameters:
|
||
/// - segmentedView: JXSegmentedView
|
||
/// - index: 目标index
|
||
func segmentedView(_ segmentedView: JXSegmentedView, canClickItemAt index: Int) -> Bool
|
||
}
|
||
|
||
/// 提供JXSegmentedViewDelegate的默认实现,这样对于遵从JXSegmentedViewDelegate的类来说,所有代理方法都是可选实现的。
|
||
public extension JXSegmentedViewDelegate {
|
||
func segmentedView(_ segmentedView: JXSegmentedView, didSelectedItemAt index: Int) { }
|
||
func segmentedView(_ segmentedView: JXSegmentedView, didClickSelectedItemAt index: Int) { }
|
||
func segmentedView(_ segmentedView: JXSegmentedView, didScrollSelectedItemAt index: Int) { }
|
||
func segmentedView(_ segmentedView: JXSegmentedView, scrollingFrom leftIndex: Int, to rightIndex: Int, percent: CGFloat) { }
|
||
func segmentedView(_ segmentedView: JXSegmentedView, canClickItemAt index: Int) -> Bool { return true }
|
||
}
|
||
|
||
/// 内部会自己找到父UIViewController,然后将其automaticallyAdjustsScrollViewInsets设置为false,这一点请知晓。
|
||
open class JXSegmentedView: UIView, JXSegmentedViewRTLCompatible {
|
||
open weak var dataSource: JXSegmentedViewDataSource? {
|
||
didSet {
|
||
dataSource?.reloadData(selectedIndex: selectedIndex)
|
||
}
|
||
}
|
||
open weak var delegate: JXSegmentedViewDelegate?
|
||
open private(set) var collectionView: JXSegmentedCollectionView!
|
||
open var contentScrollView: UIScrollView? {
|
||
willSet {
|
||
contentScrollView?.removeObserver(self, forKeyPath: "contentOffset")
|
||
}
|
||
didSet {
|
||
contentScrollView?.scrollsToTop = false
|
||
contentScrollView?.addObserver(self, forKeyPath: "contentOffset", options: .new, context: nil)
|
||
}
|
||
}
|
||
public var listContainer: JXSegmentedViewListContainer? = nil {
|
||
didSet {
|
||
listContainer?.defaultSelectedIndex = defaultSelectedIndex
|
||
contentScrollView = listContainer?.contentScrollView()
|
||
}
|
||
}
|
||
/// indicators的元素必须是遵从JXSegmentedIndicatorProtocol协议的UIView及其子类
|
||
open var indicators = [JXSegmentedIndicatorProtocol]() {
|
||
didSet {
|
||
collectionView.indicators = indicators
|
||
}
|
||
}
|
||
/// 初始化或者reloadData之前设置,用于指定默认的index
|
||
open var defaultSelectedIndex: Int = 0 {
|
||
didSet {
|
||
selectedIndex = defaultSelectedIndex
|
||
if listContainer != nil {
|
||
listContainer?.defaultSelectedIndex = defaultSelectedIndex
|
||
}
|
||
}
|
||
}
|
||
open private(set) var selectedIndex: Int = 0
|
||
/// 整体内容的左边距,默认JXSegmentedViewAutomaticDimension(等于itemSpacing)
|
||
open var contentEdgeInsetLeft: CGFloat = JXSegmentedViewAutomaticDimension
|
||
/// 整体内容的右边距,默认JXSegmentedViewAutomaticDimension(等于itemSpacing)
|
||
open var contentEdgeInsetRight: CGFloat = JXSegmentedViewAutomaticDimension
|
||
/// 点击切换的时候,contentScrollView的切换是否需要动画
|
||
open var isContentScrollViewClickTransitionAnimationEnabled: Bool = true
|
||
|
||
private var itemDataSource = [JXSegmentedBaseItemModel]()
|
||
private var innerItemSpacing: CGFloat = 0
|
||
private var lastContentOffset: CGPoint = CGPoint.zero
|
||
/// 正在滚动中的目标index。用于处理正在滚动列表的时候,立即点击item,会导致界面显示异常。
|
||
private var scrollingTargetIndex: Int = -1
|
||
private var isFirstLayoutSubviews = true
|
||
|
||
deinit {
|
||
contentScrollView?.removeObserver(self, forKeyPath: "contentOffset")
|
||
}
|
||
|
||
public override init(frame: CGRect) {
|
||
super.init(frame: frame)
|
||
|
||
commonInit()
|
||
}
|
||
|
||
required public init?(coder aDecoder: NSCoder) {
|
||
super.init(coder: aDecoder)
|
||
|
||
commonInit()
|
||
}
|
||
|
||
private func commonInit() {
|
||
let layout = UICollectionViewFlowLayout()
|
||
layout.scrollDirection = .horizontal
|
||
collectionView = JXSegmentedCollectionView(frame: CGRect.zero, collectionViewLayout: layout)
|
||
collectionView.backgroundColor = .clear
|
||
collectionView.showsVerticalScrollIndicator = false
|
||
collectionView.showsHorizontalScrollIndicator = false
|
||
collectionView.scrollsToTop = false
|
||
collectionView.register(UICollectionViewCell.self, forCellWithReuseIdentifier: "JXSegmentedViewInnerEmptyCell")
|
||
collectionView.dataSource = self
|
||
collectionView.delegate = self
|
||
if #available(iOS 10.0, *) {
|
||
collectionView.isPrefetchingEnabled = false
|
||
}
|
||
if #available(iOS 11.0, *) {
|
||
collectionView.contentInsetAdjustmentBehavior = .never
|
||
}
|
||
if segmentedViewShouldRTLLayout() {
|
||
collectionView.semanticContentAttribute = .forceLeftToRight
|
||
segmentedView(horizontalFlipForView: collectionView)
|
||
}
|
||
addSubview(collectionView)
|
||
}
|
||
|
||
open override func willMove(toSuperview newSuperview: UIView?) {
|
||
super.willMove(toSuperview: newSuperview)
|
||
|
||
var nextResponder: UIResponder? = newSuperview
|
||
while nextResponder != nil {
|
||
if let parentVC = nextResponder as? UIViewController {
|
||
parentVC.automaticallyAdjustsScrollViewInsets = false
|
||
break
|
||
}
|
||
nextResponder = nextResponder?.next
|
||
}
|
||
}
|
||
|
||
open override func layoutSubviews() {
|
||
super.layoutSubviews()
|
||
|
||
//部分使用者为了适配不同的手机屏幕尺寸,JXSegmentedView的宽高比要求保持一样。所以它的高度就会因为不同宽度的屏幕而不一样。计算出来的高度,有时候会是位数很长的浮点数,如果把这个高度设置给UICollectionView就会触发内部的一个错误。所以,为了规避这个问题,在这里对高度统一向下取整。
|
||
//如果向下取整导致了你的页面异常,请自己重新设置JXSegmentedView的高度,保证为整数即可。
|
||
let targetFrame = CGRect(x: 0, y: 0, width: bounds.size.width, height: floor(bounds.size.height))
|
||
if isFirstLayoutSubviews {
|
||
isFirstLayoutSubviews = false
|
||
collectionView.frame = targetFrame
|
||
reloadDataWithoutListContainer()
|
||
}else {
|
||
if collectionView.frame != targetFrame {
|
||
collectionView.frame = targetFrame
|
||
collectionView.collectionViewLayout.invalidateLayout()
|
||
collectionView.reloadData()
|
||
}
|
||
}
|
||
}
|
||
|
||
//MARK: - Public
|
||
public final func dequeueReusableCell(withReuseIdentifier identifier: String, at index: Int) -> JXSegmentedBaseCell {
|
||
let indexPath = IndexPath(item: index, section: 0)
|
||
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: identifier, for: indexPath)
|
||
guard cell.isKind(of: JXSegmentedBaseCell.self) else {
|
||
fatalError("Cell class must be subclass of JXSegmentedBaseCell")
|
||
}
|
||
return cell as! JXSegmentedBaseCell
|
||
}
|
||
|
||
open func reloadData() {
|
||
reloadDataWithoutListContainer()
|
||
listContainer?.reloadData()
|
||
}
|
||
|
||
open func reloadDataWithoutListContainer() {
|
||
dataSource?.reloadData(selectedIndex: selectedIndex)
|
||
dataSource?.registerCellClass(in: self)
|
||
if let itemSource = dataSource?.itemDataSource(in: self) {
|
||
itemDataSource = itemSource
|
||
}
|
||
if selectedIndex < 0 || selectedIndex >= itemDataSource.count {
|
||
defaultSelectedIndex = 0
|
||
selectedIndex = 0
|
||
}
|
||
|
||
innerItemSpacing = dataSource?.itemSpacing ?? 0
|
||
var totalItemWidth: CGFloat = 0
|
||
var totalContentWidth: CGFloat = getContentEdgeInsetLeft()
|
||
for (index, itemModel) in itemDataSource.enumerated() {
|
||
itemModel.index = index
|
||
itemModel.itemWidth = (dataSource?.segmentedView(self, widthForItemAt: index) ?? 0)
|
||
if dataSource?.isItemWidthZoomEnabled == true {
|
||
itemModel.itemWidth *= itemModel.itemWidthCurrentZoomScale
|
||
}
|
||
itemModel.isSelected = (index == selectedIndex)
|
||
totalItemWidth += itemModel.itemWidth
|
||
if index == itemDataSource.count - 1 {
|
||
totalContentWidth += itemModel.itemWidth + getContentEdgeInsetRight()
|
||
}else {
|
||
totalContentWidth += itemModel.itemWidth + innerItemSpacing
|
||
}
|
||
}
|
||
|
||
if dataSource?.isItemSpacingAverageEnabled == true && totalContentWidth < bounds.size.width {
|
||
var itemSpacingCount = itemDataSource.count - 1
|
||
var totalItemSpacingWidth = bounds.size.width - totalItemWidth
|
||
if contentEdgeInsetLeft == JXSegmentedViewAutomaticDimension {
|
||
itemSpacingCount += 1
|
||
}else {
|
||
totalItemSpacingWidth -= contentEdgeInsetLeft
|
||
}
|
||
if contentEdgeInsetRight == JXSegmentedViewAutomaticDimension {
|
||
itemSpacingCount += 1
|
||
}else {
|
||
totalItemSpacingWidth -= contentEdgeInsetRight
|
||
}
|
||
if itemSpacingCount > 0 {
|
||
innerItemSpacing = totalItemSpacingWidth / CGFloat(itemSpacingCount)
|
||
}
|
||
}
|
||
|
||
var selectedItemFrameX = innerItemSpacing
|
||
var selectedItemWidth: CGFloat = 0
|
||
totalContentWidth = getContentEdgeInsetLeft()
|
||
for (index, itemModel) in itemDataSource.enumerated() {
|
||
if index < selectedIndex {
|
||
selectedItemFrameX += itemModel.itemWidth + innerItemSpacing
|
||
}else if index == selectedIndex {
|
||
selectedItemWidth = itemModel.itemWidth
|
||
}
|
||
if index == itemDataSource.count - 1 {
|
||
totalContentWidth += itemModel.itemWidth + getContentEdgeInsetRight()
|
||
}else {
|
||
totalContentWidth += itemModel.itemWidth + innerItemSpacing
|
||
}
|
||
}
|
||
|
||
let minX: CGFloat = 0
|
||
let maxX = totalContentWidth - bounds.size.width
|
||
let targetX = selectedItemFrameX - bounds.size.width/2 + selectedItemWidth/2
|
||
collectionView.setContentOffset(CGPoint(x: max(min(maxX, targetX), minX), y: 0), animated: false)
|
||
|
||
if contentScrollView != nil {
|
||
if contentScrollView!.frame.equalTo(CGRect.zero) &&
|
||
contentScrollView!.superview != nil {
|
||
//某些情况系统会出现JXSegmentedView先布局,contentScrollView后布局。就会导致下面指定defaultSelectedIndex失效,所以发现contentScrollView的frame为zero时,强行触发其父视图链里面已经有frame的一个父视图的layoutSubviews方法。
|
||
//比如JXSegmentedListContainerView会将contentScrollView包裹起来使用,该情况需要JXSegmentedListContainerView.superView触发布局更新
|
||
var parentView = contentScrollView?.superview
|
||
while parentView != nil && parentView?.frame.equalTo(CGRect.zero) == true {
|
||
parentView = parentView?.superview
|
||
}
|
||
parentView?.setNeedsLayout()
|
||
parentView?.layoutIfNeeded()
|
||
}
|
||
|
||
contentScrollView!.setContentOffset(CGPoint(x: CGFloat(selectedIndex) * contentScrollView!.bounds.size.width
|
||
, y: 0), animated: false)
|
||
}
|
||
|
||
for indicator in indicators {
|
||
if itemDataSource.isEmpty {
|
||
indicator.isHidden = true
|
||
}else {
|
||
indicator.isHidden = false
|
||
let selectedItemFrame = getItemFrameAt(index: selectedIndex)
|
||
let indicatorParams = JXSegmentedIndicatorSelectedParams(currentSelectedIndex: selectedIndex,
|
||
currentSelectedItemFrame: selectedItemFrame,
|
||
selectedType: .unknown,
|
||
currentItemContentWidth: dataSource?.segmentedView(self, widthForItemContentAt: selectedIndex) ?? 0,
|
||
collectionViewContentSize: CGSize(width: totalContentWidth, height: bounds.size.height))
|
||
indicator.refreshIndicatorState(model: indicatorParams)
|
||
|
||
if indicator.isIndicatorConvertToItemFrameEnabled {
|
||
var indicatorConvertToItemFrame = indicator.frame
|
||
indicatorConvertToItemFrame.origin.x -= selectedItemFrame.origin.x
|
||
itemDataSource[selectedIndex].indicatorConvertToItemFrame = indicatorConvertToItemFrame
|
||
}
|
||
}
|
||
}
|
||
collectionView.reloadData()
|
||
collectionView.collectionViewLayout.invalidateLayout()
|
||
}
|
||
|
||
open func reloadItem(at index: Int) {
|
||
guard index >= 0 && index < itemDataSource.count else {
|
||
return
|
||
}
|
||
|
||
dataSource?.refreshItemModel(self, itemDataSource[index], at: index, selectedIndex: selectedIndex)
|
||
let cell = collectionView.cellForItem(at: IndexPath(item: index, section: 0)) as? JXSegmentedBaseCell
|
||
cell?.reloadData(itemModel: itemDataSource[index], selectedType: .unknown)
|
||
}
|
||
|
||
|
||
/// 代码选中指定index
|
||
/// 如果要同时触发列表容器对应index的列表加载,请再调用`listContainerView.didClickSelectedItem(at: index)`方法
|
||
///
|
||
/// - Parameter index: 目标index
|
||
open func selectItemAt(index: Int) {
|
||
selectItemAt(index: index, selectedType: .code)
|
||
}
|
||
|
||
//MARK: - KVO
|
||
open override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
|
||
if keyPath == "contentOffset" {
|
||
let contentOffset = change?[NSKeyValueChangeKey.newKey] as! CGPoint
|
||
if contentScrollView?.isTracking == true || contentScrollView?.isDecelerating == true {
|
||
//用户滚动引起的contentOffset变化,才处理。
|
||
if contentScrollView?.bounds.size.width == 0 {
|
||
// 如果contentScrollView Frame为零,直接忽略
|
||
return
|
||
}
|
||
var progress = contentOffset.x/contentScrollView!.bounds.size.width
|
||
if Int(progress) > itemDataSource.count - 1 || progress < 0 {
|
||
//超过了边界,不需要处理
|
||
return
|
||
}
|
||
if contentOffset.x == 0 && selectedIndex == 0 && lastContentOffset.x == 0 {
|
||
//滚动到了最左边,且已经选中了第一个,且之前的contentOffset.x为0
|
||
return
|
||
}
|
||
let maxContentOffsetX = contentScrollView!.contentSize.width - contentScrollView!.bounds.size.width
|
||
if contentOffset.x == maxContentOffsetX && selectedIndex == itemDataSource.count - 1 && lastContentOffset.x == maxContentOffsetX {
|
||
//滚动到了最右边,且已经选中了最后一个,且之前的contentOffset.x为maxContentOffsetX
|
||
return
|
||
}
|
||
|
||
progress = max(0, min(CGFloat(itemDataSource.count - 1), progress))
|
||
let baseIndex = Int(floor(progress))
|
||
let remainderProgress = progress - CGFloat(baseIndex)
|
||
|
||
let leftItemFrame = getItemFrameAt(index: baseIndex)
|
||
let rightItemFrame = getItemFrameAt(index: baseIndex + 1)
|
||
var rightItemContentWidth: CGFloat = 0
|
||
if baseIndex + 1 < itemDataSource.count {
|
||
rightItemContentWidth = dataSource?.segmentedView(self, widthForItemContentAt: baseIndex + 1) ?? 0
|
||
}
|
||
let indicatorParams = JXSegmentedIndicatorTransitionParams(currentSelectedIndex: selectedIndex,
|
||
leftIndex: baseIndex,
|
||
leftItemFrame: leftItemFrame,
|
||
leftItemContentWidth: dataSource?.segmentedView(self, widthForItemContentAt: baseIndex) ?? 0,
|
||
rightIndex: baseIndex + 1,
|
||
rightItemFrame: rightItemFrame,
|
||
rightItemContentWidth: rightItemContentWidth,
|
||
percent: remainderProgress)
|
||
|
||
if remainderProgress == 0 {
|
||
//滑动翻页,需要更新选中状态
|
||
//滑动一小段距离,然后放开回到原位,contentOffset同样的值会回调多次。例如在index为1的情况,滑动放开回到原位,contentOffset会多次回调CGPoint(width, 0)
|
||
if !(lastContentOffset.x == contentOffset.x && selectedIndex == baseIndex) {
|
||
scrollSelectItemAt(index: baseIndex)
|
||
}
|
||
}else {
|
||
//快速滑动翻页,当remainderRatio没有变成0,但是已经翻页了,需要通过下面的判断,触发选中
|
||
if abs(progress - CGFloat(selectedIndex)) > 1 {
|
||
var targetIndex = baseIndex
|
||
if progress < CGFloat(selectedIndex) {
|
||
targetIndex = baseIndex + 1
|
||
}
|
||
scrollSelectItemAt(index: targetIndex)
|
||
}
|
||
if selectedIndex == baseIndex {
|
||
scrollingTargetIndex = baseIndex + 1
|
||
}else {
|
||
scrollingTargetIndex = baseIndex
|
||
}
|
||
|
||
dataSource?.refreshItemModel(self, leftItemModel: itemDataSource[baseIndex], rightItemModel: itemDataSource[baseIndex + 1], percent: remainderProgress)
|
||
|
||
for indicator in indicators {
|
||
indicator.contentScrollViewDidScroll(model: indicatorParams)
|
||
if indicator.isIndicatorConvertToItemFrameEnabled {
|
||
var leftIndicatorConvertToItemFrame = indicator.frame
|
||
leftIndicatorConvertToItemFrame.origin.x -= leftItemFrame.origin.x
|
||
itemDataSource[baseIndex].indicatorConvertToItemFrame = leftIndicatorConvertToItemFrame
|
||
|
||
var rightIndicatorConvertToItemFrame = indicator.frame
|
||
rightIndicatorConvertToItemFrame.origin.x -= rightItemFrame.origin.x
|
||
itemDataSource[baseIndex + 1].indicatorConvertToItemFrame = rightIndicatorConvertToItemFrame
|
||
}
|
||
}
|
||
|
||
let leftCell = collectionView.cellForItem(at: IndexPath(item: baseIndex, section: 0)) as? JXSegmentedBaseCell
|
||
leftCell?.reloadData(itemModel: itemDataSource[baseIndex], selectedType: .unknown)
|
||
|
||
let rightCell = collectionView.cellForItem(at: IndexPath(item: baseIndex + 1, section: 0)) as? JXSegmentedBaseCell
|
||
rightCell?.reloadData(itemModel: itemDataSource[baseIndex + 1], selectedType: .unknown)
|
||
|
||
delegate?.segmentedView(self, scrollingFrom: baseIndex, to: baseIndex + 1, percent: remainderProgress)
|
||
}
|
||
}
|
||
lastContentOffset = contentOffset
|
||
}
|
||
}
|
||
|
||
//MARK: - Private
|
||
private func clickSelectItemAt(index: Int) {
|
||
guard delegate?.segmentedView(self, canClickItemAt: index) != false else {
|
||
return
|
||
}
|
||
selectItemAt(index: index, selectedType: .click)
|
||
}
|
||
|
||
private func scrollSelectItemAt(index: Int) {
|
||
selectItemAt(index: index, selectedType: .scroll)
|
||
}
|
||
|
||
private func selectItemAt(index: Int, selectedType: JXSegmentedViewItemSelectedType) {
|
||
guard index >= 0 && index < itemDataSource.count else {
|
||
return
|
||
}
|
||
|
||
if index == selectedIndex {
|
||
if selectedType == .code {
|
||
listContainer?.didClickSelectedItem(at: index)
|
||
}else if selectedType == .click {
|
||
delegate?.segmentedView(self, didClickSelectedItemAt: index)
|
||
listContainer?.didClickSelectedItem(at: index)
|
||
}else if selectedType == .scroll {
|
||
delegate?.segmentedView(self, didScrollSelectedItemAt: index)
|
||
}
|
||
delegate?.segmentedView(self, didSelectedItemAt: index)
|
||
scrollingTargetIndex = -1
|
||
return
|
||
}
|
||
|
||
let currentSelectedItemModel = itemDataSource[selectedIndex]
|
||
let willSelectedItemModel = itemDataSource[index]
|
||
dataSource?.refreshItemModel(self, currentSelectedItemModel: currentSelectedItemModel, willSelectedItemModel: willSelectedItemModel, selectedType: selectedType)
|
||
|
||
let currentSelectedCell = collectionView.cellForItem(at: IndexPath(item: selectedIndex, section: 0)) as? JXSegmentedBaseCell
|
||
currentSelectedCell?.reloadData(itemModel: currentSelectedItemModel, selectedType: selectedType)
|
||
|
||
let willSelectedCell = collectionView.cellForItem(at: IndexPath(item: index, section: 0)) as? JXSegmentedBaseCell
|
||
willSelectedCell?.reloadData(itemModel: willSelectedItemModel, selectedType: selectedType)
|
||
|
||
if scrollingTargetIndex != -1 && scrollingTargetIndex != index {
|
||
let scrollingTargetItemModel = itemDataSource[scrollingTargetIndex]
|
||
scrollingTargetItemModel.isSelected = false
|
||
dataSource?.refreshItemModel(self, currentSelectedItemModel: scrollingTargetItemModel, willSelectedItemModel: willSelectedItemModel, selectedType: selectedType)
|
||
let scrollingTargetCell = collectionView.cellForItem(at: IndexPath(item: scrollingTargetIndex, section: 0)) as? JXSegmentedBaseCell
|
||
scrollingTargetCell?.reloadData(itemModel: scrollingTargetItemModel, selectedType: selectedType)
|
||
}
|
||
|
||
if dataSource?.isItemWidthZoomEnabled == true {
|
||
if selectedType == .click || selectedType == .code {
|
||
//延时为了解决cellwidth变化,点击最后几个cell,scrollToItem会出现位置偏移bu。需要等cellWidth动画渐变结束后再滚动到index的cell位置。
|
||
let selectedAnimationDurationInMilliseconds = Int((dataSource?.selectedAnimationDuration ?? 0)*1000)
|
||
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + DispatchTimeInterval.milliseconds(selectedAnimationDurationInMilliseconds)) {
|
||
self.collectionView.scrollToItem(at: IndexPath(item: index, section: 0), at: .centeredHorizontally, animated: true)
|
||
}
|
||
}else if selectedType == .scroll {
|
||
//滚动选中的直接处理
|
||
collectionView.scrollToItem(at: IndexPath(item: index, section: 0), at: .centeredHorizontally, animated: true)
|
||
}
|
||
}else {
|
||
collectionView.scrollToItem(at: IndexPath(item: index, section: 0), at: .centeredHorizontally, animated: true)
|
||
}
|
||
|
||
if contentScrollView != nil && (selectedType == .click || selectedType == .code) {
|
||
contentScrollView!.setContentOffset(CGPoint(x: contentScrollView!.bounds.size.width*CGFloat(index), y: 0), animated: isContentScrollViewClickTransitionAnimationEnabled)
|
||
}
|
||
|
||
selectedIndex = index
|
||
|
||
let currentSelectedItemFrame = getSelectedItemFrameAt(index: selectedIndex)
|
||
for indicator in indicators {
|
||
let indicatorParams = JXSegmentedIndicatorSelectedParams(currentSelectedIndex: selectedIndex,
|
||
currentSelectedItemFrame: currentSelectedItemFrame,
|
||
selectedType: selectedType,
|
||
currentItemContentWidth: dataSource?.segmentedView(self, widthForItemContentAt: selectedIndex) ?? 0,
|
||
collectionViewContentSize: nil)
|
||
indicator.selectItem(model: indicatorParams)
|
||
|
||
if indicator.isIndicatorConvertToItemFrameEnabled {
|
||
var indicatorConvertToItemFrame = indicator.frame
|
||
indicatorConvertToItemFrame.origin.x -= currentSelectedItemFrame.origin.x
|
||
itemDataSource[selectedIndex].indicatorConvertToItemFrame = indicatorConvertToItemFrame
|
||
willSelectedCell?.reloadData(itemModel: willSelectedItemModel, selectedType: selectedType)
|
||
}
|
||
}
|
||
|
||
scrollingTargetIndex = -1
|
||
if selectedType == .code {
|
||
listContainer?.didClickSelectedItem(at: index)
|
||
}else if selectedType == .click {
|
||
delegate?.segmentedView(self, didClickSelectedItemAt: index)
|
||
listContainer?.didClickSelectedItem(at: index)
|
||
}else if selectedType == .scroll {
|
||
delegate?.segmentedView(self, didScrollSelectedItemAt: index)
|
||
}
|
||
delegate?.segmentedView(self, didSelectedItemAt: index)
|
||
}
|
||
|
||
private func getItemFrameAt(index: Int) -> CGRect {
|
||
guard index < itemDataSource.count else {
|
||
return CGRect.zero
|
||
}
|
||
var x = getContentEdgeInsetLeft()
|
||
for i in 0..<index {
|
||
let itemModel = itemDataSource[i]
|
||
var itemWidth: CGFloat = 0
|
||
if itemModel.isTransitionAnimating && itemModel.isItemWidthZoomEnabled {
|
||
//正在进行动画的时候,itemWidthCurrentZoomScale是随着动画渐变的,而没有立即更新到目标值
|
||
if itemModel.isSelected {
|
||
itemWidth = (dataSource?.segmentedView(self, widthForItemAt: itemModel.index) ?? 0) * itemModel.itemWidthSelectedZoomScale
|
||
}else {
|
||
itemWidth = (dataSource?.segmentedView(self, widthForItemAt: itemModel.index) ?? 0) * itemModel.itemWidthNormalZoomScale
|
||
}
|
||
}else {
|
||
itemWidth = itemModel.itemWidth
|
||
}
|
||
x += itemWidth + innerItemSpacing
|
||
}
|
||
var width: CGFloat = 0
|
||
let selectedItemModel = itemDataSource[index]
|
||
if selectedItemModel.isTransitionAnimating && selectedItemModel.isItemWidthZoomEnabled {
|
||
width = (dataSource?.segmentedView(self, widthForItemAt: selectedItemModel.index) ?? 0) * selectedItemModel.itemWidthSelectedZoomScale
|
||
}else {
|
||
width = selectedItemModel.itemWidth
|
||
}
|
||
return CGRect(x: x, y: 0, width: width, height: bounds.size.height)
|
||
}
|
||
|
||
private func getSelectedItemFrameAt(index: Int) -> CGRect {
|
||
guard index < itemDataSource.count else {
|
||
return CGRect.zero
|
||
}
|
||
var x = getContentEdgeInsetLeft()
|
||
for i in 0..<index {
|
||
let itemWidth = (dataSource?.segmentedView(self, widthForItemAt: i) ?? 0)
|
||
x += itemWidth + innerItemSpacing
|
||
}
|
||
var width: CGFloat = 0
|
||
let selectedItemModel = itemDataSource[index]
|
||
if selectedItemModel.isItemWidthZoomEnabled {
|
||
width = (dataSource?.segmentedView(self, widthForItemAt: selectedItemModel.index) ?? 0) * selectedItemModel.itemWidthSelectedZoomScale
|
||
}else {
|
||
width = selectedItemModel.itemWidth
|
||
}
|
||
return CGRect(x: x, y: 0, width: width, height: bounds.size.height)
|
||
}
|
||
|
||
private func getContentEdgeInsetLeft() -> CGFloat {
|
||
if contentEdgeInsetLeft == JXSegmentedViewAutomaticDimension {
|
||
return innerItemSpacing
|
||
}else {
|
||
return contentEdgeInsetLeft
|
||
}
|
||
}
|
||
|
||
private func getContentEdgeInsetRight() -> CGFloat {
|
||
if contentEdgeInsetRight == JXSegmentedViewAutomaticDimension {
|
||
return innerItemSpacing
|
||
}else {
|
||
return contentEdgeInsetRight
|
||
}
|
||
}
|
||
}
|
||
|
||
extension JXSegmentedView: UICollectionViewDataSource {
|
||
public func numberOfSections(in collectionView: UICollectionView) -> Int {
|
||
return 1
|
||
}
|
||
|
||
public func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
|
||
return itemDataSource.count
|
||
}
|
||
|
||
public func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
|
||
if let cell = dataSource?.segmentedView(self, cellForItemAt: indexPath.item) {
|
||
cell.reloadData(itemModel: itemDataSource[indexPath.item], selectedType: .unknown)
|
||
return cell
|
||
}else {
|
||
return collectionView.dequeueReusableCell(withReuseIdentifier: "JXSegmentedViewInnerEmptyCell", for: indexPath)
|
||
}
|
||
}
|
||
}
|
||
|
||
extension JXSegmentedView: UICollectionViewDelegate {
|
||
public func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
|
||
var isTransitionAnimating = false
|
||
for itemModel in itemDataSource {
|
||
if itemModel.isTransitionAnimating {
|
||
isTransitionAnimating = true
|
||
break
|
||
}
|
||
}
|
||
if !isTransitionAnimating {
|
||
//当前没有正在过渡的item,才允许点击选中
|
||
clickSelectItemAt(index: indexPath.item)
|
||
}
|
||
}
|
||
}
|
||
|
||
extension JXSegmentedView: UICollectionViewDelegateFlowLayout {
|
||
public func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets {
|
||
return UIEdgeInsets(top: 0, left: getContentEdgeInsetLeft(), bottom: 0, right: getContentEdgeInsetRight())
|
||
}
|
||
|
||
public func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
|
||
if indexPath.item >= 0, indexPath.item < itemDataSource.count {
|
||
return CGSize(width: itemDataSource[indexPath.item].itemWidth, height: collectionView.bounds.size.height)
|
||
} else {
|
||
return .zero
|
||
}
|
||
}
|
||
public func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAt section: Int) -> CGFloat {
|
||
return innerItemSpacing
|
||
}
|
||
|
||
public func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumInteritemSpacingForSectionAt section: Int) -> CGFloat {
|
||
return innerItemSpacing
|
||
}
|
||
}
|