動手寫一個快速集成網(wǎng)易新聞,騰訊視頻,頭條首頁的ScrollPageView,顯示滾動視圖


OC版簡書

OC版源碼

最終效果

更新示例.gif

示例效果.gif
示例效果1.gif

示例效果2.gif

示例效果3.gif
示例效果4.gif

示例效果5.gif

示例效果6.gif

一.構(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
    }

}

使用方法

使用方式一

Snip20160414_1.png

使用方式二

Snip20160414_3.png

更新說明: 所有更新內(nèi)容都在源碼中有示例

* 2016/04/22 增加自定義選中下標(biāo)功能, 增加 簡書個人主頁的使用示例

* 2016/05/02 增加動態(tài)更新顯示內(nèi)容


詳細(xì)請移步源碼, 如果您覺得有幫助,不妨給個star鼓勵一下, 歡迎關(guān)注

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,002評論 6 542
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 99,400評論 3 429
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 178,136評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,714評論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 72,452評論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,818評論 1 328
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,812評論 3 446
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,997評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,552評論 1 335
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 41,292評論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 43,510評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,035評論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,721評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,121評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,429評論 1 294
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,235評論 3 398
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 48,480評論 2 379

推薦閱讀更多精彩內(nèi)容