搞事情之 PJRulerPickerView 組件開發總結

PJRulerPickerView

搞事情繫列文章主要是爲了繼續延續本身的 「T」 字形戰略所作,同時也表明着畢設相關內容的學習總結。本文是實現項目中一個選擇器組件引起的思考。git

前言

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

看到設計圖後,我不由感嘆,設計小哥的腦洞真是大的能夠,徹底拋棄了常規的選擇器設計。swift

設計圖

與 UI 確認了動效後,腦海裏立馬浮現了「我不要本身寫!」的想法,但很快又意識到估計不會有這種開源組件能夠用。總之給本身埋下了這是整個項目中最難實現動效之一的種子。api

調研

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

經歷過此次的改造後,發現 collectionView 中間視圖放大的效果是基於動態改變出現 cellscale 屬性去作的,開始萌生了乾脆本身寫一個得了。學習

思考

盯着設計圖看了很久,反覆琢磨動效。最後本身總結出如下幾種實現思路:ui

  • 使用 UICollectionView 集合餘弦定理作 scale 變換,能夠隨便找一個開源組件作二次開發(時間最短)。
  • 使用 UICollectionView,每一個 cell 都是同樣大小,中間部分作「放大鏡」效果,把整個 collectionView 作 3D 轉換變爲從帶深度的一個滾輪,每次滾動都只是在修改 x 軸上的內容,z 軸和 y 軸不動(效果最好)。
  • 使用 UIScrollView 作「輪播圖」效果,全部東西都須要本身來(實現最簡單)。

其實我大部分的時間都花在了第一種方案上,由於實際動效跟第一種方案徹底一致,只不過 cell 特別小就是了。但前面也說過了在嘗試過二次修改幾個開源組件後,發現效果實在是慘不忍睹,遂放棄;第二種方案是本身首創的,也是由於動效特別像一個垂直於屏幕的滾輪,但作過 3D 變換的同窗也是知道須要調整不少參數,實在是得不償失。spa

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

腦暴手稿

實現

第一步

首先須要把素材都準備好,我很快的寫出了把全部子視圖排布在 scrollView 中的代碼。code

準備子視圖

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 開發之路

相關文章
相關標籤/搜索