搞事情繫列文章主要是爲了繼續延續本身的 「T」 字形戰略所作,同時也表明着畢設相關內容的學習總結。本文是實現項目中一個選擇器組件引起的思考。git
有人說過「一個好的產品一般會在一些細節的處理上取勝」,這一點很是好的在我身上進行了驗證。在去年完成了一版選擇器的設計後(詳情見此文章),現現在進行了第二版的實現。github
看到設計圖後,我不由感嘆,設計小哥的腦洞真是大的能夠,徹底拋棄了常規的選擇器設計。swift
與 UI 確認了動效後,腦海裏立馬浮現了「我不要本身寫!」的想法,但很快又意識到估計不會有這種開源組件能夠用。總之給本身埋下了這是整個項目中最難實現動效之一的種子。api
不出所料,在 github 上嘗試搜索過了 picker
,swpier
,slider
等衆多與選擇器相關的關鍵詞後均無果,甚至還嘗試改造了 collectionView
中間放大的組件,但一番操做後,發現實在是不堪入目。ide
經歷過此次的改造後,發現 collectionView
中間視圖放大的效果是基於動態改變出現 cell
的 scale
屬性去作的,開始萌生了乾脆本身寫一個得了。學習
盯着設計圖看了很久,反覆琢磨動效。最後本身總結出如下幾種實現思路: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
組件後我才意識到,其實遇到問題前應該先仔細的把問題在腦海在全盤推導一番,看看真正的核心問題是什麼,而不是像我以前同樣花費了大半天的時間漫無目的的尋找開源組件庫。
這個組件不難,但給我本身的影響很是大,讓我意識到了不要妄自菲薄。