前言
本篇文章主要介绍Scroller的用法,并配合上一篇文章《自定义View之scroll系列方法》中介绍的View.scrollTo()
和scrollBy()
两个方法以及View.computeScroll()
方法,实现平滑滑动效果。
并通过Scroller及view事件分发机制源码,为大家具体讲解其中的奥妙。
Scroller典型使用方式
Scroller 的典型代码是固定的,主要有如下3步:
- 创建Scroller的实例
- 调用startScroll()或fling()方法来初始化滚动数据并刷新界面
- 重写computeScroll()方法,并在其内部完成平滑滚动的逻辑
代码实现如下:
1 | //第一步,创建Scroller的实例,一般在控件初始化的时候进行 |
源码解析
Scroller
通过上面的示例,可以归纳出scroller工作的整体流程:
既然有了这么明确的流程图,那我们下面就来依据这个流程简单分析下Scroller的源码。可以发现Scroller这类的代码不多,确实是一个工具类,它只是保存了我们传递的几个参数,我们先看下构造方法:
1 | /** |
scroller总共有3个构造方法,其实前两个都调用了第三个构造方法。
主要是初始化了一些参数,其中interpolator
为插值器,用于处理滑动过程中各时间段的快慢过程。
其中computeDeceleration()
方法通过传入的摩擦系数,计算减速度(用于fling()方法中计算当前速度)。
1 | private float computeDeceleration(float friction) { |
下面我们看看与Scroller相关的startScroll()和fling()方法,源码如下:
1 | private static final int DEFAULT_DURATION = 250; |
注:这里的滑动指的是View内容的滑动而非View本身位置的改变
可以看到,scroller中的方法只是初始化了一些数据,没有做滑动相关的事。所以说这些只是工具方法而已,实质的滑动其实是需要我们在startScroll或fling后面手动调运View.invalidate()
或View.postInvalidate()
进行重绘,然后在View的draw方法中又会去调运自己的computeScroll()方法,computeScroll()方法View中是一个空实现,需要我们自己去实现,上面的代码已经实现了computeScroll()方法,我们在该方法中进行Scroller.computeScrollOffset()
判断并触发View的滑动方法和重绘。
我们再看一下Scroller的computeScrollOffset方法的实现,如下所示。
1 | //判断滚动是否还在继续,true继续,false结束 |
可以看见该方法的作用其实就是实时计算滚动的偏移量(也是一个工具方法),同时判断滚动是否结束(true代表没结束,false代表结束)。
仿ViewPager示例
既然已经将Scroller的主要方法讲解完毕了,那就小试牛刀。
代码
ScrollerLayout.java
1 | /** |
1 | public class ScrollerActivity extends AppCompatActivity { |
布局文件
activity_scroller.xml
1 | <?xml version="1.0" encoding="utf-8"?> |
重绘流程
在介绍Scroller源码过程中说到了“手动调运View.invalidate()
或View.postInvalidate()
进行重绘,然后在View的draw方法中又会去调运自己的computeScroll()方法”。
现在就从源码的角度说明一下调用View.invalidate()
后到computeScroll()的流程。
View的刷新会调用View.draw()
方法,其实在调用该方法前还有几个步骤,这里就不提了,想要了解的可以查看这篇文章《Android应用层View绘制流程与源码分析》,小编关于View绘制流程的学习也是从这里看起的。
1 | public void draw(Canvas canvas) { |
View 绘制流程
1.绘制背景
2.2和5,如果有需要,绘制渐隐(fading) 效果
3.绘制内容
4.绘制 children
6.绘制装饰物 (scrollbars)
这里看第四步dispatchDraw(),ViewGroup用于绘制子view,在View中是个空方法。
1 | protected void dispatchDraw(Canvas canvas) { |
查看ViewGroup.dispatchDraw()
方法。
1 | protected void dispatchDraw(Canvas canvas) { |
1 | protected boolean drawChild(Canvas canvas, View child, long drawingTime) { |
可以看到ViewGroup.dispatchDraw()
内调用了ViewGroup.drawChild()
。
这里引出了View.draw(Canvas canvas, ViewGroup parent, long drawingTime)
方法,这个方法不同于 View.draw(Canvas canvas)
。
1 | /** |
其中drawingWithRenderNode用于判断是否开启硬件加速。
1 | int sx = 0; |
代码运行中,先会调用 computeScroll()
方法,然后将 mScrollX 和 mScrollY 赋值给变量 sx 和 sy 变量。
终于找到了第一节中重写的computeScroll()
方法。
1 | canvas.translate(mLeft - sx, mTop - sy); |
这里解释了View.scrollTo()
、View.scrollBy()
方法为什么当参数为正数时,内容是向左移动。
回过头去看《自定义View之scroll系列方法》中关于参数正负性和内容移动方向的对应关系,是否就更加清晰了呢。
1 | if ((mPrivateFlags & PFLAG_SKIP_DRAW) == PFLAG_SKIP_DRAW) { |
之后在这里进行绘制子view。