一.基础认知

1.事件分发的对象

  • 在之前的两篇文章中,我们经常提到事件消费。我们的对象就是这些事件(点击事件)。
  • 当用户触摸屏幕时(ViewViewGroup派生的控件),将产生点击事件(Touch事件)
  • Touch事件的相关细节(发生触摸的位置、时间等)被封装成MotionEvent对象

2.事件类型

事件类型 具体动作
MotionEvent.ACTION_DOWN 按下View(所有事件的开始)
MotionEvent.ACTION_MOVE 滑动View
MotionEvent.ACTION_UP 抬起View(与DOWN对应)
MotionEvent.ACTION_CANCEL 结束事件(非人为原因)

3.事件分发的本质

将点击事件(MotionEvent)传递到某个具体的View & 处理的整个过程

4.事件分发的顺序

事件传递的顺序:Activity -> ViewGroup -> View

二. Activity的事件分发机制

  1. 一般情况下事件的开始都是 ACTION_DOWN,都会调用 onUserInteraction方法。而该方法是空的,当此activity在栈顶时,触屏点击按home,back,menu键等都会触发此方法,实现屏保功能。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    public boolean dispatchTouchEvent(MotionEvent ev) {
    if (ev.getAction() == MotionEvent.ACTION_DOWN) {
    onUserInteraction();
    }
    if (getWindow().superDispatchTouchEvent(ev)) {
    return true;
    }

    return onTouchEvent(ev);
    }

    public void onUserInteraction() {
    }
  2. getWindow() = 获取Window类的对象,Window类是抽象类,其唯一实现类 = PhoneWindow类;即此处的Window类对象 = PhoneWindow类对象。Window类的superDispatchTouchEvent() = 1个抽象方法,由子类PhoneWindow类实现。

    1
    2
    3
    4
    5
    @Override
    public boolean superDispatchTouchEvent(MotionEvent event) {

    return mDecor.superDispatchTouchEvent(event);
    }
  3. mDecor 是 顶层View(DecorView)的实例对象,DecorView类是PhoneWindow类的一个内部类,DecorView继承自FrameLayout,是所有界面的父类。FrameLayout是ViewGroup的子类,故DecorView的间接父类 = ViewGroup

    1
    2
    3
    4
    5
    public boolean superDispatchTouchEvent(MotionEvent event) {

    return super.dispatchTouchEvent(event);

    }
  4. 调用父类的方法 = ViewGroup的dispatchTouchEvent(),即将事件传递到ViewGroup去处理,详细请看上一篇文章ViewGroup的事件分发机制。

  5. 若getWindow().superDispatchTouchEvent(ev)的返回true,则Activity.dispatchTouchEvent()就返回true,则方法结束。即 :该点击事件停止往下传递 & 事件传递过程结束,否则:继续往下调用Activity.onTouchEvent。

三. 事件分发的顺序

图片可能有错误,或者有表达不清楚的地方,仅仅只是展现自己理解程度。

四. Demo证明

  1. 在一个界面的一个布局里面放置两个按钮。这样,Activity,VIewGroup,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
<?xml version="1.0" encoding="utf-8"?>
<swu.xl.viewgroup_touch.MyLinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity"
android:orientation="vertical"
android:id="@+id/root">

<swu.xl.viewgroup_touch.MyButton
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="100dp"
android:layout_gravity="center"
android:text="Button 1"
android:id="@+id/btn1"
/>

<swu.xl.viewgroup_touch.MyButton
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="100dp"
android:layout_gravity="center"
android:text="Button 2"
android:id="@+id/btn2"
/>

</swu.xl.viewgroup_touch.MyLinearLayout>
  1. 因为,View 和 ViewGroup 的 OnTouchEvent事件 都是在 内部实现的,所以我们需要自定义 ViewGroup 和 View,并在里面实现 OnTouchEvent事件。

    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
    public class MyLinearLayout extends LinearLayout {

    public static final String TAG = "MainActivity";

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

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

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
    //拦截事件,交给自己处理,super.dispatchTouchEvent()
    //若有OnTouch,交给OnTouch
    //否则交给OnTouchEvent
    return false;
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
    //不消费事件,交给Activity
    Log.d(TAG,"ViewGroup OnTouchEvent"+event.getAction());
    return super.onTouchEvent(event);
    }
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    public class MyButton extends androidx.appcompat.widget.AppCompatButton {
    public static final String TAG = "MainActivity";

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

    public MyButton(Context context, AttributeSet attrs) {
    super(context, attrs);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
    Log.d(TAG,"View OnTouchEvent"+event.getAction());
    return super.onTouchEvent(event);
    }
    }
  2. 给两个 按钮 以及 布局 都设置一个监听事件。

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
public class MainActivity extends AppCompatActivity {

public static final String TAG = MainActivity.class.getSimpleName();

@SuppressLint("ClickableViewAccessibility")
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);

MyButton btn1 = findViewById(R.id.btn1);
MyButton btn2 = findViewById(R.id.btn2);
MyLinearLayout layout = findViewById(R.id.root);

layout.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
Log.d(TAG,"ViewGroup OnTouch"+event.getAction());

//OnTouch不消费,交给OnTouchEvent
return false;
}
});

btn1.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
Log.d(TAG,"View Btn1 OnTouch"+event.getAction());
return true;
}
});
btn2.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
Log.d(TAG,"View Btn2 OnTouch"+event.getAction());
return false;
}
});
}

@Override
public boolean onTouchEvent(MotionEvent event) {
Log.d(TAG,"Activity TouchEvent");
return super.onTouchEvent(event);
}
}

4.运行效果

① 点击空白处

1
2
3
4
2020-03-25 00:22:07.049 30837-30837/swu.xl.viewgroup_touch D/MainActivity: ViewGroup OnTouch0
2020-03-25 00:22:07.050 30837-30837/swu.xl.viewgroup_touch D/MainActivity: ViewGroup OnTouchEvent0
2020-03-25 00:22:07.050 30837-30837/swu.xl.viewgroup_touch D/MainActivity: ViewGroup OnTouch1
2020-03-25 00:22:07.050 30837-30837/swu.xl.viewgroup_touch D/MainActivity: ViewGroup OnTouchEvent1

如果我们在MyLinearLayout中将OnTouchEvent方法中的return super.onTouchEvent(event),换成return false。

1
2
3
4
2020-03-25 00:27:51.924 1008-1008/swu.xl.viewgroup_touch D/MainActivity: ViewGroup OnTouch0
2020-03-25 00:27:51.924 1008-1008/swu.xl.viewgroup_touch D/MainActivity: ViewGroup OnTouchEvent0
2020-03-25 00:27:51.924 1008-1008/swu.xl.viewgroup_touch D/MainActivity: Activity TouchEvent
2020-03-25 00:27:51.925 1008-1008/swu.xl.viewgroup_touch D/MainActivity: Activity TouchEvent

我们可以看到没有更换之前,触摸事件是被ViewGroup的OnTouchEvent消费了。如果我们改为不消费,就会成功的进入到Activity的OnTouchEvent方法里面,符合我们之前的说法。

② 点击 Button 1

1
2
2020-03-25 00:22:47.246 30837-30837/swu.xl.viewgroup_touch D/MainActivity: View Btn1 OnTouch0
2020-03-25 00:22:47.247 30837-30837/swu.xl.viewgroup_touch D/MainActivity: View Btn1 OnTouch1

Button 1在OnTouch方法里面已经消费了事件,所以事件无法传递,符合我们之前的说法。

③ 点击 Button 2

1
2
3
4
2020-03-25 00:23:25.169 30837-30837/swu.xl.viewgroup_touch D/MainActivity: View Btn2 OnTouch0
2020-03-25 00:23:25.169 30837-30837/swu.xl.viewgroup_touch D/MainActivity: View OnTouchEvent0
2020-03-25 00:23:25.177 30837-30837/swu.xl.viewgroup_touch D/MainActivity: View Btn2 OnTouch1
2020-03-25 00:23:25.177 30837-30837/swu.xl.viewgroup_touch D/MainActivity: View OnTouchEvent1

Button 2 在OnTouch方法里面没有消费事件,所以事件传递到 View 的 OnTouchEvent里面被消费了,符合我们之前的说法。

参考文章

Android事件分发机制详解:史上最全面、最易懂

图解 Android 事件分发机制