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 作为一个开发者,有一个学习的氛围跟一个交流圈子特别重要这是一个我的iOS交流群:869685378,不管你是小白还是大牛欢迎入驻 ,分享BAT,阿里面试题、面试经验,讨论技术, 大家一起交流学习成长!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值