OC版簡書
OC版源碼
最終效果
一.構(gòu)思部分:
打算分為三個部分, 滑塊部分View, 內(nèi)容顯示部分View, 包含滑塊View和顯示內(nèi)容View的View,以便于可以靈活的使用
1. 滑塊部分View
1.1 要實現(xiàn)滑塊可以滾動, 考慮可以直接使用collectionView, 但是這里還是直接使用scrollView方便里面的控件布局
1.2 要實現(xiàn)滑塊的點(diǎn)擊, 可以直接使用UIButton, 但是經(jīng)過嘗試, 要讓button的frame隨著文字的寬度來自適應(yīng)實現(xiàn)比較麻煩, 所以選擇了使用UILabel添加點(diǎn)擊手勢來實現(xiàn)點(diǎn)擊事件,這里使用了closures來實現(xiàn)(可以使用代理模式)
1.3 實現(xiàn)對應(yīng)的滾動條和遮蓋同步移動的效果,文字顏色漸變功能(在點(diǎn)擊的時候直接使用一個動畫就可以簡單的完成了)
2. 內(nèi)容顯示部分View
2.1 用來作為包含子控制器的view的容器, 并且實現(xiàn)可以分頁滾動的效果
2.2 要實現(xiàn)分頁滾動, 可以使用UIScrollView來實現(xiàn), 但是這樣就要考慮UIScrollView上的各個view的復(fù)用的問題, 其中細(xì)節(jié)還是很麻煩, 所以直接使用了UICollectionView(利用他的重用機(jī)制)來實現(xiàn)
2.3 將每一個子控制器的view直接添加到對應(yīng)的每一個cell的contentView中來展示, 所以這里需要注意cell重用可能帶來的內(nèi)容顯示不正常的問題, 這里采用了每次添加contentView的內(nèi)容時移除所有的subviews(也可以直接給每個cell用不同的reuseIdentifier實現(xiàn))
2.4 實現(xiàn)實時監(jiān)控滾動的進(jìn)度提供給滑塊部分來同步調(diào)整滾動條和遮蓋,文字顏色的漸變, 并且在每次滾動完成的時候可以通知給滑塊來調(diào)整他的內(nèi)容
3. 包含滑塊View和顯示內(nèi)容View的View
3.1 因為滑塊部分View和內(nèi)容顯示部分View是相對獨(dú)立的部分, 在這里只需要實現(xiàn)兩者的通信即可
3.2 可以自定義滑塊部分View和內(nèi)容顯示部分View的frame
實現(xiàn)部分
a. 滑塊部分
1. 基本屬性
/// 所有的title設(shè)置 -> 使用了一個結(jié)構(gòu)體, 將可以自定義的部分全部暴露了出來, 使用的時候就可以比較方便的自定義很多屬性 -> 初始化時傳入的
var segmentStyle: SegmentStyle
/// 點(diǎn)擊響應(yīng)的closures
var titleBtnOnClick:((label: UILabel, index: Int) -> Void)?
/// 用來緩存所有標(biāo)題的寬度, 達(dá)到根據(jù)文字的字?jǐn)?shù)和font自適應(yīng)控件的寬度 -> 為了只計算一次文字的寬度
private var titlesWidthArray: [CGFloat] = []
/// 所有的標(biāo)題 -> 初始化時傳入的
var titles:[String]
/// 緩存標(biāo)題labels -> 以便于通過下標(biāo)直接取值
private var labelsArray: [UILabel] = []
/// 滾動條
private lazy var scrollLine: UIView? = {[unowned self] in
let line = UIView()
return self.segmentStyle.showLine ? line : nil
}()
/// 遮蓋 -> 懶加載
private lazy var coverLayer: UIView? = {[unowned self] in
let cover = UIView()
cover.layer.cornerRadius = CGFloat(self.segmentStyle.coverCornerRadius)
// 這里只有一個cover 需要設(shè)置圓角, 故不用考慮離屏渲染的消耗, 直接設(shè)置 masksToBounds 來設(shè)置圓角
cover.layer.masksToBounds = true
return self.segmentStyle.showCover ? cover : nil
}()
/// 背景圖片
var backgroundImage: UIImage? = nil {
didSet {
// 在設(shè)置了背景圖片的時候才添加imageView
if let image = backgroundImage {
backgroundImageView.image = image
insertSubview(backgroundImageView, atIndex: 0)
}
}
}
private lazy var backgroundImageView: UIImageView = {[unowned self] in
let imageView = UIImageView(frame: self.bounds)
return imageView
}()
```
邏輯處理
init(frame: CGRect, segmentStyle: SegmentStyle, titles: [String]) {
self.segmentStyle = segmentStyle
self.titles = titles
super.init(frame: frame)
// 這個函數(shù)里面設(shè)置了基本屬性中的titles, labelsArray, titlesWidthArray,并且添加了label到scrollView上
setupTitles()
// 這個函數(shù)里面設(shè)置了遮蓋, 滾動條,和label的初始化位置
setupUI()
}
func titleLabelOnClick(tapGes: UITapGestureRecognizer) -> 處理點(diǎn)擊title的時候?qū)崿F(xiàn)標(biāo)題的切換,和遮蓋,滾動條...的位置調(diào)整, 同時執(zhí)行了相應(yīng)點(diǎn)擊得兒blosure, 以便于外部相應(yīng)點(diǎn)擊方法
func adjustTitleOffSetToCurrentIndex(currentIndex: Int) -> 更改scrollview的contentOffSet來居中顯示title
// 手動滾動時需要提供動畫效果
func adjustUIWithProgress(progress: CGFloat, oldIndex: Int, currentIndex: Int) -> 提供給外部來執(zhí)行標(biāo)題切換之間的動畫效果(注意這個方法里面進(jìn)行了一些簡單的數(shù)學(xué)計算以便于"同步" 滾動滾動條和cell )
這里以滑塊的位置x變化為例, 其他類似
let xDistance = currentLabel.frame.origin.x - oldLabel.frame.origin.x
這個xDistance就是滑塊將要從一個label下面移動到下一個label下面所需要移動的路程,
這個progress是外界提供來的, 表示當(dāng)前已經(jīng)移動的百分比(0 --- 1)是多少了,所以可以改變當(dāng)前滑塊的x為之前的x + 已經(jīng)完成滾動的距離(xDistance * progress)
scrollLine?.frame.origin.x = oldLabel.frame.origin.x + xDistance * progress
這樣就達(dá)到了滑塊的位置隨著提供的progress同步移動
b 內(nèi)容顯示部分View
基本屬性
/// 所有的子控制器
private var childVcs: [UIViewController] = []
/// 用來禁止調(diào)用scrollview的代理來進(jìn)行相關(guān)的計算
var forbidTouchToAdjustPosition = false
/// 用來記錄開始滾動的offSetX -> 用于判斷滾動的方向是向左還是向右, 同時方便設(shè)置下面兩個Index
private var oldOffSetX:CGFloat = 0.0
private var oldIndex = 0
private var currentIndex = 1
weak var delegate: ContentViewDelegate?
// UICollectionView用來顯示子控制器的view的內(nèi)容
private lazy var collectionView: UICollectionView = {[weak self] in
let flowLayout = UICollectionViewFlowLayout()
let collection = UICollectionView(frame: CGRectZero, collectionViewLayout: flowLayout)
if let strongSelf = self {
flowLayout.itemSize = strongSelf.bounds.size
flowLayout.scrollDirection = .Horizontal
flowLayout.minimumLineSpacing = 0
flowLayout.minimumInteritemSpacing = 0
collection.bounces = false
collection.showsHorizontalScrollIndicator = false
collection.frame = strongSelf.bounds
collection.collectionViewLayout = flowLayout
collection.pagingEnabled = true
// 如果不設(shè)置代理, 將不會調(diào)用scrollView的delegate方法
collection.delegate = strongSelf
collection.dataSource = strongSelf
collection.registerClass(UICollectionViewCell.self, forCellWithReuseIdentifier: ContentView.cellId)
}
return collection
}()
邏輯處理
// 初始化設(shè)置frame和子控制器
init(frame:CGRect, childVcs:[UIViewController]) {
self.childVcs = childVcs
super.init(frame: frame)
// 設(shè)置collectionView的frame和添加collectionView同時做相關(guān)的數(shù)據(jù)錯誤判斷
commonInit()
}
func setContentOffSet(offSet: CGPoint , animated: Bool) // 提供給外部來設(shè)置contentOffSet -> 比如說點(diǎn)擊了滑塊切換title時同時切換內(nèi)容顯示
extension ContentView: UICollectionViewDelegate, UICollectionViewDataSource{
其中的設(shè)置了cell的內(nèi)容和個數(shù)
}
extension ContentView: UIScrollViewDelegate {
這里面使用到了監(jiān)控滾動的過程, 以便于計算滾動的進(jìn)度和頁數(shù)的改變, 同時使用代理來完成相應(yīng)的工作
主要邏輯在func scrollViewWillBeginDragging(scrollView: UIScrollView) 方法里面
}
定義了一個protocol來完成相關(guān)的操作
protocol ContentViewDelegate: class {
func contentViewMoveToIndex(fromIndex: Int, toIndex: Int, progress: CGFloat)
func contentViewDidEndMoveToIndex(currentIndex: Int)
var segmentView: ScrollSegmentView { get }
}
// 由于每個遵守這個協(xié)議的都需要執(zhí)行些相同的操作, 所以直接使用協(xié)議擴(kuò)展統(tǒng)一完成,協(xié)議遵守者只需要提供segmentView即可
extension ContentViewDelegate {
// 內(nèi)容每次滾動完成時調(diào)用, 確定title和其他的控件的位置
func contentViewDidEndMoveToIndex(currentIndex: Int) {
segmentView.adjustTitleOffSetToCurrentIndex(currentIndex)
segmentView.adjustUIWithProgress(1.0, oldIndex: currentIndex, currentIndex: currentIndex)
}
// 內(nèi)容正在滾動的時候,同步滾動滑塊的控件
func contentViewMoveToIndex(fromIndex: Int, toIndex: Int, progress: CGFloat) {
segmentView.adjustUIWithProgress(progress, oldIndex: fromIndex, currentIndex: toIndex)
}
}
c. 包含滑塊View和顯示內(nèi)容View的View
這一部分比較簡單直接看代碼就ok了
//
// ScrollPageView.swift
// ScrollViewController
//
// Created by jasnig on 16/4/6.
// Copyright ? 2016年 ZeroJ. All rights reserved.
//
import UIKit
class ScrollPageView: UIView {
static let cellId = "cellId"
var segmentStyle = SegmentStyle()
var segView: ScrollSegmentView!
var contentView: ContentView!
var titlesArray: [String] = []
/// 所有的子控制器
var childVcs: [UIViewController] = []
init(frame:CGRect, segmentStyle: SegmentStyle, titles: [String], childVcs:[UIViewController]) {
self.childVcs = childVcs
self.titlesArray = titles
self.segmentStyle = segmentStyle
assert(childVcs.count == titles.count, "標(biāo)題的個數(shù)必須和子控制器的個數(shù)相同")
super.init(frame: frame)
// 初始化設(shè)置了frame后可以在以后的任何地方直接獲取到frame了, 就不必重寫layoutsubview()方法在里面設(shè)置各個控件的frame
commonInit()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func commonInit() {
segView = ScrollSegmentView(frame: CGRect(x: 0, y: 0, width: bounds.size.width, height: 44), segmentStyle: segmentStyle, titles: titlesArray)
contentView = ContentView(frame: CGRect(x: 0, y: CGRectGetMaxY(segView.frame), width: bounds.size.width, height: bounds.size.height - 44), childVcs: childVcs)
contentView.delegate = self
addSubview(contentView)
addSubview(segView)
// 在這里調(diào)用了懶加載的collectionView, 那么之前設(shè)置的self.frame將會用于collectionView,如果在layoutsubviews()里面沒有相關(guān)的處理frame的操作, 那么將導(dǎo)致內(nèi)容顯示不正常
// 避免循環(huán)引用
segView.titleBtnOnClick = {[unowned self] (label: UILabel, index: Int) in
// 不要執(zhí)行collectionView的scrollView的滾動代理方法
self.contentView.setContentOffSet(CGPoint(x: self.contentView.bounds.size.width * CGFloat(index), y: 0), animated: false)
}
}
}
extension ScrollPageView: ContentViewDelegate {
var segmentView: ScrollSegmentView {
return segView
}
}
使用方法
使用方式一
使用方式二
更新說明: 所有更新內(nèi)容都在源碼中有示例
* 2016/04/22 增加自定義選中下標(biāo)功能, 增加 簡書個人主頁的使用示例
* 2016/05/02 增加動態(tài)更新顯示內(nèi)容
詳細(xì)請移步源碼, 如果您覺得有幫助,不妨給個star鼓勵一下, 歡迎關(guān)注