事件传递或者称之为事件分发是Android中非常重要和核心的知识点,我们在手机上的任何一次点击或者触摸都会涉及到事件传递,所以非常有必要好好了解一下相关知识。
当一个Touch事件发生后,首先是当前的Activity拿到该事件,然后Activity会将事件传递给Window,然后Window再将事件传递给当前界面的根布局(顶级父View),即按照Activity -> Window -> View的顺序,最后顶级父View再按照事件传递机制将事件传递给子View,也就是本文讨论的重点。
首先明确一下几个概念:
1、我们讨论的事件是指TouchEvent,可以称之为触摸事件,它包含ACTION_DOWN(按下)、ACTION_MOVE(移动)、ACTION_UP(抬起)、ACTION_CANCEL等。正常情况下,一次触摸手机屏幕的行为,起点是由ACTION_DOWN事件开始的,然后产生一系列的ACTION_MOVE事件,最后通常以ACTION_UP事件结束。
2、事件的传递过程对应三个很重要的方法:dispatchTouchEvent(MotionEvent ev)用来进行事件的分发;onInterceptTouchEvent(MotionEvent ev)用来判断是否拦截某个事件;onTouchEvent(MotionEvent event)对事件进行响应。一个事件如果到达了某一个View或者ViewGroup,那么一定会最先调用到这个控件的dispatchTouchEvent(MotionEvent ev)方法。
我们知道在Android中ViewGroup继承自View,ViewGroup是可以包含很多的子View和子VewGroup。如图,我们设想有一个ViewGroupA,包含了一个ViewGroupB,ViewGroupB中又包含了一个子View。然后在子View上执行了一次触摸事件,那么会执行什么样的事件传递逻辑呢?
网上相关的文章,大多数博主都是采用分析源码的方式,我也看了好几天源码,不过我今天决定用另外一个思路梳理一下事件传递的流程。
为了验证获取事件传递的逻辑,我们分别自定义三个控件ViewGroupA、ViewGroupB、SonView,其中ViewGroupA和ViewGroupB都继承自ViewGroup,SonView则直接继承自View:
package com.mliuxb.touchevent;
import android.content.Context;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
/**
* Description:自定义ViewGroupA
*/
public class ViewGroupA extends ViewGroup {
public ViewGroupA(Context context) {
super(context);
}
public ViewGroupA(Context context, AttributeSet attrs) {
super(context, attrs);
}
public ViewGroupA(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
public ViewGroupA(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
//ViewGroupA的第一个子布局,位于上半部分,大小为二分之一
View child = getChildAt(0);
child.layout(l, t, r, b / 2);
}
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
Log.i(Constant.TAG, "ViewGroupA...dispatchTouchEvent: ");
return super.dispatchTouchEvent(ev);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
Log.i(Constant.TAG, "ViewGroupA...onInterceptTouchEvent: ");
return super.onInterceptTouchEvent(ev);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
Log.i(Constant.TAG, "ViewGroupA...onTouchEvent: ");
return super.onTouchEvent(event);
}
}
package com.mliuxb.touchevent;
import android.content.Context;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
/**
* Description:自定义ViewGroupB
*/
public class ViewGroupB extends ViewGroup {
public ViewGroupB(Context context) {
super(context);
}
public ViewGroupB(Context context, AttributeSet attrs) {
super(context, attrs);
}
public ViewGroupB(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
public ViewGroupB(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
//ViewGroupB的第一个子布局,位于布局中心,大小为四分之一
View child = getChildAt(0);
child.layout(r / 4, b / 4, r / 4 * 3, b / 4 * 3);
}
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
Log.i(Constant.TAG, "ViewGroupB...dispatchTouchEvent: ");
return super.dispatchTouchEvent(ev);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
Log.i(Constant.TAG, "ViewGroupB...onInterceptTouchEvent: ");
return super.onInterceptTouchEvent(ev);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
Log.i(Constant.TAG, "ViewGroupB...onTouchEvent: ");
return super.onTouchEvent(event);
}
}
package com.mliuxb.touchevent;
import android.content.Context;
import android.support.annotation.Nullable;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
/**
* Description:自定义SonView
*/
public class SonView extends View {
public SonView(Context context) {
super(context);
}
public SonView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
public SonView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
public SonView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
Log.i(Constant.TAG, "SonView...dispatchTouchEvent: ");
return super.dispatchTouchEvent(event);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
Log.i(Constant.TAG, "SonView...onTouchEvent: ");
return super.onTouchEvent(event);
}
}
需要注意的是,继承ViewGroup必须重写onLayout()方法,并设置相关子控件的布局,为了方便我在ViewGroupA和ViewGroupB中将子控件的布局都写为固定的了。
然后分别重写dispatchTouchEvent()、onInterceptTouchEvent()、onTouchEvent()方法。其中View中并没有onInterceptTouchEvent()方法,即单纯的View不需要判断是否拦截事件(不能包含其他的View,自然不需要判断是否拦截事件),所以SonView没有onInterceptTouchEvent()方法。并在每一个方法中打印相关日志(设置"TouchEvent"为统一的日志TAG)。
接着在布局文件中按照我们设想的情景直接引用三个自定义控件:
<?xml version="1.0" encoding="utf-8"?>
<com.mliuxb.touchevent.ViewGroupA
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.mliuxb.touchevent.ViewGroupB
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/colorB">
<com.mliuxb.touchevent.SonView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/colorC"/>
</com.mliuxb.touchevent.ViewGroupB>
</com.mliuxb.touchevent.ViewGroupA>
如上,为了区别每个控件,我为ViewGroupB和SonView设置了不同的背景。此时Activity中不需要额外设置内容。运行项目效果如下:
此时我们轻触SonView的区域(即红色区域),然后就能得到如下日志结果:
由日志也可以看出:一个事件如果到达了某一个View或者ViewGroup,那么一定会最先调用到这个控件的dispatchTouchEvent()方法用来进行事件的分发。
观察这些方法的执行流程,可以得到以下一张基本的事件传递流程图:
通过以上流程,我们能够非常清楚的看出事件传递的方向是:自上而下由父控件传递给子控件。默认情况会一层层的向下传递,直到最后一个子控件;事件响应的方向是:自下而上由子控件传递给父控件。
我们都知道onInterceptTouchEvent()方法是用来判断是否拦截某个事件的,onTouchEvent()方法是对事件进行相应的。上面我们的返回值都是默认super方法的返回值,此时我们可以将ViewGroupA、ViewGroupB和SonView中的onInterceptTouchEvent()方法和onTouchEvent()方法的返回值进行打印,修改ViewGroupA日志打印如下:
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
Log.i(Constant.TAG, "ViewGroupA...dispatchTouchEvent: ");
return super.dispatchTouchEvent(ev);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
boolean onInterceptTouchEvent = super.onInterceptTouchEvent(ev);
Log.i(Constant.TAG, "ViewGroupA...onInterceptTouchEvent: " + onInterceptTouchEvent);
return onInterceptTouchEvent;
}
@Override
public boolean onTouchEvent(MotionEvent event) {
boolean onTouchEvent = super.onTouchEvent(event);
Log.i(Constant.TAG, "ViewGroupA...onTouchEvent: " + onTouchEvent);
return onTouchEvent;
}
ViewGroupB和SonView的修改类似,在此不表。同样轻触SonView区域,得到如下日志结果:
可以看出系统默认onInterceptTouchEvent()方法返回false,即默认不拦截事件,将事件传递给子控件处理。并且由于我们自定义的三个控件都没有处理事件,所以onTouchEvent()方法都默认返回false,将事件又返回给父控件处理。既然都是boolean型的返回值,那么只有两种可能,我们将各个返回值都分别替换为true试试看。
分析各种情况,并打印日志结果,结合各个方法的功能,我们可以得到下面一张完整的事件传递流程图:
![](https://i-blog.csdnimg.cn/blog_migrate/c21ddd158ea3d9037d127f59c82b1ab5.png)
(当onTouchEvent()方法返回true表示处理事件时,日志流程会打印多次,这是因为文章开头所说的TouchEvent包含ACTION_DOWN、ACTION_MOVE、ACTION_UP等事件,当onTouchEvent()方法返回true时ACTION_MOVE、ACTION_UP等后续事件会传递给相应控件,这部分有待后续分析)
根据我们的日志以及上面的事件传递流程图,我们可以得到如下结论:
事件传递的方向是:自上而下由父控件传递给子控件;
1、ViewGroupA:首先事件一定是先到达顶级父View上(即ViewGroupA),事件到达ViewGroupA则会最先调用它的dispatchTouchEvent()方法,然后该方法会去调用自身的onInterceptTouchEvent()方法,判断自己是否需要把此事件拦截下来,如果返回true表示拦截事件,自己来处理这个事件,所以此时会调用自身的onTouchEvent()方法来对这个事件进行响应。
如果返回false,表示不拦截事件,那么就会调用child.dispatchTouchEvent(event)将这个事件传递给子控件,便会有会有后续的事件向下传递的流程。onInterceptTouchEvent()方法系统默认返回false。
2、ViewGroupB:ViewGroupA的子控件ViewGroupB接收到了这个事件,如上分析还是最先调用ViewGroupB的dispatchTouchEvent()方法,ViewGroupB仍然是一个ViewGroup的类型,那么事件继续分发的逻辑依然和ViewGroupA一样,去调用自身的onInterceptTouchEvent()方法,判断自己是否需要把此事件拦截下来。
3、子View:如果父控件没有拦截事件,事件便会到达子View,依然最先调用子View的dispatchTouchEvent()方法,此时事件分发的逻辑略有不同,由于View是没有onInterceptTouchEvent的方法,因为它不需要判断是否拦截事件。所以当一个事件到达这个View的dispatchTouchEvent()方法的时候,dispatchTouchEvent()方法就调用不到onInterceptTouchEvent()方法了,它会直接调用onTouchEvent()方法,直接让这个View来响应此事件。
事件响应的方向是:自下而上由子控件回传给父控件;
4、onTouchEvent()方法也是有一个boolean返回值的,true或者false。当调用子View的onTouchEvent()方法后,返回值有两种可能,true或者false。如果返回true,则表示子View消费了此事件,事件此时终止。如果返回false,则表示子View不消费此事件,那么此时这个事件会回传给父控件,调用到父控件ViewGroupB的onTouchEvent() 方法,由父控件ViewGroupB来进行响应,那父控件的onTouchEvent()方法也是同样的逻辑,要么消费此事件,要么回传给父控件的父控件ViewGroupA。以此类推如果都不处理事件,那么事件最终会回传至Activity。
以上就是Android事件传递机制的流程梳理。
文中的Demo代码:https://github.com/beita08/AndroidTouchEvent
下一篇文章:Android事件传递机制(二)情景分析