Android 开发艺术探索笔记-3.View的事件体系
cfanr Lv4

3.1 View基础知识

1.View是所以控件的基类,ViewGroup也属于View

2.View的位置参数
由top、left、right、bottom,左上角和右下角的横纵坐标决定(相对View的父容器)。
Android3.0开始增加了几个参数:左上角坐标x和y、左上角相对父容器的偏移量translationX和translationY(默认值为0)
x = left + translationX
y = top + translationY
平移时,top、left不会发生改变,始终为原始坐标

3.MotionEvent和TouchSlop
MotionEvent事件类型:
ACTION_DOWN——手指刚接触屏幕
ACTION_MOVE——手指在屏幕上移动
ACTION_UP——手从屏幕上松开的一瞬间
通过MotionEvent可以获取x、y坐标,有两种方法:a.getX和getY,返回相对于当前View左上角x、y坐标;b.getRaw和getRawY,返回相对于手机屏幕左上角的x、y坐标

TouchSlop是系统所能识别出被认为是滑动的最小距离。可以通过如下方式获取该值:ViewConfiguration.get(getContext()).getScaledTouchSlop()

4.VelocityTracker
用于追踪手指在滑动过程中的速度,包括水平和垂直方向的速度。在View的onTouchEvent方法中追踪单击事件速度方法:

1
2
3
4
VelocityTracker mVelocityTracker = VelocityTracker.obtain();  //初始化
mVelocityTracker.addMovement(event);
mVelocityTracker.computeCurrentVelocity(1000);
float xVelocity = mVelocityTracker.getXVelocity(); //获取速度

5.GestureDetector
手势检测,用于辅助检测用户的单击、滑动、长按、双击等行为。作者建议:如果只是监听滑动相关的事件在onTouchEvent中实现;如果要监听双击这种行为的话,那么就使用GestureDetector。

6.Scroller
弹性滑动对象,用于实现View的弹性滑动。当使用View的scrollTo/scrollBy方法滑动时,是瞬间完成的,没有过渡效果,滑动体验不好,所以一般会用Scroller来实现由过渡的滑动。
Scroller本身不能让View弹性滑动,它需要和View的computeScroll方法配合使用才能共同完成这个功能

3.2 View的滑动

有三种方法可以实现View的滑动:a.通过View本身的scrollTo/scrollBy方法;b.通过动画给View施加平移来实现滑动;c.改变View的LayoutParams使View重新布局实现滑动。

1.scrollTo/scrollBy
只能改变view内容的位置而不能改变view在布局中的位置。scrollBy是基于当前位置的相对滑动,而scrollTo是基于所传参数的绝对滑动。可以通过View的getScrollX和getScrollY方法可以得到滑动的距离。

2.使用动画
主要是操作View的translationX和translationY属性,可以使用传统动画和属性动画(兼容3.0以下系统时,需要用到开源库nineoldandroids)。不过注意:3.0以下,view动画和属性动画,新位置均无法触发点击事件,同时,老位置仍然可以触发单击事件。从3.0之后,属性动画的单击事件触发位置为移动后的位置,view动画仍然在原位置。

3.改变布局参数
即改变LayoutParams,如使Button右移100px,可以设置它的marginLeft增加100px或在它左边设置一个空view,动态控制它的宽度变化(两者父容器为LinearLayout):

1
2
3
4
5
MarginLayoutParams param = (MarginLayoutParams) button.getLayoutParams();
param.width += 100;
param.leftMargin += 100;
button.requestLayout();
//或button.setLayoutParams(param)

总结:
scrollTo/scrollBy:操作简单,适合对View内容的滑动;
动画:操作简单,主要适用于没有交互的View和实现复杂的动画效果;
改变布局参数:操作稍微复杂,适用于有交互的View

3.3 弹性滑动

1.使用Scroller
工作原理:Scroller本身并不能实现View的滑动,它需要配合View的computeScroll方法才能完成弹性滑动的效果,它不断让View重绘,而每一次重绘距滑动起始时间会有一个时间间隔,通过这个时间间隔Scroller就可以得出View当前的滑动位置,然后可以通过scrollTo方法完成View的滑动。View的每一次重绘导致View进行小幅度的滑动,多次小幅度的滑动组成弹性滑动。

2.通过动画
利用动画在onAnimationUpdate方法获取fraction的动态值设置scrollTo实现弹性

3.使用延时策略
通过发送一系列延时消息从而达到渐进式变化。可以使用Handler的sendEmptyMessageDelayed(xxx)或view的postDelayed方法,也可以使用线程的sleep方法。

3.4 View的事件分发机制

参考:Android-三张图搞定Touch事件传递机制

View不处理事件流程图(View没有消费事件)
View不处理事件

View处理事件
View处理事件

事件拦截
事件拦截

3.5 View的滑动冲突

1.常见的滑动冲突
a.外部滑动方向和内部不一致
b.外部滑动方向和内部一致
c.上面两种的嵌套

2.冲突处理规则
可以依据滑动路径和水平方向所形成的夹角;可以依据水平方向和竖直方向的距离差,或依据水平和竖直方向的速度差做判断

3.滑动冲突解决方式
a.外部拦截法,即点击事件都先经过父容器的拦截处理,如果父容器需要此事件就拦截,如果不需要就不拦截。该方法需要重写父容器的onInterceptTouchEvent方法,在内部做相应的拦截即可,其他均不需要做修改。伪代码如下:

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
public boolean onInterceptTouchEvent(MotionEvent event) {
boolean intercepted = false;
int x = (int) event.getX();
int y = (int) event.getY();

switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
intercepted = false;
break;
case MotionEvent.ACTION_MOVE:
int deltaX = x - mLastXIntercept;
int deltaY = y - mLastYIntercept;
if (父容器需要拦截当前点击事件的条件,例如:Math.abs(deltaX) > Math.abs(deltaY)) {
intercepted = true;
} else {
intercepted = false;
}
break;
case MotionEvent.ACTION_UP:
intercepted = false;
break;
default:
break;
}

mLastXIntercept = x;
mLastYIntercept = y;

return intercepted;
}

书中示例代码

b.内部拦截法,即父容器不拦截任何事件,所有的事件都传递给子元素,如果子元素需要此事件就直接消耗掉,否则就交给父容器来处理。这种方法和Android中的事件分发机制不一致,需要配合requestDisallowInterceptTouchEvent方法才能正常工作。伪代码如下,需要重写子元素的dispatchTouchEvent方法:

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
public boolean dispatchTouchEvent(MotionEvent event) {
int x = (int) event.getX();
int y = (int) event.getY();

switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
getParent().requestDisallowInterceptTouchEvent(true);
break;
case MotionEvent.ACTION_MOVE:
int deltaX = x - mLastX;
int deltaY = y - mLastY;
if (当前view需要拦截当前点击事件的条件,例如:Math.abs(deltaX) > Math.abs(deltaY)) {
getParent().requestDisallowInterceptTouchEvent(false);
}
break;
case MotionEvent.ACTION_UP:
break;
default:
break;
}

mLastX = x;
mLastY = y;
return super.dispatchTouchEvent(event);
}

书中示例代码

附:Android 编程下 Touch 事件的分发和消费机制

The end.