Android 开发中经常涉及 View 的滚动,例如类似于 ScrollView 的滚动手势和滚动动画,例如用 ListView 模仿 iOS 上的左滑删除 item,例如 ListView 的下拉刷新。这些都是常见的需求,同时也都涉及 View 滚动的相关知识。
本文将解析 Android 中 View 的滚动原理,并介绍与滚动相关的两个辅助类 Scroller
和 VelocityTracker
,并通过 3 个逐渐深入的例子来加深理解。
注:
- 本文没有尝试实现上述几种功能,只阐述基本原理和基础类的使用方法。
- 文中的例子只是截取了与 View 相关的代码,完整的示例代码请见DEMO
- 本文的源码分析基于 Android API Level 21,并省略掉部分与本文关系不大的代码。
View 的滚动原理
在了解 View 的滚动原理之前,我们先来想象一个场景:我们坐在一个房间里,透过一扇窗户看窗外的风景。窗户是有大小限制的,而风景是没有大小限制的。
把上述的场景对应到 Android 的 View 显示原理上来:当一个 View 显示在界面上,它的上下左右边缘就围成了这个 View 的可视区域,我们可以称这个区域为“可视窗口”,我们平时看到的 View 的内容,都是透过这个可视窗口中看到的“风景”。View 的大小内容可以无穷大,不受可视窗口大小的限制。
另外,如果在窗外的风景中,有一个人出现在窗户右边很远的地方,那么我们在房间里就看不到那个人;如果那个人站在窗户正对着出去的地方,那么我们就可以透过窗户看到他。对应到 View 上面来,只有出现在“可视窗口”中的那部分内容可以被看到。
View 的 scroll 相关
在 View 类中,有两个变量 mScrollX
和 mScrollY
,它们记录的是 View 的内容的偏移值。mScrollX
和 mScrollY
的默认值都是 0,即默认不偏移。另外我们需要知道一点,向左滑动,mScrollX
为正数,反正为负数。假设我们令 mScrollX = 10
,那么该 View 的内容会相对于原来向左偏移 10px。 看看系统的 View 类中的源码:
|
|
通常我们比较少直接设置 mScrollX
和 mScrollY
,而是通过 View 提供的两个方法来设置。
|
|
|
|
看看两个方法的源码:
|
|
首先看 scrollTo(int x, int y)
方法,它除了设置 mScrollX
和 mScrollY
两个变量,还会触发自己重新绘制,另外还会通过 onScrollChanged
触发回调。而 scrollBy
方法其实也是调用 scrollTo
方法。
明显,两个方法的区别在于 scrollTo
方法是滚动到特定位置,参数 x
、y
代表“绝对位置”,而 scrollBy
方法是在当前位置基础上滚动特定距离,参数 x
、y
代表“相对位置”。
另外,View 还提供了 mScrollX
和 mScrollY
的 getter:
|
|
|
|
看看源码中这两个方法的注释,可以更好地理解 scroll 的概念。
|
|
例子1
为了更好地理解 mScrollX
和 mScrollY
,也为后续介绍的知识做准备,我们先看一个例子:
|
|
将以上这个自定义的 ViewGroup 放到 Activity 中,调用它的 moveToIndex(int targetIndex)
就可以实现瞬时滚动到第 n 个子 View 了。(完整示例代码见DEMO)
Scroller 类 —— 计算滚动位置的辅助类
到目前为止,我们已经能通过 View 提供的方法设置 mScrollX
、mScrollY
,来使 View “滚动”。但这种滚动都是瞬时的,换句话说,这种滚动都是无动画的。实际上我们想要做到的滚动是平滑的、有动画的,就像我们不希望窗户外面的那个人突然出现在窗户中间,这样会吓到我们,我们更希望那个人能有一个“慢慢走进视觉范围”的过程。
Scroller 类就是帮助我们实现 View 平滑滚动的一个辅助类,使用方法通常是在 View 中作为一个成员变量,用 Scroller 类来记录/计算 View 的滚动位置,再从 Scroller 类中读取出计算结果,设置到 View 中。这里注意一点:在 Scroller 中设置和计算 View 的滚动位置并不会影响 View 的滚动,只有从 Scroller 中取出计算结果并设置到 View 中时,滚动才会实际生效。
Scroller 提供了一系列方法来执行滚动、计算滚动位置,以下列出几个重要方法:
|
|
|
|
|
|
|
|
|
|
|
|
有了这几个方法,我们容易想到如何实现 View 的平滑滚动动画:
- 在开始动画时调用
startScroll
方法,传入动画开始位置、移动距离、动画时长; - 每隔一段时间,调用
computeScrollOffset
方法,计算当前时间点对应的滚动位置; - 如果上一步返回 true,代表动画仍在进行,则调用
getCurrX
和getCurrY
方法获取当前位置,并调用 View 的scrollTo
方法使 View 滚动; - 不断循环进行第 2 步,直到返回 false,代表动画结束。
这里提到“每隔一段时间”,从直觉上我们可能觉得应该有个循环,但实际上我们可以借助 View 的 computeScroll
方法来实现。先看看 computeScroll
方法的源码:
|
|
看注释可知该方法天生就是用来计算 View 的 mScrollX
和 mScrollY
值,该方法会在父 View 调用该 View 的 draw 方法之前被自动调用,View 类中默认没有实现任何内容,我们需要自己实现。所以我们只需要在该方法中,用 Scroller 计算并设置 mScrollX
和 mScrollY
的值,并判断如果动画没结束则让该 View 失效(调用 postInvalidate()
方法),触发下一次 computeScroll
,就可以实现上述循环。
例子2
这个例子的 ViewGroup 继承自例子 1 的 ViewGroup,拥有同样的子 View,区别只在于例子 2 是通过 Scroller 来滚动,实现了滚动的动画,而不再是瞬时滚动。
|
|
将以上这个自定义的 ScrollerViewGroup 放到 Activity 中,调用它的 moveToIndex(int targetIndex)
就可以实现滚动到第 n 个子 View 了。(在 Activity 中使用的完整示例代码见DEMO)
VelocityTracker —— 计算滚动速度的辅助类
到目前为止,我们已经可以实现 View 平滑的滚动动画,那么如果我们还想根据用户手指在 View 上滑动的速度和距离来控制 View 的滚动,应该怎么做?Android 系统提供了另一个辅助类 VelocityTracker 来实现类似功能。
VelocityTracker 是一个速度跟踪器,通过用户操作时(通常在 View 的 onTouchEvent 方法中)传进去一系列的 Event,该类就可以计算出用户手指滑动的速度,开发者可以方便地获取这些参数去做其他事情。或者手指滑动超过一定速度并松手,就触发翻页。
看看 VelocityTracker 类提供的几个常用的方法,这些方法分为几类:
初始化和销毁:
12// 由系统分配一个 VelocityTracker 对象,而不是 new 一个static public VelocityTracker obtain()12- // 使用完毕时调用该方法回收 VelocityTracker 对象public void recycle()添加 Event 以供追踪:
12// 不断调用该方法传入一系列 event,记录用户的操作public void addMovement(MotionEvent event)计算速度:
12// 计算调用该方法的时刻对应的速度,传入的是速度的计时单位public void computeCurrentVelocity(int units)12// 调用 computeCurrentVelocity 方法后就可以通过该方法获取之前计算的 x 方向速度public float getXVelocity()12// 调用 computeCurrentVelocity 方法后就可以通过该方法获取之前计算的 y 方向速度public float getYVelocity()
例子3
下面通过一个例子来看看 VelocityTracker 的用法。该例子的 ViewGroup 继承自例子 2 的 ViewGroup,拥有同样的子 View,区别在于除了可以用动画来滚动,还可以用手势来拖动滚动。重点看该 ViewGroup 的 onTouchEvent 方法:
|
|
在该例子中,在 View 的 onTouchEvent
方法中,在 ACTION_MOVE
手指移动中不断调用 scrollTo
方法,实现 View 跟随手指移动;同时,将 Event 不断地添加到 mVelocityTracker
速度监控器中,并在 ACTION_UP
手指抬起时从速度监控器中获取速度,当速度达到某一阈值时自动滚动到上一页或下一页。
总结
至此,我们已经了解了 View 的滚动原理,并两个辅助类来帮助控制 View 的滚动位置和滚动速度。总结一下:
- View 的显示可以理解为透过“视觉窗口”来看内容,内容可以无限大,改变 View 的
mScrollX
和mScrollY
可以看到不同的内容,实现瞬时滚动。 - 调用 View 的
scrollTo
或scrollBy
方法可以瞬时滚动 View。 - Scroller 辅助类可以协助实现 View 的滚动动画,实现方法是:调用
startScroll
方法开始滚动,并在 View 的computeScroll
方法中不断改变mScrollX
和mScrollY
来滚动 View。 - VelocityTracker 辅助类可以协助追踪 View 的滚动速度,通常是在 View 的
onTouchEvent
方法中将 Event 传进该类中来追踪。调用该类的computeCurrentVelocity
方法之后,就可以调用getXVelocity
和getYVelocity
方法分别获取 x 方向和 y 方向的速度。
有了上述的知识和工具后,我们就能实现很多与滚动相关的效果,例如本文开头提到的几个场景,后续再写些 DEMO 作为分享。
以上,感谢阅读。