Android事件传递机制(一)流程梳理

事件传递或者称之为事件分发是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试试看。

分析各种情况,并打印日志结果,结合各个方法的功能,我们可以得到下面一张完整的事件传递流程图:

事件传递流程图

(当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事件传递机制(二)情景分析

  • 0
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值