注:本文代码基于Android Sv2
一、概述
事件分发机制是View体系中一个非常重要的核心知识点,也是难点,不少Android开发者搞了几年的Android,或许也还没完全弄明白它的原理。
另外,在Android开发过程中难免会碰到滑动冲突的场景,而解决滑动冲突的理论基础就是事件分发机制,因此掌握事件分发机制十分必要。
接下来,本文会先从实践的角度总结出结论,然后从源码的角度去看事件分发的过程,最后利用前面所学知识去演示如何解决开发中常见的滑动冲突。
二、从实践的角度总结出结论
2.1、实践前的理论知识
事件分发主要涉及3个方法,代码如下:
1
2
3
4
5
6
7
8
|
// Activity、ViewGroup、View都有提供该方法,该方法的作用:事件的分发
public boolean dispatchTouchEvent(MotionEvent event)
// 仅ViewGroup提供该方法,该方法的作用:事件的拦截
public boolean onInterceptTouchEvent(MotionEvent ev)
// Activity、View都有提供该方法,该方法的作用:事件的处理
public boolean onTouchEvent(MotionEvent event)
|
这3个方法有2个共同点,第一个共同点就是都有一个入参MotionEvent,它表示当用户触摸屏幕时所进行的每一次交互(每一次交互指的是手指在UI控件上从按下、滑动再到抬起的过程),都会产生一次同一序列的触摸事件,这些触摸事件被封装进了MotionEvent类里,通过以下方法可以获取触摸事件的类型,代码如下:
1
|
val action = ev?.action
|
在事件分发过程中,常用的触摸事件类型有以下4种:
第二个共同点是都需要返回boolean类型的返回值,关于返回值的作用是什么后面会讲到。
那么,事件分发中这3个方法之间有什么关系呢?
它们之间的关系可以用以下伪代码来表示:
1
2
3
4
5
6
7
8
9
|
public boolean dispatchTouchEvent(MotionEvent ev) {
boolean result = false;
if (onInterceptTouchEvent(ev)) {
result = onTouchEvent(ev);
} else {
result = child.dispatchTouchEvent(ev);
}
return result;
}
|
通过伪代码,先大致了解下触摸事件的传递规则:对于一个根ViewGroup来说,触摸事件产生后,首先会传递给它,这时它的dispatchTouchEvent方法就会被调用,如果这个ViewGroup的onInterceptTouchEvent方法返回true就表示它要拦截当前事件,接着事件就会交给这个ViewGroup处理,即它的onTouchEvent方法就会被调用;如果这个ViewGroup的onInterceptTouchEvent方法返回false就表示它不拦截当前事件,这时当前事件就会继续传递给它的子元素,接着子元素的dispatchTouchEvent方法就会被调用,如此反复直到事件被最终处理。
2.2、实践开始
开始前,我们先编写一个UI界面,本文之后的讲解都是基于该UI界面的层次结构,UI界面运行后的效果如下:
从运行效果图可以知道,该UI界面内部嵌套了一个紫色的MyViewGroup2控件、一个绿色的MyViewGroup1控件以及一个黑色的MyView控件,布局文件代码如下:
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
|
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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">
<com.pengmj.androideventdispatch.MyViewGroup2
android:layout_width="300dp"
android:layout_height="300dp"
android:background="@color/purple_200"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<com.pengmj.androideventdispatch.MyViewGroup1
android:layout_width="200dp"
android:layout_height="200dp"
android:background="@color/teal_200"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<com.pengmj.androideventdispatch.MyView
android:layout_width="100dp"
android:layout_height="100dp"
android:background="@color/black"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</com.pengmj.androideventdispatch.MyViewGroup1>
</com.pengmj.androideventdispatch.MyViewGroup2>
</androidx.constraintlayout.widget.ConstraintLayout>
|
其中MyViewGroup2控件和MyViewGroup1控件的实现是一样的,都是继承自ConstraintLayout,以MyViewGroup2为例,代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
class MyViewGroup2 : ConstraintLayout {
constructor(context: Context) : super(context)
constructor(
context: Context,
attrs: AttributeSet?
) : super(context, attrs)
constructor(
context: Context,
attrs: AttributeSet?,
defStyleAttr: Int
) : super(context, attrs, defStyleAttr)
}
|
而MyView继承自View,代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
class MyView : View {
constructor(context: Context?) : super(context)
constructor(
context: Context?,
attrs: AttributeSet?
) : super(context, attrs)
constructor(
context: Context?,
attrs: AttributeSet?,
defStyleAttr: Int
) : super(context, attrs, defStyleAttr)
}
|
最后,MainActivity的代码如下:
1
2
3
4
5
6
7
|
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
}
}
|
之前介绍事件分发的相关方法时,代码注释里有描述过,在Activity、ViewGroup、View中都有dispatchTouchEvent方法,在Activity、View中都有onTouchEvent方法,对于ViewGroup而言,则多了一个onInterceptTouchEvent方法。
因此,本文先讲解不包含onInterceptTouchEvent方法时的事件分发过程,然后再讲解包含onInterceptTouchEvent方法时的事件分发过程。
2.2.1、ACTION_DOWN的事件分发过程(不包含onInterceptTouchEvent方法的情况)
既然先讨论的是不包含onInterceptTouchEvent方法的情况,那么只需将MainActivity、MyViewGroup2、MyViewGroup1、MyView都重写dispatchTouchEvent方法和onTouchEvent方法,为了验证在这种情况下的ACTION_DOWN事件分发过程,都打上日志进行观察,代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
override fun dispatchTouchEvent(ev: MotionEvent?): Boolean {
when (ev?.action) {
MotionEvent.ACTION_DOWN -> {
if (LOG_ACTION_DOWN) Log.e(TAG, "dispatchTouchEvent -> ACTION_DOWN")
}
}
return super.dispatchTouchEvent(ev)
}
override fun onTouchEvent(event: MotionEvent?): Boolean {
when (event?.action) {
MotionEvent.ACTION_DOWN -> {
if (LOG_ACTION_DOWN) Log.e(TAG, "onTouchEvent -> ACTION_DOWN")
}
}
return super.onTouchEvent(event)
}
|
为什么要加上if (LOG_ACTION_DOWN)判断条件呢?这是一个开关,用于日志过滤,方便观察。本文所有源码将在文末贴出。
2.2.1.1、在dispatchTouchEvent方法中拦截ACTION_DOWN事件
在dispatchTouchEvent方法中拦截ACTION_DOWN事件之前,我们需要知道dispatchTouchEvent方法需要返回boolean类型的返回值,它的返回值有3种情况:
- 情况一:返回super.dispatchTouchEvent
dispatchTouchEvent方法默认返回super.dispatchTouchEvent,表示不拦截任何事件,它是View体系中默认的事件分发过程。
运行起来后,点击MyView控件时,日志打印如下:
1
2
3
4
5
6
7
8
|
E/MainActivity: dispatchTouchEvent -> ACTION_DOWN
E/MyViewGroup2: dispatchTouchEvent -> ACTION_DOWN
E/MyViewGroup1: dispatchTouchEvent -> ACTION_DOWN
E/MyView: dispatchTouchEvent -> ACTION_DOWN
E/MyView: onTouchEvent -> ACTION_DOWN
E/MyViewGroup1: onTouchEvent -> ACTION_DOWN
E/MyViewGroup2: onTouchEvent -> ACTION_DOWN
E/MainActivity: onTouchEvent -> ACTION_DOWN
|
ACTION_DOWN事件分发过程如下图所示:
在dispatchTouchEvent方法中拦截ACTION_DOWN事件有两种方式:一是仅拦截ACTION_DOWN事件;二是拦截所有事件,这样也就包括了ACTION_DOWN事件。
仅拦截ACTION_DOWN事件的代码如下:
1
2
3
4
5
6
7
8
9
|
override fun dispatchTouchEvent(ev: MotionEvent?): Boolean {
when (ev?.action) {
MotionEvent.ACTION_DOWN -> {
if (LOG_ACTION_DOWN) Log.e(TAG, "dispatchTouchEvent -> ACTION_DOWN")
return true
}
}
return super.dispatchTouchEvent(ev)
}
|
拦截所有事件的代码如下:
1
2
3
4
5
6
7
8
|
override fun dispatchTouchEvent(ev: MotionEvent?): Boolean {
when (ev?.action) {
MotionEvent.ACTION_DOWN -> {
if (LOG_ACTION_DOWN) Log.e(TAG, "dispatchTouchEvent -> ACTION_DOWN")
}
}
return true
}
|
两种拦截ACTION_DOWN事件的方式有什么区别呢?
对于ACTION_DOWN事件来说没区别,最终打印结果是一样的,所以本文接下来采用仅拦截ACTION_DOWN事件的方式。
- 在MainActivity的dispatchTouchEvent方法中拦截ACTION_DOWN事件并返回true
修改MainActivity的dispatchTouchEvent方法为上面所说的仅拦截ACTION_DOWN事件的代码。
运行起来后,点击MyView控件时,日志打印如下:
1
|
E/MainActivity: dispatchTouchEvent -> ACTION_DOWN
|
ACTION_DOWN事件分发过程如下图所示:
- 在MyViewGroup2的dispatchTouchEvent方法中拦截ACTION_DOWN事件并返回true
修改MyViewGroup2的dispatchTouchEvent方法为上面所说的仅拦截ACTION_DOWN事件的代码。
运行起来后,点击MyView控件时,日志打印如下:
1
2
|
E/MainActivity: dispatchTouchEvent -> ACTION_DOWN
E/MyViewGroup2: dispatchTouchEvent -> ACTION_DOWN
|
ACTION_DOWN事件分发过程如下图所示:
- 在MyViewGroup1的dispatchTouchEvent方法中拦截ACTION_DOWN事件并返回true
修改MyViewGroup1的dispatchTouchEvent方法为上面所说的仅拦截ACTION_DOWN事件的代码。
运行起来后,点击MyView控件时,日志打印如下:
1
2
3
|
E/MainActivity: dispatchTouchEvent -> ACTION_DOWN
E/MyViewGroup2: dispatchTouchEvent -> ACTION_DOWN
E/MyViewGroup1: dispatchTouchEvent -> ACTION_DOWN
|
ACTION_DOWN事件分发过程如下图所示:
- 在MyView的dispatchTouchEvent方法中拦截ACTION_DOWN事件并返回true
修改MyView的dispatchTouchEvent方法为上面所说的仅拦截ACTION_DOWN事件的代码。
运行起来后,点击MyView控件时,日志打印如下:
1
2
3
4
|
E/MainActivity: dispatchTouchEvent -> ACTION_DOWN
E/MyViewGroup2: dispatchTouchEvent -> ACTION_DOWN
E/MyViewGroup1: dispatchTouchEvent -> ACTION_DOWN
E/MyView: dispatchTouchEvent -> ACTION_DOWN
|
ACTION_DOWN事件分发过程如下图所示:
通过对日志的观察,不难看出规律,可以得出结论:
在dispatchTouchEvent方法中拦截ACTION_DOWN事件并返回true之后,ACTION_DOWN事件就会直接停止传递,后面的子控件都不会接收到这个事件。
和之前返回true的情况一样,这里返回false的情况也有两种方式拦截ACTION_DOWN事件,并且两种情况下的表现对于ACTION_DOWN事件来说是没区别的,最终打印结果是一样的,所以这里演示的是仅拦截ACTION_DOWN事件。
- 在MainActivity的dispatchTouchEvent方法中拦截ACTION_DOWN事件并返回false
修改MainActivity的dispatchTouchEvent方法为如下代码:
1
2
3
4
5
6
7
8
9
|
override fun dispatchTouchEvent(ev: MotionEvent?): Boolean {
when (ev?.action) {
MotionEvent.ACTION_DOWN -> {
if (LOG_ACTION_DOWN) Log.e(TAG, "dispatchTouchEvent -> ACTION_DOWN")
return false
}
}
return super.dispatchTouchEvent(ev)
}
|
运行起来后,点击MyView控件时,日志打印如下:
1
|
E/MainActivity: dispatchTouchEvent -> ACTION_DOWN
|
ACTION_DOWN事件分发过程如下图所示:
- 在MyViewGroup2的dispatchTouchEvent方法中拦截ACTION_DOWN事件并返回false
修改MyViewGroup2的dispatchTouchEvent方法的代码为同上述MainActivity。
运行起来后,点击MyView控件时,日志打印如下:
1
2
3
|
E/MainActivity: dispatchTouchEvent -> ACTION_DOWN
E/MyViewGroup2: dispatchTouchEvent -> ACTION_DOWN
E/MainActivity: onTouchEvent -> ACTION_DOWN
|
ACTION_DOWN事件分发过程如下图所示:
- 在MyViewGroup1的dispatchTouchEvent方法中拦截ACTION_DOWN事件并返回false
修改MyViewGroup1的dispatchTouchEvent方法的代码为同上述MainActivity。
运行起来后,点击MyView控件时,日志打印如下:
1
2
3
4
5
|
E/MainActivity: dispatchTouchEvent -> ACTION_DOWN
E/MyViewGroup2: dispatchTouchEvent -> ACTION_DOWN
E/MyViewGroup1: dispatchTouchEvent -> ACTION_DOWN
E/MyViewGroup2: onTouchEvent -> ACTION_DOWN
E/MainActivity: onTouchEvent -> ACTION_DOWN
|
ACTION_DOWN事件分发过程如下图所示:
- 在MyView的dispatchTouchEvent方法中拦截ACTION_DOWN事件并返回false
修改MyView的dispatchTouchEvent方法的代码为同上述MainActivity。
运行起来后,点击MyView控件时,日志打印如下:
1
2
3
4
5
6
7
|
E/MainActivity: dispatchTouchEvent -> ACTION_DOWN
E/MyViewGroup2: dispatchTouchEvent -> ACTION_DOWN
E/MyViewGroup1: dispatchTouchEvent -> ACTION_DOWN
E/MyView: dispatchTouchEvent -> ACTION_DOWN
E/MyViewGroup1: onTouchEvent -> ACTION_DOWN
E/MyViewGroup2: onTouchEvent -> ACTION_DOWN
E/MainActivity: onTouchEvent -> ACTION_DOWN
|
ACTION_DOWN事件分发过程如下图所示:
通过对日志的观察,不难看出规律,可以得出结论:
在dispatchTouchEvent方法中返回false拦截事件之后,事件并不会直接停止传递,而是向父控件的onTouchEvent方法回传。
2.2.1.2、在onTouchEvent方法中拦截ACTION_DOWN事件
在onTouchEvent方法中拦截ACTION_DOWN事件之前,我们需要知道onTouchEvent方法需要返回boolean类型的返回值,它有3种情况:
-
情况一:返回super.onTouchEvent
-
情况二:返回true
-
情况三:返回false
其中,情况一和情况三都表示不拦截ACTION_DOWN事件,所以它们的事件分发过程和View体系中默认的事件分发过程是一毛一样的,所以这里只分析情况二,下面的结论也是针对情况二来讲。
在onTouchEvent方法中拦截ACTION_DOWN事件有两种方式:一是仅拦截ACTION_DOWN事件;二是拦截所有事件,这样也就包括了ACTION_DOWN事件。
仅拦截ACTION_DOWN事件的代码如下:
1
2
3
4
5
6
7
8
9
|
override fun onTouchEvent(event: MotionEvent?): Boolean {
when (event?.action) {
MotionEvent.ACTION_DOWN -> {
if (LOG_ACTION_DOWN) Log.e(TAG, "onTouchEvent -> ACTION_DOWN")
return true;
}
}
return super.onTouchEvent(event)
}
|
拦截所有事件的代码如下:
1
2
3
4
5
6
7
8
|
override fun onTouchEvent(event: MotionEvent?): Boolean {
when (event?.action) {
MotionEvent.ACTION_DOWN -> {
if (LOG_ACTION_DOWN) Log.e(TAG, "onTouchEvent -> ACTION_DOWN")
}
}
return true
}
|
两种拦截ACTION_DOWN事件的方式有什么区别呢?
对于ACTION_DOWN事件来说没区别,最终打印结果是一样的,所以本文接下来采用仅拦截ACTION_DOWN事件的方式。
- 在MainActivity的onTouchEvent方法中拦截ACTION_DOWN事件并返回true
修改MainActivity的onTouchEvent方法为上面所说的仅拦截ACTION_DOWN事件的代码。
运行起来后,点击MyView控件时,日志打印如下:
1
2
3
4
5
6
7
8
|
E/MainActivity: dispatchTouchEvent -> ACTION_DOWN
E/MyViewGroup2: dispatchTouchEvent -> ACTION_DOWN
E/MyViewGroup1: dispatchTouchEvent -> ACTION_DOWN
E/MyView: dispatchTouchEvent -> ACTION_DOWN
E/MyView: onTouchEvent -> ACTION_DOWN
E/MyViewGroup1: onTouchEvent -> ACTION_DOWN
E/MyViewGroup2: onTouchEvent -> ACTION_DOWN
E/MainActivity: onTouchEvent -> ACTION_DOWN
|
ACTION_DOWN事件分发过程如下图所示:
- 在MyViewGroup2的onTouchEvent方法中拦截ACTION_DOWN事件并返回true
修改MyViewGroup2的onTouchEvent方法为上面所说的仅拦截ACTION_DOWN事件的代码。
运行起来后,点击MyView控件时,日志打印如下:
1
2
3
4
5
6
7
|
E/MainActivity: dispatchTouchEvent -> ACTION_DOWN
E/MyViewGroup2: dispatchTouchEvent -> ACTION_DOWN
E/MyViewGroup1: dispatchTouchEvent -> ACTION_DOWN
E/MyView: dispatchTouchEvent -> ACTION_DOWN
E/MyView: onTouchEvent -> ACTION_DOWN
E/MyViewGroup1: onTouchEvent -> ACTION_DOWN
E/MyViewGroup2: onTouchEvent -> ACTION_DOWN
|
ACTION_DOWN事件分发过程如下图所示:
- 在MyViewGroup1的onTouchEvent方法中拦截ACTION_DOWN事件并返回true
修改MyViewGroup1的onTouchEvent方法为上面所说的仅拦截ACTION_DOWN事件的代码。
运行起来后,点击MyView控件时,日志打印如下:
1
2
3
4
5
6
|
E/MainActivity: dispatchTouchEvent -> ACTION_DOWN
E/MyViewGroup2: dispatchTouchEvent -> ACTION_DOWN
E/MyViewGroup1: dispatchTouchEvent -> ACTION_DOWN
E/MyView: dispatchTouchEvent -> ACTION_DOWN
E/MyView: onTouchEvent -> ACTION_DOWN
E/MyViewGroup1: onTouchEvent -> ACTION_DOWN
|
ACTION_DOWN事件分发过程如下图所示:
- 在MyView的onTouchEvent方法中拦截ACTION_DOWN事件并返回true
修改MyView的onTouchEvent方法为上面所说的仅拦截ACTION_DOWN事件的代码。
运行起来后,点击MyView控件时,日志打印如下:
1
2
3
4
5
|
E/MainActivity: dispatchTouchEvent -> ACTION_DOWN
E/MyViewGroup2: dispatchTouchEvent -> ACTION_DOWN
E/MyViewGroup1: dispatchTouchEvent -> ACTION_DOWN
E/MyView: dispatchTouchEvent -> ACTION_DOWN
E/MyView: onTouchEvent -> ACTION_DOWN
|
ACTION_DOWN事件分发过程如下图所示:
通过对日志的观察,不难看出规律,可以得出结论:
无论在哪个控件的onTouchEvent方法中拦截ACTION_DOWN事件并返回true,事件都会直接停止传递,后面的父控件都不会接收到这个事件。
2.2.2、ACTION_DOWN的事件分发过程(包含onInterceptTouchEvent方法的情况)
在onInterceptTouchEvent方法中拦截ACTION_DOWN事件之前,我们需要知道onInterceptTouchEvent方法需要返回boolean类型的返回值,它有3种情况:
其中,情况一和情况三都表示不拦截ACTION_DOWN事件,所以它们的事件分发过程和默认的事件分发过程是一毛一样的,所以这里只分析情况二,下面结论也是针对情况二来讲。
在onInterceptTouchEvent方法中拦截ACTION_DOWN事件有两种方式:一是仅拦截ACTION_DOWN事件;二是拦截所有事件,这样也就包括了ACTION_DOWN事件。
仅拦截ACTION_DOWN事件的代码如下:
1
2
3
4
5
6
7
8
9
|
override fun onInterceptTouchEvent(event: MotionEvent?): Boolean {
when (event?.action) {
MotionEvent.ACTION_DOWN -> {
if (LOG_ACTION_DOWN) Log.e(TAG, "onInterceptTouchEvent -> ACTION_DOWN")
return true;
}
}
return super.onTouchEvent(event)
}
|
拦截所有事件的代码如下:
1
2
3
4
5
6
7
8
|
override fun onInterceptTouchEvent(event: MotionEvent?): Boolean {
when (event?.action) {
MotionEvent.ACTION_DOWN -> {
if (LOG_ACTION_DOWN) Log.e(TAG, "onInterceptTouchEvent -> ACTION_DOWN")
}
}
return true
}
|
两种拦截ACTION_DOWN事件的方式有什么区别呢?
对于ACTION_DOWN事件来说没区别,最终打印结果是一样的,所以本文接下来采用仅拦截ACTION_DOWN事件的方式。
- 在MyViewGroup2的onInterceptTouchEvent方法中拦截ACTION_DOWN事件并返回true
修改MyViewGroup2的onInterceptTouchEvent方法为上面所说的仅拦截ACTION_DOWN事件的代码。
运行起来后,点击MyView控件时,日志打印如下:
1
2
3
4
5
|
E/MainActivity: dispatchTouchEvent -> ACTION_DOWN
E/MyViewGroup2: dispatchTouchEvent -> ACTION_DOWN
E/MyViewGroup2: onInterceptTouchEvent -> ACTION_DOWN
E/MyViewGroup2: onTouchEvent -> ACTION_DOWN
E/MainActivity: onTouchEvent -> ACTION_DOWN
|
ACTION_DOWN事件分发过程如下图所示:
- 在MyViewGroup1的onInterceptTouchEvent方法中拦截ACTION_DOWN事件并返回true
修改MyViewGroup1的onInterceptTouchEvent方法为上面所说的仅拦截ACTION_DOWN事件的代码。
运行起来后,点击MyView控件时,日志打印如下:
1
2
3
4
5
6
7
8
|
E/MainActivity: dispatchTouchEvent -> ACTION_DOWN
E/MyViewGroup2: dispatchTouchEvent -> ACTION_DOWN
E/MyViewGroup2: onInterceptTouchEvent -> ACTION_DOWN
E/MyViewGroup1: dispatchTouchEvent -> ACTION_DOWN
E/MyViewGroup1: onInterceptTouchEvent -> ACTION_DOWN
E/MyViewGroup1: onTouchEvent -> ACTION_DOWN
E/MyViewGroup2: onTouchEvent -> ACTION_DOWN
E/MainActivity: onTouchEvent -> ACTION_DOWN
|
ACTION_DOWN事件分发过程如下图所示:
通过对日志的观察,不难看出规律,可以得出结论:
若在ViewGroup的onInterceptTouchEvent方法中拦截ACTION_DOWN事件,只会改变ACTION_DOWN事件的正常流向,事件会直接流向自己的onTouchEvent方法中,并不会截断事件。
2.2.3、多个方法拦截的情况
一般我们在拦截事件时,都是共同使用onInterceptTouchEvent方法和onTouchEvent方法的,通过在onInterceptTouchEvent方法中返回true,将ACTION_DOWN消息流向自己的onTouchEvent方法中,然后在该onTouchEvent方法中返回true拦截事件。
下面以MyViewGroup1为例,在MyViewGroup1的onInterceptTouchEvent方法中拦截ACTION_DOWN事件并返回true,在MyViewGroup1的onTouchEvent方法中拦截ACTION_DOWN事件并返回true。
修改MyViewGroup1的onInterceptTouchEvent方法和onTouchEvent方法为如下代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean {
when (ev?.action) {
MotionEvent.ACTION_DOWN -> {
if (LOG_INTERCEPT_TOUCH_EVENT && LOG_ACTION_DOWN) Log.e(TAG, "onInterceptTouchEvent -> ACTION_DOWN")
return true
}
}
return super.onInterceptTouchEvent(ev)
}
override fun onTouchEvent(event: MotionEvent?): Boolean {
when (event?.action) {
MotionEvent.ACTION_DOWN -> {
if (LOG_ACTION_DOWN) Log.e(TAG, "onTouchEvent -> ACTION_DOWN")
return true;
}
}
return super.onTouchEvent(event)
}
|
运行起来后,点击MyView控件时,日志打印如下:
1
2
3
4
5
6
|
E/MainActivity: dispatchTouchEvent -> ACTION_DOWN
E/MyViewGroup2: dispatchTouchEvent -> ACTION_DOWN
E/MyViewGroup2: onInterceptTouchEvent -> ACTION_DOWN
E/MyViewGroup1: dispatchTouchEvent -> ACTION_DOWN
E/MyViewGroup1: onInterceptTouchEvent -> ACTION_DOWN
E/MyViewGroup1: onTouchEvent -> ACTION_DOWN
|
ACTION_DOWN事件分发过程如下图所示:
2.2.4、ACTION_MOVE和ACTION_UP的事件分发过程
ACTION_MOVE和ACTION_UP事件的分发过程与之前所讲的ACTION_DOWN事件并不是完全一样的,为了对比两者,我会用黑色箭头表示ACTION_DOWN的事件分发过程,用红色箭头合并表示ACTION_MOVE和ACTION_UP的事件分发过程,为什么两者都用红色箭头表示呢?这是因为ACTION_MOVE和ACTION_UP的事件流向是完全相同的,后面都以ACTION_MOVE事件来讲解。
2.2.4.1、ACTION_MOVE默认的事件分发过程
ACTION_MOVE默认的事件分发过程,和之前所讲的ACTION_DOWN事件默认的事件分发过程是一样的,也就是所有的事件分发方法都不做拦截处理。
修改MainActivity、MyView的dispatchTouchEvent方法、onTouchEvent方法为如下代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
override fun dispatchTouchEvent(ev: MotionEvent?): Boolean {
when (ev?.action) {
MotionEvent.ACTION_DOWN -> {
if (LOG_ACTION_DOWN) Log.e(TAG, "dispatchTouchEvent -> ACTION_DOWN")
}
MotionEvent.ACTION_MOVE -> {
if (LOG_ACTION_MOVE) Log.e(TAG, "dispatchTouchEvent -> ACTION_MOVE")
}
}
return super.dispatchTouchEvent(ev)
}
override fun onTouchEvent(event: MotionEvent?): Boolean {
when (event?.action) {
MotionEvent.ACTION_DOWN -> {
if (LOG_ACTION_MOVE) Log.e(TAG, "onTouchEvent -> ACTION_DOWN")
}
MotionEvent.ACTION_MOVE -> {
if (LOG_ACTION_MOVE) Log.e(TAG, "onTouchEvent -> ACTION_MOVE")
}
}
return super.onTouchEvent(event)
}
|
修改MyViewGroup2、MyViewGroup1的dispatchTouchEvent方法、onInterceptTouchEvent方法、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
28
29
30
31
32
33
34
35
|
override fun dispatchTouchEvent(ev: MotionEvent?): Boolean {
when (ev?.action) {
MotionEvent.ACTION_DOWN -> {
if (LOG_ACTION_DOWN) Log.e(TAG, "dispatchTouchEvent -> ACTION_DOWN")
}
MotionEvent.ACTION_MOVE -> {
if (LOG_ACTION_MOVE) Log.e(TAG, "dispatchTouchEvent -> ACTION_MOVE")
}
}
return super.dispatchTouchEvent(ev)
}
override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean {
when (ev?.action) {
MotionEvent.ACTION_DOWN -> {
if (LOG_INTERCEPT_TOUCH_EVENT && LOG_ACTION_DOWN) Log.e(TAG, "onInterceptTouchEvent -> ACTION_DOWN")
}
MotionEvent.ACTION_MOVE -> {
if (LOG_INTERCEPT_TOUCH_EVENT && LOG_ACTION_MOVE) Log.e(TAG, "onInterceptTouchEvent -> ACTION_MOVE")
}
}
return super.onInterceptTouchEvent(ev)
}
override fun onTouchEvent(event: MotionEvent?): Boolean {
when (event?.action) {
MotionEvent.ACTION_DOWN -> {
if (LOG_ACTION_MOVE) Log.e(TAG, "onTouchEvent -> ACTION_DOWN")
}
MotionEvent.ACTION_MOVE -> {
if (LOG_ACTION_MOVE) Log.e(TAG, "onTouchEvent -> ACTION_MOVE")
}
}
return super.onTouchEvent(event)
}
|
运行起来后,触摸MyView控件时,日志打印如下:
1
2
3
4
5
6
7
8
9
10
11
12
|
MainActivity: dispatchTouchEvent -> ACTION_DOWN
E/MyViewGroup2: dispatchTouchEvent -> ACTION_DOWN
E/MyViewGroup2: onInterceptTouchEvent -> ACTION_DOWN
E/MyViewGroup1: dispatchTouchEvent -> ACTION_DOWN
E/MyViewGroup1: onInterceptTouchEvent -> ACTION_DOWN
E/MyView: dispatchTouchEvent -> ACTION_DOWN
E/MyView: onTouchEvent -> ACTION_DOWN
E/MyViewGroup1: onTouchEvent -> ACTION_DOWN
E/MyViewGroup2: onTouchEvent -> ACTION_DOWN
E/MainActivity: onTouchEvent -> ACTION_DOWN
E/MainActivity: dispatchTouchEvent -> ACTION_MOVE
E/MainActivity: onTouchEvent -> ACTION_MOVE
|
ACTION_MOVE事件分发过程如下图所示:
2.2.4.2、在dispatchTouchEvent方法中拦截ACTION_MOVE事件
按照之前在dispatchTouchEvent方法中拦截ACTION_DOWN事件的写法,同样地,在dispatchTouchEvent方法中拦截ACTION_MOVE事件,比如说修改MyViewGroup2的dispatchTouchEvent方法,你可能会写下如下代码:
1
2
3
4
5
6
7
8
|
override fun dispatchTouchEvent(ev: MotionEvent?): Boolean {
when (ev?.action) {
MotionEvent.ACTION_MOVE -> {
if (LOG_ACTION_MOVE) Log.e(TAG, "dispatchTouchEvent -> ACTION_MOVE")
return true
}
}
return super.dispatchTouchEvent(ev)
|
然而代码运行后,发现并不能如愿地看到想要的ACTION_MOVE日志,也就是说代码没有起作用,这是因为ACTION_MOVE事件根本就不会流到MyViewGroup2的dispatchTouchEvent方法中,前面讲过,默认情况下,ACTION_MOVE事件的流向是从MainActivity的dispatchTouchEvent方法直接到MainActivity的onTouchEvent方法。
那如何让ACTION_MOVE事件继续往子控件流向呢?
解决办法有3种:仅拦截ACTION_DOWN事件、同时拦截ACTION_DOWN事件和ACTION_MOVE事件、拦截所有事件。其中前一种方式与后两种方式对于ACTION_MOVE事件的流向,它们的表现形式是不一样的;后两种方式对于ACTION_MOVE事件来说没区别,最终打印结果是一样的,所以本文后面会介绍第一种方式和第三种方式。
2.2.4.2.1、仅拦截ACTION_DOWN事件
- 在MainActivity的dispatchTouchEvent方法中拦截ACTION_DOWN事件并返回true
修改MainActivity的dispatchTouchEvent方法为如下代码:
1
2
3
4
5
6
7
8
9
10
11
12
|
override fun dispatchTouchEvent(ev: MotionEvent?): Boolean {
when (ev?.action) {
MotionEvent.ACTION_DOWN -> {
if (LOG_ACTION_DOWN) Log.e(TAG, "dispatchTouchEvent -> ACTION_DOWN")
return true
}
MotionEvent.ACTION_MOVE -> {
if (LOG_ACTION_MOVE) Log.e(TAG, "dispatchTouchEvent -> ACTION_MOVE")
}
}
return super.dispatchTouchEvent(ev)
}
|
运行起来后,触摸MyView控件时,日志打印如下:
1
2
3
|
E/MainActivity: dispatchTouchEvent -> ACTION_DOWN
E/MainActivity: dispatchTouchEvent -> ACTION_MOVE
E/MainActivity: onTouchEvent -> ACTION_MOVE
|
ACTION_MOVE事件分发过程如下图所示:
- 在MyViewGroup2的dispatchTouchEvent方法中拦截ACTION_DOWN事件并返回true
修改MyViewGroup2的dispatchTouchEvent方法为同上面MainActivity代码。
运行起来后,触摸MyView控件时,日志打印如下:
1
2
3
4
5
6
|
E/MainActivity: dispatchTouchEvent -> ACTION_DOWN
E/MyViewGroup2: dispatchTouchEvent -> ACTION_DOWN
E/MainActivity: dispatchTouchEvent -> ACTION_MOVE
E/MyViewGroup2: dispatchTouchEvent -> ACTION_MOVE
E/MyViewGroup2: onTouchEvent -> ACTION_MOVE
E/MainActivity: onTouchEvent -> ACTION_MOVE
|
ACTION_MOVE事件分发过程如下图所示:
- 在MyViewGroup1的dispatchTouchEvent方法中拦截ACTION_DOWN事件并返回true
修改MyViewGroup1的dispatchTouchEvent方法为同上面MainActivity代码。
运行起来后,触摸MyView控件时,日志打印如下:
1
2
3
4
5
6
7
8
9
10
|
E/MainActivity: dispatchTouchEvent -> ACTION_DOWN
E/MyViewGroup2: dispatchTouchEvent -> ACTION_DOWN
E/MyViewGroup2: onInterceptTouchEvent -> ACTION_DOWN
E/MyViewGroup1: dispatchTouchEvent -> ACTION_DOWN
E/MainActivity: dispatchTouchEvent -> ACTION_MOVE
E/MyViewGroup2: dispatchTouchEvent -> ACTION_MOVE
E/MyViewGroup2: onInterceptTouchEvent -> ACTION_MOVE
E/MyViewGroup1: dispatchTouchEvent -> ACTION_MOVE
E/MyViewGroup1: onTouchEvent -> ACTION_MOVE
E/MainActivity: onTouchEvent -> ACTION_MOVE
|
ACTION_MOVE事件分发过程如下图所示:
- 在MyView的dispatchTouchEvent方法中拦截ACTION_DOWN事件并返回true
修改MyView的dispatchTouchEvent方法为同上面MainActivity代码。
运行起来后,触摸MyView控件时,日志打印如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
E/MainActivity: dispatchTouchEvent -> ACTION_DOWN
E/MyViewGroup2: dispatchTouchEvent -> ACTION_DOWN
E/MyViewGroup2: onInterceptTouchEvent -> ACTION_DOWN
E/MyViewGroup1: dispatchTouchEvent -> ACTION_DOWN
E/MyViewGroup1: onInterceptTouchEvent -> ACTION_DOWN
E/MyView: dispatchTouchEvent -> ACTION_DOWN
E/MainActivity: dispatchTouchEvent -> ACTION_MOVE
E/MyViewGroup2: dispatchTouchEvent -> ACTION_MOVE
E/MyViewGroup2: onInterceptTouchEvent -> ACTION_MOVE
E/MyViewGroup1: dispatchTouchEvent -> ACTION_MOVE
E/MyViewGroup1: onInterceptTouchEvent -> ACTION_MOVE
E/MyView: dispatchTouchEvent -> ACTION_MOVE
E/MyView: onTouchEvent -> ACTION_MOVE
E/MainActivity: onTouchEvent -> ACTION_MOVE
|
ACTION_MOVE事件分发过程如下图所示:
通过对日志的观察,不难看出规律,可以得出结论:
如果View不消耗除ACTION_DOWN以外的其他事件,那么这个点击事件会消失,此时父元素的onTouchEvent并不会被调用,并且当前View可以持续收到后续的事件,最终这些消失的点击事件会传递给Activity处理。
2.2.4.2.2、拦截所有事件
- 在MainActivity的dispatchTouchEvent方法中拦截所有事件并返回true
修改MainActivity的dispatchTouchEvent方法为如下代码:
1
2
3
4
5
6
7
8
9
10
11
|
override fun dispatchTouchEvent(ev: MotionEvent?): Boolean {
when (ev?.action) {
MotionEvent.ACTION_DOWN -> {
if (LOG_ACTION_DOWN) Log.e(TAG, "dispatchTouchEvent -> ACTION_DOWN")
}
MotionEvent.ACTION_MOVE -> {
if (LOG_ACTION_MOVE) Log.e(TAG, "dispatchTouchEvent -> ACTION_MOVE")
}
}
return true
}
|
运行起来后,触摸MyView控件时,日志打印如下:
1
2
|
E/MainActivity: dispatchTouchEvent -> ACTION_DOWN
E/MainActivity: dispatchTouchEvent -> ACTION_MOVE
|
ACTION_MOVE事件分发过程如下图所示:
- 在MyViewGroup2的dispatchTouchEvent方法中拦截所有事件并返回true
修改MyViewGroup2的dispatchTouchEvent方法为同上面MainActivity代码。
运行起来后,触摸MyView控件时,日志打印如下:
1
2
3
4
|
E/MainActivity: dispatchTouchEvent -> ACTION_DOWN
E/MyViewGroup2: dispatchTouchEvent -> ACTION_DOWN
E/MainActivity: dispatchTouchEvent -> ACTION_MOVE
E/MyViewGroup2: dispatchTouchEvent -> ACTION_MOVE
|
ACTION_MOVE事件分发过程如下图所示:
- 在MyViewGroup1的dispatchTouchEvent方法中拦截所有事件并返回true
修改MyViewGroup1的dispatchTouchEvent方法为同上面MainActivity代码。
运行起来后,触摸MyView控件时,日志打印如下:
1
2
3
4
5
6
7
8
9
|
E/MainActivity: dispatchTouchEvent -> ACTION_DOWN
E/MyViewGroup2: dispatchTouchEvent -> ACTION_DOWN
E/MyViewGroup2: onInterceptTouchEvent -> ACTION_DOWN
E/MyViewGroup1: dispatchTouchEvent -> ACTION_DOWN
E/MainActivity: dispatchTouchEvent -> ACTION_MOVE
E/MyViewGroup2: dispatchTouchEvent -> ACTION_MOVE
E/MyViewGroup2: onInterceptTouchEvent -> ACTION_MOVE
E/MyViewGroup1: dispatchTouchEvent -> ACTION_MOVE
|
ACTION_MOVE事件分发过程如下图所示:
- 在MyView的dispatchTouchEvent方法中拦截所有事件并返回true
修改MyView的dispatchTouchEvent方法为同上面MainActivity代码。
运行起来后,触摸MyView控件时,日志打印如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
E/MainActivity: dispatchTouchEvent -> ACTION_DOWN
E/MyViewGroup2: dispatchTouchEvent -> ACTION_DOWN
E/MyViewGroup2: onInterceptTouchEvent -> ACTION_DOWN
E/MyViewGroup1: dispatchTouchEvent -> ACTION_DOWN
E/MyViewGroup1: onInterceptTouchEvent -> ACTION_DOWN
E/MyView: dispatchTouchEvent -> ACTION_DOWN
E/MainActivity: dispatchTouchEvent -> ACTION_MOVE
E/MyViewGroup2: dispatchTouchEvent -> ACTION_MOVE
E/MyViewGroup2: onInterceptTouchEvent -> ACTION_MOVE
E/MyViewGroup1: dispatchTouchEvent -> ACTION_MOVE
E/MyViewGroup1: onInterceptTouchEvent -> ACTION_MOVE
E/MyView: dispatchTouchEvent -> ACTION_MOVE
|
ACTION_MOVE事件分发过程如下图所示:
通过对日志的观察,不难看出规律,可以得出结论:
在dispatchTouchEvent方法中返回true拦截事件之后,ACTION_MOVE事件的流向与ACTION_DOWN事件的完全相同,事件会直接停止传递,后面的子控件都不会接收到这个事件。
2.2.4.3、在onTouchEvent方法中拦截ACTION_MOVE事件
和在dispatchTouchEvent方法中拦截ACTION_MOVE事件相似,对于希望在onTouchEvent方法中拦截ACTION_MOVE事件,解决办法有3种:仅拦截ACTION_DOWN事件、同时拦截ACTION_DOWN事件和ACTION_MOVE事件、拦截所有事件。又因为这里只有仅拦截ACTION_DOWN事件和拦截所有事件的表现是不一样的,所以本文只会演示这2种。
2.2.4.3.1、仅拦截ACTION_DOWN事件
- 在MainActivity的onTouchEvent方法中拦截ACTION_DOWN事件并返回true
修改MainActivity的onTouchEvent方法为如下代码:
1
2
3
4
5
6
7
8
9
10
11
12
|
override fun onTouchEvent(ev: MotionEvent?): Boolean {
when (ev?.action) {
MotionEvent.ACTION_DOWN -> {
if (LOG_ACTION_DOWN) Log.e(TAG, "onTouchEvent -> ACTION_DOWN")
return true
}
MotionEvent.ACTION_MOVE -> {
if (LOG_ACTION_MOVE) Log.e(TAG, "onTouchEvent -> ACTION_MOVE")
}
}
return super.onTouchEvent(ev)
}
|
运行起来后,触摸MyView控件时,日志打印如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
E/MainActivity: dispatchTouchEvent -> ACTION_DOWN
E/MyViewGroup2: dispatchTouchEvent -> ACTION_DOWN
E/MyViewGroup2: onInterceptTouchEvent -> ACTION_DOWN
E/MyViewGroup1: dispatchTouchEvent -> ACTION_DOWN
E/MyViewGroup1: onInterceptTouchEvent -> ACTION_DOWN
E/MyView: dispatchTouchEvent -> ACTION_DOWN
E/MyView: onTouchEvent -> ACTION_DOWN
E/MyViewGroup1: onTouchEvent -> ACTION_DOWN
E/MyViewGroup2: onTouchEvent -> ACTION_DOWN
E/MainActivity: onTouchEvent -> ACTION_DOWN
E/MainActivity: dispatchTouchEvent -> ACTION_MOVE
E/MainActivity: onTouchEvent -> ACTION_MOVE
|
ACTION_MOVE事件分发过程如下图所示:
- 在MyViewGroup2的onTouchEvent方法中拦截ACTION_DOWN事件并返回true
修改MyViewGroup2的onTouchEvent方法为同上面MainActivity代码。
运行起来后,触摸MyView控件时,日志打印如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
E/MainActivity: dispatchTouchEvent -> ACTION_DOWN
E/MyViewGroup2: dispatchTouchEvent -> ACTION_DOWN
E/MyViewGroup2: onInterceptTouchEvent -> ACTION_DOWN
E/MyViewGroup1: dispatchTouchEvent -> ACTION_DOWN
E/MyViewGroup1: onInterceptTouchEvent -> ACTION_DOWN
E/MyView: dispatchTouchEvent -> ACTION_DOWN
E/MyView: onTouchEvent -> ACTION_DOWN
E/MyViewGroup1: onTouchEvent -> ACTION_DOWN
E/MyViewGroup2: onTouchEvent -> ACTION_DOWN
E/MainActivity: dispatchTouchEvent -> ACTION_MOVE
E/MyViewGroup2: dispatchTouchEvent -> ACTION_MOVE
E/MyViewGroup2: onTouchEvent -> ACTION_MOVE
E/MainActivity: onTouchEvent -> ACTION_MOVE
|
ACTION_MOVE事件分发过程如下图所示:
- 在MyViewGroup1的onTouchEvent方法中拦截ACTION_DOWN事件并返回true
修改MyViewGroup1的onTouchEvent方法为同上面MainActivity代码。
运行起来后,触摸MyView控件时,日志打印如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
E/MainActivity: dispatchTouchEvent -> ACTION_DOWN
E/MyViewGroup2: dispatchTouchEvent -> ACTION_DOWN
E/MyViewGroup2: onInterceptTouchEvent -> ACTION_DOWN
E/MyViewGroup1: dispatchTouchEvent -> ACTION_DOWN
E/MyViewGroup1: onInterceptTouchEvent -> ACTION_DOWN
E/MyView: dispatchTouchEvent -> ACTION_DOWN
E/MyView: onTouchEvent -> ACTION_DOWN
E/MyViewGroup1: onTouchEvent -> ACTION_DOWN
E/MainActivity: dispatchTouchEvent -> ACTION_MOVE
E/MyViewGroup2: dispatchTouchEvent -> ACTION_MOVE
E/MyViewGroup2: onInterceptTouchEvent -> ACTION_MOVE
E/MyViewGroup1: dispatchTouchEvent -> ACTION_MOVE
E/MyViewGroup1: onTouchEvent -> ACTION_MOVE
E/MainActivity: onTouchEvent -> ACTION_MOVE
|
ACTION_MOVE事件分发过程如下图所示:
- 在MyView的onTouchEvent方法中拦截ACTION_DOWN事件并返回true
修改MyView的onTouchEvent方法为同上面MainActivity代码。
运行起来后,触摸MyView控件时,日志打印如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
E/MainActivity: dispatchTouchEvent -> ACTION_DOWN
E/MyViewGroup2: dispatchTouchEvent -> ACTION_DOWN
E/MyViewGroup2: onInterceptTouchEvent -> ACTION_DOWN
E/MyViewGroup1: dispatchTouchEvent -> ACTION_DOWN
E/MyViewGroup1: onInterceptTouchEvent -> ACTION_DOWN
E/MyView: dispatchTouchEvent -> ACTION_DOWN
E/MyView: onTouchEvent -> ACTION_DOWN
E/MainActivity: dispatchTouchEvent -> ACTION_MOVE
E/MyViewGroup2: dispatchTouchEvent -> ACTION_MOVE
E/MyViewGroup2: onInterceptTouchEvent -> ACTION_MOVE
E/MyViewGroup1: dispatchTouchEvent -> ACTION_MOVE
E/MyViewGroup1: onInterceptTouchEvent -> ACTION_MOVE
E/MyView: dispatchTouchEvent -> ACTION_MOVE
E/MyView: onTouchEvent -> ACTION_MOVE
E/MainActivity: onTouchEvent -> ACTION_MOVE
|
ACTION_MOVE事件分发过程如下图所示:
通过对日志的观察,不难看出规律,可以得出结论:
此处结论同2.2.4.2.1小节。
2.2.4.3.2、拦截所有事件
- 在MainActivity的onTouchEvent方法中拦截所有事件并返回true
修改MainActivity的onTouchEvent方法为如下代码:
1
2
3
4
5
6
7
8
9
10
11
|
override fun onTouchEvent(event: MotionEvent?): Boolean {
when (event?.action) {
MotionEvent.ACTION_DOWN -> {
Log.e(TAG, "onTouchEvent -> ACTION_DOWN")
}
MotionEvent.ACTION_MOVE -> {
Log.e(TAG, "onTouchEvent -> ACTION_MOVE")
}
}
return true
}
|
运行起来后,触摸MyView控件时,日志打印如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
E/MainActivity: dispatchTouchEvent -> ACTION_DOWN
E/MyViewGroup2: dispatchTouchEvent -> ACTION_DOWN
E/MyViewGroup2: onInterceptTouchEvent -> ACTION_DOWN
E/MyViewGroup1: dispatchTouchEvent -> ACTION_DOWN
E/MyViewGroup1: onInterceptTouchEvent -> ACTION_DOWN
E/MyView: dispatchTouchEvent -> ACTION_DOWN
E/MyView: onTouchEvent -> ACTION_DOWN
E/MyViewGroup1: onTouchEvent -> ACTION_DOWN
E/MyViewGroup2: onTouchEvent -> ACTION_DOWN
E/MainActivity: onTouchEvent -> ACTION_DOWN
E/MainActivity: dispatchTouchEvent -> ACTION_MOVE
E/MainActivity: onTouchEvent -> ACTION_MOVE
|
ACTION_MOVE事件分发过程如下图所示:
- 在MyViewGroup2的onTouchEvent方法中拦截所有事件并返回true
修改MyViewGroup2的onTouchEvent方法为同上面MainActivity代码。
运行起来后,触摸MyView控件时,日志打印如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
E/MainActivity: dispatchTouchEvent -> ACTION_DOWN
E/MyViewGroup2: dispatchTouchEvent -> ACTION_DOWN
E/MyViewGroup2: onInterceptTouchEvent -> ACTION_DOWN
E/MyViewGroup1: dispatchTouchEvent -> ACTION_DOWN
E/MyViewGroup1: onInterceptTouchEvent -> ACTION_DOWN
E/MyView: dispatchTouchEvent -> ACTION_DOWN
E/MyView: onTouchEvent -> ACTION_DOWN
E/MyViewGroup1: onTouchEvent -> ACTION_DOWN
E/MyViewGroup2: onTouchEvent -> ACTION_DOWN
E/MainActivity: dispatchTouchEvent -> ACTION_MOVE
E/MyViewGroup2: dispatchTouchEvent -> ACTION_MOVE
E/MyViewGroup2: onTouchEvent -> ACTION_MOVE
|
ACTION_MOVE事件分发过程如下图所示:
- 在MyViewGroup1的onTouchEvent方法中拦截所有事件并返回true
修改MyViewGroup1的onTouchEvent方法为同上面MainActivity代码。
运行起来后,触摸MyView控件时,日志打印如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
E/MainActivity: dispatchTouchEvent -> ACTION_DOWN
E/MyViewGroup2: dispatchTouchEvent -> ACTION_DOWN
E/MyViewGroup2: onInterceptTouchEvent -> ACTION_DOWN
E/MyViewGroup1: dispatchTouchEvent -> ACTION_DOWN
E/MyViewGroup1: onInterceptTouchEvent -> ACTION_DOWN
E/MyView: dispatchTouchEvent -> ACTION_DOWN
E/MyView: onTouchEvent -> ACTION_DOWN
E/MyViewGroup1: onTouchEvent -> ACTION_DOWN
E/MainActivity: dispatchTouchEvent -> ACTION_MOVE
E/MyViewGroup2: dispatchTouchEvent -> ACTION_MOVE
E/MyViewGroup2: onInterceptTouchEvent -> ACTION_MOVE
E/MyViewGroup1: dispatchTouchEvent -> ACTION_MOVE
E/MyViewGroup1: onTouchEvent -> ACTION_MOVE
|
ACTION_MOVE事件分发过程如下图所示:
- 在MyView的onTouchEvent方法中拦截所有事件并返回true
修改MyView的onTouchEvent方法为同上面MainActivity代码。
运行起来后,触摸MyView控件时,日志打印如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
E/MainActivity: dispatchTouchEvent -> ACTION_DOWN
E/MyViewGroup2: dispatchTouchEvent -> ACTION_DOWN
E/MyViewGroup2: onInterceptTouchEvent -> ACTION_DOWN
E/MyViewGroup1: dispatchTouchEvent -> ACTION_DOWN
E/MyViewGroup1: onInterceptTouchEvent -> ACTION_DOWN
E/MyView: dispatchTouchEvent -> ACTION_DOWN
E/MyView: onTouchEvent -> ACTION_DOWN
E/MainActivity: dispatchTouchEvent -> ACTION_MOVE
E/MyViewGroup2: dispatchTouchEvent -> ACTION_MOVE
E/MyViewGroup2: onInterceptTouchEvent -> ACTION_MOVE
E/MyViewGroup1: dispatchTouchEvent -> ACTION_MOVE
E/MyViewGroup1: onInterceptTouchEvent -> ACTION_MOVE
E/MyView: dispatchTouchEvent -> ACTION_MOVE
E/MyView: onTouchEvent -> ACTION_MOVE
|
ACTION_MOVE事件分发过程如下图所示:
通过对日志的观察,不难看出规律,可以得出结论:
无论ACTION_DOWN消息的流向是怎样的,只要最终流到onTouchEvent函数中就行。假设控件A最终在onTouchEvent函数中消费了ACTION_DOWN消息,那么ACTION_MOVE消息的流向就是先流到控件A的dispatchTouchEvent函数中,最终直接流到控件A的onTouchEvent函数中,进而消息停止传递。
2.2.4.4、多个方法拦截的情况
注:下面演示都是拦截所有事件
2.2.4.4.1、组合dispatchTouchEvent方法和onTouchEvent方法拦截ACTION_MOVE事件
在MyView的dispatchTouchEvent方法中返回false,而在MyViewGroup2的onTouchEvent方法中返回true拦截ACTION_MOVE事件。
修改MyView的dispatchTouchEvent方法为如下代码:
1
2
3
4
5
6
7
8
9
10
11
|
override fun dispatchTouchEvent(ev: MotionEvent?): Boolean {
when (ev?.action) {
MotionEvent.ACTION_DOWN -> {
Log.e(TAG, "dispatchTouchEvent -> ACTION_DOWN")
}
MotionEvent.ACTION_MOVE -> {
Log.e(TAG, "dispatchTouchEvent -> ACTION_MOVE")
}
}
return false
}
|
修改MyViewGroup2的onTouchEvent方法为如下代码:
1
2
3
4
5
6
7
8
9
10
11
|
override fun onTouchEvent(event: MotionEvent?): Boolean {
when (event?.action) {
MotionEvent.ACTION_DOWN -> {
Log.e(TAG, "onTouchEvent -> ACTION_DOWN")
}
MotionEvent.ACTION_MOVE -> {
Log.e(TAG, "onTouchEvent -> ACTION_MOVE")
}
}
return true
}
|
运行起来后,触摸MyView控件时,日志打印如下:
1
2
3
4
5
6
7
8
9
10
11
12
|
E/MainActivity: dispatchTouchEvent -> ACTION_DOWN
E/MyViewGroup2: dispatchTouchEvent -> ACTION_DOWN
E/MyViewGroup2: onInterceptTouchEvent -> ACTION_DOWN
E/MyViewGroup1: dispatchTouchEvent -> ACTION_DOWN
E/MyViewGroup1: onInterceptTouchEvent -> ACTION_DOWN
E/MyView: dispatchTouchEvent -> ACTION_DOWN
E/MyViewGroup1: onTouchEvent -> ACTION_DOWN
E/MyViewGroup2: onTouchEvent -> ACTION_DOWN
E/MainActivity: dispatchTouchEvent -> ACTION_MOVE
E/MyViewGroup2: dispatchTouchEvent -> ACTION_MOVE
E/MyViewGroup2: onTouchEvent -> ACTION_MOVE
|
ACTION_MOVE事件分发过程如下图所示:
2.2.4.4.2、组合onInterceptTouchEvent方法和onTouchEvent方法拦截ACTION_MOVE事件
在MyViewGroup1的onInterceptTouchEvent方法中返回true,而在MyViewGroup2的onTouchEvent方法中返回true拦截ACTION_MOVE事件。
修改MyViewGroup1的onInterceptTouchEvent方法为如下代码:
1
2
3
4
5
6
7
8
9
10
11
|
override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean {
when (ev?.action) {
MotionEvent.ACTION_DOWN -> {
Log.e(TAG, "onInterceptTouchEvent -> ACTION_DOWN")
}
MotionEvent.ACTION_MOVE -> {
Log.e(TAG, "onInterceptTouchEvent -> ACTION_MOVE")
}
}
return true
}
|
修改MyViewGroup2的onTouchEvent方法为如下代码:
1
2
3
4
5
6
7
8
9
10
11
|
override fun onTouchEvent(event: MotionEvent?): Boolean {
when (event?.action) {
MotionEvent.ACTION_DOWN -> {
Log.e(TAG, "onTouchEvent -> ACTION_DOWN")
}
MotionEvent.ACTION_MOVE -> {
Log.e(TAG, "onTouchEvent -> ACTION_MOVE")
}
}
return true
}
|
运行起来后,触摸MyView控件时,日志打印如下:
1
2
3
4
5
6
7
8
9
10
11
|
E/MainActivity: dispatchTouchEvent -> ACTION_DOWN
E/MyViewGroup2: dispatchTouchEvent -> ACTION_DOWN
E/MyViewGroup2: onInterceptTouchEvent -> ACTION_DOWN
E/MyViewGroup1: dispatchTouchEvent -> ACTION_DOWN
E/MyViewGroup1: onInterceptTouchEvent -> ACTION_DOWN
E/MyViewGroup1: onTouchEvent -> ACTION_DOWN
E/MyViewGroup2: onTouchEvent -> ACTION_DOWN
E/MainActivity: dispatchTouchEvent -> ACTION_MOVE
E/MyViewGroup2: dispatchTouchEvent -> ACTION_MOVE
E/MyViewGroup2: onTouchEvent -> ACTION_MOVE
|
ACTION_MOVE事件分发过程如下图所示:
2.2.4.4.3、一种特殊情况
在MyViewGroup2的onInterceptTouchEvent方法中拦截ACTION_MOVE事件返回true,而在MyView的onTouchEvent方法中返回true*。
修改MyViewGroup2的onInterceptTouchEvent方法为如下代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean {
when (ev?.action) {
MotionEvent.ACTION_DOWN -> {
Log.e(TAG, "onInterceptTouchEvent -> ACTION_DOWN")
}
MotionEvent.ACTION_MOVE -> {
Log.e(TAG, "onInterceptTouchEvent -> ACTION_MOVE")
return true
}
MotionEvent.ACTION_CANCEL -> {
Log.e(TAG, "onInterceptTouchEvent -> ACTION_CANCEL")
}
}
return super.onInterceptTouchEvent(ev)
}
|
修改MyView的onTouchEvent方法为如下代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
override fun onTouchEvent(event: MotionEvent?): Boolean {
when (event?.action) {
MotionEvent.ACTION_DOWN -> {
Log.e(TAG, "onTouchEvent -> ACTION_DOWN")
}
MotionEvent.ACTION_MOVE -> {
Log.e(TAG, "onTouchEvent -> ACTION_MOVE")
}
MotionEvent.ACTION_CANCEL -> {
Log.e(TAG, "onInterceptTouchEvent -> ACTION_CANCEL")
}
}
return true
}
|
运行起来后,触摸MyView控件时,日志打印如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
E/MainActivity: dispatchTouchEvent -> ACTION_DOWN
E/MyViewGroup2: dispatchTouchEvent -> ACTION_DOWN
E/MyViewGroup2: onInterceptTouchEvent -> ACTION_DOWN
E/MyViewGroup1: dispatchTouchEvent -> ACTION_DOWN
E/MyViewGroup1: onInterceptTouchEvent -> ACTION_DOWN
E/MyView: dispatchTouchEvent -> ACTION_DOWN
E/MyView: onTouchEvent -> ACTION_DOWN
E/MainActivity: dispatchTouchEvent -> ACTION_MOVE
E/MyViewGroup2: dispatchTouchEvent -> ACTION_MOVE
E/MyViewGroup2: onInterceptTouchEvent -> ACTION_MOVE
E/MyViewGroup1: dispatchTouchEvent -> ACTION_CANCEL
E/MyViewGroup1: onInterceptTouchEvent -> ACTION_CANCEL
E/MyView: dispatchTouchEvent -> ACTION_CANCEL
E/MyView: onTouchEvent -> ACTION_CANCEL
|
ACTION_MOVE事件分发过程如下图所示:
本来ACTION_MOVE事件依然会从MainActivity的dispatchTouchEvent方法流向子控件,但是在到达MyViewGroup2的onInterceptTouchEvent方法时,ACTION_MOVE事件被拦截了。到这里,这次的ACTION_MOVE事件就没有了,变成了ACTION_CANCEL事件继续向子控件传递,一直传递到ACTION_MOVE事件原本要传递的位置,通知所有被截断的子控件,它们的事件取消了,后面没有事件再传递过来。
三、从源码的角度去看事件分发的过程
3.1、Activity的事件分发过程
当用户触摸屏幕的时候,触摸事件就会传递给当前的Activity,由Activity的dispatchTouchEvent方法进行事件分发,所以先从该方法进行分析。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
// android.app.Activity
public boolean dispatchTouchEvent(MotionEvent ev) {
// 分析1
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
// 分析2
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
// 分析3
return onTouchEvent(ev);
}
|
分析1:这里是个if条件判断,如果触摸事件的类型是ACTION_DOWN,那么调用onUserInteraction方法,onUserInteraction方法的解释如下:
1
2
3
4
5
6
7
|
// android.app.Activity
// 当用户与屏幕交互(例如触摸、键盘输入或轨迹球滚动)时,系统会调用该方法
// 此回调和onUserLeaveHint方法旨在帮助Activity智能管理状态栏通知;具体来说,用于帮助Activity确定取消通知的适当时间
// 所有对Activity的onUserLeaveHint回调的调用都将伴随对onUserInteraction调用。这确保Activity将被告知相关的用户活动,例如下拉通知窗格并触摸那里的项目
public void onUserInteraction() {
}
|
分析2:这里有也是个if条件判断,它调用了getWindow的superDispatchTouchEvent方法,一旦if条件为true成立,那么直接return true,整个dispatchTouchEvent方法便会执行完毕,后面的onTouchEvent方法不会被触发。
继续看getWindow方法,代码如下:
1
2
3
4
5
|
// android.app.Activity
public Window getWindow() {
return mWindow;
}
|
getWindow方法返回的是一个mWindow,而mWindow的初始化在Activity的attach方法,代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
// android.app.Activity
final void attach(Context context, ActivityThread aThread,
Instrumentation instr, IBinder token, int ident,
Application application, Intent intent, ActivityInfo info,
CharSequence title, Activity parent, String id,
NonConfigurationInstances lastNonConfigurationInstances,
Configuration config, String referrer, IVoiceInteractor voiceInteractor,
Window window, ActivityConfigCallback activityConfigCallback, IBinder assistToken,
IBinder shareableActivityToken) {
attachBaseContext(context);
mFragments.attachHost(null /*parent*/);
mWindow = new PhoneWindow(this, window, activityConfigCallback);
mWindow.setWindowControllerCallback(mWindowControllerCallback);
mWindow.setCallback(this);
mWindow.setOnWindowDismissedCallback(this);
// ...
}
|
可以看到,mWindow是一个PhoneWindow对象,也就是说之前if条件中getWindow的superDispatchTouchEvent方法实际上是调用了PhoneWindow对象的superDispatchTouchEvent方法,那么继续看PhoneWindow对象的superDispatchTouchEvent方法,代码如下:
1
2
3
4
5
6
|
// com.android.internal.policy.PhoneWindow
@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
return mDecor.superDispatchTouchEvent(event);
}
|
PhoneWindow对象的superDispatchTouchEvent方法又转接给了mDecor的superDispatchTouchEvent方法,而mDecor是一个DecorView对象,它是窗口的顶层视图,包含窗口装饰,DecorView初始化的代码如下:
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
|
// com.android.internal.policy.PhoneWindow
@Override
public void setContentView(int layoutResID) {
// Note: FEATURE_CONTENT_TRANSITIONS may be set in the
// decor, when theme attributes and the like are crysta
// before this happens.
if (mContentParent == null) {
installDecor();
} else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
mContentParent.removeAllViews();
}
// ...
}
private void installDecor() {
mForceDecorInstall = false;
if (mDecor == null) {
mDecor = generateDecor(-1);
mDecor.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);
mDecor.setIsRootNamespace(true);
if (!mInvalidatePanelMenuPosted && mInvalidatePanelMenuFeatures != 0) {
mDecor.postOnAnimation(mInvalidatePanelMenuRunnable);
}
} else {
mDecor.setWindow(this);
}
// ...
}
protected DecorView generateDecor(int featureId) {
// System process doesn't have application context and in that case we need to directly
// the context we have. Otherwise we want the application context, so we don't cling to
// activity.
// ...
return new DecorView(context, featureId, this, getAttributes());
}
|
DecorView是在installDecor方法中被初始化的,此处代码和Activity的setContentView方法的源码相关联,只是这里的分析不是本文的重点,所以不会展开讲解。
OK,回到之前流程,那么调用的是DecorView的superDispatchTouchEvent方法,代码如下:
1
2
3
4
5
6
7
8
9
|
// com.android.internal.policy.DecorView
public class DecorView extends FrameLayout implements RootViewSurfaceTaker, WindowCallbacks {
public boolean superDispatchTouchEvent(MotionEvent event) {
return super.dispatchTouchEvent(event);
}
}
|
DecorView继承自FrameLayout,FrameLayout继承自ViewGroup,而FrameLayout没有重写dispatchTouchEvent方法,那么super.dispatchTouchEvent调用的就是ViewGroup的dispatchTouchEvent方法了。
至此,getWindow的superDispatchTouchEvent方法让触摸事件的分发从Activity流转到了ViewGroup。
分析3:如果getWindow的superDispatchTouchEvent方法返回false,那么if条件判断不成立,就会继续执行Activity的onTouchEvent方法。
继续看Activity的onTouchEvent方法,代码如下:
1
2
3
4
5
6
7
8
9
10
|
// android.app.Activity
public boolean onTouchEvent(MotionEvent event) {
if (mWindow.shouldCloseOnTouch(this, event)) {
finish();
return true;
}
return false;
}
|
这里调用了mWindow的shouldCloseOnTouch方法,我们知道,mWindow的实现类是PhoneWindow,所以shouldCloseOnTouch方法的具体实现应该就在PhoneWindow对象里,可惜PhoneWindow中并没有实现该方法,所以只能找Window对象本身了,果然Window类下有shouldCloseOnTouch方法的实现,代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
|
// android.view.Window
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
public boolean shouldCloseOnTouch(Context context, MotionEvent event) {
final boolean isOutside =
event.getAction() == MotionEvent.ACTION_UP && isOutOfBounds(context, event)
|| event.getAction() == MotionEvent.ACTION_OUTSIDE;
if (mCloseOnTouchOutside && peekDecorView() != null && isOutside) {
return true;
}
return false;
}
|
isOutside表示触摸位置是否在DecorView的外部,它的判断逻辑为:如果触摸事件为ACTION_UP事件并且触摸事件的坐标x,y超出了DecorView边界时,则认为触摸位置在DecorView的外部;如果触摸事件为ACTION_OUTSIDE,那么也认为触摸位置在DecorView的外部。
然后来到if条件的判断了,第一个条件是一个mCloseOnTouchOutside布尔值,它表示点击DecorView外部时是否可以关闭。如果将Activity设置成Dialog样式的时候,可以通过setCloseOnTouchOutside方法设置mCloseOnTouchOutside布尔值为true;第二个条件是一个peekDecorView方法,它返回的是mDecor对象,也就是判断mDecor对象是否为null;第三个条件就是我们的isOutside值了。
如果同时满足上面所说的3个条件,那么整个shouldCloseOnTouch方法就会返回true,否则返回false。但是一般情况下很少将Activity设置为Dialog样式,所以也就用不到setCloseOnTouchOutside方法,所以mCloseOnTouchOutside的值false,那么if条件判断为false,整个shouldCloseOnTouch方法返回false,所以最终onTouchEvent方法也是返回false。
3.2、ViewGroup的事件分发过程
上面分析了Activity的事件分发过程,可以知道,getWindow的superDispatchTouchEvent方法让触摸事件的分发从Activity流转到了ViewGroup,那么继续看ViewGroup的dispatchTouchEvent方法,因为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
|
// android.view.ViewGroup
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
// ...
// 分析1
boolean handled = false;
if (onFilterTouchEventForSecurity(ev)) {
final int action = ev.getAction();
final int actionMasked = action & MotionEvent.ACTION_MASK;
// 分析2
// Handle an initial down.
if (actionMasked == MotionEvent.ACTION_DOWN) {
// Throw away all previous state when starting a new touch gesture.
// The framework may have dropped the up or cancel event for the previous gesture
// due to an app switch, ANR, or some other state change.
cancelAndClearTouchTargets(ev);
resetTouchState();
}
// 分析3
// Check for interception.
final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
// 分析3.1
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (!disallowIntercept) {
// 分析3.1.1
intercepted = onInterceptTouchEvent(ev);
ev.setAction(action); // restore action in case it was changed
} else {
// 分析3.1.2
intercepted = false;
}
} else {
// There are no touch targets and this action is not an initial down
// so this view group continues to intercept touches.
// 分析3.2
intercepted = true;
}
// ...
}
// ...
return handled;
}
|
分析1:handled布尔值用来标记事件分发的处理结果,并最终作为dispatchTouchEvent方法的返回值;onFilterTouchEventForSecurity方法是做一个安全性检查,默认返回true;但是在我们执行敏感控件的点击事件时,有可能会被恶意软件在此之上加上一个不可接受事件的窗口,骗取我们的点击,那么解决办法是调用View的setFilterTouchesWhenObscured方法设置true,它会往View的setFlags方法中添加FILTER_TOUCHES_WHEN_OBSCURED,表示当视图的窗口被另一个可见窗口遮挡时框架丢弃触摸事件,这时onFilterTouchEventForSecurity方法就会返回false,从而dispatchTouchEvent方法返回false。
分析2:if判断如果触摸事件的类型为ACTION_DOWN,表示新的ACTION_DOWN事件来了,也就是同一序列的触摸事件的起点,所以需要调用cancelAndClearTouchTargets方法来取消并清除所有触摸目标,以及调用resetTouchState方法来重置所有触摸状态以准备新的循环。这两方法里面的重点是清空mFirstTouchTarget,重置FLAG_DISALLOW_INTERCEPT标记,这里cancelAndClearTouchTargets方法和resetTouchState方法的源码就不列出来了,可自行查阅。
分析3:intercepted布尔值用来表示是否拦截事件;接下来是一个if判断,条件一是触摸事件的类型为ACTION_DOWN,条件二是mFirstTouchTarget != null。mFirstTouchTarget和后面的代码逻辑有关,它的作用就是当ViewGroup不拦截事件并将事件交由子元素处理时,mFirstTouchTarget会被赋值并指向子元素,这时mFirstTouchTarget != null成立。
接下来,如果触摸事件的类型为ACTION_DOWN,或者mFirstTouchTarget != null时,就会进入分析3.1,布尔值disallowIntercept用来表示不允许父View拦截事件,它和FLAG_DISALLOW_INTERCEPT这个标志位有关,并且该标志位的位运算可以通过requestDisallowInterceptTouchEvent方法设置,从而改变disallowIntercept的值。
默认情况下,也就是没设置FLAG_DISALLOW_INTERCEPT标志位时,disallowIntercept的值为false,此时就会进入分析3.1.1,接着调用onInterceptTouchEvent方法拦截事件,如果onInterceptTouchEvent方法返回true,表示ViewGroup会拦截当前事件,那么intercepted被赋值为true,反之赋值为false。来看下onInterceptTouchEvent方法,代码如下:
1
2
3
4
5
6
7
8
9
10
11
|
// android.view.ViewGroup
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;
}
|
onInterceptTouchEvent方法比较简单,if条件第一个判断触摸事件是否来自鼠标设备;第二个判断触摸事件的类型是否是ACTION_DOWN;第三个检查是否按下了鼠标按键;第四个判断触摸位置是否在滚动条上。在大多数情况下if条件不会成立,也就是可以认为默认情况下onInterceptTouchEvent方法返回false。
如果通过requestDisallowInterceptTouchEvent方法设置FLAG_DISALLOW_INTERCEPT标志位,此时disallowIntercept为true,那么就不满足if(!disallowIntercept)条件了,所以不会再调用onInterceptTouchEvent方法,而是走的else逻辑,从而进入分析3.1.2,将intercepted赋值为false。
注意:FLAG_DISALLOW_INTERCEPT一旦设置后,ViewGroup将无法拦截除了ACTION_DOWN以外的其他点击事件。
为什么?
因为前面说过,ViewGroup在分发事件时,如果是ACTION_DOWN就会重置FLAG_DISALLOW_INTERCEPT这个标记位,将导致子View中设置的这个标记位无效。当面对ACTION_DOWN事件时,ViewGroup总是会调用自己的onInterceptTouchEvent方法来询问自己是否要拦截事件。因此,子View调用requestDisallowInterceptTouchEvent方法并不能影响ViewGroup对ACTION_DOWN事件的处理。
如果ViewGroup拦截事件,那么子View就无法获得事件,当ACTION_MOVE和ACTION_UP事件到来时,由于(actionMasked == MotionEvent.ACTION_DOWN ||mFirstTouchTarget ! = null)这个条件为false,将导致ViewGroup的onInterceptTouchEvent不会再被调用,然后走的是else逻辑,也就是进入分析3.2,将intercepted赋值为true。
到这里,我们已经知道,intercepted的值如果为true,表示ViewGroup要拦截事件,反之不拦截,接下来,继续看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
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
|
// android.view.ViewGroup
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
// ...
boolean handled = false;
if (onFilterTouchEventForSecurity(ev)) {
// ...
// Check for cancelation.
final boolean canceled = resetCancelNextUpFlag(this)
|| actionMasked == MotionEvent.ACTION_CANCEL;
// Update list of touch targets for pointer down, if needed.
final boolean isMouseEvent = ev.getSource() == InputDevice.SOURCE_MOUSE;
final boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0
&& !isMouseEvent;
TouchTarget newTouchTarget = null;
boolean alreadyDispatchedToNewTouchTarget = false;
if (!canceled && !intercepted) {
// If the event is targeting accessibility focus we give it to the
// view that has accessibility focus and if it does not handle it
// we clear the flag and dispatch the event to all children as usual.
// We are looking up the accessibility focused host to avoid keeping
// state since these events are very rare.
View childWithAccessibilityFocus = ev.isTargetAccessibilityFocus()
? findChildWithAccessibilityFocus() : null;
if (actionMasked == MotionEvent.ACTION_DOWN
|| (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
final int actionIndex = ev.getActionIndex(); // always 0 for down
final int idBitsToAssign = split ? 1 << ev.getPointerId(actionIndex)
: TouchTarget.ALL_POINTER_IDS;
// Clean up earlier touch targets for this pointer id in case they
// have become out of sync.
removePointersFromTouchTargets(idBitsToAssign);
final int childrenCount = mChildrenCount;
if (newTouchTarget == null && childrenCount != 0) {
final float x =
isMouseEvent ? ev.getXCursorPosition() : ev.getX(actionIndex);
final float y =
isMouseEvent ? ev.getYCursorPosition() : ev.getY(actionIndex);
// Find a child that can receive the event.
// Scan children from front to back.
final ArrayList<View> preorderedList = buildTouchDispatchChildList();
final boolean customOrder = preorderedList == null
&& isChildrenDrawingOrderEnabled();
final View[] children = mChildren;
for (int i = childrenCount - 1; i >= 0; i--) {
final int childIndex = getAndVerifyPreorderedIndex(
childrenCount, i, customOrder);
final View child = getAndVerifyPreorderedView(
preorderedList, children, childIndex);
// If there is a view that has accessibility focus we want it
// to get the event first and if not handled we will perform a
// normal dispatch. We may do a double iteration but this is
// safer given the timeframe.
if (childWithAccessibilityFocus != null) {
if (childWithAccessibilityFocus != child) {
continue;
}
childWithAccessibilityFocus = null;
i = childrenCount - 1;
}
if (!child.canReceivePointerEvents()
|| !isTransformedTouchPointInView(x, y, child, null)) {
ev.setTargetAccessibilityFocus(false);
continue;
}
newTouchTarget = getTouchTarget(child);
if (newTouchTarget != null) {
// Child is already receiving touch within its bounds.
// Give it the new pointer in addition to the ones it is handling.
newTouchTarget.pointerIdBits |= idBitsToAssign;
break;
}
resetCancelNextUpFlag(child);
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
// Child wants to receive touch within its bounds.
mLastTouchDownTime = ev.getDownTime();
if (preorderedList != null) {
// childIndex points into presorted list, find original index
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;
}
// The accessibility focus didn't handle the event, so clear
// the flag and do a normal dispatch to all children.
ev.setTargetAccessibilityFocus(false);
}
if (preorderedList != null) preorderedList.clear();
}
if (newTouchTarget == null && mFirstTouchTarget != null) {
// Did not find a child to receive the event.
// Assign the pointer to the least recently added target.
newTouchTarget = mFirstTouchTarget;
while (newTouchTarget.next != null) {
newTouchTarget = newTouchTarget.next;
}
newTouchTarget.pointerIdBits |= idBitsToAssign;
}
}
}
}
}
|
如果不是ACTION_CANCEL事件或者ViewGroup不拦截事件时,倒叙遍历子View,然后调用dispatchTransformedTouchEvent方法进行分发事件,注意该方法第三个参数child,此时child不为null,继续看dispatchTransformedTouchEvent方法,代码如下:
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
|
// android.view.ViewGroup
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
View child, int desiredPointerIdBits) {
final boolean handled;
// ...
// Perform any necessary transformations and dispatch.
if (child == null) {
handled = super.dispatchTouchEvent(transformedEvent);
} else {
final float offsetX = mScrollX - child.mLeft;
final float offsetY = mScrollY - child.mTop;
transformedEvent.offsetLocation(offsetX, offsetY);
if (! child.hasIdentityMatrix()) {
transformedEvent.transform(child.getInverseMatrix());
}
handled = child.dispatchTouchEvent(transformedEvent);
}
// Done.
transformedEvent.recycle();
return handled;
}
|
前面说过,此时child不为null,那么就会调用child的dispatchTouchEvent方法进行事件的分发,如果child的dispatchTouchEvent方法返回true,那么if(dispatchTransformedTouchEvent)条件成立,就会执行后面的addTouchTarget方法,给mFirstTouchTarget赋值,addTouchTarget方法代码如下:
1
2
3
4
5
6
7
8
|
// android.view.ViewGroup
private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) {
final TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
target.next = mFirstTouchTarget;
mFirstTouchTarget = target;
return target;
}
|
从上面的addTouchTarget方法的内部结构可以看出,mFirstTouchTarget其实是一种单链表结构。那么如果子元素的dispatchTouchEvent返回false,ViewGroup就会把事件分发给下一个子元素(如果还有下一个子元素的话)。
接下来,继续看ViewGroup拦截事件的逻辑,剩余的dispatchTouchEvent方法代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
// android.view.ViewGroup
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
boolean handled = false;
if (onFilterTouchEventForSecurity(ev)) {
// ...
// Dispatch to touch targets.
if (mFirstTouchTarget == null) {
// No touch targets so treat this as an ordinary view.
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
} else {
}
}
return handled;
}
|
如果遍历所有的子元素后事件都没有被合适地处理,这包含两种情况:第一种是ViewGroup没有子元素;第二种是子元素处理了点击事件,但是在dispatchTouchEvent中返回了false,这一般是因为子元素在onTouchEvent中返回了false。在这两种情况下,ViewGroup会自己处理点击事件。注意dispatchTransformedTouchEvent方法的第三个参数child,此时child为null,从前面的分析可以知道,它会调用super.dispatchTouchEvent(event),很显然,这里就转到了View的dispatchTouchEvent方法,即点击事件开始交由View来处理。
3.3、View的事件分发过程
上面分析了ViewGroup的事件分发过程,可以知道,ViewGroup拦截事件后,让触摸事件的分发从ViewGroup流转到了View,那么继续看View的dispatchTouchEvent方法,这里省略了一些不重要的代码,剩余代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
// android.view.View
public boolean dispatchTouchEvent(MotionEvent event) {
boolean result = false;
if (onFilterTouchEventForSecurity(event)) {
//noinspection SimplifiableIfStatement
ListenerInfo li = mListenerInfo;
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;
}
|
可以看到View的dispatchTouchEvent方法处理比较简单,首先会判断有没有设置OnTouchListener,如果OnTouchListener中的onTouch方法返回true,那么onTouchEvent就不会被调用,可见OnTouchListener的优先级高于onTouchEvent,如果没有设置mOnTouchListener监听器,或者onTouch方法返回false时,那么就会调用View的onTouchEvent方法,继续看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
28
29
|
// android.view.View
public boolean onTouchEvent(MotionEvent event) {
final float x = event.getX();
final float y = event.getY();
final int viewFlags = mViewFlags;
final int action = event.getAction();
final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
|| (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
|| (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;
if ((viewFlags & ENABLED_MASK) == DISABLED
&& (mPrivateFlags4 & PFLAG4_ALLOW_CLICK_WHEN_DISABLED) == 0) {
if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
setPressed(false);
}
mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
// A disabled view that is clickable still consumes the touch
// events, it just doesn't respond to them.
return clickable;
}
if (mTouchDelegate != null) {
if (mTouchDelegate.onTouchEvent(event)) {
return true;
}
}
return false;
}
|
从上面代码可以知道,View处于不可用时依然会消耗事件。如果View设置有代理,那么还会执行TouchDelegate的onTouchEvent方法。好了,继续看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
28
29
30
31
32
33
34
35
36
|
// android.view.View
public boolean onTouchEvent(MotionEvent event) {
if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
switch (action) {
case MotionEvent.ACTION_UP:
boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
// This is a tap, so remove the longpress check
removeLongPressCallback();
// Only perform take click actions if we were in the pressed state
if (!focusTaken) {
// Use a Runnable and post this rather than calling
// performClick directly. This lets other visual state
// of the view update before click actions start.
if (mPerformClick == null) {
mPerformClick = new PerformClick();
}
if (!post(mPerformClick)) {
performClickInternal();
}
}
}
}
mIgnoreNextUpEvent = false;
break;
}
return true;
}
return false;
}
|
只要View满足clickable,当ACTION_UP事件发生时,最终会触发performClick方法,如果View设置了OnClickListener,那么performClick方法内部会调用它的onClick方法,代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
// android.view.View
public boolean performClick() {
// We still need to call this method to handle the cases where p
// externally, instead of through performClickInternal()
notifyAutofillManagerOnClick();
final boolean result;
final ListenerInfo li = mListenerInfo;
if (li != null && li.mOnClickListener != null) {
playSoundEffect(SoundEffectConstants.CLICK);
li.mOnClickListener.onClick(this);
result = true;
} else {
result = false;
}
sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
notifyEnterOrExitForAutoFillIfNeeded(true);
return result;
}
|
至此,事件分发机制的源码就分析完成了。
四、解决开发中常见的滑动冲突
经过前面理论知识的准备,下面就要进入解决开发中常见的滑动冲突的实战环节。
4.1、常见的滑动冲突场景&处理思路
当内外两层View都可以滑动的时候,就会产生滑动冲突,常见的滑动冲突场景如下图所示:
此种场景可以根据当前滑动方向是横向还是纵向来判断事件到底该交给谁来处理。
那滑动方向怎么判断呢?
在滑动过程中会有两个点的坐标,通过两个点的坐标就可以计算手指的移动距离,如图所示:
手指移动后横向距离变化为dx,纵向距离变化为dy,如果dx>dy,那么此次滑动就算作横向滑动;相反,则认为此次滑动是纵向滑动。
当内外两层都在同一个方向可以滑动的时候,显然存在逻辑问题。因为当手指开始滑动的时候,系统无法知道用户到底是想让哪一层滑动,所以当手指滑动的时候就会出现问题,要么只有一层能滑动,要么就是内外两层都滑动得很卡顿。
这种得根据业务需求,通过下面的拦截与禁止拦截的方法,决定在什么情况下滑动哪个View。
上面图中没放出来,是场景一和场景二都存在的情况,也就是它们之间互相嵌套,或者多层嵌套,然而不管多么复杂,解决思路都是一毛一样的,按照上面提的解决思路一层一层处理即可。
4.2、解决滑动冲突的办法
4.2.1、外部拦截法
所谓外部拦截法是指点击事情都先经过父容器的拦截处理,如果父容器需要此事件就拦截,如果不需要此事件就不拦截,这样就可以解决滑动冲突的问题。
外部拦截法需要重写父容器的onInterceptTouchEvent方法,在内部做相应的拦截即可,这种方法的伪代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
override fun onInterceptTouchEvent(ev: MotionEvent): Boolean {
var intercepted = super.onInterceptTouchEvent(ev)
when (ev.action) {
MotionEvent.ACTION_DOWN -> {
intercepted = false
}
MotionEvent.ACTION_MOVE -> {
if(父控件需要当前点击事件){
intercepted = true
}else{
intercepted = false
}
}
MotionEvent.ACTION_UP -> {
intercepted = false
}
}
return intercepted
}
|
解释下上面代码,在onInterceptTouchEvent方法中,首先是ACTION_DOWN这个事件,父容器必须返回false,即不拦截ACTION_DOWN事件,这是因为一旦父容器拦截了ACTION_DOWN,那么后续的ACTION_MOVE和ACTION_UP事件都会直接交由父容器处理,这个时候事件没法再传递给子元素了;
其次是ACTION_MOVE事件,这个事件可以根据需要来决定是否拦截,如果父容器需要拦截就返回true,否则返回false;
最后是ACTION_UP事件,这里必须要返回false,因为ACTION_UP事件本身没有太多意义。考虑一种情况,假设事件交由子元素处理,如果父容器在ACTION_UP时返回了true,就会导致子元素无法接收到ACTION_UP事件,这个时候子元素中的onClick事件就无法触发,但是父容器比较特殊,一旦它开始拦截任何一个事件,那么后续的事件都会交给它来处理,而ACTION_UP作为最后一个事件也必定可以传递给父容器,即便父容器的onInterceptTouchEvent方法在ACTION_UP时返回了false。
4.2.2、内部拦截法
内部拦截法是指父容器不拦截任何事件,所有的事件都传递给子元素,如果子元素需要此事件就直接消耗掉,否则就交由父容器进行处理,需要配合requestDisallowInterceptTouchEvent方法才能正常工作,这种方法的伪代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
|
override fun dispatchTouchEvent(event: MotionEvent): Boolean {
when (event.action) {
MotionEvent.ACTION_MOVE -> {
if(自己需要当前点击事件){
parent.requestDisallowInterceptTouchEvent(true)
}else{
parent.requestDisallowInterceptTouchEvent(false)
}
}
}
return super.dispatchTouchEvent(event)
}
|
解释下上面代码,我留意到一些文章中会写到在子控件的ACTION_DOWN消息中使用getParent().requestDisallowInterceptTouchEvent(true);,这是无效的,因为如果父控件拦截了ACTION_DOWN消息,则这里写的函数根本不会执行。
4.3、滑动冲突实战
先上布局文件:
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
|
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
</data>
<androidx.core.widget.NestedScrollView
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<androidx.appcompat.widget.AppCompatEditText
android:id="@+id/edit_text"
android:layout_width="match_parent"
android:layout_height="@dimen/edit_text_height"
android:text="@string/edit_text_content"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<View
android:layout_width="match_parent"
android:layout_height="1000dp"
android:background="@color/teal_200"
android:gravity="center"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/edit_text" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.core.widget.NestedScrollView>
</layout>
|
上面是一个EditText,但是这个EditText的内容超出了显示区域,所以它的内容是可以上下滑动的,下面是一个View,高度给得非常大,用来支撑外部的NestedScrollView足够可以滑动,因为NestedScrollView也可以上下滑动,由于双方都可以上下滑动,导致冲突了,运行效果图如下:
可以看到,在没有处理滑动冲突,在EditText向上滑动时,依然是整体NestedScrollView在滑动,这属于常见的滑动冲突场景里的情况二了,OK,下面就来解决冲突。
使用外部拦截法来解决滑动冲突问题的话,需要重写NestedScrollView,它的代码如下:
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
|
class MyNestedScrollView : NestedScrollView {
constructor(context: Context) : super(context) {
doInit(context)
}
constructor(
context: Context,
attrs: AttributeSet?
) : super(context, attrs) {
doInit(context)
}
constructor(
context: Context,
attrs: AttributeSet?,
defStyleAttr: Int
) : super(context, attrs, defStyleAttr) {
doInit(context)
}
private var editTextHeight: Int = 0
private fun doInit(context: Context) {
editTextHeight =
context.resources.getDimensionPixelSize(R.dimen.edit_text_height)
}
private var downY: Int = 0
override fun onInterceptTouchEvent(ev: MotionEvent): Boolean {
var intercepted = super.onInterceptTouchEvent(ev)
when (ev.action) {
MotionEvent.ACTION_DOWN -> {
downY = ev.y.toInt()
intercepted = false
}
MotionEvent.ACTION_MOVE -> {
intercepted = downY > editTextHeight
}
MotionEvent.ACTION_UP -> {
intercepted = false
}
}
return intercepted
}
}
|
解释下上面代码,因为EditText的高度是固定的,在这个高度之前触摸的位置都是EditText的内容区域,此时让NestedScrollView不要拦截事件,在这个内容区域值之外的位置,让NestedScrollView拦截事件。
可以看到,在使用外部拦截法时,需要提前知道不拦截消息的区域,这样才能做好消息处理,所以也只有在子控件的位置和大小是固定的并且能获取到的情况下,外部拦截法才是有用的。
将自定义的MyNestedScrollView替换布局文件里的NestedScrollView即可,运行后效果如下:
使用内部拦截法来解决滑动冲突问题的话,需要重写EditText,它的代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
class MyEditText : AppCompatEditText {
constructor(context: Context) : super(context)
constructor(
context: Context,
attrs: AttributeSet
) : super(context, attrs)
constructor(
context: Context,
attrs: AttributeSet,
defStyleAttr: Int
) : super(context, attrs, defStyleAttr)
override fun dispatchTouchEvent(event: MotionEvent): Boolean {
when (event.action) {
MotionEvent.ACTION_MOVE -> {
parent.requestDisallowInterceptTouchEvent(true)
}
}
return super.dispatchTouchEvent(event)
}
}
|
因为在ACTION_MOVE消息到来时,EditText需要自己处理消息,所以这里并没有内部拦截法中判断是否需要该事件的if(自己需要这类点击事件)代码。
将自定义的MyEditText替换布局文件里的EditText即可,运行后效果同用外部拦截法解决滑动冲突。
本文源码地址: AndroidEventDispatch