效果图:
基本的思路:
首先将整个分解成两个视图 上面的titleView
和中间的contentView
,titleView
我们一般遇到的效果都是要么是固定的无法拖动,要么是title
标题太多一行无法放下需要拖动来完成
titleView
我们所遇到比较多的样式一般都是:下划线移动、字体的放大、遮罩视图,所以我们将这些样式统一分类到titleStyle
中进行管理,需要的时候在把效果打开
import UIKit
class HJTitleStyle {
//是否可以滚动
var isScrollEnable : Bool = false
//titleView的高度
var titleHeight : CGFloat = 44
//默认的文字颜色
var normalColor : UIColor = UIColor(r: 0, g: 0, b: 0)
//选中的文字颜色
var selectColor : UIColor = UIColor(r: 255, g: 127, b: 0)
//字体大小
var font : UIFont = UIFont.systemFont(ofSize: 14)
//间距
var Margin : CGFloat = 20
//是否显示底部滚动条
var isShowBottomLine : Bool = false
// 底部滚动条颜色
var BottomLineColor : UIColor = UIColor(r: 255, g: 127, b: 0)
//滚动条高度
var BottomLineHeight : CGFloat = 2.0
//是否进行缩放
var isNeedScale : Bool = false
//缩放大小
var ScaleRange : CGFloat = 1.2
//是否显示遮盖
var isShowCover : Bool = false
//遮盖的高度
var CoverHeight : CGFloat = 25
//遮盖的颜色
var CoverColor : UIColor = UIColor.white
//遮盖与文字的间隙
var CoverMargin : CGFloat = 5
//遮盖的圆角
var CoverRadius : CGFloat = 12
}
titleView
的样式基本设置:
// 设置Label
fileprivate func setupLabels(){
for (i,title) in titles.enumerated() {
let label = UILabel()
label.tag = i
label.text = title
label.textAlignment = .center
label.textColor = i == 0 ? style.selectColor : style.normalColor
label.font = style.font
label.isUserInteractionEnabled = true
let tap = UITapGestureRecognizer(target: self, action: #selector(LabelClickTap(_:)))
label.addGestureRecognizer(tap)
labels.append(label)
scrollView.addSubview(label)
}
}
// 设置Label的Frame
fileprivate func setupLaeblFrame(){
var titleW : CGFloat = 0
let titleH : CGFloat = bounds.height
var titleX : CGFloat = 0
let titleY : CGFloat = 0
let count = titles.count
for (index,titleLaebel) in labels.enumerated() {
if style.isScrollEnable {
//字体的宽度来计算label的宽度
titleW = (titles[index] as NSString).boundingRect(with: CGSize(width:CGFloat(MAXFLOAT),height:0), options: .usesLineFragmentOrigin, attributes: [NSFontAttributeName:style.font], context: nil).width
if index == 0 {//第一个Label
titleX = style.Margin * 0.5
if style.isShowBottomLine {
BottomLine.frame.origin.x = titleX
BottomLine.frame.size.width = titleW
}
}else{
//如果不是第一个label 则labels数组则要减去刚刚的第一个已经设置好的label数量
let parlabel = labels[index - 1]
titleX = parlabel.frame.maxX + style.Margin
}
}else{//不能滚动
titleW = frame.width / CGFloat(count)
titleX = CGFloat(index) * titleW
if index == 0 && style.isShowBottomLine {
BottomLine.frame.origin.x = titleX
BottomLine.frame.size.width = titleW
}
}
titleLaebel.frame = CGRect(x: titleX, y: titleY, width: titleW, height: titleH)
//放大
if index == 0 {
let scale = self.style.isNeedScale ? style.ScaleRange : 1.0
titleLaebel.transform = CGAffineTransform(scaleX:scale,y:scale)
}
}
//如果可以滚动 则设置scrollView的contenSize
scrollView.contentSize = style.isScrollEnable ? CGSize(width:labels.last!.frame.maxX + style.Margin * 0.5 , height: 0) : CGSize.zero
}
// 设置底部滚动条
fileprivate func setupBottomLine(){
scrollView.addSubview(BottomLine)
BottomLine.frame = labels.first!.frame
BottomLine.frame.size.height = style.BottomLineHeight
BottomLine.frame.origin.y = bounds.height - style.BottomLineHeight
}
//设置遮盖视图
fileprivate func setupCoverView(){
scrollView.insertSubview(CoverView, at: 0)
let firtLabel = labels[0]
let coverH : CGFloat = self.style.CoverHeight
var coverW : CGFloat = firtLabel.frame.size.width
var coverX : CGFloat = firtLabel.frame.origin.x
let coverY : CGFloat = (frame.size.height - self.style.CoverHeight) * 0.5
if style.isScrollEnable {
coverX -= style.CoverMargin
coverW += style.CoverMargin * 2
}
CoverView.frame = CGRect(x: coverX, y: coverY, width: coverW, height: coverH)
CoverView.layer.cornerRadius = style.CoverRadius
CoverView.layer.masksToBounds = true
}
titleView
的基本效果出来了,但是无法如果title
过多的时候无法将所需要的titleLabel
显示在中间,所以需要对titleLabel
的位置进行设置:
//MARK: -设置被选中的Label自动滚动到中间
func TitleLabelEnableScroll(){
//判断样式是否需要滚动 如果样式需不需滚动 如果不需要则TitleLabel不需要滚动到中间
guard style.isScrollEnable else { return }
//获取目标Label
let targetLabel = labels[currentIndex]
//计算目标Label 和 中间位置的偏移量
var offSetx = targetLabel.center.x - bounds.width * 0.5
// 左边临界值 如果偏移量小于0 则把偏移量设置为0 这样第一个Label就无法滚动到中间
if offSetx < 0 {
offSetx = 0
//右边临界值 最大偏移值=内容视图-宽度 这样不会导致最后一个滚到中间
}else if offSetx > scrollView.contentSize.width - scrollView.bounds.width{
offSetx = scrollView.contentSize.width - scrollView.bounds.width
}
scrollView.setContentOffset(CGPoint(x:offSetx,y:0), animated: true)
}
}
基本的样式设置完成,最主要的效果还未实现,需要实现效果 我们需要几个参数:当前titleLabel
的index
目标titleLabel
的index
还需要一个进度值progress
//MARK: -对外调用的方法
extension HJTitleView {
func setTitleWithProgress(progress : CGFloat, sourceIndex : Int, targetIndex:Int) {
//取出当前Label 和 目标Label
let sourceLabel = labels[sourceIndex]
let targetLabel = labels[targetIndex]
//颜色差值
let diffVulesColor = (selectColorRGB.0 - normalColorRGB.0,selectColorRGB.1 - normalColorRGB.1,selectColorRGB.2 - normalColorRGB.2)
//颜色变化
sourceLabel.textColor = UIColor(r:selectColorRGB.0 - diffVulesColor.0 * progress,g:selectColorRGB.1 - diffVulesColor.1 * progress, b:selectColorRGB.2 - diffVulesColor.2 * progress)
targetLabel.textColor = UIColor(r:normalColorRGB.0 + diffVulesColor.0 * progress,g:normalColorRGB.1 + diffVulesColor.1 * progress ,b: normalColorRGB.2 + diffVulesColor.2 * progress)
//记录最新的Index
currentIndex = targetIndex
//移动位置差值
let moveToX = targetLabel.frame.origin.x - sourceLabel.frame.origin.x
let moveToW = targetLabel.frame.width - sourceLabel.frame.width
//计算 滚动条的移动范围
if style.isShowBottomLine {
BottomLine.frame.origin.x = sourceLabel.frame.origin.x + moveToX * progress
BottomLine.frame.size.width = sourceLabel.frame.size.width + moveToW * progress
}
// 计算放大的效果
if style.isNeedScale {
let diffScale = (style.ScaleRange - 1.0) * progress
sourceLabel.transform = CGAffineTransform(scaleX: style.ScaleRange - diffScale, y: style.ScaleRange - diffScale)
targetLabel.transform = CGAffineTransform(scaleX: 1.0 + diffScale,y: 1.0 + diffScale)
}
// 计算遮盖视图滚动
if style.isShowCover {
CoverView.frame.origin.x = style.isScrollEnable ? (sourceLabel.frame.origin.x - style.CoverMargin + moveToX * progress) : (sourceLabel.frame.origin.x + moveToX * progress)
CoverView.frame.size.width = style.isScrollEnable ? (sourceLabel.frame.size.width + 2 * style.CoverMargin + moveToW * progress) : (sourceLabel.frame.size.width + moveToW * progress)
}
}
样式的完成,则下一步是如何让我们的contentView
跟着联动,设置titleView
的delegate方法
protocol HJTitleViewDelegate : class {
func titleView(_ titleView : HJTitleView,selectedIndex index:Int)
}
通过代理方法告诉contentView
我(titleView
)现在在什么位置,你需要配合我(titleView
)滚动到相应的控制器
ContentView中设置:
//MARK: -设置ContentView的Index对外方法
extension HJContentView {
func contentViewSetupCurrentIndex(_ Currentindex : Int) {
// 记录需要进行的点击事件
isRepeatScrollDelegate = true
//滚动到的位置
let offSetX = CGFloat(Currentindex) * collectionView.frame.size.width
collectionView.setContentOffset(CGPoint(x:offSetX,y:0), animated: false)
}
}
PageView中遵守TitleViewDelegate
//MARK: -遵守TitleViewDelegate
extension HJPageView : HJTitleViewDelegate {
func titleView(_ titleView: HJTitleView, selectedIndex index: Int) {
ContentView.contentViewSetupCurrentIndex(index)
print(index)
}
}
titleView
中的效果实现完成,那么需要实现的下一步则是拖动contentView
实现titleView
的动画效果,在实现效果的时候我们需要考虑好是左滑动还是右滑动,所以一般我们都是contentView
都是使用UIScrollView
,亦或者使用继承自UIScrollView
的UICollectionView
//定义目标Label的targetIndex 和 progress
var targetIndex : Int = 0
var progress : CGFloat = 0.0
//当前位置的下标
let currentIndex = Int(startOffsetX / scrollView.bounds.size.width)
if startOffsetX < scrollView.contentOffset.x {//左滑动
targetIndex = currentIndex + 1
// //防止过度滑动越界 最后一个子控制器的下标
if targetIndex > ChildVC.count - 1 {
targetIndex = ChildVC.count - 1
}
//进度值
progress = (scrollView.contentOffset.x - startOffsetX) / scrollView.bounds.size.width
}else{//右滑动
targetIndex = currentIndex - 1
//防止过度滑动越界 第一个子控制器的下标
if targetIndex < 0 {
targetIndex = 0
}
//进度值
progress = (startOffsetX - scrollView.contentOffset.x) / scrollView.bounds.size.width
}
delegate?.contentView(self, currentIndex: currentIndex, targetIndex: targetIndex, progress: progress)
}
以上只是大概的思路解析,如若不懂的可以去下载Demo逐步解析
下载地址:HJPageView-父子控制器联动