服务器之家:专注于服务器技术及软件下载分享
分类导航

PHP教程|ASP.NET教程|Java教程|ASP教程|编程技术|正则表达式|C/C++|IOS|C#|Swift|Android|VB|R语言|JavaScript|易语言|vb.net|

服务器之家 - 编程语言 - IOS - 你知道Tab Bar图标原来还可以这样玩吗

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

2021-05-21 15:29potato04 IOS

这篇文章主要给大家介绍了关于Tab Bar图标另外一些大家不知道的玩法,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面来一起学习学习吧

背景

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

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

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

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

你知道Tab Bar图标原来还可以这样玩吗
图2 本文完成的最终效果

实现分析

写代码之前,咱先讨论下实现的方法,相信你已经猜到标签页的图标显然已经不是图片,而是一个自定义的uiview。将一个视图挂载到原本图标的位置并不是一件难事,稍微有些复杂的是数字滚轮效果的实现,别看它数字不停地在滚动,仔细看其实最多显示2种数字,也就说只要2个label就够了。

基于篇幅,文章不会涉及右侧的时钟效果,感兴趣请直接参考源码。

数字滚轮

打开项目tabbarinteraction,新建文件wheelview.swift,它是uiview的子类。首先设置好初始化函数:

?
1
2
3
4
5
6
7
8
9
10
class wheelview: uiview {
 required init?(coder adecoder: nscoder) {
  super.init(coder: adecoder)
  setupview()
 }
 override init(frame: cgrect) {
  super.init(frame: frame)
  setupview()
 }
}

接着创建两个label实例,代表滚轮中的上下两个label:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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对齐。

?
1
2
3
4
5
6
7
8
9
10
11
12
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,分别表示滚动的总体进度和显示的全部内容。

?
1
2
var progress: float = 0.0
var contents = [string]()

我们接下来要根据这两个变量计算出当前两个label显示的内容以及它们的缩放位置。这些计算都在progress的didset里完成:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
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))))
 }
}

最后我们还要向外公开一些样式进行自定义:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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,这样就不会影响原先的事件响应。

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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()方法底部新增代码:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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方法的调用,所以在这个方法里更新滚动的进度再合适不过了:

?
1
2
3
4
5
6
7
8
// 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
 }
}

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

示例代码下载

总结

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

原文链接:https://juejin.im/post/5c9fa72d6fb9a05e6835c8a6

延伸 · 阅读

精彩推荐
  • IOSiOS中tableview 两级cell的展开与收回的示例代码

    iOS中tableview 两级cell的展开与收回的示例代码

    本篇文章主要介绍了iOS中tableview 两级cell的展开与收回的示例代码,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧...

    J_Kang3862021-04-22
  • IOSiOS布局渲染之UIView方法的调用时机详解

    iOS布局渲染之UIView方法的调用时机详解

    在你刚开始开发 iOS 应用时,最难避免或者是调试的就是和布局相关的问题,下面这篇文章主要给大家介绍了关于iOS布局渲染之UIView方法调用时机的相关资料...

    windtersharp7642021-05-04
  • IOSIOS开发之字典转字符串的实例详解

    IOS开发之字典转字符串的实例详解

    这篇文章主要介绍了IOS开发之字典转字符串的实例详解的相关资料,希望通过本文能帮助到大家,让大家掌握这样的方法,需要的朋友可以参考下...

    苦练内功5832021-04-01
  • IOSiOS通过逆向理解Block的内存模型

    iOS通过逆向理解Block的内存模型

    自从对 iOS 的逆向初窥门径后,我也经常通过它来分析一些比较大的应用,参考一下这些应用中某些功能的实现。这个探索的过程乐趣多多,不仅能满足自...

    Swiftyper12832021-03-03
  • IOS解析iOS开发中的FirstResponder第一响应对象

    解析iOS开发中的FirstResponder第一响应对象

    这篇文章主要介绍了解析iOS开发中的FirstResponder第一响应对象,包括View的FirstResponder的释放问题,需要的朋友可以参考下...

    一片枫叶4662020-12-25
  • IOS关于iOS自适应cell行高的那些事儿

    关于iOS自适应cell行高的那些事儿

    这篇文章主要给大家介绍了关于iOS自适应cell行高的那些事儿,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的...

    daisy6092021-05-17
  • IOSiOS 雷达效果实例详解

    iOS 雷达效果实例详解

    这篇文章主要介绍了iOS 雷达效果实例详解的相关资料,需要的朋友可以参考下...

    SimpleWorld11022021-01-28
  • IOSIOS 屏幕适配方案实现缩放window的示例代码

    IOS 屏幕适配方案实现缩放window的示例代码

    这篇文章主要介绍了IOS 屏幕适配方案实现缩放window的示例代码,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要...

    xiari5772021-06-01