当前位置: 首页 > 编程笔记 >

你知道Tab Bar图标原来还可以这样玩吗

潘宝
2023-03-14
本文向大家介绍你知道Tab Bar图标原来还可以这样玩吗,包括了你知道Tab Bar图标原来还可以这样玩吗的使用技巧和注意事项,需要的朋友参考一下

示例代码下载 (本地下载)

背景

框架自带的 Tab Bar 相信大家已经熟悉得不能再熟悉了,一般使用的时候不过是设置两个图标代表选中和未选中两种状态,难免有一些平淡。后来很多控件就在标签选中时进行一些比较抓眼球的动画,不过我觉得大部分都是为了动画而动画。直到后来我看到Outlook客户端的动画时,我才意识到原来还可以跟用户的交互结合在一起。

图1 标签图标跟随手势进行不同的动画

有意思吧,不过本文并不是要仿制个一模一样的出来,会有稍微变化:


图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.0
var 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.0 递增到 0.999 过程中,
   * 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()方法底部新增代码:

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

上述代码的目的是最终找到对应标签UITabBarButton内类型为UITabBarSwappableImageView的视图并替换它。看上去相当复杂,但是它尽可能地避免出现意外情况导致程序异常。只要以后UIkit不更改类型UITabBarButton和UITabBarSwappableImageView,以及他们的包含关系,程序基本不会出现意外,最多导致自定义的视图挂载不上去而已。另外一个好处是FirstViewController不用去担心它被添加到TabBarController中的第几个标签上。总体来说这个方法并不完美,但目前似乎也没有更好的方法?

实际上还可以将上面的代码剥离出来,放到名为TabbarInteractable的protocol的默认实现上。有需要的ViewController只要宣布遵守该协议,然后在viewDidLoad方法中调用一个方法即可实现整个替换过程。

只剩下最后一步了,我们知道UITableView是UIScrollView的子类。在它滚动的时候,FirsViewController作为UITableView的delegate,同样会收到scrollViewDidScroll方法的调用,所以在这个方法里更新滚动的进度再合适不过了:

// MARK: UITableViewDelegate
extension 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
 }
}

把项目跑起来看看吧,你会得到文章开头的效果。

总结

以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,谢谢大家对小牛知识库的支持。

 类似资料:
  • 本文向大家介绍.Net中的集合排序可以这么玩你知道吗,包括了.Net中的集合排序可以这么玩你知道吗的使用技巧和注意事项,需要的朋友参考一下 C#集合类型概述 集合是.NET FCL(Framework Class Library)中很重要的一部分。所有的集合类都继承自IEnumerable。集合类总体可分为一下几类:关联/非关联型集合,顺序/随机访问集合,顺序/无序集合,泛型/非泛型集合,线程安全

  • 本文向大家介绍你知道nextTick的原理吗?相关面试题,主要包含被问及你知道nextTick的原理吗?时的应答技巧和注意事项,需要的朋友参考一下 用法:在下次 DOM 更新循环结束之后执行延迟回调。在修改数据之后立即使用这个方法,获取更新后的 DOM。 Vue官网 提到DOM的更新是异步执行的,只要数据发生变化,将会开启一个队列,并缓冲在同一事件循环中发生的所有数据变更。如果同一个 watche

  • 本文向大家介绍你知道 标签的target属性规定在何处打开链接文档吗?相关面试题,主要包含被问及你知道 标签的target属性规定在何处打开链接文档吗?时的应答技巧和注意事项,需要的朋友参考一下 _self 自身打开,效果即跳转 _blank 新开标签页打开 _parent 父 _top 顶级 我们常用的恐怕一般只有2种,_blank,_self 另外有个细节是为什么链接是a标签,而不是b,c呢,

  • 错误:不知道这样的主机 我在努力 并且做 bootstrap-spring.application.name=pluralsight-toll-service 应用道具 应用程序 如何使用名称从客户端调用服务

  • 本文向大家介绍你知道Virtual DOM的工作原理吗?相关面试题,主要包含被问及你知道Virtual DOM的工作原理吗?时的应答技巧和注意事项,需要的朋友参考一下 JSX经过编译后转成 createElement 语法,所有组件通过创建的 element 组成一个节点树就是 vdom 树。每次数据发生变化后,通过新生成的树与原来树进行对比(这个过程是边遍历边进行的,非前后行为)最终会生成新的树

  • 本文向大家介绍你知道koa的中间件原理吗?相关面试题,主要包含被问及你知道koa的中间件原理吗?时的应答技巧和注意事项,需要的朋友参考一下 如果一个koa中间件执行的时候,里面没有await next()的话,那么后面的中间件都不会执行。。 其实是支持普通函数的,不一定非得,可以如下使用 顺便贴下我的文章^_^ 若川:学习 koa 源码的整体架构,浅析koa洋葱模型原理和co原理