歡迎關注
最酷最in的雲資訊

Tab Bar 的圖標原來還可以這樣玩

示例代碼下載

背景


框架自帶的 Tab Bar 相信大家已經熟悉得不能再熟悉了,一般使用的時候不過是設置兩個圖標代表選中和未選中兩種狀態,難免有一些平淡。後來很多控件就在標簽選中時進行一些比較抓眼球的動畫,不過我覺得大部分都是為了動畫而動畫。直到後來我看到Outlook客戶端的動畫時,我才意識到原來還可以跟用戶的交互結合在一起。

Tab Bar 的圖標原來還可以這樣玩

圖1 標簽圖標跟隨手勢進行不同的動畫

有意思吧,不過本文並不是要仿制個一模一樣的出來,會有稍微變化︰

Tab Bar 的圖標原來還可以這樣玩

Tab Bar 的圖標原來還可以這樣玩

圖2 本文完成的最終效果

實現分析


寫代碼之前,咱先討論下實現的方法,相信你已經猜到標簽頁的圖標顯然已經不是圖片,而是一個自定義的UIView。將一個視圖掛載到原本圖標的位置並不是一件難事,稍微有些復雜的是數字滾輪效果的實現,別看它數字不停地在滾動,仔細看其實最多顯示2種數字,也就說只要2個Label就夠了。

基于篇幅,文章不會涉及右側的時鐘效果,感興趣請直接參考源碼。

數字滾輪


打開項目TabBarInteraction,新建文件WheelView.swift,它是UIView的子類。首先設置好初始化函數︰

class WheelView: UIView {    required init?(coder aDecoder: NSCoder) {        super.init(coder: aDecoder)        setupView()    }    override init(frame: CGRect) {        super.init(frame: frame)        setupView()    }}

接著創建兩個Label實例,代表滾輪中的上下兩個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}

現在來完成setupView()方法,在這方法中將上述兩個Label添加到視圖中,然後設置約束將它們的四邊都與layoutMarginsGuide對齊。

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)        ])    }}

復制代碼有人可能會問現在這樣兩個Label不是重疊的狀態嗎?不著急,接下來我們會根據參數動態地調整它們的大小和位置。

添加兩個實例變量progress和contents,分別表示滾動的總體進度和顯示的全部內容。

var progress: Float = 0.0var contents = [String]()

我們接下來要根據這兩個變量計算出當前兩個Label顯示的內容以及它們的縮放位置。這些計算都在progress的didSet里完成︰

var progress: Float = 0.0 {    didSet {        progress = min(max(progress, 0.0), 1.0)         guard contents.count > 0 else { return }                /** 根據 progress 和 contents 計算出上下兩個 label 顯示的內容以及 label 的壓縮程度和位置         *         *  Example:          *  progress = 0.4, contents = ["A","B","C","D"]         *         *  1)計算兩個label顯示的內容         *  topIndex = 4 * 0.4 = 1.6, topLabel.text = contents[1] = "B"         *  bottomIndex = 1.6 + 1 = 2.6, bottomLabel.text = contents[2] = "C"          *           *  2) 計算兩個label如何壓縮和位置調整,這是實現滾輪效果的原理         *  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              *  |└───────────┘|             |             |                               *  └─────────────┘             └─────────────┘                               *         * 可以想象出,當 indexOffset 從 0..<1 過程中,         * topLabel 從滿視圖越縮越小至0,而 bottomLabel剛好相反越變越大至滿視圖,即形成一次完整的滾動         */        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))))    }}

最後我們還要向外公開一些樣式進行自定義︰

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 } }    }    /// 邊框寬度    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 } }    }}

至此,整個滾輪效果已經完成。

掛載視圖


在FirstViewController中實例化剛才自定義的視圖,設置好字體、邊框、背景色、Contents等內容,別忘了isUserInteractionEnabled設置為false,這樣就不會影響原先的事件響應。

 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}

復制代碼然後要把視圖掛載到原先的圖標上,viewDidLoad()方法底部新增代碼︰

4月23日更新︰自定義視圖替換 tabbar 圖標的方法現在更通用了

override func viewDidLoad() {    ...    var parentController = self.parent    while !(parentController is UITabBarController) {        if parentController?.parent == nil { return }        parentController = parentController?.parent    }    let tabbarControlelr = parentController as! UITabBarController        var controllerIndex = -1    findControllerIndexLoop: for (i, child) in tabbarControlelr.children.enumerated() {        var stack = [child]        while stack.count > 0 {            let count = stack.count            for j in stride(from: 0, to: count, by: 1) {                if stack[j] is Self {                    controllerIndex = i                    break findControllerIndexLoop                }                for vc in stack[j].children {                    stack.append(vc)                }            }            for _ in 1...count {                stack.remove(at: 0)            }        }    }    if controllerIndex == -1 { return }    var tabBarButtons = tabbarControlelr.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)    ]) }

上述代碼的目的是最終找到對應標簽UITabBarButton內類型為UITabBarSwappableImageView的視圖並替換它。看上去相當復雜,但是它盡可能地避免出現意外情況導致程序異常。只要以後UIkit不更改類型UITabBarButton和UITabBarSwappableImageView,以及他們的包含關系,程序基本不會出現意外,最多導致自定義的視圖掛載不上去而已。另外一個好處是FirstViewController不用去擔心它被添加到TabBarController中的第幾個標簽上。總體來說這個方法並不完美,但目前似乎也沒有更好的方法?

實際上還可以將上面的代碼剝離出來,放到名為TabbarInteractable的protocol的默認實現上。有需要的ViewController只要宣布遵守該協議,然後在viewDidLoad方法中調用一個方法即可實現整個替換過程。

只剩下最後一步了,我們知道UITableView是UIScrollView的子類。在它滾動的時候,FirsViewController作為UITableView的delegate,同樣會收到scrollViewDidScroll方法的調用,所以在這個方法里更新滾動的進度再合適不過了:

// MARK: UITableViewDelegateextension FirstViewController: UITableViewDelegate {    func scrollViewDidScroll(_ scrollView: UIScrollView) {        //`progress`怎麼計算取決于你需求,這里的是為了把`tableview`當前可見區域最底部的2個數字給顯示出來。        let progress = Float((scrollView.contentOffset.y + tableView.bounds.height - tableView.rowHeight) / scrollView.contentSize.height)        wheelView.progress = progress    }}

把項目跑起來看看吧,你會得到文章開頭的效果。

【全文完】

作者︰potato04

鏈接︰https://juejin.im/post/5c9fa72d6fb9a05e6835c8a6

贊(0) 打賞
未經允許不得轉載︰ » Tab Bar 的圖標原來還可以這樣玩
分享到︰ 更多 (0)

評論 搶沙發

中文字幕亚洲无线码|国产亚洲观看视频在线|性欧美长视频免费观看

關于我們聯系我們

覺得文章有用就打賞一下文章作者

支付寶掃一掃打賞

微信掃一掃打賞