你知道Tab Bar圖標(biāo)原來(lái)還可以這樣玩嗎
背景
框架自帶的 Tab Bar 相信大家已經(jīng)熟悉得不能再熟悉了,一般使用的時(shí)候不過(guò)是設(shè)置兩個(gè)圖標(biāo)代表選中和未選中兩種狀態(tài),難免有一些平淡。后來(lái)很多控件就在標(biāo)簽選中時(shí)進(jìn)行一些比較抓眼球的動(dòng)畫(huà),不過(guò)我覺(jué)得大部分都是為了動(dòng)畫(huà)而動(dòng)畫(huà)。直到后來(lái)我看到Outlook客戶端的動(dòng)畫(huà)時(shí),我才意識(shí)到原來(lái)還可以跟用戶的交互結(jié)合在一起。

圖1 標(biāo)簽圖標(biāo)跟隨手勢(shì)進(jìn)行不同的動(dòng)畫(huà)
有意思吧,不過(guò)本文并不是要仿制個(gè)一模一樣的出來(lái),會(huì)有稍微變化:

圖2 本文完成的最終效果
實(shí)現(xiàn)分析
寫(xiě)代碼之前,咱先討論下實(shí)現(xiàn)的方法,相信你已經(jīng)猜到標(biāo)簽頁(yè)的圖標(biāo)顯然已經(jīng)不是圖片,而是一個(gè)自定義的UIView。將一個(gè)視圖掛載到原本圖標(biāo)的位置并不是一件難事,稍微有些復(fù)雜的是數(shù)字滾輪效果的實(shí)現(xiàn),別看它數(shù)字不停地在滾動(dòng),仔細(xì)看其實(shí)最多顯示2種數(shù)字,也就說(shuō)只要2個(gè)Label就夠了。
基于篇幅,文章不會(huì)涉及右側(cè)的時(shí)鐘效果,感興趣請(qǐng)直接參考源碼。
數(shù)字滾輪
打開(kāi)項(xiàng)目TabBarInteraction,新建文件WheelView.swift,它是UIView的子類(lèi)。首先設(shè)置好初始化函數(shù):
class WheelView: UIView {
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setupView()
}
override init(frame: CGRect) {
super.init(frame: frame)
setupView()
}
}
接著創(chuàng)建兩個(gè)Label實(shí)例,代表滾輪中的上下兩個(gè)Label:
private lazy var toplabel: UILabel = {
return createDefaultLabel()
}()
private lazy var bottomLabel: UILabel = {
return createDefaultLabel()
}()
private func createDefaultLabel() -> UILabel {
let label = UILabel()
label.textAlignment = NSTextAlignment.center
label.adjustsFontSizeToFitWidth = true
label.translatesAutoresizingMaskIntoConstraints = false
return label
}
現(xiàn)在來(lái)完成setupView()方法,在這方法中將上述兩個(gè)Label添加到視圖中,然后設(shè)置約束將它們的四邊都與layoutMarginsGuide對(duì)齊。
private func setupView() {
translatesAutoresizingMaskIntoConstraints = false
for label in [toplabel, bottomLabel] {
addSubview(label)
NSLayoutConstraint.activate([
label.topAnchor.constraint(equalTo: layoutMarginsGuide.topAnchor),
label.bottomAnchor.constraint(equalTo: layoutMarginsGuide.bottomAnchor),
label.leftAnchor.constraint(equalTo: layoutMarginsGuide.leftAnchor),
label.rightAnchor.constraint(equalTo: layoutMarginsGuide.rightAnchor)
])
}
}
有人可能會(huì)問(wèn)現(xiàn)在這樣兩個(gè)Label不是重疊的狀態(tài)嗎?不著急,接下來(lái)我們會(huì)根據(jù)參數(shù)動(dòng)態(tài)地調(diào)整它們的大小和位置。
添加兩個(gè)實(shí)例變量progress和contents,分別表示滾動(dòng)的總體進(jìn)度和顯示的全部?jī)?nèi)容。
var progress: Float = 0.0 var contents = [String]()
我們接下來(lái)要根據(jù)這兩個(gè)變量計(jì)算出當(dāng)前兩個(gè)Label顯示的內(nèi)容以及它們的縮放位置。這些計(jì)算都在progress的didSet里完成:
var progress: Float = 0.0 {
didSet {
progress = min(max(progress, 0.0), 1.0)
guard contents.count > 0 else { return }
/** 根據(jù) progress 和 contents 計(jì)算出上下兩個(gè) label 顯示的內(nèi)容以及 label 的壓縮程度和位置
*
* Example:
* progress = 0.4, contents = ["A","B","C","D"]
*
* 1)計(jì)算兩個(gè)label顯示的內(nèi)容
* topIndex = 4 * 0.4 = 1.6, topLabel.text = contents[1] = "B"
* bottomIndex = 1.6 + 1 = 2.6, bottomLabel.text = contents[2] = "C"
*
* 2) 計(jì)算兩個(gè)label如何壓縮和位置調(diào)整,這是實(shí)現(xiàn)滾輪效果的原理
* indexOffset = 1.6 % 1 = 0.6
* halfHeight = bounds.height / 2
* ┌─────────────┐ ┌─────────────┐
* |┌───────────┐| scaleY | |
* || || 1-0.6=0.4 | | translationY
* || topLabel || ----------> |┌─ topLabel─┐| ------------------
* || || |└───────────┘| -halfHeight * 0.6 ⎞ ┌─────────────┐
* |└───────────┘| | | ⎥ |┌─ toplabel─┐|
* └─────────────┘ └─────────────┘ ⎟ |└───────────┘|
* ❯ |┌───────────┐|
* ┌─────────────┐ ┌─────────────┐ ⎟ ||bottomLabel||
* |┌───────────┐| scaleY | | ⎟ |└───────────┘|
* || || 0.6 |┌───────────┐| translationY ⎠ └─────────────┘
* ||bottomLabel|| ----------> ||bottomLabel|| -----------------
* || || |└───────────┘| halfHeight * 0.4
* |└───────────┘| | |
* └─────────────┘ └─────────────┘
*
* 可以想象出,當(dāng) indexOffset 從 0.0 遞增到 0.999 過(guò)程中,
* topLabel 從滿視圖越縮越小至0,而 bottomLabel剛好相反越變?cè)酱笾翝M視圖,即形成一次完整的滾動(dòng)
*/
let topIndex = min(max(0.0, Float(contents.count) * progress), Float(contents.count - 1))
let bottomIndex = min(topIndex + 1, Float(contents.count - 1))
let indexOffset = topIndex.truncatingRemainder(dividingBy: 1)
toplabel.text = contents[Int(topIndex)]
toplabel.transform = CGAffineTransform(scaleX: 1.0, y: CGFloat(1 - indexOffset))
.concatenating(CGAffineTransform(translationX: 0, y: -(toplabel.bounds.height / 2) * CGFloat(indexOffset)))
bottomLabel.text = contents[Int(bottomIndex)]
bottomLabel.transform = CGAffineTransform(scaleX: 1.0, y: CGFloat(indexOffset))
.concatenating(CGAffineTransform(translationX: 0, y: (bottomLabel.bounds.height / 2) * (1 - CGFloat(indexOffset))))
}
}
最后我們還要向外公開(kāi)一些樣式進(jìn)行自定義:
extension WheelView {
/// 前景色變化事件
override func tintColorDidChange() {
[toplabel, bottomLabel].forEach { $0.textColor = tintColor }
layer.borderColor = tintColor.cgColor
}
/// 背景色
override var backgroundColor: UIColor? {
get { return toplabel.backgroundColor }
set { [toplabel, bottomLabel].forEach { $0.backgroundColor = newValue } }
}
/// 邊框?qū)挾?
var borderWidth: CGFloat {
get { return layer.borderWidth }
set {
layoutMargins = UIEdgeInsets(top: newValue, left: newValue, bottom: newValue, right: newValue)
layer.borderWidth = newValue
}
}
/// 字體
var font: UIFont {
get { return toplabel.font }
set { [toplabel, bottomLabel].forEach { $0.font = newValue } }
}
}
至此,整個(gè)滾輪效果已經(jīng)完成。
掛載視圖
在FirstViewController中實(shí)例化剛才自定義的視圖,設(shè)置好字體、邊框、背景色、Contents等內(nèi)容,別忘了isUserInteractionEnabled設(shè)置為false,這樣就不會(huì)影響原先的事件響應(yīng)。
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
tableView.delegate = self
tableView.dataSource = self
tableView.register(UITableViewCell.self, forCellReuseIdentifier: "DefaultCell")
tableView.rowHeight = 44
wheelView = WheelView(frame: CGRect.zero)
wheelView.font = UIFont.systemFont(ofSize: 15, weight: .bold)
wheelView.borderWidth = 1
wheelView.backgroundColor = UIColor.white
wheelView.contents = data
wheelView.isUserInteractionEnabled = false
}
然后要把視圖掛載到原先的圖標(biāo)上,viewDidLoad()方法底部新增代碼:
override func viewDidLoad() {
...
guard let parentController = self.parent as? UITabBarController else { return }
let controllerIndex = parentController.children.firstIndex(of: self)!
var tabBarButtons = parentController.tabBar.subviews.filter({
type(of: $0).description().isEqual("UITabBarButton")
})
guard !tabBarButtons.isEmpty else { return }
let tabBarButton = tabBarButtons[controllerIndex]
let swappableImageViews = tabBarButton.subviews.filter({
type(of: $0).description().isEqual("UITabBarSwappableImageView")
})
guard !swappableImageViews.isEmpty else { return }
let swappableImageView = swappableImageViews.first!
tabBarButton.addSubview(wheelView)
swappableImageView.isHidden = true
NSLayoutConstraint.activate([
wheelView.widthAnchor.constraint(equalToConstant: 25),
wheelView.heightAnchor.constraint(equalToConstant: 25),
wheelView.centerXAnchor.constraint(equalTo: swappableImageView.centerXAnchor),
wheelView.centerYAnchor.constraint(equalTo: swappableImageView.centerYAnchor)
])
}
上述代碼的目的是最終找到對(duì)應(yīng)標(biāo)簽UITabBarButton內(nèi)類(lèi)型為UITabBarSwappableImageView的視圖并替換它??瓷先ハ喈?dāng)復(fù)雜,但是它盡可能地避免出現(xiàn)意外情況導(dǎo)致程序異常。只要以后UIkit不更改類(lèi)型UITabBarButton和UITabBarSwappableImageView,以及他們的包含關(guān)系,程序基本不會(huì)出現(xiàn)意外,最多導(dǎo)致自定義的視圖掛載不上去而已。另外一個(gè)好處是FirstViewController不用去擔(dān)心它被添加到TabBarController中的第幾個(gè)標(biāo)簽上??傮w來(lái)說(shuō)這個(gè)方法并不完美,但目前似乎也沒(méi)有更好的方法?
實(shí)際上還可以將上面的代碼剝離出來(lái),放到名為T(mén)abbarInteractable的protocol的默認(rèn)實(shí)現(xiàn)上。有需要的ViewController只要宣布遵守該協(xié)議,然后在viewDidLoad方法中調(diào)用一個(gè)方法即可實(shí)現(xiàn)整個(gè)替換過(guò)程。
只剩下最后一步了,我們知道UITableView是UIScrollView的子類(lèi)。在它滾動(dòng)的時(shí)候,F(xiàn)irsViewController作為UITableView的delegate,同樣會(huì)收到scrollViewDidScroll方法的調(diào)用,所以在這個(gè)方法里更新滾動(dòng)的進(jìn)度再合適不過(guò)了:
// MARK: UITableViewDelegate
extension FirstViewController: UITableViewDelegate {
func scrollViewDidScroll(_ scrollView: UIScrollView) {
//`progress`怎么計(jì)算取決于你需求,這里的是為了把`tableview`當(dāng)前可見(jiàn)區(qū)域最底部的2個(gè)數(shù)字給顯示出來(lái)。
let progress = Float((scrollView.contentOffset.y + tableView.bounds.height - tableView.rowHeight) / scrollView.contentSize.height)
wheelView.progress = progress
}
}
把項(xiàng)目跑起來(lái)看看吧,你會(huì)得到文章開(kāi)頭的效果。
總結(jié)
以上就是這篇文章的全部?jī)?nèi)容了,希望本文的內(nèi)容對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,謝謝大家對(duì)腳本之家的支持。
相關(guān)文章
iOS內(nèi)存管理Tagged Pointer使用原理詳解
這篇文章主要為大家介紹了iOS內(nèi)存管理Tagged Pointer使用原理詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-01-01
iOS監(jiān)聽(tīng)手機(jī)鎖屏狀態(tài)
iPhone的鎖屏監(jiān)測(cè)分為兩種方式監(jiān)聽(tīng),本文給大家介紹的非常詳細(xì),具體內(nèi)容詳情大家通過(guò)本文詳細(xì)了解下吧2017-05-05
iOS實(shí)現(xiàn)無(wú)限循環(huán)圖片輪播器的封裝
這篇文章主要為大家詳細(xì)介紹了iOS實(shí)現(xiàn)無(wú)限循環(huán)圖片輪播器的封裝,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2016-03-03
詳解iOS應(yīng)用開(kāi)發(fā)中使用設(shè)計(jì)模式中的抽象工廠模式
這篇文章主要介紹了iOS應(yīng)用開(kāi)發(fā)中使用設(shè)計(jì)模式中的抽象工廠模式,示例代碼為傳統(tǒng)的Objective-C,需要的朋友可以參考下2016-03-03
iOS緩存文件大小顯示功能和一鍵清理功能的實(shí)現(xiàn)方法
緩存占用了系統(tǒng)的大量空間,如何實(shí)時(shí)動(dòng)態(tài)的顯示緩存的大小,使用戶清晰的了解緩存的積累情況,有效的進(jìn)行一鍵清理呢?下面小編通過(guò)本文給大家介紹iOS緩存文件大小顯示功能和一鍵清理功能的實(shí)現(xiàn)方法,一起看看吧2016-10-10
iOS 統(tǒng)計(jì)Xcode項(xiàng)目代碼行數(shù)的實(shí)例
下面小編就為大家分享一篇iOS 統(tǒng)計(jì)Xcode項(xiàng)目代碼行數(shù)的實(shí)例,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2018-01-01
iOS開(kāi)發(fā)-調(diào)用系統(tǒng)相機(jī)和相冊(cè)獲取照片示例
這篇文章主要介紹了iOS開(kāi)發(fā)-調(diào)用系統(tǒng)相機(jī)和相冊(cè)獲取照片示例的相關(guān)資料,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下。2017-02-02
iOS App使用SQLite之句柄的定義及數(shù)據(jù)庫(kù)的基本操作
SQLite中在定義過(guò)句柄之后就可以指向數(shù)據(jù)庫(kù),從而利用iOS應(yīng)用程序進(jìn)行打開(kāi)或關(guān)閉等各種操作,這里我們就來(lái)看一下iOS App使用SQLite之句柄的定義及數(shù)據(jù)庫(kù)的基本操作2016-06-06

