| 导语 ViewPager是一个很常用的Android组件,其提供的接口和功能基本已经可以满足项目的大部分需要,但如果需要定制一些不一样的行为,比如实现一个类似iOS多任务那样的卡片列表控件,熟悉和修改ViewPager源码来实现就会简单得多。 所以,有了本篇。
本篇有2000字,阅读起来大概要10分钟。
分析一个自定义ViewGroup的源码,一般可以从以下3个方面入手:
1. 自定义ViewGroup对自己以及子View的宽高限制规则,即onMeasure方法。
2. 自定义ViewGroup对子View的布局摆放规则,即onLayout方法。
3. 自定义ViewGroup的触摸事件处理,即dispatchTouchEvent,onInterceptTouchEvent,onTouchEvent 3个方法。
对于ViewPager来说,除了上述3个方面,还可以再加上一点:
4. 子View是怎么add到ViewGroup的,我们知道ViewPager的每一个item是通过Adapter获取的,那ViewPager是在哪里调用addView把item加上去的?
以下分析基于androidx.viewpager.widget.ViewPager的源码(androidx是啥?可以理解为替代以前support.v4/v7这些包的统一集合,最新版本的AndroidStudio3.4新建工程已经默认替换了,support包找不到了,可以在gradle.properties里设置关掉androidx,微笑)。
onMeasure的作用是View对自己的宽高进行计算和赋值,如果是ViewGroup,还需要去调用每一个子View的onMeasure让子View也进行宽高计算,onMeasure的具体用法这里不细讲,推荐一篇教材:https://hencoder.com/ui-2-2/ ,讲得很通俗易懂。
下面直接看ViewPager的onMeasure做了什么,主要分成3块。
这部分主要做了两件事,第一是调用setMeasuredDimension给自己的宽高赋值,大小是getDefaultSize获得的,除非是写死固定宽高,否则父View提供给ViewPager的空间多大就多大;第二是计算出child的可用宽高,用第一步计算出的宽高减去padding,就是child的可用宽高,这里的childWidth指的是一个item view的width,而不是ViewPager所有child加起来的width。
这部分通过注释也知道,是对Decor view进行测量,Decor View是啥?可以是viewpager顶部的tab,也可以是底部的下标,是独立于item view的部分,这里不讲这部分,Android已经实现了PagerTabStrip和PagerTitleStrip两个Decor View,可以参考他们的实现,一般我们需要tab的话也可以自己实现一个,再和ViewPager进行组合,有时更灵活。
最后这部分,首先调用了populate,这是ViewPager很重要的一个方法,第4部分讲addView的时候会讲,可以理解为把当前需要显示的item view填充到屏幕上;然后就是对每一个child进行measure,需要注意的是,前面测量Decor View(如果有的话)的时候是会把可用宽高减去Decor View的宽高,剩下的才是item view的可用宽高。
onLayout的作用是ViewGroup对子view的摆放位置进行计算,也即算出子view的left,top,right,bottom四个属性值,具体用法可以参考这篇教程:https://hencoder.com/ui-2-3/。
下面我们看看ViewPager的onLayout做了什么,主要分为2部分。
第一部分和onMeausre类似,是对Decor View的onLayout处理,这里不讲,值得注意的是,Decor View的摆放位置可以是上下左右四个方向,具体可以看看源码。
第二部分就是对每个item view进行layout处理,这里重点看childLeft这个值,childLef=paddingLeft+loff,loff=childWidth*ii.offset,offset这里简单理解就是屏幕上显示的所有item的index,比如第一个item的offset就是0,那么第一个item的left就是paddingLeft,第2个item的offset是1,其left就是paddingLeft+childWidth,所以ViewPager的item都是一个个横向排列着,和LinearLayout类似。
不过这里只是简化了说,offset的功能还不止表示index,因为ViewPager的item之间是可以设置pageMargin(可以是负值)的,可以利用这个pageMargin来做卡片重叠的效果,所以offset的值其实还和pageMargin有关,具体计算的代码在calculatePageOffsets这个方法里,这里不讲。
View的触摸事件分发顺序是dispatchTouchEvent –> onInterceptTouchEvent -> onTouchEvent,关于这3个事件的区别,这里也不细讲。
我们直接看ViewPager对这3个事件的处理是怎样的。
ViewPager没有重写,哦耶!一般也不需要重写这个函数。
onInterceptTouchEvent的作用是判断是否要拦截事件,返回true则后续事件会传给onTouchEvent处理,这里重点看down和move事件。
先看down事件,第一部分是初始化触摸坐标和相关变量,比较简单;第二部分是当ViewPager处于SCROLL_STATE_SETTLING(快要滑到最终位置)时,先停止其滚动,mIsBeingDragged=true,想想平时对一个滚动中的ViewPager按下去,ViewPager是先暂停下来,然后可以继续滑动。这里的mIsBeingDragged变量很重要,onInterceptTouchEvent的返回值就是mIsBeingDragged,返回true说明ViewPager正在被拖动,需要到onTouchEvent处理。
再来看看move事件,主要工作是判断手指左右滑动的距离,超过一定阙值后就把mIsBeingDragged设为true,说明ViewPager要消费这个事件,最终拖动逻辑在onTouchEvent处理。
onInterceptTouchEvent返回true后,后续的事件就会到onTouchEvent这边来,这里重点看move和up事件。
move事件主要做两件事,第一件事是当mIsBeingDragged为false时,重新检测一下当前是否符合左右滑动的条件(mIsBeingDragged为false时为啥会回调onTouchEvent呢?可能是触摸方向是上下滑动没触发到onInterceptTouchEvent的条件,事件分发给子view,子View又没处理,所以事件又回调到ViewPager的onTouchEvent);第二件事是调用performDrag对ViewPager进行滚动,performDrag本质上也是调用scrollTo进行滚动,细节可以去看看源码。
再来看up事件,主要就做一件事,就是根据松手时当前滑动的位置,计算出最终要切换到哪个item,最终调用setCurrentItemInternal进行切换,而且带动画。
如果你想做出iOS多任务列表那种效果,就是快速滑动松手后,整个列表还能跟着惯性滚下去,可以考虑在这里做一个fling处理,微笑。
ViewPager的每一个item view都是通过Adapter返回的(严格说是Adapter返回Fragment,Fragment返回item view),那ViewPager是不是直接调用Adapter的getItem获取View,然后调用addView把view添加上去呢?
搜索ViewPager的addView方法,虽然覆写了,但ViewPager内部没有任何调用,真是神奇。
直接断点addView调试一下,调用堆栈如下:
从堆栈可以看出,起始方法是ViewPager的populate方法(第一部分讲onMeasure提到的那个方法),中间经过FragmentManager各种状态处理,最终到了moveToState,调用了ViewPager的addView方法把itemview添加了上去,下面讲一下这两个方法。
这个方法有点长,这里不贴代码,其作用主要是,根据当前的item位置,把当前要显示的item填充到屏幕上,对于已经不需要显示的item,会调用adapter.destroyItem销毁,对于还没创建的item,会调用adapter.instantiateItem初始化。最后调用adapter.finishUpdate触发状态更新。
这个方法是FragmentManager更新Fragment状态的地方,addView的调用也在这个地方。
可以看到,第一次创建Fragment后的状态就是Fragment.CREATED,这里的container在我们的例子里指的就是ViewPager,container.addView就把当前item的view添加到ViewPager里。
ViewPager是一个很强大也很常用的View,其源码有3000多行,本篇只对核心的4个方面进行分析,如果需要对ViewPager进行源码修改来自定义某些行为,可以优先考虑从这4个方面去修改。
期待后续!
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。