683 lines
28 KiB
Swift
683 lines
28 KiB
Swift
//
|
||
// JYPageMenuView.swift
|
||
// JYPageController
|
||
//
|
||
// Created by wang tao on 2022/7/14.
|
||
//
|
||
|
||
import UIKit
|
||
|
||
@objc public protocol JYSegmentedViewDataSource {
|
||
|
||
///segmentedView中items数量. segmentedView items count
|
||
func numberOfSegmentedViewItems() -> Int
|
||
|
||
///segmentedView中item标题文案. segmentedView item title
|
||
func segmentedView(_ segmentedView: JYSegmentedView, titleAt index: Int) -> String
|
||
|
||
///segmentedView自定义item,实现了该方法且返回了UIView的话会忽略该index上的获取title的方法。
|
||
///例如 index=1的时候return button,那么 segmentedView(_ segmentedView: JYSegmentedView, titleAt index: Int)取title的时候会跳过index=1
|
||
///注意:返回的自定义view需要设置Size
|
||
///CustomView has higher priority than title,when return customView, ignore title. CustomView need set frame.size
|
||
@objc optional func segmentedView(_ segmentedView: JYSegmentedView, customViewAt index: Int) -> UIView?
|
||
|
||
///segmentedView item 右上角的角标,return的UIView需要设置frame.size
|
||
///item badgeView (eg. label/red dot/icon, need set frame.size)
|
||
@objc optional func segmentedView(_ segmentedView: JYSegmentedView, badgeViewAt index: Int) -> UIView?
|
||
|
||
}
|
||
|
||
@objc public protocol JYSegmentedViewDelegate {
|
||
|
||
@objc optional func segmentedView(_ segmentedView: JYSegmentedView, didSelectItemAt index: Int)
|
||
}
|
||
|
||
|
||
|
||
|
||
public class JYSegmentedView: UIView {
|
||
|
||
///样式配置 config
|
||
var config: JYPageConfig = JYPageConfig()
|
||
|
||
///代理 delegate
|
||
weak public var delegate: JYSegmentedViewDelegate?
|
||
|
||
///数据源 datasource
|
||
weak public var dataSource: JYSegmentedViewDataSource? {
|
||
didSet {
|
||
getItemsCount()
|
||
}
|
||
}
|
||
|
||
///当前选中的index
|
||
private var selectedIndex: Int = 0
|
||
|
||
///item数量
|
||
private var itemsCount: Int = 0
|
||
|
||
///items数组
|
||
private var items = [JYSegmentedViewItem]()
|
||
|
||
///item背景图
|
||
private var itemBackgroundViews = [UIImageView]()
|
||
|
||
///第一次初始化和reload的时候用来标记,layoutSubview中layoutItems方法只调用一次。原因:在对默认选中的item多次设置transform的时,tranform有值,但获取到的frame却没有变
|
||
var layoutOnceToken: Bool = false
|
||
|
||
|
||
|
||
override init(frame: CGRect) {
|
||
super.init(frame: frame)
|
||
|
||
clipsToBounds = true
|
||
addSubview(contentView)
|
||
contentView.addSubview(indicator)
|
||
}
|
||
|
||
public convenience init(pageConfig: JYPageConfig) {
|
||
self.init()
|
||
|
||
config = pageConfig
|
||
|
||
if config.indicatorStyle == .singleLine {
|
||
indicator.backgroundColor = config.indicatorColor
|
||
indicator.layer.cornerRadius = config.indicatorCornerRadius
|
||
}
|
||
|
||
if config.indicatorStyle == .none {
|
||
indicator.removeFromSuperview()
|
||
}
|
||
|
||
if config.indicatorStyle == .customView {
|
||
indicator.removeFromSuperview()
|
||
indicator = config.customIndicator ?? UIView()
|
||
contentView.addSubview(indicator)
|
||
}
|
||
}
|
||
|
||
|
||
required init?(coder: NSCoder) {
|
||
fatalError("init(coder:) has not been implemented")
|
||
}
|
||
|
||
public override func willMove(toSuperview newSuperview: UIView?) {
|
||
super.willMove(toSuperview: newSuperview)
|
||
if newSuperview != nil {
|
||
reload()
|
||
}
|
||
}
|
||
|
||
public override func layoutSubviews() {
|
||
super.layoutSubviews()
|
||
|
||
contentView.frame = self.bounds
|
||
layoutItems()
|
||
indicatorMoveTo(index: selectedIndex, animate: false)
|
||
resetHorContentOffset(animate: false)
|
||
}
|
||
|
||
private func getItemsCount() {
|
||
guard let source = dataSource else {
|
||
return
|
||
}
|
||
itemsCount = source.numberOfSegmentedViewItems()
|
||
}
|
||
|
||
//MARK: - Public
|
||
///单独使用segmentedView,动态改变数据源的时候调用(数据源改变之后先reload,如需要设置默认index,再调select)。其他场景不需要主动调用
|
||
public func reload() {
|
||
guard let source = dataSource, source.numberOfSegmentedViewItems() > 0 else {
|
||
return
|
||
}
|
||
|
||
getItemsCount()
|
||
addItems()
|
||
layoutOnceToken = false
|
||
layoutItems()
|
||
|
||
|
||
resetHorContentOffset(animate: false)
|
||
indicatorMoveTo(index: selectedIndex, animate: false)
|
||
|
||
contentView.sendSubviewToBack(indicator)
|
||
itemBackgroundViews.forEach { contentView.sendSubviewToBack($0) }
|
||
|
||
}
|
||
|
||
///获取当前选中的index
|
||
public func currentSelectedIndex() -> Int {
|
||
return selectedIndex
|
||
}
|
||
|
||
///获取segmentedview的contentsize
|
||
public func contentSize() -> CGSize {
|
||
return contentView.contentSize
|
||
}
|
||
|
||
///更新/设置segmentedview的frame
|
||
public func updateFrame(frame: CGRect) {
|
||
self.frame = frame
|
||
layoutIfNeeded()
|
||
resetHorContentOffset(animate: false)
|
||
}
|
||
|
||
///为第index位置上的item添加badgeView
|
||
public func addSegmentedItemBadgeView(_ badgeView: UIView, atIndex index: Int) {
|
||
guard let item = itemWithIndex(index) else {
|
||
return
|
||
}
|
||
|
||
if item.hasBadgeView {
|
||
item.badgeView?.removeFromSuperview()
|
||
}
|
||
item.badgeView = badgeView
|
||
contentView.addSubview(badgeView)
|
||
updateItemsFrame()
|
||
}
|
||
|
||
///移除指定index的item的badgeView
|
||
public func removeSegmentedItemBadgeView(atIndex index: Int) {
|
||
guard let menuItem = itemWithIndex(index) else {
|
||
return
|
||
}
|
||
|
||
if menuItem.hasBadgeView {
|
||
menuItem.badgeView?.removeFromSuperview()
|
||
menuItem.badgeView = nil
|
||
updateItemsFrame()
|
||
}
|
||
}
|
||
|
||
///页面滚动过程中,持续调用
|
||
public func segmentedViewScroll(by pageView: UIScrollView) {
|
||
changeItemsByScrollViewDidScroll(scrollView: pageView)
|
||
updateItemsFrame()
|
||
}
|
||
|
||
///页面滚动停止,scrollVIew代理方法scrollEndDecelerating中调用
|
||
public func segmentedViewScrollEnd(byScrollEndDecelerating pageView: UIScrollView) {
|
||
|
||
let offsetX = pageView.contentOffset.x
|
||
let currentIndex = Int(offsetX/pageView.frame.width)
|
||
selectedIndex = currentIndex
|
||
itemWithIndex(selectedIndex)?.selected = true
|
||
itemWithIndex(selectedIndex)?.font = UIFont.systemFont(ofSize: config.normalTitleFont, weight: config.selectedTitleFontWeight)
|
||
for item in items {
|
||
if (item.tag - kMenuItemTagExtenValue) != selectedIndex {
|
||
item.selected = false
|
||
item.font = UIFont.systemFont(ofSize: config.normalTitleFont, weight: config.normalTitleFontWeight)
|
||
}
|
||
}
|
||
|
||
resetHorContentOffset(animate: true)
|
||
indicatorMoveTo(index: selectedIndex, animate: true)
|
||
}
|
||
|
||
///设置选中index
|
||
public func select(_ index: Int) {
|
||
if index < itemsCount {
|
||
itemWithIndex(index)?.selected = true
|
||
selectedIndex = index
|
||
reload()
|
||
delegate?.segmentedView?(self, didSelectItemAt: selectedIndex)
|
||
}
|
||
}
|
||
|
||
//MARK: - Private
|
||
///滚动过程中item的渐变效果
|
||
private func changeItemsByScrollViewDidScroll(scrollView: UIScrollView) {
|
||
if scrollView.frame.size.width <= 0 {
|
||
return
|
||
}
|
||
|
||
let offsetX = scrollView.contentOffset.x
|
||
let rate = offsetX.truncatingRemainder(dividingBy: scrollView.frame.size.width)/scrollView.frame.size.width
|
||
|
||
if rate <= 0 {
|
||
return
|
||
}
|
||
|
||
var selectedItem = itemWithIndex(selectedIndex)
|
||
var targetItem: JYSegmentedViewItem?
|
||
|
||
if offsetX > CGFloat(selectedIndex) * scrollView.frame.size.width {
|
||
|
||
let targetIndex = Int(offsetX/scrollView.frame.size.width) + 1
|
||
targetItem = itemWithIndex(targetIndex)
|
||
|
||
//备注:处理手指连续快速向左滑动时候,selectedIndex没有及时改变造成计算fromItem和toItem错乱问题
|
||
if selectedIndex != Int(offsetX/scrollView.frame.size.width) {
|
||
itemWithIndex(selectedIndex)?.transform = .identity
|
||
if let item = targetItem {
|
||
item.transform = CGAffineTransform(scaleX:maxScale, y: maxScale)
|
||
}
|
||
selectedIndex = Int(offsetX/scrollView.frame.size.width)
|
||
selectedItem = itemWithIndex(selectedIndex)
|
||
resetHorContentOffset(animate: true)
|
||
updateItemColor()
|
||
}
|
||
}else{
|
||
|
||
let targetIndex = Int(offsetX/scrollView.frame.size.width)
|
||
targetItem = itemWithIndex(targetIndex)
|
||
|
||
//备注:处理手指连续快速向右滑动时候,selectedIndex没有及时改变造成计算fromItem和toItem错乱问题
|
||
if selectedIndex != Int(offsetX/scrollView.frame.size.width) + 1 {
|
||
itemWithIndex(selectedIndex)?.transform = .identity
|
||
if let item = targetItem {
|
||
item.transform = CGAffineTransform(scaleX:maxScale , y: maxScale)
|
||
}
|
||
selectedIndex = Int(offsetX/scrollView.frame.size.width) + 1
|
||
selectedItem = itemWithIndex(selectedIndex)
|
||
resetHorContentOffset(animate: true)
|
||
updateItemColor()
|
||
}
|
||
}
|
||
|
||
guard let toItem = targetItem, let fromItem = selectedItem else {
|
||
return
|
||
}
|
||
|
||
if fromItem.tag < toItem.tag {
|
||
fromItem.rate = 1 - rate
|
||
toItem.rate = rate
|
||
let fromItemCurrentScaleX = maxScale - (maxScale - 1) * rate
|
||
let fromItemCurrentScaleY = maxScale - (maxScale - 1) * rate
|
||
let toItemCurrentScaleX = 1 + (maxScale - 1) * rate
|
||
let toItemCurrentScaleY = 1 + (maxScale - 1) * rate
|
||
|
||
fromItem.transform = CGAffineTransform(scaleX: fromItemCurrentScaleX, y: fromItemCurrentScaleY)
|
||
toItem.transform = CGAffineTransform(scaleX: toItemCurrentScaleX, y: toItemCurrentScaleY)
|
||
}else {
|
||
fromItem.rate = rate
|
||
toItem.rate = 1 - rate
|
||
let fromItemCurrentScaleX = maxScale - (maxScale - 1) * (1 - rate)
|
||
let fromItemCurrentScaleY = maxScale - (maxScale - 1) * (1 - rate)
|
||
let toItemCurrentScaleX = 1 + (maxScale - 1) * (1 - rate)
|
||
let toItemCurrentScaleY = 1 + (maxScale - 1) * (1 - rate)
|
||
|
||
fromItem.transform = CGAffineTransform(scaleX: fromItemCurrentScaleX, y: fromItemCurrentScaleY)
|
||
toItem.transform = CGAffineTransform(scaleX: toItemCurrentScaleX, y: toItemCurrentScaleY)
|
||
}
|
||
|
||
if config.indicatorStyle != .none {
|
||
indicatorMove(fromItem: fromItem, toItem: toItem, offsetX: offsetX, rate: rate, pageView: scrollView)
|
||
}
|
||
}
|
||
|
||
private func updateItemColor() {
|
||
items.forEach { item in
|
||
if item.tag == selectedIndex + kMenuItemTagExtenValue {
|
||
item.selected = true
|
||
}else{
|
||
item.selected = false
|
||
}
|
||
}
|
||
}
|
||
|
||
private func indicatorMove(fromItem: JYSegmentedViewItem, toItem: JYSegmentedViewItem, offsetX: CGFloat, rate: CGFloat, pageView: UIScrollView) {
|
||
|
||
let scrollViewWidth = pageView.frame.width
|
||
var currentIndicatorWidth: CGFloat = config.indicatorWidth > 0 ? config.indicatorWidth : fromItem.frame.width
|
||
let indicatorMaxWidth = abs(toItem.center.x - fromItem.center.x)
|
||
let tempOffetX = offsetX.truncatingRemainder(dividingBy: scrollViewWidth)
|
||
|
||
switch config.indicatorStyle {
|
||
case .singleLine:
|
||
if config.indicatorStickyAnimation {//下划线粘性动画
|
||
if tempOffetX <= scrollViewWidth/2 {
|
||
let percent_min_max = tempOffetX/scrollViewWidth*2
|
||
currentIndicatorWidth = currentIndicatorWidth + percent_min_max * (indicatorMaxWidth - currentIndicatorWidth)
|
||
}else{
|
||
let percent_max_min = (tempOffetX - scrollViewWidth/2)/scrollViewWidth*2
|
||
currentIndicatorWidth = currentIndicatorWidth + (1 - percent_max_min)*(indicatorMaxWidth - currentIndicatorWidth)
|
||
}
|
||
|
||
if fromItem.tag < toItem.tag {
|
||
indicator.center = CGPoint(x: fromItem.center.x + rate * indicatorMaxWidth, y: indicator.center.y)
|
||
}else{
|
||
indicator.center = CGPoint(x: fromItem.center.x - (1 - rate) * indicatorMaxWidth, y: indicator.center.y)
|
||
}
|
||
|
||
var frame = indicator.frame
|
||
frame.size.width = currentIndicatorWidth
|
||
frame.size.height = indicator.frame.size.height
|
||
indicator.frame = frame
|
||
}else {
|
||
if fromItem.tag < toItem.tag {
|
||
indicator.center = CGPoint(x: fromItem.center.x + rate * indicatorMaxWidth, y: indicator.center.y)
|
||
}else{
|
||
indicator.center = CGPoint(x: fromItem.center.x - (1 - rate) * indicatorMaxWidth, y: indicator.center.y)
|
||
}
|
||
|
||
var frame = indicator.frame
|
||
frame.size.width = currentIndicatorWidth
|
||
frame.size.height = indicator.frame.size.height
|
||
indicator.frame = frame
|
||
}
|
||
case .customView:
|
||
if fromItem.tag < toItem.tag {
|
||
indicator.center = CGPoint(x: fromItem.center.x + rate * indicatorMaxWidth, y: indicator.center.y)
|
||
}else{
|
||
indicator.center = CGPoint(x: fromItem.center.x - (1 - rate) * indicatorMaxWidth, y: indicator.center.y)
|
||
}
|
||
|
||
default: break
|
||
|
||
}
|
||
}
|
||
|
||
private func addItems() {
|
||
guard let source = dataSource else{
|
||
return
|
||
}
|
||
|
||
for item in items {
|
||
item.removeFromSuperview()
|
||
item.badgeView?.removeFromSuperview()
|
||
}
|
||
items.removeAll()
|
||
|
||
itemBackgroundViews.forEach { $0.removeFromSuperview() }
|
||
itemBackgroundViews.removeAll()
|
||
|
||
for i in 0 ..< itemsCount {
|
||
let bgView = UIImageView()
|
||
bgView.layer.masksToBounds = true
|
||
contentView.addSubview(bgView)
|
||
itemBackgroundViews.append(bgView)
|
||
|
||
let customView = source.segmentedView?(self, customViewAt: i)
|
||
var item = JYSegmentedViewItem()
|
||
|
||
if let customItem = customView {
|
||
item = JYSegmentedViewItem(customItemView: customItem)
|
||
}else{
|
||
let title = source.segmentedView(self, titleAt: i)
|
||
item = JYSegmentedViewItem(text: title)
|
||
}
|
||
item.tag = i + kMenuItemTagExtenValue
|
||
item.config = config
|
||
item.delegate = self
|
||
contentView.addSubview(item)
|
||
items.append(item)
|
||
|
||
if let badgeView = source.segmentedView?(self, badgeViewAt: i) {
|
||
item.badgeView = badgeView
|
||
contentView.addSubview(badgeView)
|
||
}
|
||
}
|
||
}
|
||
|
||
///更改指示器位置
|
||
private func indicatorMoveTo(index: Int, animate: Bool) {
|
||
guard let menuItem = itemWithIndex(selectedIndex), config.indicatorStyle != .none, itemsCount > 0 else {
|
||
return
|
||
}
|
||
|
||
var indicatorRect: CGRect = .zero
|
||
if config.indicatorStyle == .singleLine || config.indicatorStyle == .customView {
|
||
if config.indicatorWidth > 0 {//设置了指示器的宽度或者高度
|
||
indicatorRect = CGRect(x: (menuItem.frame.width - config.indicatorWidth)/2 + menuItem.frame.origin.x, y: frame.height - config.indicatorBottom - config.indicatorHeight, width: config.indicatorWidth, height: config.indicatorHeight)
|
||
}else {
|
||
indicatorRect = CGRect(x: CGRectGetMinX(menuItem.frame), y: frame.height - config.indicatorBottom - config.indicatorHeight, width: menuItem.frame.width, height: config.indicatorHeight)
|
||
}
|
||
}
|
||
// else if config.indicatorStyle == .customView {
|
||
// if let indicator = config.customIndicator {
|
||
// indicatorRect = CGRect(x: (menuItem.frame.width - indicator.frame.width)/2 + menuItem.frame.origin.x, y: frame.height - config.indicatorBottom - indicator.frame.height, width: indicator.frame.width, height: indicator.frame.height)
|
||
// }
|
||
// }
|
||
|
||
var duration: Double = 0
|
||
if animate {
|
||
duration = kMenuItemAnimateDuration
|
||
}
|
||
UIView.animate(withDuration: duration, delay: 0, options: .curveEaseInOut, animations: {
|
||
self.indicator.frame = indicatorRect
|
||
}, completion: nil)
|
||
}
|
||
|
||
///选中item之后判断是是否需要调整contentOffsetX
|
||
private func resetHorContentOffset(animate: Bool) {
|
||
guard let selectedItem = itemWithIndex(selectedIndex) else {
|
||
return
|
||
}
|
||
let contentSize = contentView.contentSize
|
||
let width = contentView.frame.size.width
|
||
if contentSize.width > width {
|
||
//计算当前选中的item中心点是否超过scrollView宽度的一半
|
||
let itemCenterX = selectedItem.center.x
|
||
if itemCenterX < width/2 {
|
||
contentView.setContentOffset(CGPoint(x: 0, y: 0), animated: animate)
|
||
}else{
|
||
if (contentSize.width - itemCenterX) > width/2 {
|
||
//item中心点相对于屏幕左边的距离
|
||
let itemCenterByScreen = selectedItem.frame.origin.x - contentView.contentOffset.x + selectedItem.frame.size.width/2
|
||
let itemCenterToContentCenterDis = width/2 - itemCenterByScreen
|
||
contentView.setContentOffset(CGPoint(x:contentView.contentOffset.x - itemCenterToContentCenterDis, y: 0), animated: animate)
|
||
}else{
|
||
contentView.setContentOffset(CGPoint(x:contentSize.width - width, y: 0), animated: animate)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
|
||
private func layoutItems() {
|
||
var totalWidth: CGFloat = 0
|
||
if frame.size.height > 1, frame.size.width > 1, layoutOnceToken == false {
|
||
for (index,item) in items.enumerated() {
|
||
|
||
//备注:itemWidth取max,解决无论字体从regular->medium 还是medium->regular,文字都能正确显示
|
||
let selectedItemWidth = sizeForItem(item, font: UIFont.systemFont(ofSize: config.normalTitleFont, weight: config.selectedTitleFontWeight)).width
|
||
let normalItemWidth = sizeForItem(item, font: UIFont.systemFont(ofSize: config.normalTitleFont, weight: config.normalTitleFontWeight)).width
|
||
|
||
var itemWidth = max(normalItemWidth,selectedItemWidth) + config.textMargin * 2
|
||
var itemHeight = sizeForItem(item, font: UIFont.systemFont(ofSize: config.normalTitleFont, weight: config.selectedTitleFontWeight)).height
|
||
|
||
if item.type == .customView {
|
||
itemWidth = item.customView?.frame.size.width ?? 0
|
||
itemHeight = item.customView?.frame.size.height ?? 0
|
||
}
|
||
|
||
if index == 0 {
|
||
item.frame = CGRect(x: config.leftPadding, y: config.itemTop ?? (frame.size.height - itemHeight)/2, width: itemWidth, height: itemHeight)
|
||
totalWidth = totalWidth + itemWidth + config.leftPadding
|
||
}else{
|
||
item.frame = CGRect(x: totalWidth + config.itemsMargin, y:config.itemTop ?? (frame.size.height - itemHeight)/2, width: itemWidth, height: itemHeight)
|
||
totalWidth = totalWidth + itemWidth + config.itemsMargin
|
||
}
|
||
|
||
if index == selectedIndex {
|
||
item.transform = CGAffineTransform(scaleX: maxScale, y: maxScale)
|
||
item.textColor = config.selectedTitleColor
|
||
item.font = UIFont.systemFont(ofSize: config.normalTitleFont, weight: config.selectedTitleFontWeight)
|
||
}else{
|
||
item.transform = .identity
|
||
item.font = normalFont
|
||
item.textColor = config.normalTitleColor
|
||
}
|
||
|
||
let bgView = self.itemBackgroundViews[index]
|
||
bgView.backgroundColor = config.itemBackgroundColor
|
||
|
||
if config.itemBackgroundHeight > 0 {
|
||
bgView.frame = CGRect(x: item.frame.origin.x, y: item.frame.origin.y, width: item.frame.size.width, height: config.itemBackgroundHeight)
|
||
} else {
|
||
bgView.frame = item.frame
|
||
}
|
||
bgView.center = item.center
|
||
bgView.layer.cornerRadius = config.itemBackgroundCornerRadius
|
||
|
||
}
|
||
///调用该方法原因: 设置完选中的item.transform之后更新frame, itemMargin, contentsize
|
||
updateItemsFrame()
|
||
layoutOnceToken = true
|
||
}
|
||
}
|
||
|
||
|
||
///更新itemsframe
|
||
private func updateItemsFrame() {
|
||
let width = calculateTotalWidth()
|
||
var startX: CGFloat = config.leftPadding
|
||
if config.alignment == .center, width < frame.width {
|
||
startX = (frame.width - width)/2 + config.leftPadding
|
||
}
|
||
|
||
if config.alignment == .right, width < frame.width {
|
||
startX = frame.width - width
|
||
}
|
||
|
||
var totalWidth: CGFloat = 0
|
||
for (index,item) in items.enumerated() {
|
||
if index == 0 {
|
||
item.frame = CGRect(x: startX , y: item.frame.origin.y, width: item.frame.width, height: item.frame.height)
|
||
item.badgeView?.frame = CGRect(x: item.frame.width + config.badgeViewOffset.x, y: item.frame.origin.y - item.badgeViewHeight/2 + config.badgeViewOffset.y, width: item.badgeViewWidth, height: item.badgeViewHeight)
|
||
totalWidth = startX + item.frame.width
|
||
}else{
|
||
item.frame = CGRect(x: totalWidth + config.itemsMargin , y: item.frame.origin.y, width: item.frame.width, height: item.frame.height)
|
||
item.badgeView?.frame = CGRect(x: item.frame.width + item.frame.origin.x + config.badgeViewOffset.x, y: item.frame.origin.y - item.badgeViewHeight/2 + config.badgeViewOffset.y, width: item.badgeViewWidth, height: item.badgeViewHeight)
|
||
totalWidth = totalWidth + item.frame.width + config.itemsMargin
|
||
}
|
||
|
||
let bgView = self.itemBackgroundViews[index]
|
||
bgView.backgroundColor = config.itemBackgroundColor
|
||
|
||
if config.itemBackgroundHeight > 0 {
|
||
bgView.frame = CGRect(x: item.frame.origin.x, y: item.frame.origin.y, width: item.frame.size.width, height: config.itemBackgroundHeight)
|
||
} else {
|
||
bgView.frame = item.frame
|
||
}
|
||
bgView.center = item.center
|
||
}
|
||
contentView.contentSize = CGSize(width: totalWidth + config.rightPadding, height: frame.size.height)
|
||
}
|
||
|
||
private func calculateTotalWidth() -> CGFloat {
|
||
var totalWidth: CGFloat = 0
|
||
for (index,item) in items.enumerated() {
|
||
if index == 0 {
|
||
totalWidth = totalWidth + item.frame.width + item.badgeViewWidth + config.badgeViewOffset.x
|
||
}else{
|
||
totalWidth = totalWidth + item.frame.width + item.badgeViewWidth + config.itemsMargin + config.badgeViewOffset.x
|
||
}
|
||
}
|
||
return totalWidth
|
||
}
|
||
|
||
private func itemWithIndex(_ index: Int) -> JYSegmentedViewItem? {
|
||
if let item = contentView.viewWithTag(index+kMenuItemTagExtenValue) as? JYSegmentedViewItem {
|
||
return item
|
||
}
|
||
return nil
|
||
}
|
||
|
||
|
||
//MARK: - Lazy
|
||
private lazy var contentView : UIScrollView = {
|
||
let scrollView = UIScrollView.init()
|
||
scrollView.showsHorizontalScrollIndicator = false
|
||
scrollView.bounces = false
|
||
if #available(iOS 11.0, *) {
|
||
scrollView.contentInsetAdjustmentBehavior = .never
|
||
}
|
||
return scrollView
|
||
}()
|
||
|
||
private lazy var indicator: UIView = {
|
||
let view = UIView()
|
||
view.backgroundColor = config.indicatorColor
|
||
view.layer.cornerRadius = config.indicatorCornerRadius
|
||
return view
|
||
}()
|
||
|
||
private lazy var maxScale: CGFloat = {
|
||
return config.selectedTitleFont/config.normalTitleFont
|
||
}()
|
||
|
||
private lazy var normalFont: UIFont = {
|
||
let font = UIFont.systemFont(ofSize: config.normalTitleFont, weight: config.normalTitleFontWeight)
|
||
return font
|
||
}()
|
||
|
||
private lazy var selectedFont: UIFont = {
|
||
let font = UIFont.systemFont(ofSize: config.selectedTitleFont, weight: config.selectedTitleFontWeight)
|
||
return font
|
||
}()
|
||
|
||
|
||
//MARK: - Contant
|
||
private let kMenuItemTagExtenValue: Int = 1000000
|
||
private let kMenuItemAnimateDuration: Double = 0.3
|
||
}
|
||
|
||
|
||
//MARK: - JYSegmentedViewItemDelegate - 选中item事件逻辑处理extension
|
||
extension JYSegmentedView: JYSegmentedViewItemDelegate {
|
||
|
||
func segmentedItemDidSelected(_ item: JYSegmentedViewItem) {
|
||
let targetIndex = item.tag - kMenuItemTagExtenValue
|
||
segmentedViewSelectedItemChange(fromIndex: selectedIndex, toIndex: targetIndex)
|
||
selectedIndex = targetIndex
|
||
indicatorMoveTo(index: targetIndex, animate: true)
|
||
delegate?.segmentedView?(self, didSelectItemAt: targetIndex)
|
||
}
|
||
|
||
private func segmentedViewSelectedItemChange(fromIndex: Int, toIndex: Int) {
|
||
|
||
guard let fromItem = itemWithIndex(fromIndex), let toItem = itemWithIndex(toIndex) else {
|
||
return
|
||
}
|
||
|
||
toItem.selected = true
|
||
fromItem.selected = false
|
||
var rate: CGFloat = 0
|
||
let timer = DispatchSource.makeTimerSource(queue: DispatchQueue.main)
|
||
timer.schedule(deadline: .now(), repeating: kMenuItemAnimateDuration/100)
|
||
timer.setEventHandler(handler: {
|
||
rate = rate + 0.01
|
||
fromItem.rate = 1 - rate
|
||
toItem.rate = rate
|
||
})
|
||
timer.resume()
|
||
|
||
UIView.animate(withDuration: kMenuItemAnimateDuration, delay: 0, options: .curveEaseInOut, animations: {
|
||
fromItem.textColor = self.config.normalTitleColor
|
||
toItem.textColor = self.config.selectedTitleColor
|
||
fromItem.transform = CGAffineTransform(scaleX: 1, y: 1)
|
||
toItem.transform = CGAffineTransform(scaleX: self.maxScale, y: self.maxScale)
|
||
self.updateItemsFrame()
|
||
|
||
}) { finished in
|
||
timer.cancel()
|
||
toItem.textColor = self.config.selectedTitleColor
|
||
toItem.font = UIFont.systemFont(ofSize: self.config.normalTitleFont, weight: self.config.selectedTitleFontWeight)
|
||
fromItem.textColor = self.config.normalTitleColor
|
||
fromItem.font = self.normalFont
|
||
|
||
self.resetHorContentOffset(animate: true)
|
||
}
|
||
}
|
||
|
||
///计算item的大小
|
||
private func sizeForItem(_ item: JYSegmentedViewItem, font: UIFont) -> CGSize {
|
||
let attributes = [NSAttributedString.Key.font: font]
|
||
var titleSize: CGSize = NSString(string: item.text ?? "").boundingRect(with: CGSize.init(width: CGFloat(MAXFLOAT), height: CGFloat(MAXFLOAT)), options: .usesLineFragmentOrigin, attributes: attributes, context: nil).size
|
||
|
||
if titleSize.width < config.itemMinWidth, config.itemMinWidth > 0 {
|
||
titleSize = CGSize(width: config.itemMinWidth, height: titleSize.height)
|
||
}
|
||
|
||
if titleSize.width > config.itemMaxWidth, config.itemMaxWidth > 0 {
|
||
titleSize = CGSize(width: config.itemMaxWidth, height: titleSize.height)
|
||
}
|
||
return titleSize
|
||
}
|
||
|
||
}
|
||
|
||
|