APP常见的滑动导航实现:TYPagerController源码
写在前面
滑动视图导航是APP中经常用到的一种视图,自己之前也造过相关的轮子。
偶然间发现了TYPagerController这个第三方库,其加入了NSCache对ScrollView的性能进行了优化,很值得学习,所以便有了此文来记录,方便日后查看。
在其首页上有具体的效果演示,在这里我就不多做介绍了。
看完本系列文章,我相信大家都能写一个自己的导航控制了。
一、总体介绍
这就是一个常见的滑动导航控制器:
1.1 滑动导航视图控制器的总体构成
主要分两个部分:
- 上方的TabBar(CollectionCell、UnderLineView构成)
- 下方的ScrollView(ViewControllerView构成)
滑动导航控制器的总体流程也很简单:
- 将ViewController.view加入到下方的ScrollView中
- 根据数据源Titles对上方TabBar中CollectionCell上的Label赋值
- 处理下方ScorllView与上方TabBar之间的协同问题
这里面有几个坑:
流程1会存在比较严重的性能问题;
流程3需要规划在相应的处理方法中做正确的事情。
1.2 TYPagerController整体介绍
TYPagerController针对上文提到的流程1、3存在的问题做出了相应的改进,也是我们需要重点关注的地方:
- 通过Cache对ScrollView进行性能优化
- 处理好协同关系
其整体是自上而下的继承关系:
其中:
- TYPagerController,基类,继承自UIViewController,其本质就是一个自定义的ScrollViewController
- TYTabPagerController,在基类的基础上,增加了头部TabBar(本质是一个CollectionView)
- TYTabButtonPagerController,为头部TabBar注册了一个常用的Cell
我们一般直接用TYTabButtonPagerController就可以了,如果有自定义cell的需求,可以使用TYTabPagerController注册自己的cell。
二、TYPagerController基类介绍
实现一个滑动导航,如果不加头部的切换TabBar,思路跟初始化一般的ScrollView是一样:
- 初始化ScrollView
- 根据需要显示的页面数量和宽度(一般是一整个屏幕宽度),确定ContentView的Frame
TYPagerController基类,在此基础上,加入了缓存、代理和总体逻辑流程控制。
其中,缓存部分就是很简单的使用了NSCache,代理我们简单介绍一下,主要是看一下作者对滑动试图导航的流程控制是怎样的。
三、代理协议
协议部分,跟常用的tableView一个思路,定义数据源和代理:
- dataSource:提供与index对应的VC
- delegate:处理transition相关逻辑
四、TYPagerController基类流程分析
本章共计三部分,重点在2、3部分:
- init
- LifeCycle
- 滑动逻辑处理
原作者对部分子方法和参数的命名容易产生歧义,所以我对其进行了一部分重构,这样大家读起来会顺畅一些。
初始化布局阶段,整体流程是这样的:
4.1 init
初始化部分很简单,就是对一些必要的数据进行赋值:
4.2 Life Cycle
生命周期这部分,主要是初始化ScrollView,根据dataSource进行布局。
4.2.1 updateContentViewIfNeeded:
- 作用:根据contentView.frame.size判断,是否已经update过ContentView了。
- 实现:比较对象是一个设定值,下文resizeContentView的时候,会对contentView的frame设置这个值。
- 存在理由:避免计算资源浪费。
4.2.2 updateContentView:
- 作用:调整ContentView(ScrollView)布局
- 实现:从dataSource获取到ViewControllers的数量,进而计算ScrollView的ContentSize。
小小结
至此呢,作者做的事情跟传统使用ScrollView的思路是一模一样的(初始化Scrollview,配置contentSize),只不过中间加入了对statusBar高度适应方面的判断。
4.2.3 layoutSubViewsInContentView
- 作用:添加Subviews至ContentView(ScrollView)
- 实现:仅显示需要显示的VC,并利用Cache避免VC的销毁
讨论
scrollView的contentSize确定下来以后,就需要添加subviews了。
这时候,最简单的做法就是,一次性添加所有subviews到scrollView上。
但是这样会带来很严重的内存占用(想象一下你有30个tableview,每个tableview有1000+的cell)。
这时候就需要更好的解决办法,我们只添加需要显示的subviews就是了:
- 确定一个需要显示的index range(滑动过程中,需要显示的VC将不止一个,所以需要一个range)
- 移除range外的VC,只显示range范围内的VC
这样做的好处,自然是节省内存;
但带来的问题则是,需要反复的init、dealloc我们的ViewController,带来不必要的性能损失。
解决办法呢,加入缓存呗。
使用NSCache也好,自己创建Dict也好,能避免我们的ViewController被销毁就行。
这样修改之后的大体流程是这样的:
- 确定一个需要显示的index range
- 根据index从visibleVCs、Cache或者DataSource中获取VC
- 根据不同情况,将获得到的VC加入到childVC、visibleVC或者Cache中
- 如果visibleVCs中有range之外的VC,则将其及其视图从visibleVCs、childVC和视图层级中移除
加入缓存之后,我们的ViewController不会被反复的init、alloc,同时也会显著降低内存占用(ViewController的View没有加入视图层级中)。
我们来看一下具体的代码:
其中,内联函数可能有的童鞋不了解,其可以理解为宏定义,只不过内联函数在宏定义的基础上,加入了返回值校验等等一般函数具有的功能的同时,可以避免函数入栈操作,从而节省开支。
从上面的代码可以看出,该函数主要是根据offset和width计算出visibleRange。
后面ScrollView的代理方法中,会在滑动过程中实时的调用这个内联函数,计算出range数据,然后再根据range进行添加删除VC的操作。
addControllersInVisibleRange
- 作用:添加VC至ContentView
- 实现:从VisibleVCs、Cache或dataSource处获得VC,加入到ScrollView上,并分情况将其记录到visibleVCs、Cache中
removeControllersOutOfVisibleRange
- 作用:删除可视范围外的VC
- 实现:分别从VisibleVCs、视图树以及VC树中删除符合条件(outof)的VC
至此,init及life Cycle的分析已经完成了,接下来是运行时的交互部分了。
4.3 滑动逻辑处理
基类中函数调用顺序是这个样子的:
很容易理解:
- 根据目前offset确定起始、目标index和滑动比例(progress)
- 根据index获取顶部TabBar对应的cell(下一篇的内容)
- 根据cell的frame和滑动比做动画(下一篇的内容)
- 重新布局Subviews(增加可见范围内Index对应的View,移除可见范围外的View)
scrollViewDidScroll
细心的朋友可能发现了,这里进行了两次index的计算,至于原因嘛,我们去看看具体实现代码好了。
configurePagerIndexByScrollProgress
作用:计算fromIndex、toIndex和滑动比
实现:对offsetX/width分别取整数部分(index)和小数部分(滑动比)
说明:方法名中对progress的计算就是取offsetX/width的小数部分
计算progress的方法,是实时的。
也就是说,只要offset发生了变化,该方法就会调用,子类处理progress的方法也会被调用。
所以,适合用来处理需要实时反馈的事件,比如控制underLineView的Frame,调整Label的transform、color等。
configurePagerIndex
作用:计算fromIndex、toIndex,在index发生改变的时候才会触发处理方法
至此,就很明了了。
configurePagerIndex虽然也是计算index,但是有一个阈值和比较的步骤存在,这样只有当offsetX的改变超过阈值并且index确实更改了之后,才会调用子类和代理的处理方法。
比如,修改collectionView的offset使currentIndex居中这样只需要在currentIndex改变的情况下处理的事情,就可以在该方法中调用。
五、总结
回头看一下我们开篇提到的两个『坑』,TYPagerController是如何处理的:
- 通过增加显示『窗口』和Cache,优化了ScrollView作为VC容器的性能
- 基类根据不同情况(实时或者只处理一次),调用相应子类方法,分别处理ScrollView的滑动事件
本篇已经将优化部分分析完了;
接下来的一篇,会着重分析子类中transition方法的实现。