效果圖:


基本的思路:
首先將整個(gè)分解成兩個(gè)視圖 上面的titleView 和中間的contentView,titleView我們一般遇到的效果都是要么是固定的無(wú)法拖動(dòng),要么是title標(biāo)題太多一行無(wú)法放下需要拖動(dòng)來(lái)完成
titleView我們所遇到比較多的樣式一般都是:下劃線移動(dòng)、字體的放大、遮罩視圖,所以我們將這些樣式統(tǒng)一分類到titleStyle中進(jìn)行管理,需要的時(shí)候在把效果打開(kāi)
import UIKit
class HJTitleStyle {
//是否可以滾動(dòng)
var isScrollEnable : Bool = false
//titleView的高度
var titleHeight : CGFloat = 44
//默認(rèn)的文字顏色
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
//是否顯示底部滾動(dòng)條
var isShowBottomLine : Bool = false
// 底部滾動(dòng)條顏色
var BottomLineColor : UIColor = UIColor(r: 255, g: 127, b: 0)
//滾動(dòng)條高度
var BottomLineHeight : CGFloat = 2.0
//是否進(jìn)行縮放
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的樣式基本設(shè)置:
// 設(shè)置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)
}
}
// 設(shè)置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 {
//字體的寬度來(lái)計(jì)算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 {//第一個(gè)Label
titleX = style.Margin * 0.5
if style.isShowBottomLine {
BottomLine.frame.origin.x = titleX
BottomLine.frame.size.width = titleW
}
}else{
//如果不是第一個(gè)label 則labels數(shù)組則要減去剛剛的第一個(gè)已經(jīng)設(shè)置好的label數(shù)量
let parlabel = labels[index - 1]
titleX = parlabel.frame.maxX + style.Margin
}
}else{//不能滾動(dòng)
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)
}
}
//如果可以滾動(dòng) 則設(shè)置scrollView的contenSize
scrollView.contentSize = style.isScrollEnable ? CGSize(width:labels.last!.frame.maxX + style.Margin * 0.5 , height: 0) : CGSize.zero
}
// 設(shè)置底部滾動(dòng)條
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
}
//設(shè)置遮蓋視圖
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的基本效果出來(lái)了,但是無(wú)法如果title過(guò)多的時(shí)候無(wú)法將所需要的titleLabel顯示在中間,所以需要對(duì)titleLabel的位置進(jìn)行設(shè)置:
//MARK: -設(shè)置被選中的Label自動(dòng)滾動(dòng)到中間
func TitleLabelEnableScroll(){
//判斷樣式是否需要滾動(dòng) 如果樣式需不需滾動(dòng) 如果不需要?jiǎng)tTitleLabel不需要滾動(dòng)到中間
guard style.isScrollEnable else { return }
//獲取目標(biāo)Label
let targetLabel = labels[currentIndex]
//計(jì)算目標(biāo)Label 和 中間位置的偏移量
var offSetx = targetLabel.center.x - bounds.width * 0.5
// 左邊臨界值 如果偏移量小于0 則把偏移量設(shè)置為0 這樣第一個(gè)Label就無(wú)法滾動(dòng)到中間
if offSetx < 0 {
offSetx = 0
//右邊臨界值 最大偏移值=內(nèi)容視圖-寬度 這樣不會(huì)導(dǎo)致最后一個(gè)滾到中間
}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)
}
}
基本的樣式設(shè)置完成,最主要的效果還未實(shí)現(xiàn),需要實(shí)現(xiàn)效果 我們需要幾個(gè)參數(shù):當(dāng)前titleLabel的index 目標(biāo)titleLabel的index 還需要一個(gè)進(jìn)度值progress
//MARK: -對(duì)外調(diào)用的方法
extension HJTitleView {
func setTitleWithProgress(progress : CGFloat, sourceIndex : Int, targetIndex:Int) {
//取出當(dāng)前Label 和 目標(biāo)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
//移動(dòng)位置差值
let moveToX = targetLabel.frame.origin.x - sourceLabel.frame.origin.x
let moveToW = targetLabel.frame.width - sourceLabel.frame.width
//計(jì)算 滾動(dòng)條的移動(dòng)范圍
if style.isShowBottomLine {
BottomLine.frame.origin.x = sourceLabel.frame.origin.x + moveToX * progress
BottomLine.frame.size.width = sourceLabel.frame.size.width + moveToW * progress
}
// 計(jì)算放大的效果
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)
}
// 計(jì)算遮蓋視圖滾動(dòng)
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跟著聯(lián)動(dòng),設(shè)置titleView的delegate方法
protocol HJTitleViewDelegate : class {
func titleView(_ titleView : HJTitleView,selectedIndex index:Int)
}
通過(guò)代理方法告訴contentView我(titleView)現(xiàn)在在什么位置,你需要配合我(titleView)滾動(dòng)到相應(yīng)的控制器
ContentView中設(shè)置:
//MARK: -設(shè)置ContentView的Index對(duì)外方法
extension HJContentView {
func contentViewSetupCurrentIndex(_ Currentindex : Int) {
// 記錄需要進(jìn)行的點(diǎn)擊事件
isRepeatScrollDelegate = true
//滾動(dòng)到的位置
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中的效果實(shí)現(xiàn)完成,那么需要實(shí)現(xiàn)的下一步則是拖動(dòng)contentView實(shí)現(xiàn)titleView的動(dòng)畫(huà)效果,在實(shí)現(xiàn)效果的時(shí)候我們需要考慮好是左滑動(dòng)還是右滑動(dòng),所以一般我們都是contentView都是使用UIScrollView,亦或者使用繼承自UIScrollView的UICollectionView
//定義目標(biāo)Label的targetIndex 和 progress
var targetIndex : Int = 0
var progress : CGFloat = 0.0
//當(dāng)前位置的下標(biāo)
let currentIndex = Int(startOffsetX / scrollView.bounds.size.width)
if startOffsetX < scrollView.contentOffset.x {//左滑動(dòng)
targetIndex = currentIndex + 1
// //防止過(guò)度滑動(dòng)越界 最后一個(gè)子控制器的下標(biāo)
if targetIndex > ChildVC.count - 1 {
targetIndex = ChildVC.count - 1
}
//進(jìn)度值
progress = (scrollView.contentOffset.x - startOffsetX) / scrollView.bounds.size.width
}else{//右滑動(dòng)
targetIndex = currentIndex - 1
//防止過(guò)度滑動(dòng)越界 第一個(gè)子控制器的下標(biāo)
if targetIndex < 0 {
targetIndex = 0
}
//進(jìn)度值
progress = (startOffsetX - scrollView.contentOffset.x) / scrollView.bounds.size.width
}
delegate?.contentView(self, currentIndex: currentIndex, targetIndex: targetIndex, progress: progress)
}
以上只是大概的思路解析,如若不懂的可以去下載Demo逐步解析
下載地址:HJPageView-父子控制器聯(lián)動(dòng)