搞事情之 PJRulerPickerView 元件開發總結

PJHubs發表於2019-05-16

PJRulerPickerView

搞事情繫列文章主要是為了繼續延續自己的 “T” 字形戰略所做,同時也代表著畢設相關內容的學習總結。本文是實現專案中一個選擇器元件引發的思考。

前言

有人說過“一個好的產品通常會在一些細節的處理上取勝”,這一點非常好的在我身上進行了驗證。在去年完成了一版選擇器的設計後(詳情見此文章),現如今進行了第二版的實現。

看到設計圖後,我不禁感嘆,設計小哥的腦洞真是大的可以,完全拋棄了常規的選擇器設計。

設計圖

與 UI 確認了動效後,腦海裡立馬浮現了“我不要自己寫!”的想法,但很快又意識到估計不會有這種開源元件可以用。總之給自己埋下了這是整個專案中最難實現動效之一的種子。

調研

不出所料,在 github 上嘗試搜尋過了 pickerswpierslider 等眾多與選擇器相關的關鍵詞後均無果,甚至還嘗試改造了 collectionView 中間放大的元件,但一番操作後,發現實在是不堪入目。

經歷過這次的改造後,發現 collectionView 中間檢視放大的效果是基於動態改變出現 cellscale 屬性去做的,開始萌生了乾脆自己寫一個得了。

思考

盯著設計圖看了好久,反覆琢磨動效。最後自己總結出以下幾種實現思路:

  • 使用 UICollectionView 集合餘弦定理做 scale 變換,可以隨便找一個開源元件做二次開發(時間最短)。
  • 使用 UICollectionView,每個 cell 都是一樣大小,中間部分做“放大鏡”效果,把整個 collectionView 做 3D 轉換變為從帶深度的一個滾輪,每次滾動都只是在修改 x 軸上的內容,z 軸和 y 軸不動(效果最好)。
  • 使用 UIScrollView 做“輪播圖”效果,所有東西都需要自己來(實現最簡單)。

其實我大部分的時間都花在了第一種方案上,因為實際動效跟第一種方案完全一致,只不過 cell 特別小就是了。但前面也說過了在嘗試過二次修改幾個開源元件後,發現效果實在是慘不忍睹,遂放棄;第二種方案是自己獨創的,也是因為動效特別像一個垂直於螢幕的滾輪,但做過 3D 變換的同學也是知道需要調整很多引數,實在是得不償失。

最好用了一個最簡單直接方法,用 UIScrollView 硬造。

腦暴手稿

實現

第一步

首先需要把素材都準備好,我很快的寫出了把所有子檢視排布在 scrollView 中的程式碼。

準備子檢視

private func initView() {
    let scrollView = UIScrollView(frame: CGRect(x: 0, y: 0, width: pj_width, height: pj_height))
    scrollView.showsHorizontalScrollIndicator = false
    scrollView.showsVerticalScrollIndicator = false
    addSubview(scrollView)
    var finalW: CGFloat = 0
    for index in 0..<pickCount {
        let inner = 10
        let sv = UIView(frame: CGRect(x: 10 + index * inner, y: Int(scrollView.pj_height / 2), width: 1, height: 4))
        sv.backgroundColor = .lightGray
        scrollView.addSubview(sv)

        if index == pickCount - 1 {
            finalW = sv.right
        }
    }
    scrollView.contentSize = CGSize(width: finalW, height: 0)
}

第二步

需要把靠近螢幕中間的幾個檢視按規則進行拉高。花費了一些時間來尋找把中間檢視拉高的引數,調整了一下。

調整中間區域的子檢視

private func initView() {

    let scrollView = UIScrollView(frame: CGRect(x: 0, y: 0, width: pj_width, height: pj_height))
    scrollView.showsHorizontalScrollIndicator = false
    scrollView.showsVerticalScrollIndicator = false 
    addSubview(scrollView)

    var finalW: CGFloat = 0
    for index in 0..<pickCount {

        // 子檢視之間的間距
        let inner = 10
        // sv 為每個子檢視
        let sv = UIView(frame: CGRect(x: 10 + index * inner, y: Int(scrollView.pj_height / 2), width: 1, height: 4))
        sv.backgroundColor = .lightGray
        sv.tag = index + 100
        scrollView.addSubview(sv)

        // 當前子檢視是否在中心區域範圍內
        if abs(sv.centerX - centerX) < 5 {

            sv.pj_height = 18
            sv.pj_width = 2
            sv.backgroundColor = .black
            // 先賦值給中心檢視
            centerView = sv

        } else if abs(sv.centerX - centerX) < 16 {

            sv.pj_height = 14
            sv.pj_width = 1

        } else if abs(sv.centerX - centerX) < 26 {

            sv.pj_height = 8
            sv.pj_width = 1

        } else {

            sv.pj_height = 4
            sv.pj_width = 1

        }

        sv.y = (scrollView.pj_height - sv.pj_height) * 0.5

        if index == pickCount - 1 {

            finalW = sv.right

        }
    }

    scrollView.contentSize = CGSize(width: finalW, height: 0)
}

第三步

滾動時需要實時計算中間區域檢視的高度。有了初始化檢視時的判斷條件,直接拿來用即可,只不過需要加上 scrollView 滑動的 x 軸偏移量。

實時計算

extension PJRulerPickerView: UIScrollViewDelegate {

    func scrollViewDidScroll(_ scrollView: UIScrollView) {

        let offSetX = scrollView.contentOffset.x
        let _ = scrollView.subviews.filter {

            if abs($0.centerX - offSetX - centerX) < 5 {

                $0.pj_height = 18
                $0.pj_width = 2
                $0.backgroundColor = .black

            } else if abs($0.centerX - offSetX  - centerX) < 16 {

                $0.pj_height = 14
                $0.pj_width = 1
                $0.backgroundColor = .lightGray

            } else if abs($0.centerX - offSetX - centerX) < 26 {

                $0.pj_height = 8
                $0.pj_width = 1
                $0.backgroundColor = .lightGray

            } else {

                $0.pj_height = 4
                $0.pj_width = 1
                $0.backgroundColor = .lightGray

            }

            $0.y = (scrollView.pj_height - $0.pj_height) * 0.5
            return true   
        }
    }
}

第四步

做到這基本上就簡單的完成了需求,一點都不復雜有沒有!!!真是不知道為什麼要花費大半天的時間去找開源庫,去做二次開發。

在向 UI 確定動效的過程中,被告知左右兩邊的檢視不能被“拖沒”,意思就是關閉“彈簧效果”,使用 scrollView.bounces = false 屬性進行關閉。

此時發現允許使用者撥動 100 次,但因為“彈簧效果”的關閉導致了可滾動的內容變少了。思考了一下後,運用了一些簡單的數學計算讓 scrollView 多渲染了頭部和尾部佔據的滾動內容。

private func initView() {

    let scrollView = UIScrollView(frame: CGRect(x: 0, y: 0, width: pj_width, height: pj_height))
    addSubview(scrollView)
    scrollView.delegate = self
    scrollView.showsHorizontalScrollIndicator = false
    scrollView.showsVerticalScrollIndicator = false
    scrollView.bounces = false

    // 從螢幕左邊到螢幕中心佔據的個數
    // 10.5 為每一個子檢視的寬度 + 左邊距,多加 1 是把第一個渲染出來的中心檢視也加上
    startIndex = (Int(ceil(centerX / 10.5)) + 1)
    // 總共需要渲染的子檢視加上頭尾佔據的個數
    pickCount += startIndex * 2

    var finalW: CGFloat = 0

    for index in 0..<pickCount {

        // 子檢視之間的間距
        let inner = 10
        // sv 為每個子檢視
        let sv = UIView(frame: CGRect(x: 10 + index * inner, y: Int(scrollView.pj_height / 2), width: 1, height: 4))
        sv.backgroundColor = .lightGray
        scrollView.addSubview(sv)

        // 當前子檢視是否在中心區域範圍內
        if abs(sv.centerX - centerX) < 5 {

            sv.pj_height = 18
            sv.pj_width = 2
            sv.backgroundColor = .black
            // 先賦值給中心檢視
            centerView = sv

        } else if abs(sv.centerX - centerX) < 16 {

            sv.pj_height = 14
            sv.pj_width = 1

        } else if abs(sv.centerX - centerX) < 26 {

            sv.pj_height = 8
            sv.pj_width = 1

        } else {

            sv.pj_height = 4
            sv.pj_width = 1

        }

        sv.y = (scrollView.pj_height - sv.pj_height) * 0.5

        if index == pickCount - 1 {

            finalW = sv.right

        }
    }

    scrollView.contentSize = CGSize(width: finalW, height: 0)
}

第五步

現在基本上解決了 UI 問題,最後只需要把使用者撥動的次數暴露出去即可。思考了一會後,得出這麼個結論:計算使用者當前撥動選擇器的次數,實際上就是計算中間檢視“變黑”了幾次。想明白後,我很快的寫下了程式碼:

private func initView() {

    let scrollView = UIScrollView(frame: CGRect(x: 0, y: 0, width: pj_width, height: pj_height))
    addSubview(scrollView)
    scrollView.delegate = self
    scrollView.showsHorizontalScrollIndicator = false
    scrollView.showsVerticalScrollIndicator = false
    scrollView.bounces = false

    // 從螢幕左邊到螢幕中心佔據的個數
    startIndex = (Int(ceil(centerX / 10.5)) + 1)
    // 總共需要渲染的子檢視加上頭尾佔據的個數
    pickCount += startIndex * 2

    var finalW: CGFloat = 0

    for index in 0..<pickCount {

        // 子檢視之間的間距
        let inner = 10
        // sv 為每個子檢視
        let sv = UIView(frame: CGRect(x: 10 + index * inner, y: Int(scrollView.pj_height / 2), width: 1, height: 4))
        sv.backgroundColor = .lightGray
        sv.tag = index + 100
        scrollView.addSubview(sv)

        // 當前子檢視是否在中心區域範圍內
        if abs(sv.centerX - centerX) < 5 {

            sv.pj_height = 18
            sv.pj_width = 2
            sv.backgroundColor = .black
            // 先賦值給中心檢視
            centerView = sv

        } else if abs(sv.centerX - centerX) < 16 {

            sv.pj_height = 14
            sv.pj_width = 1

        } else if abs(sv.centerX - centerX) < 26 {

            sv.pj_height = 8
            sv.pj_width = 1

        } else {

            sv.pj_height = 4
            sv.pj_width = 1

        }

        sv.y = (scrollView.pj_height - sv.pj_height) * 0.5

        if index == pickCount - 1 {

            finalW = sv.right

        }
    }

    scrollView.contentSize = CGSize(width: finalW, height: 0)
}

extension PJRulerPickerView: UIScrollViewDelegate {

    func scrollViewDidScroll(_ scrollView: UIScrollView) {

        let offSetX = scrollView.contentOffset.x
        let _ = scrollView.subviews.filter {

            if abs($0.centerX - offSetX - centerX) < 5 {

                $0.pj_height = 18
                $0.pj_width = 2
                $0.backgroundColor = .black

                // 如果本次的中心檢視不是上一次的中心檢視,說明中心檢視進行了替換
                if centerView.tag != $0.tag {

                    centerView = $0
                    // 在此處可以進行計算撥動次數
                }
            } else if abs($0.centerX - offSetX  - centerX) < 16 {

                $0.pj_height = 14
                $0.pj_width = 1
                $0.backgroundColor = .lightGray
            } else if abs($0.centerX - offSetX - centerX) < 26 {

                $0.pj_height = 8
                $0.pj_width = 1
                $0.backgroundColor = .lightGray
            } else {

                $0.pj_height = 4
                $0.pj_width = 1
                $0.backgroundColor = .lightGray
            }

            $0.y = (scrollView.pj_height - $0.pj_height) * 0.5
            return true
        }
    }
}

我使用了一箇中間變數去作為中間檢視的引用,並在建立子檢視時給其加上 tag 用於標記。思考了一下後,受到前幾次的思考影響,導致了計算使用者撥動過幾次的方法也不假思索的做了一些數學計算,最後我是這麼做的:

extension PJRulerPickerView: UIScrollViewDelegate {

    func scrollViewDidScroll(_ scrollView: UIScrollView) {

        let offSetX = scrollView.contentOffset.x
        let _ = scrollView.subviews.filter {

            if abs($0.centerX - offSetX - centerX) < 5 {

                $0.pj_height = 18
                $0.pj_width = 2
                $0.backgroundColor = .black

                // 如果本次的中心檢視不是上一次的中心檢視
                if centerView.tag != $0.tag {

                    PJTapic.select()
                    centerView = $0

                    // 使用者撥動的次數
                    print(Int(ceil($0.centerX / 10.5)) - startIndex)
                }

            } else if abs($0.centerX - offSetX  - centerX) < 16 {

                $0.pj_height = 14
                $0.pj_width = 1
                $0.backgroundColor = .lightGray

            } else if abs($0.centerX - offSetX - centerX) < 26 {

                $0.pj_height = 8
                $0.pj_width = 1
                $0.backgroundColor = .lightGray

            } else {

                $0.pj_height = 4
                $0.pj_width = 1
                $0.backgroundColor = .lightGray

            }

            $0.y = (scrollView.pj_height - $0.pj_height) * 0.5
            return true
        }
    }
}

在剛才寫這篇文章時,我發現了一個特別傻的地方,我都已經把每個子檢視所代表的位置記錄進了 tag 中,為社麼還要重新計算一遍當前中間檢視的位置?意識到這個問題後,還修改了一些其它地方,最終 PJRulerPickerView 的全部程式碼如下:

//
//  PJRulerPicker.swift
//  PIGPEN
//
//  Created by PJHubs on 2019/5/16.
//  Copyright © 2019 PJHubs. All rights reserved.
//

import UIKit

class PJRulerPickerView: UIView {

    /// 獲取撥動次數
    var moved: ((Int) -> Void)?
    /// 需要撥動的次數
    var pickCount  = 0
    // 中心檢視
    private var centerView = UIView()
    private var startIndex = 0

    override init(frame: CGRect) {

        super.init(frame: frame)
    }

    required init?(coder aDecoder: NSCoder) {

        fatalError("init(coder:) has not been implemented")

    }

    convenience init(frame: CGRect, pickCount: Int) {

        self.init(frame: frame)
        self.pickCount = pickCount
        initView()

    }

    private func initView() {

        let scrollView = UIScrollView(frame: CGRect(x: 0, y: 0, width: pj_width, height: pj_height))
        addSubview(scrollView)
        scrollView.delegate = self
        scrollView.showsHorizontalScrollIndicator = false
        scrollView.showsVerticalScrollIndicator = false
        scrollView.bounces = false

        // 從螢幕左邊到螢幕中心佔據的個數
        startIndex = (Int(ceil(centerX / 10.5)))
        // 總共需要渲染的子檢視加上頭尾佔據的個數
        pickCount += startIndex * 2 + 1

        var finalW: CGFloat = 0

        for index in 0..<pickCount {

            // 子檢視之間的間距
            let inner = 10
            // sv 為每個子檢視
            let sv = UIView(frame: CGRect(x: 10 + index * inner, y: Int(scrollView.pj_height / 2), width: 1, height: 4))
            sv.backgroundColor = .lightGray
            sv.tag = index + 100
            scrollView.addSubview(sv)

            // 當前子檢視是否在中心區域範圍內
            if abs(sv.centerX - centerX) < 5 {

                sv.pj_height = 18
                sv.pj_width = 2
                sv.backgroundColor = .black
                // 先賦值給中心檢視
                centerView = sv

            } else if abs(sv.centerX - centerX) < 16 {

                sv.pj_height = 14
                sv.pj_width = 1

            } else if abs(sv.centerX - centerX) < 26 {

                sv.pj_height = 8
                sv.pj_width = 1

            } else {

                sv.pj_height = 4
                sv.pj_width = 1

            }

            sv.y = (scrollView.pj_height - sv.pj_height) * 0.5

            if index == pickCount - 1 {

                finalW = sv.right

            }
        }

        scrollView.contentSize = CGSize(width: finalW, height: 0)
    }
}

extension PJRulerPickerView: UIScrollViewDelegate {

    func scrollViewDidScroll(_ scrollView: UIScrollView) {

        let offSetX = scrollView.contentOffset.x
        let _ = scrollView.subviews.filter {

            if abs($0.centerX - offSetX - centerX) < 5 {

                $0.pj_height = 18
                $0.pj_width = 2
                $0.backgroundColor = .black

                // 如果本次的中心檢視不是上一次的中心檢視
                if centerView.tag != $0.tag {

                    PJTapic.select()
                    centerView = $0

//                    moved?(Int(ceil($0.centerX / 10.5)) - startIndex)
                    moved?($0.tag - 100 - startIndex)
//                    print($0.tag - 100 - startIndex)
                }

            } else if abs($0.centerX - offSetX  - centerX) < 16 {

                $0.pj_height = 14
                $0.pj_width = 1
                $0.backgroundColor = .lightGray

            } else if abs($0.centerX - offSetX - centerX) < 26 {

                $0.pj_height = 8
                $0.pj_width = 1
                $0.backgroundColor = .lightGray

            } else {

                $0.pj_height = 4
                $0.pj_width = 1
                $0.backgroundColor = .lightGray

            }

            $0.y = (scrollView.pj_height - $0.pj_height) * 0.5
            return true
        }
    }
}

總結

完成 PJRulerPickerView 元件後我才意識到,其實遇到問題前應該先仔細的把問題在腦海在全盤推導一番,看看真正的核心問題是什麼,而不是像我之前一樣花費了大半天的時間漫無目的的尋找開源元件庫。

這個元件不難,但給我自己的影響非常大,讓我意識到了不要妄自菲薄。

PJ 的 iOS 開發之路

本作品採用《CC 協議》,轉載必須註明作者和本文連結
優秀的人遵守規則,頂尖的人創造規則

相關文章