前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >ViewPager源码分析

ViewPager源码分析

原创
作者头像
帅气的程序员
修改2020-08-21 21:34:58
1.7K0
修改2020-08-21 21:34:58
举报
文章被收录于专栏:Android开发之路

| 导语 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,微笑)。

1. onMeasure

onMeasure的作用是View对自己的宽高进行计算和赋值,如果是ViewGroup,还需要去调用每一个子View的onMeasure让子View也进行宽高计算,onMeasure的具体用法这里不细讲,推荐一篇教材:https://hencoder.com/ui-2-2/ ,讲得很通俗易懂。

下面直接看ViewPager的onMeasure做了什么,主要分成3块。

1.1 给自己宽高赋值

这部分主要做了两件事,第一是调用setMeasuredDimension给自己的宽高赋值,大小是getDefaultSize获得的,除非是写死固定宽高,否则父View提供给ViewPager的空间多大就多大;第二是计算出child的可用宽高,用第一步计算出的宽高减去padding,就是child的可用宽高,这里的childWidth指的是一个item view的width,而不是ViewPager所有child加起来的width。

1.2 对Decor View处理

这部分通过注释也知道,是对Decor view进行测量,Decor View是啥?可以是viewpager顶部的tab,也可以是底部的下标,是独立于item view的部分,这里不讲这部分,Android已经实现了PagerTabStrip和PagerTitleStrip两个Decor View,可以参考他们的实现,一般我们需要tab的话也可以自己实现一个,再和ViewPager进行组合,有时更灵活。

1.3 对item view处理

最后这部分,首先调用了populate,这是ViewPager很重要的一个方法,第4部分讲addView的时候会讲,可以理解为把当前需要显示的item view填充到屏幕上;然后就是对每一个child进行measure,需要注意的是,前面测量Decor View(如果有的话)的时候是会把可用宽高减去Decor View的宽高,剩下的才是item view的可用宽高。

2. onLayout

       onLayout的作用是ViewGroup对子view的摆放位置进行计算,也即算出子view的left,top,right,bottom四个属性值,具体用法可以参考这篇教程:https://hencoder.com/ui-2-3/

下面我们看看ViewPager的onLayout做了什么,主要分为2部分。

2.1 对Decor View处理

第一部分和onMeausre类似,是对Decor View的onLayout处理,这里不讲,值得注意的是,Decor View的摆放位置可以是上下左右四个方向,具体可以看看源码。

2.2 对item 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这个方法里,这里不讲。

3. 触摸事件处理

     View的触摸事件分发顺序是dispatchTouchEvent –> onInterceptTouchEvent -> onTouchEvent,关于这3个事件的区别,这里也不细讲。

我们直接看ViewPager对这3个事件的处理是怎样的。

3.1 dispatchTouchEvent

       ViewPager没有重写,哦耶!一般也不需要重写这个函数。

3.2 onInterceptTouchEvent

       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处理。

3.3 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处理,微笑。

4. addView

       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添加了上去,下面讲一下这两个方法。

4.1 populate

这个方法有点长,这里不贴代码,其作用主要是,根据当前的item位置,把当前要显示的item填充到屏幕上,对于已经不需要显示的item,会调用adapter.destroyItem销毁,对于还没创建的item,会调用adapter.instantiateItem初始化。最后调用adapter.finishUpdate触发状态更新。

4.2 moveToState

这个方法是FragmentManager更新Fragment状态的地方,addView的调用也在这个地方。

可以看到,第一次创建Fragment后的状态就是Fragment.CREATED,这里的container在我们的例子里指的就是ViewPager,container.addView就把当前item的view添加到ViewPager里。

5. 总结

       ViewPager是一个很强大也很常用的View,其源码有3000多行,本篇只对核心的4个方面进行分析,如果需要对ViewPager进行源码修改来自定义某些行为,可以优先考虑从这4个方面去修改。

期待后续!

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1. onMeasure
    • 1.1 给自己宽高赋值
      • 1.2 对Decor View处理
        • 1.3 对item view处理
        • 2. onLayout
          • 2.1 对Decor View处理
            • 2.2 对item view处理
            • 3. 触摸事件处理
              • 3.1 dispatchTouchEvent
                • 3.2 onInterceptTouchEvent
                  • 3.3 onTouchEvent
                  • 4. addView
                    • 4.1 populate
                      • 4.2 moveToState
                      • 5. 总结
                      领券
                      问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档