Android 开发中,很多情况下,我们需要对触摸事件进行处理,但是当面对错综复杂的 Android 布局时,我们如何准确的将一个用户的触摸事件传递到对应的控件中并让它进行处理呢?
首先,我们先假设这里有这样一个布局:
我们可以很清楚的看到,一个很明显的嵌套布局,外面两个红色的和黄色的都是布局,中间一个紫色的控件。如果此时,我们单击一下 myView 这个控件,触摸事件(单击也是触摸事件)是怎么传递的呢?
Android 中,触摸事件的传递是由外向内的,也就是说,这个触摸事件从 myLinearLayout 开始(由更上面一层的组件将触摸事件传递给 myLinearLayout),依次通过 myFrameLayout,最后传递到 MyView 这个控件中,因为 myView 没有子控件。所以这个事件就由 myView 控件进行处理,然后将处理的结果返回到它的父控件:myFrameLayout ,之后继续返回给 myLinearLayout 控件。。。
当然,我们上面看到的情况是最一般的情况,触摸事件由外向里传递,处理结果由里向外传递。我们也可以通过重写控件或者布局里面的一些方法来拦截触摸事件。
首先,对于 ViewGroup 来说,我们可以选择性的重写下面三个方法:
public boolean dispatchTouchEvent(MotionEvent ev);
public boolean onInterceptTouchEvent(MotionEvent ev);
public boolean onTouchEvent(MotionEvent ev);
我们分别来看一下这三个方法:
这个方法的作用是把触摸事件的分发方法,其返回值代表触摸事件是否被当前 View 处理完成(true/false)。
这个方法是触摸事件拦截机制的核心方法,官方文档解释的很详细,简单来说就是如果这个方法的返回值是 true,那么当前触摸事件就不会传递给子 View 控件(即被当前 ViewGroup 控件拦截并由当前 ViewGroup控件的 onTouchEvent(MotionEvent event) 方法进行处理),如果返回值是 false ,那么这个触摸事件就会传递给子 View 控件,由子 View 控件去处理。
这个是 ViewGroup 控件处理触摸事件的方法,一般来说,ViewGroup 控件的触摸事件在这个方法中处理。如果这个方法返回 true,证明当前触摸事件被当前 ViewGroup 控件处理完成并消耗了,如果返回 false,证明当前触摸事件没有被当前 ViewGroup 控件处理完成。
结合我们上面所讲的,笔者用一张图来表示这三个方法的影响关系(触摸事件由外向里的传递过程。这里是笔者个人的理解):
用伪代码表示 ViewGroup 中三个方法的调用关系:
public boolean dispatchTouchEvent(MotionEvent e) {
bool result = false;
if(interceptTouchEvent(e)) {
result = onTouchEvent(e);
} else {
result = child.dispatchTouchEvent(e);
}
return result;
}
上面的三个方法是 ViewGroup 对象中拥有的,而对于 View 对象来说,只有下面两个方法:
public boolean dispatchTouchEvent(MotionEvent ev);
public boolean onTouchEvent(MotionEvent ev);
View 对象没有 onInterceptTouchEvent 方法,即没有拦截事件的方法(因为 View 对象已经是最内层 View 控件,它没有子 View 了),所以不存在拦截事件这个说法,如果触摸事件传递到最内层 View 控件,那么这个 View 控件的 onTouchEvent 方法一定会被调用用于处理触摸事件。其伪代码如下:
/**
* dispatchTouchEvent(MotionEvent ev) 方法的返回值代表这个 View 是否成功处理触摸事件
*/
public boolean dispatchTouchEvent(MotionEvent ev) {
return onTouchEvent(ev);
}
接下来要明白:
1、无论是对于 View 还是 ViewGroup来说,一个 触摸事件(MotionEvent 对象) 只要能传递给这个 View/ViewGroup ,
那么这个 View/ViewGroup 的 dispatchTouchEvent(MotionEvent event) 就一定会被调用
2、如果一个 View/ViewGroup 的 onTouchEvent(MotionEvent event) 方法被调用,
那么其返回值代表这个 View/ViewGroup 有没有成功的处理这个事件,
如果返回的 true,那么这个触摸事件接下来的一系列(直到手指松开之前) 都会传递给这个 View/ViewGroup 处理,
但是这个过程中其父 ViewGroup 仍然可以通过 interceptTouchEvent(MotionEvent e) 方法拦截这个触摸事件,
如果在传递的过程中被拦截了,那么久不会传递到这个 View/ViewGroup 上。
3、无论何时,只要一个 View/ViewGroup 的 onTouchEvent(MotionEvent event) 方法返回了 false,
证明这个 View/ViewGroup 没有处理完成这个触摸事件,
那么接下来的一系列的触摸事件都不会传递给当前 View/ViewGroup 处理。
暂时不明白也没关系,下面的例子会有介绍
这是事件由外至内的传递过程,那么如果触摸事件传递到最低层的 View 控件之后怎么传递呢?上文提到过,事件处理之后一般的过程是由里向外传递,也就是说最里层的 View 控件的 onTouchEvent 处理完了之后,然后逐渐向外传递触摸事件(即将触摸事件传递给外层的 ViewGroup ,并由 ViewGroup 的 onTouchEvent 方法继续处理),直到传递到最外层的 ViewGroup 。当然,这里我们也可以通过改变 View 控件的 onTouchEvent 方法的返回值来该表触摸事件的传递:返回 false:这个触摸事件需要外层 ViewGroup 处理,传递这个触摸事件给外层 ViewGroup,返回 true:这个触摸事件已经被当前View/ViewGroup 处理完成了,不会传递给外层 ViewGroup。
我们还是用一张图来表示触摸事件的由里向外传递过程:
好了,下面通过例子来看一下:
建立一个Android 工程:
为了实现两个 ViewGroup 并且重写里面的事件拦截的三个方法,我们需要继承 ViewGroup,这里为了简单起见,笔者直接继承了一个 LinearLayout 和一个 FrameLayout 。同样的 myView 也是继承了 Button, 看看代码:
MyLinearLayout.java:
import android.content.Context;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.widget.LinearLayout;
/**
* Created by 指点 on 2017/3/21.
*/
public class MyLinearLayout extends LinearLayout {
private static final String str = "LinearLayout";
public MyLinearLayout(Context context) {
super(context);
}
public MyLinearLayout(Context context, AttributeSet attrs) {
super(context, attrs);
}
/*
* 分发触摸事件的方法,当这个 ViewGroup 能够接收到触摸事件的时候,
* 这个方法首先被调用,用于分发接收到的触摸事件,父类方法默认返回 false
*/
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
Log.i(str, "dispatchTouchEvent");
return super.dispatchTouchEvent(event);
}
/*
* 重写父类的拦截触摸事件方法,,ViewGroup 独有的方法,
* 如果返回值为 true,那么这个触摸事件由这个 ViewGroup 处理,
* 会调用 onTouchEvent 方法,并且拦截触摸事件,不让子 View 接收到。
*/
@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
super.onInterceptTouchEvent(event);
Log.i(str, "onInterceptTouchEvent");
return false;
}
/*
* 如果接收到的触摸事件由这个 View/ViewGroup 处理,那么调用这个方法用于处理这个触摸事件
*/
@Override
public boolean onTouchEvent(MotionEvent event) {
super.onTouchEvent(event);
Log.i(str, "onTouchEvent");
return false;
}
}
MyFrameLayout.java:
import android.content.Context;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.widget.FrameLayout;
/**
* Created by 指点 on 2017/3/21.
*/
public class MyFrameLayout extends FrameLayout {
private static final String str = "FrameLayout";
public MyFrameLayout(Context context) {
super(context);
}
public MyFrameLayout(Context context, AttributeSet attrs) {
super(context, attrs);
}
/*
* 分发触摸事件的方法,当这个 ViewGroup 接收到触摸事件的时候,
* 这个方法首先被调用,用于分发接收到的触摸事件,父类方法默认返回false
*/
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
Log.i(str , "dispatchTouchEvent");
return super.dispatchTouchEvent(event);
}
/*
* 重写父类的拦截触摸事件方法,ViewGroup 独有的方法,
* 如果返回值为 true,那么这个触摸事件由这个 ViewGroup 处理,
* 会调用 onTouchEvent 方法,
*/
@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
super.onInterceptTouchEvent(event);
Log.i(str , "onInterceptTouchEvent");
return false;
}
/*
* 如果接收到的触摸事件由这个 ViewGroup 处理,那么调用这个方法用于处理这个触摸事件
*/
@Override
public boolean onTouchEvent(MotionEvent event) { // 重写父类的处理触摸事件的方法
super.onTouchEvent(event);
Log.i(str, "onTouchEvent");
return false;
}
}
MyView.java:
import android.content.Context;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.widget.Button;
/**
* Created by 指点 on 2017/3/21.
*/
public class MyView extends Button {
private static final String str = "MyView";
public MyView(Context context) {
super(context);
}
public MyView(Context context, AttributeSet attrs) {
super(context, attrs);
}
/*
* 分发触摸事件的方法,当这个 View 接收到触摸事件的时候,
* 这个方法首先被调用,用于分发接收到的触摸事件,父类的方法默认返回 false
*/
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
Log.i(str, "dispatchTouchEvent");
return super.dispatchTouchEvent(event);
}
/*
* 因为这已经是一个 View,因此它是触摸事件处理的最底层,如果触摸事件能够传递给它,
* 那么它的 onTouchEvent 方法一定会被调用
*/
@Override
public boolean onTouchEvent(MotionEvent event) { // 重写父类的处理触摸事件的方法
super.onTouchEvent(event);
Log.i(str, "onTouchEvent");
return false;
}
}
这里分别重写了 LinearLayout 、FrameLayout 、Button 的对应事件处理方法,返回值均为 false,并且在方法中打上了 LogCat。
接下来是 布局文件 activity_main.xml:
最后是 MainActivity.java:
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.Button;
public class MainActivity extends AppCompatActivity {
private MyFrameLayout myFrameLayout = null;
private Button button = null;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
}
下面来看看结果:
单击按钮后看看LogCat:
这里的触摸事件确实是从外到内传递,经 MyView 的 onTouchEvent 方法处理之后,又向外传递到 LinearLayout 。因为我们这里的代码中的 onInterceptTouchEvent 方法和 onTouchEvent 方法均是返回 false,所以这里并没有任何事件拦截现象,现在我们把 LinearLayout 中的 onInterceptTouchEvent 方法的返回值改为 true 试试:
可以看到,这里只调用了 LinearLayout 中的 onTouchEvent 方法就结束了,证明触摸事件确实被 LinearLayout 控件拦截并处理了。如果只把 FrameLayout 中的 onInterceptTouchEvent 方法的返回值改为 true 呢?
同样的,这里触摸事件经 LinearLayout 传递到 FrameLayout 后,被 FrameLayout 拦截处理,所以这里 MyView 仍然没有接收到触摸事件,而是直接由 FrameLayout 向外传递给 LinearLayout 。
上面是对触摸事件由外向内传递的实验,那么由内向外呢?
我们把 MyView 的 onTouchEvent 方法的返回值改为 true,LinearLayout 、FrameLayout 方法的 onInterceptTouchEvent 方法和 onTouchEvent 方法返回值全改为 false:
我们会发现,会出现上图中两遍同样的LogCat信息,因为单击是两个动作(ACTION_DOWN、ACTION_UP)。
可能小伙伴要问了,为什么就这里有两遍一样LogCat 信息,上次的代码就没有呢。这里其实就是我们上问文讲的 :
如果一个 View/ViewGroup 的 onTouchEvent(MotionEvent event) 方法被调用,
那么其返回值代表这个 View/ViewGroup 有没有成功的处理这个事件,如果返回的 true,
那么这个触摸事件接下来的一系列(直到手指松开之前) 都会传递给这个 View/ViewGroup 处理
即为上文中的第 2 点,很明显,我们上次的代码中所有 onTouchEvent(MotionEvent e) 方法返回的全是 false,对应于上文中第 3 点所讲的:
无论何时,只要一个 View/ViewGroup 的 onTouchEvent(MotionEvent event) 方法返回了 false,
证明这个 View/ViewGroup 没有处理完成这个触摸事件,
那么接下来的一系列的触摸事件都不会传递给当前 View/ViewGroup 处理。
所以之前只有一遍 LogCat ,即只有 ACTION_DOWN 类型的 MotionEvent 对象被传递了,ACTION_UP 类型的 MotionEvent 对象并没有传递给这个 View 处理。
接下来我们会发现 FrameLayout 、 LinearLayout 的 onTouchEvent 方法都不会被调用,因为触摸事件在 MyView 的 onTouchEvent 事件中就被处理消耗掉了(也可以理解为被拦截了),所以自然不能传给 外层的 ViewGroup 。
如果你把 FrameLayout 的 onTouchEvent 方法的返回值设为 true ,其余的设置为 false,你会得到下面的结果:
同样是两个一样的LogCat,类似的,触摸事件在 FrameLayout 的 onTouchEvent 方法中被拦截了。因而 LinearLayout 不能接收到触摸事件,它的 onTouchEvent 方法不会被调用。
好了,对于Android 事件分发拦截,总结起来就是:
先由外向里,再由里向外。
由外向里的过程中:onInterceptTouchEvent 方法(ViewGroup才有)的返回值决定是否拦截触摸事件(true:拦截,false:不拦截)。如果 ViewGroup 拦截了触摸事件,那么其 onTouchEvent 就会被调用用来处理触摸事件。
由里向外的过程中:onTouchEvent 方法的返回值决定是否处理完成触摸事件(true:已经处理完成,不需要给父 ViewGroup 处理,false:还没处理完成 ,需要传递给父 ViewGroup 处理)。