Android 事件分发机制

学而时习之,不亦说乎。学过的东西常温习、实践,并领略其中的深意,才能变得越发专业。今天来温习一下Android的事件分发机制。

一、事件分发机制原理

如下图所示:

三层View(包括Activity),从外到内依次为: EventActivityViewGroupAViewB

之前总结的如下结论,可以根据代码具体验证下:

/**
 * 事件分发机制基本原理概述:
 * 顶层View先逐层分发事件、再逐层选择消费事件、如果最底层View未消费事件,则从最底层开始逐步上报,最终可形成一个循环事件链路.
 * 
 * 具体事件分发流程如下:
 * --EventActivity 调用dispatchTouchEvent分发事件(if返回true,该事件分发到此结束) if返回false(默认)--继续往下分发事件至ViewGroupA
 * --ViewGroupA 调用dispatchTouchEvent分发事件(if返回true,该事件分发到此结束) if返回false(默认)--调用onInterceptTouchEvent事件(if返回true,则调用自己的onTouchEvent来进行消费) if返回false--继续往下分发事件至ViewB
 * --ViewB 调用dispatchTouchEvent分发事件(if返回true,该事件分发到此结束) if返回false(默认)--调用onTouchEvent事件(如果返回true,则在此 ViewB 消费该事件)if返回false--开始上报至ViewGroupA
 * --ViewGroupA 中调用onTouchEvent事件(if返回true,则在此 ViewGroupA 消费该事件) if返回false--上报至EventActivity
 * --EventActivity 中调用onTouchEvent事件,无论返回true or false都最终由该 EventActivity 消费该事件。
 * 
 * 遍历是为了寻找真正消费事件的View(ViewGroup):
 * 当EventActivity收到Touch事件时,将遍历子View进行Down事件的分发,ViewGroup的遍历可以看成是递归的。分发的目的是为了能够真正找到要消费该事件的View。
 * 当已经找到消费该事件的ViewGroup之后,该ViewGroup的子View将不会再收到Down事件的触发,即递归遍历只走一遍,找到能消费此事件的ViewGroup(or View)为止。
 *
 * 分发时onInterceptTouchEvent事件并不一定会调用:
 * 当已经确认(如ViewGroupA)已经拦截并消费了该事件,那么以后不会在调用其onInterceptTouchEvent方法(在已经拦截并消费的情况下,就没有必要每次都询问了),但是dispatchTouchEvent无论如何是每次都需要调用的。
 *
 */

源码(返回值均是默认值)如下:

EventActivity源码示例:
public class EventActivity extends Activity {

    private static final String TAG = "EventActivity";

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        requestWindowFeature(Window.FEATURE_NO_TITLE);
        this.getWindow().getDecorView().setBackgroundColor(Color.YELLOW);
        setContentView(R.layout.main);
    }

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        switch (ev.getAction()) {
            case MotionEvent.ACTION_DOWN:
                Logger.d(TAG + "--dispatchTouchEvent--DOWN");
                break;
            case MotionEvent.ACTION_MOVE:
                Logger.d(TAG + "--dispatchTouchEvent--MOVE");
                break;
            case MotionEvent.ACTION_UP:
                Logger.d(TAG + "--dispatchTouchEvent--UP");
                break;
            default:
                break;
        }
        return super.dispatchTouchEvent(ev);
    }

    @Override
    public void onUserInteraction() {
        super.onUserInteraction();
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        int action = event.getAction();
        switch (action) {
            case MotionEvent.ACTION_DOWN:
                Logger.d(TAG + "--onTouchEvent--DOWN");
                break;
            case MotionEvent.ACTION_MOVE:
                Logger.d(TAG + "--onTouchEvent--MOVE");
                break;
            case MotionEvent.ACTION_UP:
                Logger.d(TAG + "--onTouchEvent--UP");
                break;
            default:
                break;
        }
        return super.onTouchEvent(event);
    }
}
ViewGroupA源码示例:
package com.xiaoqlu.motionevent.touchview;

import android.content.Context;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.widget.LinearLayout;
import com.xiaoqlu.motionevent.util.Logger;

/**
 * @author hongri
 */
public class ViewGroupA extends LinearLayout {

    private final String TAG = "ViewGroupA";

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

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        int action = ev.getAction();
        switch (action) {
            case MotionEvent.ACTION_DOWN:
                Logger.d(TAG + "--dispatchTouchEvent--DOWN");
                break;
            case MotionEvent.ACTION_MOVE:
                Logger.d(TAG + "--dispatchTouchEvent--MOVE");
                break;
            case MotionEvent.ACTION_UP:
                Logger.d(TAG + "--dispatchTouchEvent--UP");
                break;
            default:
                break;
        }
        return super.dispatchTouchEvent(ev);
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        int action = ev.getAction();
        switch (action) {
            case MotionEvent.ACTION_DOWN:
                Logger.d(TAG + "--onInterceptTouchEvent--DOWN");
                break;
            case MotionEvent.ACTION_MOVE:
                Logger.d(TAG + "--onInterceptTouchEvent--MOVE");
                break;
            case MotionEvent.ACTION_UP:
                Logger.d(TAG + "--onInterceptTouchEvent--UP");
                break;
            default:
                break;
        }
        return super.onInterceptTouchEvent(ev);
    }

    @Override
    public boolean onTouchEvent(MotionEvent ev) {
        int action = ev.getAction();
        switch (action) {
            case MotionEvent.ACTION_DOWN:
                Logger.d(TAG + "--onTouchEvent--DOWN");
                break;
            case MotionEvent.ACTION_MOVE:
                Logger.d(TAG + "--onTouchEvent--MOVE");
                break;
            case MotionEvent.ACTION_UP:
                Logger.d(TAG + "--onTouchEvent--UP");
                break;
            default:
                break;
        }
        return super.onTouchEvent(ev);
    }
}
ViewB源码示例:
package com.xiaoqlu.motionevent.touchview;

import android.content.Context;
import android.support.v7.widget.AppCompatTextView;
import android.util.AttributeSet;
import android.view.MotionEvent;
import com.xiaoqlu.motionevent.util.Logger;

/**
 * @author hongri
 */
public class ViewB extends AppCompatTextView {
    private final String TAG = "ViewB";

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

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        int action = ev.getAction();
        switch (action) {
            case MotionEvent.ACTION_DOWN:
                Logger.d(TAG + "--dispatchTouchEvent--DOWN");
                break;
            case MotionEvent.ACTION_MOVE:
                Logger.d(TAG + "--dispatchTouchEvent--MOVE");
                break;
            case MotionEvent.ACTION_UP:
                Logger.d(TAG + "--dispatchTouchEvent--UP");
                break;
            default:
                break;
        }
        return super.dispatchTouchEvent(ev);
    }

    @Override
    public boolean onTouchEvent(MotionEvent ev) {
        int action = ev.getAction();
        switch (action) {
            case MotionEvent.ACTION_DOWN:
                Logger.d(TAG + "--onTouchEvent--DOWN");
                break;
            case MotionEvent.ACTION_MOVE:
                Logger.d(TAG + "--onTouchEvent--MOVE");
                break;
            case MotionEvent.ACTION_UP:
                Logger.d(TAG + "--onTouchEvent--UP");
                break;
            default:
                break;
        }
        return super.onTouchEvent(ev);
    }
}

日志如下:

通过查看源码与Log对比下上面的结论,是不是已经理解了呢。

二、解决滑动事件冲突

我们都知道解决滑动冲突的方式主要有两种:

1、外部拦截法

外部拦截法重写父容器的onInterceptTouchEvent方法,在内部做相应的拦截即可,如上事件分析中已做介绍,这里不再说明。

2、内部拦截法:

内部拦截法,是指父容器不拦截任何事件,所有的事件都传递给子元素,如果子元素需要此事件就直接消耗掉,否则就交由父容器进行处理,需要配合 requestDisallowInterceptTouchEvent 方法才能正常工作。

现在我们修改ViewGroupA代码如下:

@Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        int action = ev.getAction();
        switch (action) {
            case MotionEvent.ACTION_DOWN:
                Logger.d(TAG + "--onInterceptTouchEvent--DOWN");
                break;
            case MotionEvent.ACTION_MOVE:
                Logger.d(TAG + "--onInterceptTouchEvent--MOVE");
                break;
            case MotionEvent.ACTION_UP:
                Logger.d(TAG + "--onInterceptTouchEvent--UP");
                break;
            default:
                break;
        }
        //此处有修改
        if (action == MotionEvent.ACTION_DOWN) {
            return super.onInterceptTouchEvent(ev);
        } else {
            return true;
        }
    }

    @Override
    public boolean onTouchEvent(MotionEvent ev) {
        int action = ev.getAction();
        switch (action) {
            case MotionEvent.ACTION_DOWN:
                Logger.d(TAG + "--onTouchEvent--DOWN");
                break;
            case MotionEvent.ACTION_MOVE:
                Logger.d(TAG + "--onTouchEvent--MOVE");
                break;
            case MotionEvent.ACTION_UP:
                Logger.d(TAG + "--onTouchEvent--UP");
                break;
            default:
                break;
        }
        //此处有修改
        return true;
    }

现在我们修改ViewB代码如下:

@Override
    public boolean onTouchEvent(MotionEvent ev) {
        int action = ev.getAction();
        switch (action) {
            case MotionEvent.ACTION_DOWN:
                Logger.d(TAG + "--onTouchEvent--DOWN");
                mLastTouchY = ev.getY();
                //此处有修改
                getParent().requestDisallowInterceptTouchEvent(true);
                break;
            case MotionEvent.ACTION_MOVE:
                Logger.d(TAG + "--onTouchEvent--MOVE");
                mDragDistance = ev.getY() - mLastTouchY;
                //此处有修改
                /**
                 * 当Y轴滑动距离大于20px时,由父类拦截事件
                 */
                if (Math.abs(mDragDistance) > 20) {
                    Logger.d(TAG + "--允许父类拦截--");
                    getParent().requestDisallowInterceptTouchEvent(false);
                } else {
                    getParent().requestDisallowInterceptTouchEvent(true);
                }
                break;
            case MotionEvent.ACTION_UP:
                Logger.d(TAG + "--onTouchEvent--UP");
                break;
            default:
                break;
        }
        return true;
    }

以上修改表示:当滑动Y轴距离小于20px的时候,子ViewB消费该事件,当滑动Y轴距离大于20px的时候,父ViewGroupA消费该事件(此时的核心点就是需要将 requestDisallowInterceptTouchEvent 置位false,即表示允许父类拦截该事件)。

打印日志如下,是不是很清晰呢。

需要源码对其他场景做测试的可以直接去github主页下载源码:查看源码

三、典型问题记录:

Q:对常用的Touch或click事件进行一个优先级排序?

A:优先级:onTouchListener > onTouchEvent > onLongClickListener > onClickListener

Q:onTouch和onTouchEvent有什么区别,又该如何使用?

A:这两个方法都是在View的dispatchTouchEvent中调用的,onTouch优先于onTouchEvent执行。如果在onTouch方法中通过返回true将事件消费掉,onTouchEvent将不会再执行

另外,onTouch能够得到执行需要两个前提条件,第一mOnTouchListener的值不能为空,第二当前点击的控件必须是enable的。因此如果你有一个控件是非enable的,那么给它注册onTouch事件将永远得不到执行。对于这一类控件,如果我们想要监听它的touch事件,就必须通过在该控件中重写onTouchEvent方法来实现。

Q:requestDisallowInterceptTouchEvent 可以在子元素中干扰父元素的事件分发吗?如果可以,是全部都可以干扰吗?

A:肯定可以,但是down事件干扰不了

Q:一旦有事件传递给View,View的onTouchEvent一定会被调用吗?

A:View没有onInterceptTouchEvent方法,一旦有事件传递给View,View的onTouchEvent就一定会被调用

Q:dispatchTouchEvent每次都会被调用吗?

A:是的,onInterceptTouchEvent则不会。

Q:ACTION_CANCEL 事件触发时机?

A:ACTION_CANCEL 事件是收到前驱事件后,后续事件被父控件拦截的情况下产生,onTouchEvent的事件回传到父控件只会发生在ACTION_DOWN事件中。

Q:父亲Down事件拦截后,还能传递给子View吗?

A:不能

Q:requestDisallowInterceptTouchEvent 发生在 onInterceptTouchEvent 之前还是之后?

A:之前

在 DispatchTouchEvent 源码中,有如下代码,即可得出答案 (查看):

            // Check for interception.
            final boolean intercepted;
            if (actionMasked == MotionEvent.ACTION_DOWN
                    || mFirstTouchTarget != null) {
                final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
                if (!disallowIntercept) {
                    intercepted = onInterceptTouchEvent(ev);
                    ev.setAction(action); // restore action in case it was changed
                } else {
                    intercepted = false;
                }
            } else {
                // There are no touch targets and this action is not an initial down
                // so this view group continues to intercept touches.
                intercepted = true;
            }

参考:

Android事件分发机制完全解析,带你从源码的角度彻底理解(上)【郭霖】

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

红日666

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值