View 的事件体系

View 基础知识

ViewGroup

ViewGroup 可被翻译为控件组,可理解为 View 的组合,它可以包含很多 View 以及 ViewGroup,它也继承自 View,比如 LinearLayout、RelativeLayout 等控件都是继承自 ViewGroup。

当手指触摸屏幕上时,手指所在的区域既能在 ViewGroup 显示范围内,也可能在其内部 View 控件上。因此它内部的事件分发的重心是处理当前 Group 和子 View 之间的逻辑关系:

  • 当前 Group 是否需要拦截 touch 事件;

  • 是否需要将 touch 事件继续分发给子 View;

  • 如何将 touch 事件分发给子 View。

View

View 是 Android 中所有控件的基类,是一种界面层的控件的一种抽象,它代表了一个控件,比如 TextView、ImageView 等控件都是继承自 View。

View 是一个单纯的控件,不能再被细分,内部也并不会存在子 View,所以它的事件分发的重点在于当前 View 如何去处理 touch 事件,并根据相应的手势逻辑进行一些列的效果展示(比如滑动,放大,点击,长按等)。

  • 是否存在 TouchListener;

  • 是否自己接收处理 touch 事件(主要逻辑在 onTouchEvent 方法中)。

坐标系

Android 系统中有两种坐标系,分别是 Android 坐标系和 View 坐标系。

Android 坐标系

在 Android 中,将屏幕左上角的顶点作为原点,向右方向是 X 轴正方向,向下是 Y 轴正方向。另外在触控事件中,使用 getRawX() 和 getRawY() 方法获得的坐标也是 Android 坐标系的坐标。

VIew 坐标系

View 坐标系与 Android 坐标系共同存在。

View 的位置参数,View 的位置主要由它的四个顶点来决定,分别对应 View 的四个属性:
它们是相对坐标,相对父容器来说,在 Android 中,x 轴和 y 轴的正方向分别为右和下。

  • top:左上角纵坐标,getTop()。
  • left:左上角横坐标,getLeft()。
  • right:右下角横坐标,getRight。
  • bottom:右下角纵坐标,getBottom()。
  • width = right - left(系统也提供了 getWidth() 用来获取 View 自身宽度,源码中 mRight - mLeft )
  • height = bottom - top(系统也提供了 getHeight() 用来获取 View 自身高度,源码中 mBottom - mTop )

注意:View 在平移的过程中,top 和 left 表示的是原始左上角的位置信息,其值并不会发生改变,此时发生改变的是 x、y、translationX 和 translationY 这四个参数:
它们也是相对于父容器的坐标,View 也为它们提供了 get/set 方法。

  • x、y:View 左上角坐标。
  • translationX、translationY:View 左上角相对于父容器的偏移量,默认值是0。
  • y = top + translationY

MotionEvent 和 TouchSlop

MotionEvent

无论是 View 还是 ViewGroup,最终的点击事件都会由 onTouchEvent(MotionEvent event) 方法来处理。MotionEvent 相关于手指接触屏幕后所产生的一系列事件。同时,通过 MotionEvent 对象可以得到点击事件发生的 x 和 y 坐标:

  • getX/getY:返回触摸点相对于当前 View 左上角的坐标(视图坐标)。
  • getRawX/getRawY:返回触摸点相对于手机屏幕左上角的坐标(绝对坐标)。

TouchSlop
它是系统所能识别出的被认为是滑动的最小距离。这是一个常量,和设备有关,不同设备上的值可能不同。

1
2
// 获取这个常量
ViewConfiguration.get(this).getScaledTouchSlop();

VelocityTracker、GestureDetector 和 Scroller

VelocityTracker
速度追踪,用于追踪手指在滑动过程中的速度,包括水平和竖直方向的速度。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 首先,在 View 的 onTouchEvent 方法中追踪当前单击事件的速度。
VelocityTracker velocityTracker = VelocityTracker.obtain();
velocityTracker.addMovement(event);

// 当想知道当前的滑动速度时
// 先计算速度,也就是 1000ms 内手指所滑过的像素数。
velocityTracker.computeCurrentVelocity(1000);
// 速度可为负数,比如从右向左滑,水平方向为负值。
int xVelocity = (int) velocityTracker.getXVelocity();
int yVelocity = (int) velocityTracker.getYVelocity();
// 速度的计算公式:
// 速度 = (终点位置 - 起点位置) / 时间段

// 当不需要时
velocityTracker.clear();
velocityTracker.recycle();

GestureDetector
手势检测,用于辅助检测用户的单击、滑动、长按、双击等行为。

1
2
3
4
5
6
7
8
9
10
11
12
// 创建 GestureDetector 对象并实现 OnGestureListener 或 OnDoubleTapListener 接口
GestureDetector gestureDetector = new GestureDetector(this);
// 解决长按屏幕后无法拖动的现象
gestureDetector.setIsLongpressEnabled(false);

// 接管目标 View 的 onTouchEvent 方法
boolean consume = gestureDetector.onTouchEvent(event);
return consume;

// 实际开发中,可自己实现所需的监听
// 建议当监听滑动相关时,自己实现。当监听双击这种行为,使用 GestureDetector。

Scroller
弹性滑动对象,用于实现 View 的弹性滑动。

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
@SuppressLint("AppCompatCustomView")
public class MyView extends TextView{

private Scroller mScroller ;

public MyView(Context context) {
super(context);
init();
}

/**
* XML 布局
*/
public MyView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
init();
}

/**
* 便于测试看效果
*/
private Handler handler = new Handler(){
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
// 使用 Scroller 来实现弹性滑动
// 向"右下"移动
smoothScrollTo(-100,-100);

// 这两个方法是瞬间完成的
// scrollTo(x,y); 移动到 (x,y) 这个坐标点
// scrollBy(dx,dy); 移动的增量为 dx,dy。

// * 注意:它们移动的是当前 View 的内容
// * ((ViewGroup)getParent()).scrollTo(-100,-100);
}
};

private void init(){
mScroller = new Scroller(getContext());
handler.sendEmptyMessageDelayed(1,1500);
}

private void smoothScrollTo(int destX,int destY){
int scrollX = getScrollX();
int delta = destX - scrollX;
int scrollY = getScrollY();
int delta1 = destY - scrollY;
// 1000ms 内滑向 destX
mScroller.startScroll(scrollX,scrollY,delta,delta1,1000);
invalidate();
}

/**
* Scroller 本身无法让 View 弹性滑动,
* 需要和这个方法配合使用才能共同完成。
*/
@Override
public void computeScroll() {
if (mScroller!=null)
if (mScroller.computeScrollOffset()){
scrollTo(mScroller.getCurrX(),mScroller.getCurrY());
postInvalidate();
}
}
}

View 的滑动

基本思想:当点击事件传到 View 时,系统记下触摸点的坐标,手指移动时系统记下移动后触摸的坐标并算出偏移量,并通过偏移量来修改 View 的坐标。实现 View 滑动有很多种方法,比如:layout()、offsetLeftAndRight()、offsetTopAndBottom()、LayoutParams、动画、scrollTo 与 scrollBy,以及 Scroller。

layout() 方法

View 进行绘制时会调用 onLayout 方法来设置显示的位置,因此我们同样可通过修改 View 的 left、top、right、bottom 这 4 种属性来控制 View 的坐标。

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
// 随手指移动
public class CustomView extends View {

private int lastX;
private int lastY;

public CustomView(Context context) {
super(context);
}

public CustomView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}

@Override
public boolean onTouchEvent(MotionEvent event) {
// 获取手指触摸点的横坐标和纵坐标
int x = (int) event.getX();
int y = (int) event.getY();

switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
lastX = x;
lastY = y;
break;
case MotionEvent.ACTION_MOVE:
// 计算移动的距离
int offsetX = x - lastX;
int offsetY = y - lastY;
// 调用 layout 方法来重新放置它的位置
layout(getLeft() + offsetX, getTop() + offsetY,
getRight() + offsetX, getBottom() + offsetY);
break;
}
return true;
}
}

offsetLeftAndRight() 与 offsetTopAndBottom()

与 layout() 方法的效果和使用方式都差不多,修改 ACTION_MOVE 中代码。

1
2
3
4
5
6
7
8
9
case MotionEvent.ACTION_MOVE:
// 计算移动的距离
int offsetX = x - lastX;
int offsetY = y - lastY;
// 对 left 和 right 进行偏移
offsetLeftAndRight(offsetX);
// 对 top 和 bottom 进行偏移
offsetTopAndBottom(offsetY);
break;

使用 scrollTo/scrollBy

这是 View 本身提供的方法,它们只能改变 View 内容的位置而不是 View 在布局中的位置:

  • scrollTo(x,y):表示移动到一个具体的坐标点。
  • scrollBy(dx,dy):表示移动的增量为 dx 和 dy。(内部最终也是调用了 scrollTo)

View 内部的两个属性,它们的单位是像素,可通过 getScrollX 和 getScrollY 得到:

  • mScrollX:
    它的值总是等于 View 左边缘和 View 内容左边缘在水平方向上的距离
    从左向右滑动,为负值,反之为正值。
  • mScrollY:
    它的值总是等于 View 上边缘和 View 内容上边缘在竖直方向上的距离
    从上向下滑动,为负值,反之为正值。
1
2
3
4
5
6
7
8
9
case MotionEvent.ACTION_MOVE:
// 计算移动的距离
int offsetX = x - lastX;
int offsetY = y - lastY;
// 设置负值的意义在于,因为参考对象不同导致的差异。所有控件都在一张画布上,手机屏幕来显示其内容,并且画布大小可能会超过手机屏幕的大小。
// 假设 scrollBy(60,60),理论上向右下角移动。但实际上,画布位置不动,画布内的控件位置自然也不变,而手机向右下角移动,导致的结果就是,控件的显示位置向左上角偏移了。
// 所以,设置负值才能达到想要的效果。
((View)getParent()).scrollBy(-offsetX,-offsetY);
break;

使用动画

主要是操作 View 的 translationX 和 translationY 属性,既可以采用传统的 View 动画,也可以采用属性动画。

注意:
View 动画是对 View 的影像做操作它并不能真正改变 View 的位置参数,包括宽/高,并且如果希望动画后的状态得以保留还必须将 fillAfter 属性设置为 true,否则动画完成后其动画效果会消失。
而属性动画并不会存在上述问题,但在 Android 3.0 以下无法使用属性动画,需要使用动画兼容库。

改变布局参数

LayoutParams 主要保存了一个 View 的布局参数,因此可通过 LayoutParams 来改变 View 的布局参数从而达到改变 View 位置的效果。

1
2
3
4
5
6
7
8
9
10
case MotionEvent.ACTION_MOVE:
// 计算移动的距离
int offsetX = x - lastX;
int offsetY = y - lastY;
// 根据父控件指定 params
LinearLayout.LayoutParams params = (LinearLayout.LayoutParams) getLayoutParams();
params.leftMargin = getLeft() + offsetX;
params.topMargin = getTop() + offsetY;
setLayoutParams(params);
break;

除了使用布局的 LayoutParams 外,还可以使用 ViewGroup.MarginLayoutParams 来实现。

1
2
3
4
5
6
7
8
9
10
case MotionEvent.ACTION_MOVE:
// 计算移动的距离
int offsetX = x - lastX;
int offsetY = y - lastY;
// 根据父控件指定 params
ViewGroup.MarginLayoutParams params = (ViewGroup.MarginLayoutParams) getLayoutParams();
params.leftMargin = getLeft() + offsetX;
params.topMargin = getTop() + offsetY;
setLayoutParams(params);
break;

各种滑动方式对比

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

使用属性动画,实现一个跟手滑动的效果:

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
@SuppressLint("AppCompatCustomView")
public class MyView extends View{

private int mLastX ;
private int mLastY ;

public MyView(Context context) {
super(context);
}

public MyView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}

@Override
public boolean onTouchEvent(MotionEvent event) {
// 不能使用 getX 和 getY
// 因为要获取当前点击事件在屏幕中的坐标,而不是相对于 View 本身的坐标。
int x = (int) event.getRawX();
int y = (int) event.getRawY();

switch (event.getAction()){
case MotionEvent.ACTION_DOWN:
break;
case MotionEvent.ACTION_MOVE:
// 获取两次滑动之间的位移
int deltaX = x - mLastX;
int deltaY = y - mLastY;
Log.e("TAG","move,deltaX: "+deltaX+" deltaY: "+deltaY);

moveMothod(deltaX,deltaY);
// moveMethod1(deltaX,deltaY);
// startAnimation(deltaX,deltaY);
break;
case MotionEvent.ACTION_UP:
break;
default:
break;
}
mLastX = x;
mLastY = y;
return true;
}

/**
* View 动画
*/
private void moveMothod(int dx,int dy){
setTranslationX(this.getTranslationX()+dx);
setTranslationY(this.getTranslationY()+dy);
}

/**
* 属性动画
*/
private void startAnimation(int dx, int dy) {
ObjectAnimator x = ObjectAnimator.ofFloat(this, "translationX", (getTranslationX()+dx));
ObjectAnimator y = ObjectAnimator.ofFloat(this, "translationY", (getTranslationY()+dy));

AnimatorSet animatorSet = new AnimatorSet();
animatorSet.playTogether(x, y);
animatorSet.setDuration(0);
animatorSet.start();
}

/**
* 改变布局参数
*/
private void moveMethod1(int dx, int dy) {
ViewGroup.MarginLayoutParams marginLayoutParams= (ViewGroup.MarginLayoutParams) getLayoutParams();
marginLayoutParams.leftMargin+=dx;
marginLayoutParams.topMargin+=dy;
setLayoutParams(marginLayoutParams);
}
}

弹性滑动

本质就是将一次大的滑动分成若干次小的滑动,并在一段时间内完成。

使用 Scroller

区别于 scrollTo 和 scrollBy 方法实现滑动时的过程是瞬间完成的,可使用 Scroller 来实现有过渡效果的滑动。

Scroller 本身是不能实现 View 的滑动,它需要与 View 的 computeScroll() 方法配合才能实现弹性滑动的效果。

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
public class CustomView extends View {

private Scroller scroller;

public CustomView(Context context) {
super(context);
init(context);
}

public CustomView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
init(context);
}

private void init(Context context) {
scroller = new Scroller(context);
}

/**
* 重写 computeScroll 方法,系统会在绘制 View 时在 draw 方法中调用该方法。
*/
@Override
public void computeScroll() {
super.computeScroll();
if (scroller.computeScrollOffset()){
// 调用父类的 scrollTo 方法并通过 Scroller 来不断获取当前的滚动值,
// 每滑动一小段距离就调用 invalidate 方法不断地进行重绘,重绘就会调用此 computeScroll 方法。
((View)getParent()).scrollTo(scroller.getCurrX(),scroller.getCurrY());
invalidate();
}
}

/**
* 最后在 Activity 中调用此方法
* mCustomView.smoothScrollTo(-400,0),沿 X 轴向右平移 400 像素。
*/
public void smoothScrollTo(int destX,int destY){
int scrollX = getScrollX();
int delta = destX - scrollX;
// 2000ms 内沿 X 轴平移 delta 像素。
scroller.startScroll(scrollX,0,delta,0,2000);
invalidate();
}
}
  • startScroll:只是保存了传递的几个参数,并没有做滑动相关的事。
  • invalidate:通过它来实现的滑动。
    它会导致 View 重绘,在 View 的 draw 方法中会调用 computeScroll 方法(空实现,要自己实现)。
  • computeScroll:
    它会向 Scroller 获取当前的 scrollX 和 scrollY,并通过 scrollTo 方法实现滑动,接着调用 postInvalidate 方法进行二次重绘。此次重绘和第一次一样,还是会导致 computeScroll 方法被调用,从而反复执行这个流程。
  • computeScrollOffset:它会根据时间的流逝来计算出当前的 scrollX 和scrollY 的值。
    返回值 true,表示滑动还未结束。

注:invalidate 与 postInvalidate 的区别:

  • invalidate 须在 UI 线程调用。
  • postInvalidate 可在工作线程中调用。

通过动画

动画本身就是一种渐进的过程。

使用延时策略

核心思想是通过发送一系列延时消息从而达到一种渐进式的效果。
但采用这种方式无法精确的定时,因为系统的消息调度也是需要时间的,并且时间不定。

View 的事件分发机制

Android touch 事件的分发是 Android 工程师必备技能之一。关于事件分发主要有几个方向可以展开深入分析:

  • touch 事件是如何从驱动层传递给 Framework 层的 InputManagerService;

  • WMS 是如何通过 ViewRootImpl 将事件传递到目标窗口;

  • touch 事件到达 DecorView 后,是如何一步步传递到内部的子 View 中的。

其中与上层软件开发息息相关的就是第 3 条,也是重点关注内容。

点击事件用 MotionEvent 来表示,当一个点击事件产生后,事件最先传递给 Activity 。Activity 包含一个 Window 对象,这个对象是由 PhoneWindow 来实现的。PhoneWindow 将 DecorView 作为整个应用窗口的根 View,而这个 DecorView 又将屏幕划分为两个区域:TitleView 和 ContentView,平常做应用所写的布局正是展示在 ContentView 中的。

点击事件的传递规则

当点击屏幕时便产生了点击事件,这个事件被封装成了一个类:MotionEvent。当 MotionEvent 产生后,系统就会将它传递给 View 的层级,MotionEvent 在 View 中的层级传递过程就是点击事件分发。

事件分发,分析的对象就是 MotionEvent,它的分发过程由三个很重要的方法共同完成:

  • public boolean dispatchTouchEvent(MotionEvent event):用来进行事件的分发。
    如果事件能够传递给当前 View,那么方法一定会被调用,返回结果受当前 View 的 onTouchEvent 和下级 View 的 dispatchTouchEvent 方法的影响,表示是否消耗当前事件。
  • public boolean onInterceptTouchEvent(MotionEvent event):用来进行事件的拦截。
    在上述方法内部调用,用来判断是否拦截某个事件,如果当前 ViewGroup 拦截了某个事件,那么在同一个事件序列中,此方法不会被再次调用,返回结果表示是否拦截当前事件。(需要注意的是 View 没有提供该方法)
  • public boolean onTouchEvent(MotionEvent event):用来处理点击事件
    在 dispatchTouchEvent 方法中调用,返回结果表示是否消耗当前事件,如果不消耗,则在同一个事件序列中,当前 View 无法再次接收到事件。

整个 View 之间的事件分发,实质上就是一个大的递归函数,而这个递归函数就是 dispatchTouchEvent 方法。在这个递归的过程中会适时调用 onInterceptTouchEvent 来拦截事件,或者调用 onTouchEvent 方法来处理事件。

它们的关系可以用如下伪代码表示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* 对于根 ViewGroup
* 点击事件首先传递给它
*/
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
boolean consume = false;
// ViewGroup 的方法
if (onInterceptTouchEvent(event)){
// true,表示要拦截,并调用 onTouchEvent 方法。
consume = onTouchEvent(event);
}else {
// false,表示不拦截当前事件,会继续传递给它的子元素,
// 并调用其 dispatchTouchEvent 方法。
consume = child.dispatchTouchEvent(event);
}
return consume;
}

onInterceptTouchEvent 方法和 onTouchEvent 方法都在 dispatchTouchEvent 方法中调用。接下来根据这段伪代码分析一下点击事件分发的传递规则。

首先点击事件由上而下的传递规则,当点击事件产生后会由 Activity 来处理,传递给 PhoneWindow,再传递给 DecorView,最后传递给顶层的 ViewGroup。一般在事件传递中只考虑 ViewGroup 的 onInterceptTouchEvent 方法,因为一般情况下我们不会重写 dispatchTouchEvent 方法。对于根 ViewGroup,点击事件首先传递给它的 dispatchTouchEvent 方法,如果该 ViewGroup 的 onInterceptTouchEvent 方法返回 true,则表示它要拦截这个事件,这个事件就会交由它的 onTouchEvent 方法处理,如果 onInterceptTouchEvent 方法返回 false,则表示它不拦截这个事件,则这个事件会交给它的子元素的 dispatchTouchEvent 方法来处理,如此反复下去。如果传递给底层的 View,View 是没有子 View 的,就会调用 View 的 dispatchTouchEvent 方法,一般情况下最终会调用 View 的 onTouchEvent 方法。(事件由上而下传递返回值的规则:为 true,则拦截,不继续向下传递,为 false,则不拦截,继续向下传递。)

点击事件由下而上的传递。当点击事件传递底层的 View 时,如果其 onTouchEvent 方法返回 true,则事件由底层的 View 消耗并处理,如果返回 false 则表示该 View 不做处理,则传递给父 View 的 onTouchEvent 处理,如果父 View 的 onTouchEvent 依旧返回 false,则继续传递给该父 View 的父 View 处理,如此反复下去。(事件由下而上传递返回值的规则:为 true,则处理了,不继续向上传递,为 false,则不处理,继续向上传递。)

当一个 View 需要处理事件时:

onTouchListener、onTouchEvent()、onClickListener 的先后顺序:

  • 当它设置了 OnTouchListener,那么 OnTouchListener 中的 onTouch 方法会被回调。
  • 如果 onTouch 的返回值为 false,那么 View 的 onTouchEvent 会被调用,true 则不会调用。
  • 给 View 设置的 OnTouchListener,其优先级比 onTouchEvent 要高。
  • 在 onTouchEvent 中如果设置了 OnClickListener,那么它的 onClick 方法会被调用。
  • OnClickListener 的优先级最低,即处于事件传递的尾端。

当一个点击事件产生后,传递过程如下:

针对 ACTION_DOWN:

  • 如果没有对控件里面的方法进行重写或更改返回值,而直接用super调用父类的默认实现,那么整个事件流向应该是从Activity—->ViewGroup—>View 从上往下调用dispatchTouchEvent方法,一直到叶子节点(View)的时候,再由View—>ViewGroup—>Activity从下往上调用onTouchEvent方法。

  • dispatchTouchEvent 和 onTouchEvent 一旦return true(事件被消费了),代表事件停止传递了(到达终点)(没有谁能再收到这个事件)。

  • dispatchTouchEvent 和 onTouchEvent ,return false 的时候事件都回传给父控件的onTouchEvent处理。

    • 对于dispatchTouchEvent 返回 false 的含义应该是:事件停止往子View传递和分发同时开始往父控件回溯(父控件的onTouchEvent开始从下往上回传直到某个onTouchEvent return true),事件分发机制就像递归,return false 的意义就是递归停止然后开始回溯。
    • 对于onTouchEvent return false 就比较简单了,它就是不消费事件,并让事件继续往父控件的方向从下往上流动。
  • dispatchTouchEvent、onTouchEvent、onInterceptTouchEvent
    ViewGroup 和View的这些方法的默认实现就是会让整个事件按照U型完整走完,所以 return super.xxxxxx() 就会让事件依照U型的方向的完整走完整个事件流动路径),中间不做任何改动,不回溯、不终止,每个环节都走到。

  • onInterceptTouchEvent 的作用

    Intercept 的意思就拦截,每个ViewGroup每次在做分发的时候,问一问拦截器要不要拦截(也就是问问自己这个事件要不要自己来处理)如果要自己处理那就在onInterceptTouchEvent方法中 return true就会交给自己的onTouchEvent的处理,如果不拦截就是继续往子控件往下传。默认是不会去拦截的,因为子View也需要这个事件,所以onInterceptTouchEvent拦截器return super.onInterceptTouchEvent()和return false是一样的,是不会拦截的,事件会继续往子View的dispatchTouchEvent传递。

  • ViewGroup 和 View 的dispatchTouchEvent方法返回super.dispatchTouchEvent()的时候事件流走向。

    • 首先看下ViewGroup 的dispatchTouchEvent,之前说的return true是终结传递。return false 是回溯到父View的onTouchEvent,然后ViewGroup怎样通过dispatchTouchEvent方法能把事件分发到自己的onTouchEvent处理呢,return true和false 都不行,那么只能通过Interceptor把事件拦截下来给自己的onTouchEvent,所以ViewGroup dispatchTouchEvent方法的super默认实现就是去调用onInterceptTouchEvent,记住这一点。
    • 那么对于 View 的dispatchTouchEvent return super.dispatchTouchEvent()的时候呢事件会传到哪里呢,很遗憾View没有拦截器。但是同样的道理return true是终结。return false 是回溯会父类的onTouchEvent,怎样把事件分发给自己的onTouchEvent 处理呢,那只能return super.dispatchTouchEvent,View类的dispatchTouchEvent()方法默认实现就是能帮你调用View自己的onTouchEvent方法的。

ViewGroup 和 View 的dispatchTouchEvent 是做事件分发,那么这个事件可能分发出去的四个目标

注:——> 后面代表事件目标需要怎么做。
1、 自己消费,终结传递。——->return true ;
2、 给自己的onTouchEvent处理——-> 调用super.dispatchTouchEvent()系统默认会去调用 onInterceptTouchEvent,在onInterceptTouchEvent return true就会去把事件分给自己的onTouchEvent处理。
3、 传给子View——>调用super.dispatchTouchEvent()默认实现会去调用 onInterceptTouchEvent 在onInterceptTouchEvent return false,就会把事件传给子类。
4、 不传给子View,事件终止往下传递,事件开始回溯,从父View的onTouchEvent开始事件从下到上回归执行每个控件的onTouchEvent——->return false;
注: 由于View没有子View所以不需要onInterceptTouchEvent 来控件是否把事件传递给子View还是拦截,所以View的事件分发调用super.dispatchTouchEvent()的时候默认把事件传给自己的onTouchEvent处理(相当于拦截),对比ViewGroup的dispatchTouchEvent 事件分发,View的事件分发没有上面提到的4个目标的第3点。

ViewGroup和View的onTouchEvent方法是做事件处理的,那么这个事件只能有两个处理方式

1、自己消费掉,事件终结,不再传给谁—–>return true;
2、继续从下往上传,不消费事件,让父View也能收到到这个事件—–>return false;View的默认实现是不消费的。所以super==false。

ViewGroup的onInterceptTouchEvent方法对于事件有两种情况

1、拦截下来,给自己的onTouchEvent处理—>return true;
2、不拦截,把事件往下传给子View—->return false,ViewGroup默认是不拦截的,所以super==false;

关于ACTION_MOVE 和 ACTION_UP

红色的箭头代表ACTION_DOWN 事件的流向
蓝色的箭头代表ACTION_MOVE 和 ACTION_UP 事件的流向

如果在某个控件的dispatchTouchEvent 返回true消费终结事件,那么收到ACTION_DOWN 的函数也能收到 ACTION_MOVE和ACTION_UP。

如果在哪个View的onTouchEvent 返回true,那么ACTION_MOVE和ACTION_UP的事件从上往下传到这个View后就不再往下传递了,而直接传给自己的onTouchEvent 并结束本次事件传递过程。

Android 中 touch 事件的传递机制是怎样的
1、Touch事件传递的相关 API 有 dispatchTouchEvent、onTouchEvent、onInterceptTouchEvent。
2、Touch事件相关的类有View、ViewGroup、Activity。
3、Touch事件会被封装成MotionEvent对象,该对象封装了手势按下、移动、松开等动作。
4、Touch事件通常从Activity#dispatchTouchEvent发出,只要没有被消费,会一直往下传递,到最底层的View。
5、如果Touch事件传递到的每个View都不消费事件,那么Touch事件会反向向上传递,最终交由Activity#onTouchEvent处理。
6、onInterceptTouchEvent为ViewGroup特有,可以拦截事件。
7、Down事件到来时,如果一个View没有消费该事件,那么后续的MOVE/UP事件都不会再给它。

事件传递机制的一些结论:

  • 同一个事件序列从手指接触屏幕的那一刻起(down 事件开始,中间含有不定数的 move 事件),到手指离开屏幕的那一刻结束(up 事件结束)。
  • 一般一个事件序列只能被一个 View 拦截且消耗,但它可以通过 onTouchEvent 强行传递给其他 View 处理。
  • 当 View 决定拦截事件,则这一个事件序列都只能有它来处理,并且 onInterceptTouchEvent 不会再被调用。
  • 当 View 开始处理事件,如果不消耗 ACTION_DOWN 事件(onTouchEvent 返回了 false),那么同一事件序列中的其他事件都不会再交给它来处理,并且事件将重新交由它的父元素去处理(父元素的 onTouchEvent 被调用)。
  • 如果 View 不消耗除 ACTION_DOWN 以外的其他事件,那这个点击事件会消失,此时父元素的 onTouchEvent 并不会被调用,并且当前 View 可以持续收到后续的事件,最终这些消失的点击事件会传递给 Activity 处理。
  • ViewGroup 默认不拦截任何事件。
  • View 没有 onInterceptTouchEvent 方法,一旦有点击事件传递给它,那么 onTouchEvent 方法会被调用。
  • View 的 onTouchEvent 默认都会消耗事件(返回 ture),除非它是不可点击的(clickable 和 longClickable 同时为 false)。View 的 longClickable 属性默认为 false,clickable 属性要分情况,Button 的默认为 true,而 TextView 默认为 false。
  • View 的 enable 属性不影响 onTouchEvent 的默认返回值。哪怕一个 View 是 disable 状态的,只要它的 clickable 或者 longClickable 有一个为 true,那么它的 onTouchEvent 就返回 true。
  • onClick 会发生的前提是当前 View 是可点击的,并且它收到了 down 和 up 的事件。
  • 事件传递过程是由外向内的,即事件总是先传递给父元素,然后再由父元素分发给子 View,通过 requestDisallowInterceptTouchEvent 方法可以在子元素中干预父元素的事件分发过程,但是 ACTION_DOWN 事件除外。

事件分发的源码解析

点击事件产生后,首先传递给当前的 Activity,这会调用 Activity 的 dispatchTouchEvent 方法,当然具体的事件处理工作都是交由 Activity 中的 PhoneWindow 来完成的,然后 PhoneWindow 再把事件处理工作交给 DecorView,之后再由 DecorView 将事件处理工作交给根 ViewGroup。首先查看 ViewGroup 的 dispatchTouchEvent 方法,其主要分为 3 大步骤。

  • 步骤 1:判断当前 ViewGroup 是否需要拦截此 touch 事件,如果拦截则此次 touch 事件不再会传递给子 View(或者以 CANCEL 的方式通知子 View)。

  • 步骤 2:如果没有拦截,则将事件分发给子 View 继续处理,如果子 View 将此次事件捕获,则将 mFirstTouchTarget 赋值给捕获 touch 事件的 View。

  • 步骤 3:根据 mFirstTouchTarget 重新分发事件。

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
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
...
// 判断事件是否为 DOWN 事件,如果是,则进行初始化。因为一个完整的事件序列是以 DOWN 开始,以 UP 结束的。所以如果是 DOWN 事件,那么说明这是一个新的事件序列,故而需要初始化之前的状态。
// 所有 touch 事件都是从 DOWN 事件开始的, DOWN 事件的处理结果会直接影响后续 MOVE、UP 事件的逻辑。
if (actionMasked == MotionEvent.ACTION_DOWN) {
cancelAndClearTouchTargets(ev);
// 此方法会将 mFirstTouchTarget 的值置为 null。mFirstTouchTarget 的意义在于,当前 ViewGroup 是否拦截了事件,如果拦截了则值 =null,如果没有拦截并交由子 View 来处理,则值 !=null。
resetTouchState();
}

/**
* 检查当前 ViewGroup 是否需要拦截事件
*/
final boolean intercepted;
// 假设当前 ViewGroup 拦截了此事件,mFirstTouchTarget != null 则为 false,如果这时触发的是 DOWN 事件,则会执行 onInterceptTouchEvent 方法,如果触发的是 MOVE 和 UP 事件,则不再执行 onInterceptTouchEvent 方法,而是直接设置 intercepted = true,此后的一个事件序列均由这个 ViewGroup 处理。
// 也就是说,如果事件为 DOWN 事件,则调用 onInterceptTouchEvent 进行拦截判断;或者 mFirstTouchTarget 不为 null,代表已经有子 View 捕获了这个事件,子 View 的 dispatchTouchEvent 返回 true 就是代表捕获 touch 事件。
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
// FLAG_DISALLOW_INTERCEPT 标志位,它主要是禁止 ViewGroup 拦截除了 DOWN 之外的事件,一般通过子 View 的 requestDisallowInterceptTouchEvent 来设置。
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (!disallowIntercept) {
intercepted = onInterceptTouchEvent(ev);
ev.setAction(action);
} else {
intercepted = false;
}
} else {
intercepted = true;
}
...
return handled;
}

当 ViewGroup 要拦截事件时,那么后续的事件序列都将交给他处理,而不用再调用 onInterceptTouchEvent 方法了。所以,onInterceptTouchEvent 方法并不是每次事件都会调用的。onInterceptTouchEvent 方法默认返回 false,不进行拦截。如果想要让 ViewGroup 拦截事件,那么应该在自定义的 ViewGroup 中重写这个方法。

1
2
3
4
5
6
7
8
9
public boolean onInterceptTouchEvent(MotionEvent ev) {
if (ev.isFromSource(InputDevice.SOURCE_MOUSE)
&& ev.getAction() == MotionEvent.ACTION_DOWN
&& ev.isButtonPressed(MotionEvent.BUTTON_PRIMARY)
&& isOnScrollbarThumb(ev.getX(), ev.getY())) {
return true;
}
return false;
}

接着查看 ViewGroup 的 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
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
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
...
/**
* 将事件分发给子 View
* 事件主动分发的前提是事件为 DOWN 事件,执行以下代码。
* 只有 DOWN 事件会传递给子 View 进行捕获判断,一旦子 View 捕获成功,后续的 MOVE 和 UP 事件是通过遍历 mFirstTouchTarget 链表,查找之前接受 ACTION_DOWN 的子 View,并将触摸事件分配给这些子 View。也就是说后续的 MOVE、UP 等事件的分发交给谁,取决于它们的起始事件 Down 是由谁捕获的。
*/
final View[] children = mChildren;
// 首先遍历 ViewGroup 的子元素,判断子元素是否能够接收到点击事件,如果子元素能够接收到点击事件,则交由子元素来处理。需要注意的是,这个 for 循环是倒序遍历的,即从最上层的子 View 开始往内层遍历。
for (int i = childrenCount - 1; i >= 0; i--) {
final int childIndex = getAndVerifyPreorderedIndex(
childrenCount, i, customOrder);
final View child = getAndVerifyPreorderedView(
preorderedList, children, childIndex);

if (childWithAccessibilityFocus != null) {
if (childWithAccessibilityFocus != child) {
continue;
}
childWithAccessibilityFocus = null;
i = childrenCount - 1;
}
// 判断触摸点位置是否在子 View 的范围内或者子 View 是否在播放动画。如果均不符合则执行 continue 语句,表示这个子 View 不符合条件,开始遍历下一个子 View。
if (!child.canReceivePointerEvents()
|| !isTransformedTouchPointInView(x, y, child, null)) {
ev.setTargetAccessibilityFocus(false);
continue;
}
newTouchTarget = getTouchTarget(child);
if (newTouchTarget != null) {

newTouchTarget.pointerIdBits |= idBitsToAssign;
break;
}
resetCancelNextUpFlag(child);
// 调用 dispatchTransformedTouchEvent 方法将事件分发给子 View,在 dispatchTransformedTouchEvent 内部会做判断,如果有子 View,则调用子 View 的 dispatchTouchEvent(event) 方法,如果子 View 捕获事件成功,则将 mFirstTouchTarget 赋值给子 View。如果 ViewGroup 没有子 View,则调用 super.dispatchTouchEvent(event) 方法。
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
mLastTouchDownTime = ev.getDownTime();
if (preorderedList != null) {
for (int j = 0; j < childrenCount; j++) {
if (children[childIndex] == mChildren[j]) {
mLastTouchDownIndex = j;
break;
}
}
} else {
mLastTouchDownIndex = childIndex;
}
mLastTouchDownX = ev.getX();
mLastTouchDownY = ev.getY();
newTouchTarget = addTouchTarget(child, idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true;
break;
}
ev.setTargetAccessibilityFocus(false);
}
...
/**
* 根据 mFirstTouchTarget,再次向子 View 分发事件。
* mFirstTouchTarget 是一个 TouchTarget 类型的链表结构。它的作用就是用来记录捕获了 DOWN 事件的 View,具体保存的是 child 变量。可是为什么是链表类型的结构呢?因为 Android 设备是支持多指操作的,每一个手指的 DOWN 事件都可以当做一个 TouchTarget 保存起来。
*/
// 如果此时 mFirstTouchTarget 为 null,说明并没有子 View 对事件进行了捕获操作。这种情况下,直接调用 dispatchTransformedTouchEvent 方法,并传入 child 为 null,最终会调用 super.dispatchTouchEvent 方法。实际上最终会调用自身的 onTouchEvent 方法,进行处理 touch 事件。也就是说:如果没有子 View 捕获处理 touch 事件,ViewGroup 会通过自身的 onTouchEvent 方法进行处理。
if (mFirstTouchTarget == null) {
// 传入的参数 null,为 child。
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
} else {
TouchTarget predecessor = null;
TouchTarget target = mFirstTouchTarget;
// 表明已经有子 View 捕获了 touch 事件。
while (target != null) {
final TouchTarget next = target.next;
if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
handled = true;
} else {
// 如果 intercepted boolean 变量是 true。这种情况下,事件主导权会重新回到父视图 ViewGroup 中,并传递给子 View 的分发事件中传入一个 cancelChild == true。
final boolean cancelChild = resetCancelNextUpFlag(target.child)
|| intercepted;
// mFirstTouchTarget 不为 null,说明有子 View 对 touch 事件进行了捕获,则直接将当前以及后续的事件交给 mFirstTouchTarget 指向的 View 进行处理。
// 也就是说,在子 View 的 dispatchTouchEvent 中返回 true,代表它捕获消费了这个 DOWN 事件。这种情况下子 View 会被添加到父视图中的 mFirstTouchTarget 中。因此后续的 MOVE 和 UP 事件都会经过父 View 的 onInterceptTouchEvent 进行拦截判断。
if (dispatchTransformedTouchEvent(ev, cancelChild,
target.child, target.pointerIdBits)) {
handled = true;
}
...
}
predecessor = target;
target = next;
}
}
}

容易被遗漏的 CANCEL 事件,查看 dispatchTransformedTouchEvent 方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
View child, int desiredPointerIdBits) {
...
final int oldAction = event.getAction();
if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
event.setAction(MotionEvent.ACTION_CANCEL);
if (child == null) {
handled = super.dispatchTouchEvent(event);
} else {
handled = child.dispatchTouchEvent(event);
}
event.setAction(oldAction);
return handled;
}
...
}

当传入的参数 cancel 为 true,并且 child 不为 null,最终这个事件会被包装为一个 ACTION_CANCEL 事件传给 child。什么情况下会触发这段逻辑呢?

总结一下就是:当父视图的 onInterceptTouchEvent 先返回 false,然后在子 View 的 dispatchTouchEvent 中返回 true(表示子 View 捕获事件),关键步骤就是在接下来的 MOVE 的过程中,父视图的 onInterceptTouchEvent 又返回 true,intercepted 被重新置为 true,此时上述逻辑就会被触发,子控件就会收到 ACTION_CANCEL 的 touch 事件。

实际上有个很经典的例子可以用来演示这种情况:
当在 Scrollview 中添加自定义 View 时,ScrollView 默认在 DOWN 事件中并不会进行拦截,事件会被传递给 ScrollView 内的子控件。只有当手指进行滑动并到达一定的距离之后,onInterceptTouchEvent 方法返回 true,并触发 ScrollView 的滚动效果。当 ScrollView 进行滚动的瞬间,内部的子 View 会接收到一个 CANCEL 事件,并丢失touch焦点。

因此,我们平时自定义View时,尤其是有可能被ScrollView或者ViewPager嵌套使用的控件,不要遗漏对CANCEL事件的处理,否则有可能引起UI显示异常。

总结
dispatchTouchEvent 的事件的流程机制,这一过程主要分 3 部分:

  • 判断是否需要拦截 —> 主要是根据 onInterceptTouchEvent 方法的返回值来决定是否拦截;

  • 在 DOWN 事件中将 touch 事件分发给子 View —> 这一过程如果有子 View 捕获消费了 touch 事件,会对 mFirstTouchTarget 进行赋值;

  • 最后一步,DOWN、MOVE、UP 事件都会根据 mFirstTouchTarget 是否为 null,决定是自己处理 touch 事件,还是再次分发给子 View。

整个事件分发中的几个特殊的点。

  • DOWN 事件的特殊之处:事件的起点;决定后续事件由谁来消费处理;

  • mFirstTouchTarget 的作用:记录捕获消费 touch 事件的 View,是一个链表结构;

  • CANCEL 事件的触发场景:当父视图先不拦截,然后在 MOVE 事件中重新拦截,此时子 View 会接收到一个 CANCEL 事件。

ViewGroup 是继承 View 的,接下来查看 View 的 dispatchTouchEvent(event) 方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public boolean dispatchTouchEvent(MotionEvent event) {
...
boolean result = false;
...
if (onFilterTouchEventForSecurity(event)) {
if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
result = true;
}
ListenerInfo li = mListenerInfo;
// 如果 OnTouchListener 不为 null 并且 onTouch 方法返回 true,则表示事件被消费,就不会执行 onTouchEvent(event),否则就会执行 onTouchEvent(event)。
// 可以看出 OnTouchListener 中的 onTouch 方法优先级要高于 onTouchEvent(event)。
if (li != null && li.mOnTouchListener != null
&& (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {
result = true;
}
if (!result && onTouchEvent(event)) {
result = true;
}
}
...
return result;
}

在 onTouchEvent 方法内部,只要 View 的 CLICKABLE 和 LONG_CLICKABLE 有一个为 true,那么 onTouchEvent 就会返回 true 消费这个事件。CLICKABLE 和 LONG_CLICKABLE 代表 View 可以被点击和长按点击,可通过 View 的 setClickable 和 setLongClickable 方法来设置,也可以通过 View 的 setOnCLickListener 和 setOnLongClickable 来设置,它们会自动将 View 设置为 CLICKABLE 和 LONG_CLICKABLE。

接着在 ACTION_UP 事件中会调用 performClick 方法。 此方法中,如果 View 设置了点击事件 OnClickListener,那么它的 onClick 方法就会被执行。

Activity 对点击事件的分发过程
事件交给 Activity 所属的 Window 进行分发,返回 true 则整个事件循环结束,返回 false 则意味着事件没人处理,所有 View 的 onTouchEvent 都返回 false,则 Activity 的 onTouchEvent 会被调用。

Window 对点击事件的分发过程
Window 的实现类 PhoneWindow 将事件直接传递给了 DecorView,而 DecorView 继承自 FrameLayout 且是父 View,所以最终事件会传递给顶级 View(根 View,一般是 ViewGroup)。

顶级 View 对点击事件的分发过程
点击事件到达顶级 View,并调用它的 dispatchTouchEvent 方法。
如果它拦截事件,则 onInterceptTouchEvent 返回 ture。
这时如果它的 mOnTouchEvent 被设置,则 onTouch 会被调用,否则 onTouchEvent 会被调用。
在 onTouchEvent 中,如果设置了 mOnClickListener ,则 onClick 会被调用。
如果它不拦截事件,则事件会传递给它所在的点击事件链上的子 View。

View 对点击事件的处理过程
这里的 View 不包含 ViewGroup。所以也无需向下传递事件。


View 的滑动冲突

在界面中只要内外两层同时可以滑动,这个时候就会产生滑动冲突。

Android 下解决滑动冲突的常见思路是什么?
相关的滑动组件重写 onInterceptTouchEvent,然后判断根据 xy 值,来决定是否要拦截当前操作。

常见的滑动冲突场景

  • 内外滑动方向不一致
    这种一般是 ViewPager 和 Fragment 配合使用的效果。但 ViewPager 相较于 ScrollView 等,其内部处理了这种冲突。
  • 内外滑动方向一致
  • 上面两种情况的嵌套
    虽看似复杂,但就是几个单一的滑动冲突的叠加,只需分别处理内层和中层,中层和外层之间的冲突即可。

滑动冲突的处理规则

  • 外部左右滑动,内部上下滑动
    判断滑动方向,当用户左右滑动时,由外部 View 拦截点击事件,当上下滑动时,由内部 View 拦截。
  • 可依据滑动路径和水平方向所形成的夹角判断方向
  • 可依据水平和竖直方向上的距离差判断滑动方向
  • 可依据水平和竖直方向的速度差判断滑动方向
  • 内外方向一致
    根据业务需要处理
  • 嵌套情况
    根据业务需要处理

滑动冲突的解决办法

外部拦截法

指点击事件都先经过父容器的拦截处理,需重写父容器的 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
31
32
33
34
35
36
/**
* 伪代码
* 典型逻辑
*/
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
boolean intercepted = false;
int x = (int) ev.getX();
int y = (int) ev.getY();
switch (ev.getAction()){
case MotionEvent.ACTION_DOWN:
// 表示不拦截 ACTION_DOWN 事件
// 否则后续的 move 和 up 事件都会直接交由父容器处理
intercepted = false;
break;
case MotionEvent.ACTION_MOVE:
// 针对不同的滑动冲突,只需修改这个条件
// 其它均不需也不能修改
if (父容器需要当前点击事件){
intercepted = true;
}else {
intercepted = false;
}
break;
case MotionEvent.ACTION_UP:
// 必须返回 false
// 因为这个事件本身没有太多意义
intercepted = false;
break;
default:
break;
}
mLastXIntercept = x;
mLastYIntercept = y;
return intercepted;
}

内部拦截法

指父容器不拦截任何事件,所有事件都传递给子元素,如果子元素需要此事件就直接消耗掉,否则交由父容器处理,需配合 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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
/**
* 伪代码
* 典型代码
*
* 子元素中:
*/
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
int x = (int) ev.getX();
int y = (int) ev.getY();
switch (ev.getAction()){
case MotionEvent.ACTION_DOWN:
getParent().requestDisallowInterceptTouchEvent(true);
break;
case MotionEvent.ACTION_MOVE:
int deltaX = x - mLastX;
int deltaY = y - mLastY;
// 针对不同的滑动策略,只需修改这个条件
// 其它均不需也不能修改
if (父容器需要此类点击事件){
getParent().requestDisallowInterceptTouchEvent(false);
}
break;
case MotionEvent.ACTION_UP:
break;
default:
break;
}
mLastX = x;
mLastY = y;
return super.dispatchTouchEvent(ev);
}

/**
* 父元素中:
*
* 除了子元素需要做处理,父元素也要默认拦截除了 down 以外的其他事件,
* 这样当调用 requestDisallowInterceptTouchEvent(false); 父元素才能继续拦截所需事件。
*/
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
int action = ev.getAction();
if (action == MotionEvent.ACTION_DOWN){
return false;
}else {
return true;
}
}

链接

参考资料:
Android 开发艺术探索

Android 进阶之光

图解 Android 事件分发机制

传送门:GitHub