APP常见的滑动导航实现:TYPagerController源码分析(一)

写在前面

滑动视图导航是APP中经常用到的一种视图,自己之前也造过相关的轮子。
偶然间发现了TYPagerController这个第三方库,其加入了NSCache对ScrollView的性能进行了优化,很值得学习,所以便有了此文来记录,方便日后查看。
在其首页上有具体的效果演示,在这里我就不多做介绍了。

看完本系列文章,我相信大家都能写一个自己的导航控制了。

一、总体介绍

这就是一个常见的滑动导航控制器:


1.1 滑动导航视图控制器的总体构成

主要分两个部分:

  • 上方的TabBar(CollectionCell、UnderLineView构成)
  • 下方的ScrollView(ViewControllerView构成)

滑动导航控制器的总体流程也很简单:

  1. 将ViewController.view加入到下方的ScrollView中
  2. 根据数据源Titles对上方TabBar中CollectionCell上的Label赋值
  3. 处理下方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是一样:

  1. 初始化ScrollView
  2. 根据需要显示的页面数量和宽度(一般是一整个屏幕宽度),确定ContentView的Frame

TYPagerController基类,在此基础上,加入了缓存、代理和总体逻辑流程控制。

其中,缓存部分就是很简单的使用了NSCache,代理我们简单介绍一下,主要是看一下作者对滑动试图导航的流程控制是怎样的。

三、代理协议

协议部分,跟常用的tableView一个思路,定义数据源和代理:

  • dataSource:提供与index对应的VC
  • delegate:处理transition相关逻辑

四、TYPagerController基类流程分析

本章共计三部分,重点在2、3部分:

  1. init
  2. LifeCycle
  3. 滑动逻辑处理

原作者对部分子方法和参数的命名容易产生歧义,所以我对其进行了一部分重构,这样大家读起来会顺畅一些。

初始化布局阶段,整体流程是这样的:

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就是了:

  1. 确定一个需要显示的index range(滑动过程中,需要显示的VC将不止一个,所以需要一个range)
  2. 移除range外的VC,只显示range范围内的VC

这样做的好处,自然是节省内存;
但带来的问题则是,需要反复的init、dealloc我们的ViewController,带来不必要的性能损失。

解决办法呢,加入缓存呗。
使用NSCache也好,自己创建Dict也好,能避免我们的ViewController被销毁就行。
这样修改之后的大体流程是这样的:

  1. 确定一个需要显示的index range
  2. 根据index从visibleVCs、Cache或者DataSource中获取VC
  3. 根据不同情况,将获得到的VC加入到childVC、visibleVC或者Cache中
  4. 如果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 滑动逻辑处理

基类中函数调用顺序是这个样子的:


很容易理解:

  1. 根据目前offset确定起始、目标index和滑动比例(progress)
  2. 根据index获取顶部TabBar对应的cell(下一篇的内容)
  3. 根据cell的frame和滑动比做动画(下一篇的内容)
  4. 重新布局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方法的实现。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,053评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,527评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,779评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,685评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,699评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,609评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,989评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,654评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,890评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,634评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,716评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,394评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,976评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,950评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,191评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,849评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,458评论 2 342

推荐阅读更多精彩内容