作者: ztelur
联系方式:segmentfault,csdn,github
本文仅供个人学习,不用于任何形式商业目的,转载请注明原作者、文章来源,链接,版权归原文作者所有。
本文是Android滚动相关的系列文章的第二篇,主要总结一下使用手势相关的代码逻辑。主要是单点拖动,多点拖动,fling和OveScroll的实现。每个手势都会有代码片段。
对android滚动相关的知识还不太了解的同学可以先阅读一下文章:
为了节约你的时间,我特地将文章大致内容总结如下:
- 手势Drag的实现和原理
- 手势Fling的实现和原理
- OverScroll效果和EdgeEffect效果的实现和原理。
详细代码请查看我的github
Drag
Drag是最为基本的手势:用户可以使用手指在屏幕上滑动,以拖动屏幕相应内容移动。实现Drag手势其实很简单,步骤如下:
- 在
ACTION_DOWN
事件发生时,调用getX
和getY
函数获得事件发生的x,y坐标值,并记录在mLastX
和mLastY
变量中。 - 在
ACTION_MOVE
事件发生时,调用getX
和getY
函数获得事件发生的x,y坐标值,将其与mLastX
和mLastY
比较,如果二者差值大于一定限制(ScaledTouchSlop),就执行scrollBy
函数,进行滚动,最后更新mLastX
和mLastY
的值。 - 在
ACTION_UP
和ACTION_CANCEL
事件发生时,清空mLastX
,mLastY
。
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
多触点Drag
上边的代码只适用于单点触控的手势,如果你是两个手指触摸屏幕,那么它只会根据你第一个手指滑动的情况来进行屏幕滚动。更为致命的是,当你先松开第一个手指时,由于我们少监听了ACTION_POINTER_UP
事件,将会导致屏幕突然滚动一大段距离,因为第二个手指移动事件的x,y值会和第一个手指移动时留下的mLastX
和mLastY
比较,导致屏幕滚动。
如果我们要监听并处理多触点的事件,我们还需要对ACTION_POINTER_DOWN
和ACTION_POINTER_UP
事件进行监听,并且在ACTION_MOVE
事件时,要记录所有触摸点事件发生的x,y值。
- 当
ACTION_POINTER_DOWN
事件发生时,我们要记录第二触摸点事件发生的x,y值为mSecondaryLastX
和mSecondaryLastY
,和第二触摸点pointer的id为mSecondaryPointerId
- 当
ACTION_MOVE
事件发生时,我们除了根据第一触摸点pointer的x,y值进行滚动外,也要更新mSecondayLastX
和mSecondaryLastY
- 当
ACTION_POINTER_UP
事件发生时,我们要先判断是哪个触摸点手指被抬起来啦,如果是第一触摸点,那么我们就将坐标值和pointer的id都更换为第二触摸点的数据;如果是第二触摸点,就只要重置一下数据即可。
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
Fling
当用户手指快速划过屏幕,然后快速立刻屏幕时,系统会判定用户执行了一个Fling手势。视图会快速滚动,并且在手指立刻屏幕之后也会滚动一段时间。Drag表示手指滑动多少距离,界面跟着显示多少距离,而fling是根据你的滑动方向与轻重,还会自动滑动一段距离。Filing手势在android交互设计中应用非常广泛:电子书的滑动翻页、ListView滑动删除item、滑动解锁等。所以如何检测用户的fling手势是非常重要的。
在检测Fling时,你需要检测手指在屏幕上滑动的速度,这是你就需要VelocityTracker
和Scroller
这两个类啦。
- 我们首先使用
VelocityTracker.obtain()
这个方法获得其实例 - 然后每次处理触摸时间时,我们将触摸事件通过
addMovement
方法传递给它 - 最后在处理
ACTION_UP
事件时,我们通过computeCurrentVelocity
方法获得滑动速度; - 我们判断滑动速度是否大于一定数值(MinFlingSpeed),如果大于,那么我们调用
Scroller
的fling
方法。然后调用invalidate()
函数。 - 我们需要重载
computeScroll
方法,在这个方法内,我们调用Scroller
的computeScrollOffset()
方法啦计算当前的偏移量,然后获得偏移量,并调用scrollTo
函数,最后调用postInvalidate()
函数。 - 除了上述的操作外,我们需要在处理
ACTION_DOWN
事件时,对屏幕当前状态进行判断,如果屏幕现在正在滚动(用户刚进行了Fling手势),我们需要停止屏幕滚动。
具体这一套流程是如何运转的,我会在下一篇文章中详细解释,大家也可以自己查阅代码或者google来搞懂其中的原理。
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
OverScroll
在Android手机上,当我们滚动屏幕内容到达内容边界时,如果再滚动就会有一个发光效果。而且界面会进行滚动一小段距离之后再回复原位,这些效果是如何实现的呢?我们需要使用Scroller
和scrollTo
的升级版OverScroller
和overScrollBy
了,还有发光的EdgeEffect
类。
我们先来了解一下相关的API,理解了这些接口参数的含义,你就可以轻松使用这些接口来实现上述的效果啦。
- 1
- 2
- 3
- 4
- 5
- 1
- 2
- 3
- 4
- 5
- int deltaX,int deltaY : 偏移量,也就是当前要滚动的x,y值。
- int scrollX,int scrollY : 当前的mScrollX和mScrollY的值。
- int scrollRangeX,int scrollRangeY: 标示可以滚动的最大的x,y值,也就是你视图真实的长和宽。也就是说,你的视图可视大小可能是100,100,但是视图中的内容的大小为200,200,所以,上述两个值就为200,200
- int maxOverScrollX,int maxOverScrollY:允许超过滚动范围的最大值,x方向的滚动范围就是0~maxOverScrollX,y方向的滚动范围就是0~maxOverScrollY。
- boolean isTouchEvent:是否在
onTouchEvent
中调用的这个函数。所以,当你在computeScroll
中调用这个函数时,就可以传入false。
- 1
- 1
- int scrollX,int scrollY:就是x,y方向的滚动距离,就相当于
mScrollX
和mScrollY
。你既可以直接把二者赋值给相应的成员变量,也可以使用scrollTo
函数。 - boolean clampedX,boolean clampY:表示是否到达超出滚动范围的最大值。如果为true,就需要调用
OverScroll
的springBack
函数来让视图回复原来位置。
- 1
- 1
- int startX,int startY:标示当前的滚动值,也就是
mScrollX
和mScrollY
的值。 - int minX,int maxX:标示x方向的合理滚动值
- int minY,int maxY:标示y方向的合理滚动值。
相信看完上述的API之后,大家会有很多的疑惑,所以这里我来举个例子。
假设视图大小为100*100。当你一直下拉到视图上边缘,然后在下拉,这时,mScrollY
已经达到或者超过正常的滚动范围的最小值了,也就是0,但是你的maxOverScrollY传入的是10,所以,mScrollY
最小可以到达-10,最大可以为110。所以,你可以继续下拉。等到mScrollY
到达或者超过-10时,clampedY就为true,标示视图已经达到可以OverScroll的边界,需要回滚到正常滚动范围,所以你调用springBack(0,0,0,100)。
然后我们再来看一下发光效果是如何实现的。
使用EdgeEffect
类。一般来说,当你只上下滚动时,你只需要两个EdgeEffect
实例,分别代表上边界和下边界的发光效果。你需要在下面两个情景下改变EdgeEffect
的状态,然后在draw()
方法中绘制EdgeEffect
- 处理
ACTION_MOVE
时,如果发现y方向的滚动值超过了正常范围的最小值时,你需要调用上边界实例的onPull
方法。如果是超过最大值,那么就是调用下边界的onPull
方法。 - 在
computeScroll
函数中,也就是说Fling手势执行过程中,如果发现y方向的滚动值超过正常范围时的最小值时,调用onAbsorb
函数。
然后就是重载draw
方法,让EdgeEffect
实例在画布上绘制自己。你会发现,你必须对画布进行移动或者旋转来让EdgeEffect
绘制出上边界或者下边界的发光的效果,因为EdgeEffect
对象自己是没有上下左右的概念的。
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
- 75
- 76
- 77
- 78
- 79
- 80
- 81
- 82
- 83
- 84
- 85
- 86
- 87
- 88
- 89
- 90
- 91
- 92
- 93
- 94
- 95
- 96
- 97
- 98
- 99
- 100
- 101
- 102
- 103
- 104
- 105
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
- 75
- 76
- 77
- 78
- 79
- 80
- 81
- 82
- 83
- 84
- 85
- 86
- 87
- 88
- 89
- 90
- 91
- 92
- 93
- 94
- 95
- 96
- 97
- 98
- 99
- 100
- 101
- 102
- 103
- 104
- 105
后记
本篇文章是系列文章的第二篇,大家可能已经知道如何实现各类手势,但是对其中的机制和原理还不是很了解,之后的第三篇会讲解从本篇代码的视角讲解一下android视图绘制的原理和Scroller的机制,希望大家多多关注。
文章转自:http://blog.csdn.net/u012422440/article/details/51090459
====================================================================================
ListView 的 OverScrollBy 方法 detalY 与 scrollY 参数解析
1. 为什么要分析这两个参数
overScrollBy 翻译过来是“滚动越界后的变化”。这是一个在 View 里定义的方法 ,当 View 已经滚动到顶部或者底部之后,如果继续滑动,这个方法就会响应,方法参数传递了后续滚动的变化量。
但是在使用中发现在使用,当达到顶部后,由手指保持滑动和由惯性导致滑动,两种行为产生了有趣的数值变化,因此对其真实的数据含义进行了分析如下。
2. 基本数据
2.1自定义 MyListView 继承 ListView,覆写方法的如下。
/**
* @param deltaY Change in Y in pixels。在Y方向变化的像素大小
* @param scrollY Current Y scroll value in pixels before applying deltaY。在应用deltaY之前的Y位置
*/
protected boolean overScrollBy(int deltaX, int deltaY,
int scrollX,int scrollY,
int scrollRangeX, int scrollRangeY,
int maxOverScrollX, int maxOverScrollY,
boolean isTouchEvent) {
System.out.println("deltaY="+deltaY+";scrollY="+scrollY+";isTouchEvent="+isTouchEvent);
returnsuper.overScrollBy(deltaX, deltaY, scrollX, scrollY, scrollRangeX,scrollRangeY, maxOverScrollX, maxOverScrollY, isTouchEvent);
}
2.2 列表到达顶部的截图,出现闪烁的波纹即表示达到了列表顶部
3.从 Log 猜测数据的含义
3.1 当到达顶部之后,由惯性导致滑动时的 Log 数据
I/System.out: deltaY=-197; scrollY=0; isTouchEvent=false
I/System.out: deltaY=1; scrollY=-9; isTouchEvent=false
I/System.out: deltaY=1; scrollY=-8; isTouchEvent=false
I/System.out: deltaY=3; scrollY=-7; isTouchEvent=false
I/System.out: deltaY=2; scrollY=-4; isTouchEvent=false
I/System.out: deltaY=2; scrollY=-2; isTouchEvent=false
I/System.out: deltaY=0; scrollY=0; isTouchEvent=false
3.2 当到达顶端后,由手指继续滑动时的 Log 数据
I/System.out: deltaY=-4; scrollY=0; isTouchEvent=true
I/System.out: deltaY=-5; scrollY=0; isTouchEvent=true
I/System.out: deltaY=-5; scrollY=0; isTouchEvent=true
I/System.out: deltaY=-9; scrollY=0; isTouchEvent=true
I/System.out: deltaY=-14; scrollY=0; isTouchEvent=true
I/System.out: deltaY=-8; scrollY=0; isTouchEvent=true
I/System.out: deltaY=-29; scrollY=0; isTouchEvent=true
I/System.out: deltaY=-26; scrollY=0; isTouchEvent=true
I/System.out: deltaY=-27; scrollY=0; isTouchEvent=true
I/System.out: deltaY=-15; scrollY=0; isTouchEvent=true
I/System.out: deltaY=-12; scrollY=0; isTouchEvent=true
I/System.out: deltaY=-6; scrollY=0; isTouchEvent=true
I/System.out: deltaY=-5; scrollY=0; isTouchEvent=true
I/System.out: deltaY=-2; scrollY=0; isTouchEvent=true
I/System.out: deltaY=-1; scrollY=0; isTouchEvent=true
3.4 参数分析
首先,当惯性滑动时打印的 isTouchEvent 为 false,当手指继续滑动时 isTouchEvent 为 true。由此可以方便的区分滚动数据的来源。
其次,当惯性滑动时 scrollY 从 -9 变化为 ,可以理解是到达顶部之后列表弹动时Y轴的滚动变化。由手指滑动时始终为 ,因为此时列表并没有出现滚动。
最后,deltaY 的值根据注释可以知道是两次滚动之间的Y变化量,我们截取一段惯性滚动的 Log 来分析。
I/System.out: deltaY=1; scrollY=-8; isTouchEvent=false
I/System.out: deltaY=3; scrollY=-7; isTouchEvent=false
I/System.out: deltaY=2; scrollY=-4; isTouchEvent=false
第一行和第二行的 scrollY 分别为 -8、-7,而第一行的 deltaY 为 1,确实是他们的差值。
第二行和第三行的 scrollY 分别为 -7、-4,而第二行的 deltaY 为 3,也确实是他们的差值。
也就是说deltaY 是前后两次移动的差值,并且应该是“新的Y位置 - 旧的Y位置”。
3.5 奇怪的底层运算
从惯性滑动的 deltaY 算式,推断由手指拖拽时的 deltaY 值。虽然 Log 里的 scrollY 始终为 ,但是手指是由上向下滑动,可以假设开始时手指在 ,向下移动后达到 100,那么根据算式可以得到 “100 - 0 = 100”,但是从 Log 里可以看到,由手指造成的滚动 scrollY 始终为负值。
也就是他的表达式应该为“旧的Y位置 - 新的Y位置”,这是第一个奇怪的地方,也就是滚动到列表顶部的 deltaY 算法似乎有多个。
第二个不合理在于,细看惯性滑动的 Log
I/System.out: deltaY=1; scrollY=-8; isTouchEvent=false
I/System.out: deltaY=3; scrollY=-7; isTouchEvent=false
I/System.out: deltaY=2; scrollY=-4; isTouchEvent=false
第一行的 scrollY 为 -8,变化了 1 个像素,所以到第二行的时候 scrollY 变成了 -7。但是,这里的含义似乎是说“我根据当前的位置,计算出来一个变化量,然后滚动列表”。正常来说,应该是“我先移动列表,再根据上一次的位置来计算变化量”,从 Log 来看,是违背计算习惯的。
所以,研究这里的两个参数的运算方式也就比较有意思了。
4.overSceollBy 方法被谁调用,传递了什么值
既然需要研究系统代码执行,那就只能是使用 Debug 调试来查看运行过程了,首先要确定到哪里打断点。
在文章开始说过,overScrollBy 是一个在 View 里定义的方法 ,但是 View 并没有处理具体的调用逻辑。真正调用 overSrollBy 方法的是那些可以滚动的控件,比如AbsListView、HorizontalScrollView、ScrollView。
经过查看,在 ListView 里没有调用 overScrollBy 方法,而在 AbsListView 里由 4 处调用,全部打上断点,运行等待执行。
4.1当手指下拉时执行的代码
private void scrollIfNeeded(int x, int y, MotionEvent vtev) {
int rawDeltaY = y - mMotionY;
// .......
final int deltaY = rawDeltaY;
int incrementalDeltaY =
mLastY != Integer.MIN_VALUE ? y - mLastY + scrollConsumedCorrection : deltaY;
// .....
int overscroll = -incrementalDeltaY -
(motionViewRealTop - motionViewPrevTop);
// ....
final boolean atOverscrollEdge = overScrollBy(0, overscroll,
0, mScrollY, 0, 0, 0, mOverscrollDistance, true);
//....
4.2当手指下拉时执行的 Debug 截图
4.3当手指下拉时执行的数据分析
可以看到最终传递的 overscroll 变量,在一般情况下,就是等于 incrementalDeltaY 的值,而incrementalDeltaY 最终数据是来自下面的代码
int incrementalDeltaY =
mLastY != Integer.MIN_VALUE ? y - mLastY + scrollConsumedCorrection : deltaY;
先看简单的变量 deltaY,它是来自 rawDeltaY,在 scrollIfNeeded 方法第一句可以看到,
int rawDeltaY = y - mMotionY;
这里的 scrollIfNeeded 方法最终是在 onTouchEvent 方法的 ACTION_MOVE 事件被调用,y 就是事件发生时的手指位置。而 mMotionY 的值根据查看文档说明,
/**
* The Y value associated with the the down motion event
* 在down事件时的手指Y位置
*/
int mMotionY;
也就是手指按下时的位置。那么 rawDeltaY 也就是 "当前手指位置 - 手指按下的位置"。
但是这只是 mLastY != Integer.MIN_VALUE 不成立时的值,这个条件什么时候会成立呢,经过查找,mLastY 是在 onTouchEvent 的 ACTION_DOWN 事件时被赋值为 Integer.MIN_VALUE。
也就是只有在手指按下的时候, incrementalDeltaY 使用 rawDetalY 的值,其他时候都是使用 y - mLastY + scrollConsumedCorrection,经过调试, scrollConsumedCorrection 一般都是保持在 ,而mLastY 记录的就是上一次手指的位置,那么的表达式就是"当前手指位置 - 手指按下的位置",计算出来的 incrementalDeltaY 在赋值给 overscroll 变量前,又被转换为了 -incrementalDeltaY ,那么表达式最终就是 "手指按下的位置 - 当前手指位置"。
转换一下语义的话就是我们之前猜测的"旧的Y位置 - 新的Y位置",符合预期。
4.4当惯性滑动到顶部时的代码
privateclass FlingRunnable implements Runnable {
// .....
@Override
public void run() {
switch (mTouchMode) {
//....
case TOUCH_MODE_OVERFLING: {
final OverScroller scroller = mScroller;
if (scroller.computeScrollOffset()) {
final int scrollY = mScrollY;
final int currY = scroller.getCurrY();
final int deltaY = currY - scrollY;
if (overScrollBy(0, deltaY, 0, scrollY, 0, 0,
0, mOverflingDistance, false)) {
// ....
}
}
}
}
4.5 当惯性滑动到顶部时的 Debug 截图
4.6当惯性滑动到顶部时的数据分析
可以看到,由手指滑动和惯性滑动确实是执行了不同的代码块。
这里的代码是在屏幕自动滑动时执行,而 deltaY 是来自:
final int deltaY = currY - scrollY;
其中,currY 是根据波纹动画时间计算的下一次移动位置;scrollY 是来自 View 类,在记录了上一次滚动后的位置。
由此证实了之前猜测的算法,当惯性滑动时,deltaY 的值是 “新的Y位置 - 旧的Y位置”。
打印出下面的 Log 也可以理解了,
I/System.out: deltaY=1; scrollY=-8; isTouchEvent=false
I/System.out: deltaY=3; scrollY=-7; isTouchEvent=false
I/System.out: deltaY=2; scrollY=-4; isTouchEvent=false
这里是先有了下一步的移动位置,然后才计算出来 deltaY。只是新的位置还没有真的使用到 View上,所以返回的是上一次的位置。
5.总结
• deltaY 的值确实是根据不同的操作,使用了不同的算法。当手势拖拽的时候是使用 "旧的Y位置 - 新的Y位置",当惯性滑动的时候是使用"新的Y位置 - 旧的Y位置"
• scrollY 的值是上一次移动的值,deltaY 是''列表移动之后"根据新位置计算出来的偏移量。之所以打印时先出现 deltaY,后出现移动的新位置,是因为新位置还没有应用到列表上。
文章转自:http://weibo.com/ttarticle/p/show?id=2309404041662291690476