Android touch 事件分发

Android touch 事件的分发是 Android 工程师必备技能之一。关于事件分发主要有几个方向可以展开深入分析:

  • touch 事件是如何从驱动层传递给 Framework 层的 InputManagerService;
  • WMS 是如何通过 ViewRootImpl 将事件传递到目标窗口;
  • touch 事件到达 DecorView 后,是如何一步步传递到内部的子 View 中的。

其中与上层软件开发息息相关的就是第 3 条,也是本文的重点。

Touch事件

我们知道一次完整的Touch事件序列为:

ACTION_DOWN ----> ACTION_MOVE ----> ACTION_UP / ACTION_CANCEL(人为取消的情况)

而对于Touch事件的分发,不管是View还是ViewGroup都和一下的三个方法有关系:

  • dispatchTouchEvent():事件分发
  • onInterceptTouchEvent():事件拦截(只有ViewGroup才有该方法)
  • onTouchEvent():事件消费

Touch事件相关方法

  • 1、public boolean dispatchTouchEvent(MotionEvent ev):
    事件分发方法,分发Event所调用
  • 2、public boolean onInterceptTouchEvent(MotionEvent
    ev):事件拦截方法,拦截Event所调用
  • 3、public boolean onTouchEvent(MotionEvent
    event):事件响应方法,处理Event所调用

拥有上述事件的类

  • 1、Activity类(Activity及其各种继承子类)

    dispatchTouchEvent()、onTouchEvent()

  • 2、ViewGroup类(LinearLayout、FrameLayout、ListView等…)

    dispatchTouchEvent()、onInterceptTouchEvent()、onTouchEvent()

  • 3、View类(Button、TextView等…)

    dispatchTouchEvent()、onTouchEvent()

PS:需要特别注意一点就是ViewGroup中额外拥有onInterceptTouchEvent()方法,其他两个方法为这三种类所共同拥有。

方法的简单用途解析

我们可以发现这三个方法的返回值都为boolean类型,其实它们就是通过返回值来决定下一步的传递处理方向。

  • 1、dispatchTouchEvent() ——用来分发事件所用

    该方法会将根元素的事件自上而下依次分发到内层子元素中,直到被终止或者到达最里层元素,该方法也是采用一种隧道方式来分发。在其中会调用onInterceptTouchEvent()和onTouchEvent(),一般不会去重写。

    返回false则不拦截继续往下分发,如果返回true则拦截住该事件不在向下层元素分发,在dispatchTouchEvent()方法中默认返回false。

  • 2、onInterceptTouchEvent() ——用来拦截事件所用

    该方法在ViewGroup源代码中实现就是返回false不拦截事件,Touch事件就会往下传递给其子View。

    如果我们重写该方法并且将其返回true,该事件将会被拦截,并且被当前ViewGroup处理,调用该类的onTouchEvent()方法。

  • 3、onTouchEvent() ——用来处理事件

    返回true则表示该View能处理该事件,事件将终止向上传递(传递给其父View)

    返回false表示不能处理,则把事件传递给其父View的onTouchEvent()方法来处理

当一个点击事件发生时,从Activity的事件分发开始(Activity.dispatchTouchEvent())

在这里插入图片描述

ViewGroup 事件分发示意图

在这里插入图片描述在这里插入图片描述整个touch事件的传递过程为: Activity.dispatchTouchEvent() -> PhoneWindow.superDispatchTouchEvent() -> DecorView.superDispatchTouchEvent() -> ViewGroup.dispatchTouchEvent() -> View.dispatchTouchEvent()

而消费过程则相反: View.onTouchEvent() -> ViewGroup.onTouchEvent() -> DecorView.onTouchEvent() -> Activity.onTouchEvent()

ViewGroup中包含多个子view时会将touch事件分配给包含在点击位置处的子view

ViewGroup和子view同时注册了监听器OnClickListener 监听事件由子view进行消费

在一次完整的touch事件(ACTION_DOWN -> ACTION_MOVE -> ACTION_UP)传递过程中,touch事件应该被

同一个view进行消费,全部接受或者全部拒绝

只要接受ACTION_DOWN事件就意味着接受所有的事件,拒绝接受ACTION_DOWN 则不会接受后续的内容

如果当前正在处理的touch事件被上层的view拦截,会接收到一个ACTION_CANCEL,后续事件不会再传递过来

父容器拿到触摸事件,默认不拦截(onInterceptTouchEvent() return false),分发给孩子(dispatchTransformedTouchEvent()),

看孩子是否消费, 孩子不消费,事件又传回父容器(onTouchEvent()),看父容器是否消费。

  • 父容器拿到触摸事件,如果ACTION_DWON事件传递下去没有孩子消费,那么后续的事件就不会传了。(没有消费mFirstTouchTarget == null)

  • 父容器拿到触摸事件,默认不拦截,分发给孩子,看孩子是否消费,孩子消费,事件传递结束。

  • 父容器拿到触摸事件,拦截,事件不会分发给孩子,交给自己是否消费(调用父容器自己的onTouchEvent)

父容器拦截touch事件要分为两种:

  • 1.拦截ACTION_DOWN 直接导致mFirstTouchTarget为null 那么直接调用ViewGroup的父类即View类中的dispatchTouchEvent()

    方法即dispatchTouchEvent() -> onTouch() ->
    onTouchEvent()此时会将ViewGroup当做一个普通的View进行处理

  • 2.拦截ACTION_DOWN之后的ACTION_MOVE ACTION_UP事件如果拦截这些事件中的一个事件会将该事件转换成一个

    ACTION_CANCEL给消费了这个事件的子view 因此本次的touch事件是不会
    传递给拦截了touch事件的viewgroup的而是

    当下次touch事件到来时才会传递给该viewgroup的onTouchEvent()方法来处理并且会将mFirstTouchTarget置为null
    当下次

    touch事件到来时由于mFirstTouchTatget为Null会直接调用自己的onTouchEvent()方法
    而不会在传递个子view了

另外还有就是关于setOnTouchListener与 onTouchEvent的关系:

setOnTouchListener的onTouch方法和onTouchEvent()方法都会在我们View的dispatchTouchEvent()方法里面执行

不过onTouch()方法会优先于我们的onTouchEvent()方法而且如果OnTouchListener不为空直接执行onTouch()方法

并且直接返回true就不会在调用onTouchEvent()方法了

  1. 先后关系:onTouch先于onTouchEvent执行

  2. 如果onTouch返回true,表示消费,onTouchEvent就不会执行。

View 的事件分发示意图

在这里插入图片描述在这里插入图片描述从View的touch事件传递流程得出以下几点:

  • 1.OnTouchListener()的onTouch()方法是优先于View的OnTouchEvent()方法执行的如果OnTouchListener的onTouch()方法
    返回了true表示消费了touch事件那么后续View的onTouchEvent()方法也就不会再执行了那么View的onClick()
    onLongClick() 等方法也就不会再接着执行了(onClick()
    onLongClick()等方法都是在onTouchEvnet()方法中进行执行的)
  • 2.如果View是未激活的即处于DISABLED状态但是是可点击的(CLICKABLE LONG_CLICKABLE CONTEXT_CLICKALE)那么view也会 消费掉touch事件但是不会响应OnClickListener的onClick()方法
    onLongClickListener的onLongCLick()方法等
  • 3.只要是View可点击的并且处于ENABLED状态那么就一定返回true即一定会消费touch事件
  • 4.在View的onTouchEvent()方法中处理我们常见的点击事件如:ACTION_DOWN 中处理长点击onLongClick() ACTION_UP中处理点击onClick()等
  • 5.onTouch() onTouchEvent()中事件是否被消费了由方法的返回值来决定 而不是由我们是否在方法中使用了 touch事件MotionEvent来决定的
  • 6.View的事件调度顺序是dispatchTouchEvent() -> onTouchListener() -> onTouchEvent() -> onLongCLick() -> onClick()
  • 7.View是没有onIterceptTouchEvent()即没有拦截touch事件的方法的,ViewGroup才有
  • 8.View的onTouchEvent()方法中只要view是clickable(CLICKABLE LONG_CLICKABLE CONTEXT_CLICKABLE)的那么 就会消费这次touch事件(从ACTION_DOWN 到 ACTION_UP结束)
    在安卓中有些控件默认是clickable的比如Button checkbox 等 而TextView LinearLayout等是non
    clickable的

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

View中dispatchTouchEvent方法的源码:


    public boolean dispatchTouchEvent(MotionEvent event) {

        boolean result = false;

        if (onFilterTouchEventForSecurity(event)) {
            if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
                result = true;
            }
            //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;
    }

onTouch和onTouchEvent执行顺序

 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;
            }
  • 从源码中可以看出,这两个方法都是在View的dispatchTouchEvent中调用的,onTouch优先于onTouchEvent执行。如果在onTouch方法中通过返回true将事件消费掉,onTouchEvent将不会再执行。
  • 另外需要注意的是,onTouch能够得到执行需要两个前提条件,第一mOnTouchListener的值不能为空,第二当前点击的控件必须是enable的。因此如果你有一个控件是非enable的,那么给它注册onTouch事件将永远得不到执行。对于这一类控件,如果我们想要监听它的touch事件,就必须通过在该控件中重写onTouchEvent方法来实现。

OnClick OnLongClick等对外的监听是在哪里处理的?

OnClick事件是先ACTION_DOWN之后再ACTION_UP,所以必定要在onTouchEvent()处理。同理,OnLongClick是在保持ACTION_DOWN一段时间后发生,因此也要在onTouchEvent()中处理。看看源码,发现果然是在这里:

//以下源码均为忽略了不想关部分,只保留了重点
public boolean onTouchEvent(MotionEvent event) {
    //...
    if (((viewFlags & CLICKABLE) == CLICKABLE ||
            (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) ||
            (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE) {
        switch (action) {
            case MotionEvent.ACTION_UP:
                    if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
                        // 处理click
                        if (!focusTaken) {
                            if (mPerformClick == null) {
                                mPerformClick = new PerformClick();
                            }
                            if (!post(mPerformClick)) {
                                performClick();
                            }
                        }
                    }
                }
                break;

            case MotionEvent.ACTION_DOWN:
                // a short period in case this is a scroll.
                if (isInScrollingContainer) {
                    //...
                } else {
                    // 处理longclick
                    setPressed(true, x, y);
                    checkForLongClick(0, x, y);
                }
                break;

            case MotionEvent.ACTION_CANCEL:
                setPressed(false);
                //...
                mIgnoreNextUpEvent = false;
                break;

            case MotionEvent.ACTION_MOVE:
                //...
                break;
        }

        return true;
    }

    return false;
}

执行onClick

public boolean performClick() {
    sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
    if (mOnClickListener != null) {
        playSoundEffect(SoundEffectConstants.CLICK);
        mOnClickListener.onClick(this);
        return true;
    }
    return false;
}

执行OnLongClick

// 处理longclick
                    setPressed(true, x, y);
                    checkForLongClick(0, x, y);
================================================

private void checkForLongClick(long delay, float x, float y, int classification) {
        if ((mViewFlags & LONG_CLICKABLE) == LONG_CLICKABLE || (mViewFlags & TOOLTIP) == TOOLTIP) {
            mHasPerformedLongPress = false;

            if (mPendingCheckForLongPress == null) {
                mPendingCheckForLongPress = new CheckForLongPress();
            }
            mPendingCheckForLongPress.setAnchor(x, y);
            mPendingCheckForLongPress.rememberWindowAttachCount();
            mPendingCheckForLongPress.rememberPressedState();
            mPendingCheckForLongPress.setClassification(classification);
            postDelayed(mPendingCheckForLongPress, delay);
        }
    }
    ===================================
    private final class CheckForLongPress implements Runnable {
       
        @Override
        public void run() {
            if ((mOriginalPressedState == isPressed()) && (mParent != null)
                    && mOriginalWindowAttachCount == mWindowAttachCount) {
                recordGestureClassification(mClassification);
                // 执行onLongClick
                if (performLongClick(mX, mY)) {
                    mHasPerformedLongPress = true;
                }
            }
        }
    }
    ====================================
     public boolean performLongClick(float x, float y) {
        mLongClickX = x;
        mLongClickY = y;
        final boolean handled = performLongClick();
        mLongClickX = Float.NaN;
        mLongClickY = Float.NaN;
        return handled;
    }
    ====================
private boolean performLongClickInternal(float x, float y) {
       

        boolean handled = false;
        final ListenerInfo li = mListenerInfo;
        if (li != null && li.mOnLongClickListener != null) {
        // 执行onLongClick
            handled = li.mOnLongClickListener.onLongClick(View.this);
        }
        return handled;
    }

何时 触发CANCEL 事件

看ViewGroup的dispatchTouchEvent方法
在这里插入图片描述上图红框中表明已经有子 View 捕获了 touch 事件,但是蓝色框中的 intercepted boolean 变量又是 true。这种情况下,事件主导权会重新回到父视图 ViewGroup 中,并传递给子 View 的分发事件中传入一个 cancelChild == true。

看一下 dispatchTransformedTouchEvent 方法的部分源码如下:
在这里插入图片描述因为之前传入参数 cancel 为 true,并且 child 不为 null,最终这个事件会被包装为一个 ACTION_CANCEL 事件传给 child。

什么情况下会触发这段逻辑呢?

总结一下就是:当父视图的 onInterceptTouchEvent 先返回 false,然后在子 View 的 dispatchTouchEvent 中返回 true(表示子 View 捕获事件),关键步骤就是在接下来的 MOVE 的过程中,父视图的 onInterceptTouchEvent 又返回 true,intercepted 被重新置为 true,此时上述逻辑就会被触发,子控件就会收到 ACTION_CANCEL 的 touch 事件。

实际上有个很经典的例子可以用来演示这种情况:
当在 Scrollview 中添加自定义 View 时,ScrollView 默认在 DOWN 事件中并不会进行拦截,事件会被传递给 ScrollView 内的子控件。只有当手指进行滑动并到达一定的距离之后,onInterceptTouchEvent 方法返回 true,并触发 ScrollView 的滚动效果。当 ScrollView 进行滚动的瞬间,内部的子 View 会接收到一个 CANCEL 事件,并丢失touch焦点。

比如以下代码:
在这里插入图片描述CaptureTouchView 是一个自定义的 View,其源码如下:
在这里插入图片描述CaptureTouchView 的 onTouchEvent 返回 true,表示它会将接收到的 touch 事件进行捕获消费。

上述代码执行后,当手指点击屏幕时 DOWN 事件会被传递给 CaptureTouchView,手指滑动屏幕将 ScrollView 上下滚动,刚开始 MOVE 事件还是由 CaptureTouchView 来消费处理,但是当 ScrollView 开始滚动时,CaptureTouchView 会接收一个 CANCEL 事件,并不再接收后续的 touch 事件。具体打印 log 如下:
在这里插入图片描述因此,我们平时自定义View时,尤其是有可能被ScrollView或者ViewPager嵌套使用的控件,不要遗漏对CANCEL事件的处理,否则有可能引起UI显示异常。

参考文章
Android事件分发机制学习笔记
面试:讲讲 Android 的事件分发机制
Android事件分发机制详解:史上最全面、最易懂
Android事件分发机制完全解析,带你从源码的角度彻底理解(上)
Android自定义View之Touch事件分发机制源码解析
Android的Touch事件分发机制简单探析

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值