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


OC版簡(jiǎn)書

OC版源碼

最終效果

更新示例.gif

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

示例效果2.gif

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

示例效果5.gif

示例效果6.gif

一.構(gòu)思部分:

打算分為三個(gè)部分, 滑塊部分View, 內(nèi)容顯示部分View, 包含滑塊View和顯示內(nèi)容View的View,以便于可以靈活的使用

1. 滑塊部分View

1.1 要實(shí)現(xiàn)滑塊可以滾動(dòng), 考慮可以直接使用collectionView, 但是這里還是直接使用scrollView方便里面的控件布局

1.2 要實(shí)現(xiàn)滑塊的點(diǎn)擊, 可以直接使用UIButton, 但是經(jīng)過(guò)嘗試, 要讓button的frame隨著文字的寬度來(lái)自適應(yīng)實(shí)現(xiàn)比較麻煩, 所以選擇了使用UILabel添加點(diǎn)擊手勢(shì)來(lái)實(shí)現(xiàn)點(diǎn)擊事件,這里使用了closures來(lái)實(shí)現(xiàn)(可以使用代理模式)

1.3 實(shí)現(xiàn)對(duì)應(yīng)的滾動(dòng)條和遮蓋同步移動(dòng)的效果,文字顏色漸變功能(在點(diǎn)擊的時(shí)候直接使用一個(gè)動(dòng)畫就可以簡(jiǎn)單的完成了)

2. 內(nèi)容顯示部分View

2.1 用來(lái)作為包含子控制器的view的容器, 并且實(shí)現(xiàn)可以分頁(yè)滾動(dòng)的效果

2.2 要實(shí)現(xiàn)分頁(yè)滾動(dòng), 可以使用UIScrollView來(lái)實(shí)現(xiàn), 但是這樣就要考慮UIScrollView上的各個(gè)view的復(fù)用的問(wèn)題, 其中細(xì)節(jié)還是很麻煩, 所以直接使用了UICollectionView(利用他的重用機(jī)制)來(lái)實(shí)現(xiàn)

2.3 將每一個(gè)子控制器的view直接添加到對(duì)應(yīng)的每一個(gè)cell的contentView中來(lái)展示, 所以這里需要注意cell重用可能帶來(lái)的內(nèi)容顯示不正常的問(wèn)題, 這里采用了每次添加contentView的內(nèi)容時(shí)移除所有的subviews(也可以直接給每個(gè)cell用不同的reuseIdentifier實(shí)現(xiàn))

2.4 實(shí)現(xiàn)實(shí)時(shí)監(jiān)控滾動(dòng)的進(jìn)度提供給滑塊部分來(lái)同步調(diào)整滾動(dòng)條和遮蓋,文字顏色的漸變, 并且在每次滾動(dòng)完成的時(shí)候可以通知給滑塊來(lái)調(diào)整他的內(nèi)容

3. 包含滑塊View和顯示內(nèi)容View的View

3.1 因?yàn)榛瑝K部分View和內(nèi)容顯示部分View是相對(duì)獨(dú)立的部分, 在這里只需要實(shí)現(xiàn)兩者的通信即可

3.2 可以自定義滑塊部分View和內(nèi)容顯示部分View的frame

實(shí)現(xiàn)部分

a. 滑塊部分

1. 基本屬性

/// 所有的title設(shè)置 -> 使用了一個(gè)結(jié)構(gòu)體, 將可以自定義的部分全部暴露了出來(lái), 使用的時(shí)候就可以比較方便的自定義很多屬性  -> 初始化時(shí)傳入的
var segmentStyle: SegmentStyle

/// 點(diǎn)擊響應(yīng)的closures
var titleBtnOnClick:((label: UILabel, index: Int) -> Void)?
/// 用來(lái)緩存所有標(biāo)題的寬度, 達(dá)到根據(jù)文字的字?jǐn)?shù)和font自適應(yīng)控件的寬度 -> 為了只計(jì)算一次文字的寬度
private var titlesWidthArray: [CGFloat] = []
/// 所有的標(biāo)題 -> 初始化時(shí)傳入的
var titles:[String]
 /// 緩存標(biāo)題labels -> 以便于通過(guò)下標(biāo)直接取值
private var labelsArray: [UILabel] = []


    /// 滾動(dòng)條
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)
    // 這里只有一個(gè)cover 需要設(shè)置圓角, 故不用考慮離屏渲染的消耗, 直接設(shè)置 masksToBounds 來(lái)設(shè)置圓角
    cover.layer.masksToBounds = true
    
    return self.segmentStyle.showCover ? cover : nil

}()


    /// 背景圖片
var backgroundImage: UIImage? = nil {
    didSet {
        // 在設(shè)置了背景圖片的時(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)
    // 這個(gè)函數(shù)里面設(shè)置了基本屬性中的titles, labelsArray, titlesWidthArray,并且添加了label到scrollView上
    setupTitles()
    // 這個(gè)函數(shù)里面設(shè)置了遮蓋, 滾動(dòng)條,和label的初始化位置
    setupUI()
}


func titleLabelOnClick(tapGes: UITapGestureRecognizer) -> 處理點(diǎn)擊title的時(shí)候?qū)崿F(xiàn)標(biāo)題的切換,和遮蓋,滾動(dòng)條...的位置調(diào)整, 同時(shí)執(zhí)行了相應(yīng)點(diǎn)擊得兒blosure, 以便于外部相應(yīng)點(diǎn)擊方法

func adjustTitleOffSetToCurrentIndex(currentIndex: Int) -> 更改scrollview的contentOffSet來(lái)居中顯示title


 // 手動(dòng)滾動(dòng)時(shí)需要提供動(dòng)畫效果
func adjustUIWithProgress(progress: CGFloat,  oldIndex: Int, currentIndex: Int) -> 提供給外部來(lái)執(zhí)行標(biāo)題切換之間的動(dòng)畫效果(注意這個(gè)方法里面進(jìn)行了一些簡(jiǎn)單的數(shù)學(xué)計(jì)算以便于"同步" 滾動(dòng)滾動(dòng)條和cell )
這里以滑塊的位置x變化為例, 其他類似

let xDistance = currentLabel.frame.origin.x - oldLabel.frame.origin.x
這個(gè)xDistance就是滑塊將要從一個(gè)label下面移動(dòng)到下一個(gè)label下面所需要移動(dòng)的路程,

這個(gè)progress是外界提供來(lái)的, 表示當(dāng)前已經(jīng)移動(dòng)的百分比(0 --- 1)是多少了,所以可以改變當(dāng)前滑塊的x為之前的x + 已經(jīng)完成滾動(dòng)的距離(xDistance * progress)
scrollLine?.frame.origin.x = oldLabel.frame.origin.x + xDistance * progress

這樣就達(dá)到了滑塊的位置隨著提供的progress同步移動(dòng)

b 內(nèi)容顯示部分View

基本屬性

/// 所有的子控制器
    private var childVcs: [UIViewController] = []
    /// 用來(lái)禁止調(diào)用scrollview的代理來(lái)進(jìn)行相關(guān)的計(jì)算
    var forbidTouchToAdjustPosition = false
    /// 用來(lái)記錄開始滾動(dòng)的offSetX -> 用于判斷滾動(dòng)的方向是向左還是向右, 同時(shí)方便設(shè)置下面兩個(gè)Index
    private var oldOffSetX:CGFloat = 0.0
    private var oldIndex = 0
    private var currentIndex = 1
    
    weak var delegate: ContentViewDelegate?
    
    // UICollectionView用來(lái)顯示子控制器的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è)置代理, 將不會(huì)調(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同時(shí)做相關(guān)的數(shù)據(jù)錯(cuò)誤判斷
        commonInit()
    }
    
    
    func setContentOffSet(offSet: CGPoint , animated: Bool)  // 提供給外部來(lái)設(shè)置contentOffSet    -> 比如說(shuō)點(diǎn)擊了滑塊切換title時(shí)同時(shí)切換內(nèi)容顯示
    
    
    
extension ContentView: UICollectionViewDelegate, UICollectionViewDataSource{
    其中的設(shè)置了cell的內(nèi)容和個(gè)數(shù)

}


extension ContentView: UIScrollViewDelegate {

這里面使用到了監(jiān)控滾動(dòng)的過(guò)程, 以便于計(jì)算滾動(dòng)的進(jìn)度和頁(yè)數(shù)的改變, 同時(shí)使用代理來(lái)完成相應(yīng)的工作
主要邏輯在func scrollViewWillBeginDragging(scrollView: UIScrollView) 方法里面
}


定義了一個(gè)protocol來(lái)完成相關(guān)的操作
protocol ContentViewDelegate: class {
    func contentViewMoveToIndex(fromIndex: Int, toIndex: Int, progress: CGFloat)
    func contentViewDidEndMoveToIndex(currentIndex: Int)
    var segmentView: ScrollSegmentView { get }
}

// 由于每個(gè)遵守這個(gè)協(xié)議的都需要執(zhí)行些相同的操作, 所以直接使用協(xié)議擴(kuò)展統(tǒng)一完成,協(xié)議遵守者只需要提供segmentView即可
extension ContentViewDelegate {
    
    // 內(nèi)容每次滾動(dòng)完成時(shí)調(diào)用, 確定title和其他的控件的位置
    func contentViewDidEndMoveToIndex(currentIndex: Int) {
        segmentView.adjustTitleOffSetToCurrentIndex(currentIndex)
        segmentView.adjustUIWithProgress(1.0, oldIndex: currentIndex, currentIndex: currentIndex)
    }
    
    // 內(nèi)容正在滾動(dòng)的時(shí)候,同步滾動(dòng)滑塊的控件
    func contentViewMoveToIndex(fromIndex: Int, toIndex: Int, progress: CGFloat) {
        segmentView.adjustUIWithProgress(progress, oldIndex: fromIndex, currentIndex: toIndex)
    }
}



c. 包含滑塊View和顯示內(nèi)容View的View

這一部分比較簡(jiǎn)單直接看代碼就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)題的個(gè)數(shù)必須和子控制器的個(gè)數(shù)相同")
        super.init(frame: frame)
        // 初始化設(shè)置了frame后可以在以后的任何地方直接獲取到frame了, 就不必重寫layoutsubview()方法在里面設(shè)置各個(gè)控件的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將會(huì)用于collectionView,如果在layoutsubviews()里面沒有相關(guān)的處理frame的操作, 那么將導(dǎo)致內(nèi)容顯示不正常
        // 避免循環(huán)引用
        segView.titleBtnOnClick = {[unowned self] (label: UILabel, index: Int) in
            
            // 不要執(zhí)行collectionView的scrollView的滾動(dòng)代理方法
            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

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

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

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


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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容