精华内容
下载资源
问答
  • 30分钟搞清楚Android Touch事件分发机制,Touch事件分发中只有两个主角:ViewGroup和View,想要深入学习的朋友可以参考本
  • 主要介绍了Android Touch事件分发过程,详细描述了Android Touch事件的主要处理流程,有助于深入理解Android程序设计,需要的朋友可以参考下
  • 主要为大家详细介绍了Android Touch事件分发,内容很详细,感兴趣的朋友可以参考一下
  • Android touch 事件分发

    2020-10-13 02:08:23
    Android touch 事件分发Android 工程师必备技能之一。关于事件分发主要有几个方向可以展开深入分析: touch 事件是如何从驱动层传递给 Framework 层的 InputManagerService; WMS 是如何通过 ViewRootImpl 将...

    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事件分发机制简单探析

    展开全文
  • Android开发主要是在智能手机上,而智能手机基本都是触屏手机,用户的交互绝大部分都是通过触碰屏幕完成的,所以Android的View体系中touch事件的传递处理机制就是重中之重。列表跟着手指移动方向滑动、按钮响应点击...

    引言

    Android开发主要是在智能手机上,而智能手机基本都是触屏手机,用户的交互绝大部分都是通过触碰屏幕完成的,所以Android的View体系中touch事件的传递处理机制就是重中之重。列表跟着手指移动方向滑动、按钮响应点击,这些都是因为touch事件正确传递给了这些View,它们才能做出响应。

    我们知道所有界面都是基于View和ViewGroup的,结构上是一层一层组织的:

    viewHierarchy

    而且整个View tree的顶层就是ViewGroup,所以touch事件传递的重点就在于ViewGroup分发到子View的过程,事件流动的方向。

    分析

    以下源码分析基于API26源码

    下面就通过分析,替touch事件回答"是什么"、“从哪来”、"到哪去"这三大哲学问题。

    MotionEvent

    在framewor中,代表触碰屏幕的动作在被抽象成了MotionEvent类。使用手机时手机在屏幕上滑动,系统会把手指的动作映射成多个MotionEvent分发下去。

    除了操作屏幕,操作手机还可以通过按键,不管是实体键还是更常见的虚拟按键。这类事件是由KeyEvent表示的。而MotionEvent和KeyEvent都是InputEvent的子类。这很容易理解,从操作系统的角度来讲,屏幕和按键都是输入设备。

    手指操作屏幕可以分成多种形式,包括落下、抬起、拖动,快速滑动等等。所以每个MotionEvent都有action表示代表的是什么操作。可以通过getAction()获取。每个MotionEvent中还包括了x和y值,表示这个事件对应的位置,分别可以通过getX()和getY()获取。

    最常用的事件类型有:

    • ACTION_DOWN 表示手指落在屏幕上
    • ACTION_MOVE 表示手指在屏幕上滑动
    • ACTION_UP 表示手指从屏幕上抬起
    • ACTION_CANCEL 表示这次事件序列取消了

    比如最常见的查看一个列表的时候,手指在屏幕上滑动拖动列表滑动的操作,正常情况下,系统会先发出一个ACTION_DOWN事件,然后是一系列的ACTION_MOVE事件,然后以一个ACTION_UP事件结尾。

    事件的起点

    事件自然是系统分发出来的,在事件传递到Activity的时候,把此时的栈打印出来,就可以看到从Framework层面来说事件分发的起点是哪里:

    at TouchEventActivity.dispatchTouchEvent(TouchEventActivity.java:48)
            at com.android.internal.policy.impl.PhoneWindow$DecorView.dispatchTouchEvent(PhoneWindow.java:2486)
            at android.view.View.dispatchPointerEvent(View.java:8868)
            at android.view.ViewRootImpl$ViewPostImeInputStage.processPointerEvent(ViewRootImpl.java:4777)
            at android.view.ViewRootImpl$ViewPostImeInputStage.onProcess(ViewRootImpl.java:4609)
            at android.view.ViewRootImpl$InputStage.deliver(ViewRootImpl.java:4085)
            at android.view.ViewRootImpl$InputStage.onDeliverToNext(ViewRootImpl.java:4138)
            at android.view.ViewRootImpl$InputStage.forward(ViewRootImpl.java:4104)
            at android.view.ViewRootImpl$AsyncInputStage.forward(ViewRootImpl.java:4241)
            at android.view.ViewRootImpl$InputStage.apply(ViewRootImpl.java:4112)
            at android.view.ViewRootImpl$AsyncInputStage.apply(ViewRootImpl.java:4298)
            at android.view.ViewRootImpl$InputStage.deliver(ViewRootImpl.java:4085)
            at android.view.ViewRootImpl$InputStage.onDeliverToNext(ViewRootImpl.java:4138)
            at android.view.ViewRootImpl$InputStage.forward(ViewRootImpl.java:4104)
            at android.view.ViewRootImpl$InputStage.apply(ViewRootImpl.java:4112)
            at android.view.ViewRootImpl$InputStage.deliver(ViewRootImpl.java:4085)
            at android.view.ViewRootImpl.deliverInputEvent(ViewRootImpl.java:6593)
            at android.view.ViewRootImpl.doProcessInputEvents(ViewRootImpl.java:6567)
            at android.view.ViewRootImpl.enqueueInputEvent(ViewRootImpl.java:6520)
            at android.view.ViewRootImpl$WindowInputEventReceiver.onInputEvent(ViewRootImpl.java:6773)
            at android.view.InputEventReceiver.dispatchInputEvent(InputEventReceiver.java:185)
            at android.os.MessageQueue.nativePollOnce(Native Method)
            at android.os.MessageQueue.next(MessageQueue.java:148)
            at android.os.Looper.loop(Looper.java:151)
            at android.app.ActivityThread.main(ActivityThread.java:5898)
            at java.lang.reflect.Method.invoke(Native Method)
            at java.lang.reflect.Method.invoke(Method.java:372)
            at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:1019)
            at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:814)
    

    在ActivityThread主线程的loop里循环里,走到了InputEventReceiver,再走到ViewRootImpl,再走到DecorView,再走到Activity的dispatchTouchEvent,然后就是继续分发给这个Activity上的各个View。

    从主线程的loop走到InputEventReceiver#dispatchInputEvent在SDK里是看不到调用的,是从native调用过来的,dispatchInputEvent方法上的注释说明了这点。

    // Called from native code.
    private void dispatchInputEvent(int seq, InputEvent event) {
        mSeqMap.put(event.getSequenceNumber(), seq);
        onInputEvent(event);
    }
    

    至于native具体是怎么获取屏幕上的事件再传过来的,因为Android基于Linux,更底层的输入驱动和机制类似linux,可以理解成每个输入设备都有一个对应的文件符挂载在系统上以供读取,硬件不断往文件写入,系统不断读取文件。更具体的作为Android应用开发者也不是很了解,就不具体讨论了。可以通过android的adb工具大致看下输入设备的情况。连上手机,输入adb shell getevent -l,滑动下屏幕,可以看到一些输出,每个手机不一样,下面是我的魅族手机的输出:

    ~ » adb shell getevent -l
    add device 1: /dev/input/event7
      name:     "sm8150-meizu-snd-card USBC Jack"
    add device 2: /dev/input/event6
      name:     "main_touch"
    add device 3: /dev/input/event1
      name:     "qti-haptics"
    add device 4: /dev/input/event0
      name:     "qpnp_pon"
    add device 5: /dev/input/event2
      name:     "uinput-goodix"
    add device 6: /dev/input/event3
      name:     "ndt"
    add device 7: /dev/input/event5
      name:     "gpio-keys"
    add device 8: /dev/input/event4
      name:     "qbt1000_key_input"
    /dev/input/event6: EV_KEY       BTN_TOUCH            DOWN
    /dev/input/event6: EV_ABS       ABS_MT_TRACKING_ID   0000a31f
    /dev/input/event6: EV_ABS       ABS_MT_POSITION_X    000001c1
    /dev/input/event6: EV_ABS       ABS_MT_POSITION_Y    00000664
    /dev/input/event6: EV_ABS       ABS_MT_TOUCH_MAJOR   0000002f
    /dev/input/event6: EV_ABS       ABS_MT_PRESSURE      0000002f
    

    大致可以猜出来,main_touch设备就是我们的手机屏幕。

    事件传递方向

    事件产生了,开始传递到当前Activity了,下面是我们更关心的步骤了:在当前展示的view hierarchy上,touch事件是怎么传递的,怎么让列表动起来,让按钮响应点击事件的。

    责任链模式

    整个touch事件传递的过程采用的是类似设计模式中责任链模式(Chain of Responsibility Pattern)的设计。

    举一个责任链模式的例子:假设员工请假的时候,大于3天需要直属leader审批,大于7天需要部门经理审批,大于15天需要CEO审批。假如有一个员工提交了一个请假16天的工单:

    程序员:世界那么大,我要请假16天去看看
    Leader:16天有点久,让经理处理
    经理:16天有点久,让老板处理
    CEO:准了

    这就是具体的责任链模式。

    改进的责任链模式

    touch事件的传递过程类似上面的过程,不同的是传递的方向是会折返的。

    先说结论:ViewTree是层级组织的,上一层ViewGroup在收到touch事件的时候,会尝试传递给自己的child,并且child会告诉parent自己有没有消耗掉事件;如果它的child也是ViewGroup,又会重复这个操作;所以touch事件首先会从ViewTree最顶部传递到ViewTree的某个叶子节点的View,如果这个View没有消耗掉事件,处理权会从叶子节点传递回根节点。完整轨迹类似一个U形状。这样能保证所有节点都有机会处理touch事件。

    传递过程中之所以需要通过返回值向parent回报事件有没有被消耗,是因为默认当一组touch事件确定传递个某个view之后,这次touch事件序列中后续所有事件都会传递给它而不是别的view。所以需要当消耗了事件后需要回报给parent,这样parent才能记住后续传递事件的目标

    再举一个类似的例子:

    老板:结合外部市场,我觉得今年我们应该加上这么一个功能。我是老板(onInterceptTouchEvent)这个事情就给小A你来跟进吧 (dispatchTouchEvent)
    经理A:最近没空啊(onInterceptTouchEvent)。小C啊,有个功能你来做下(dispatchTouchEvent)
    程序员C:我擦这个不想做啊(onInterceptTouchEvent),小D啊,有个功能你来做下(dispatchTouchEvent)
    实习生D:学长,这个我不会做呀(onTouchEvent)
    程序员C:经理,这个我不会做呀(onTouchEvent)
    经理A:擦,那只能我自己做了(onTouchEvent)。老板,搞定了。

    事件传递到的每一个节点,都会尝试把事件给自己的子节点处理,子节点没处理的才会尝试自己处理。

    下面的一个简单布局中,所有View都没有处理touch,所以touch经历了最长的传递过程。通过在关键节点打的log,可以看到当点击最里面的CView的时候事件传递的具体情况。

    <?xml version="1.0" encoding="utf-8"?>
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:orientation="vertical" android:layout_width="match_parent"
        android:gravity="center"
        android:layout_height="match_parent"
        android:background="#ffffffff">
    
        <com.commondemo.view.LogFrameView
            android:id="@+id/a"
            android:layout_width="300dp"
            android:layout_height="300dp"
            android:background="#ffff0000">
            <com.commondemo.view.LogFrameView
                android:id="@+id/b"
                android:layout_width="200dp"
                android:layout_height="200dp"
                android:layout_gravity="center"
                android:background="#ff00ff00">
                <com.commondemo.view.LogViw
                    android:id="@+id/c"
                    android:layout_width="100dp"
                    android:layout_height="100dp"
                    android:layout_gravity="center"
                    android:background="#ff0000ff"/>
            </com.commondemo.view.LogFrameView>
        </com.commondemo.view.LogFrameView>
    </LinearLayout>
    

    demo

    activity:     dispatchTouchEvent:     ACTION_DOWN begin
    AView:        dispatchTouchEvent:     ACTION_DOWN begin
    AView:     onInterceptTouchEvent:     ACTION_DOWN begin
    AView:     onInterceptTouchEvent:     ACTION_DOWN return false
    BView:        dispatchTouchEvent:     ACTION_DOWN begin
    BView:     onInterceptTouchEvent:     ACTION_DOWN begin
    BView:     onInterceptTouchEvent:     ACTION_DOWN return false
    CView:        dispatchTouchEvent:     ACTION_DOWN begin
    CView:              onTouchEvent:     ACTION_DOWN begin
    CView:              onTouchEvent:     ACTION_DOWN return false
    CView:        dispatchTouchEvent:     ACTION_DOWN return false
    BView:              onTouchEvent:     ACTION_DOWN begin
    BView:              onTouchEvent:     ACTION_DOWN return false
    BView:        dispatchTouchEvent:     ACTION_DOWN return false
    AView:              onTouchEvent:     ACTION_DOWN begin
    AView:              onTouchEvent:     ACTION_DOWN return false
    AView:        dispatchTouchEvent:     ACTION_DOWN return false
    activity:           onTouchEvent:     ACTION_DOWN begin
    activity:           onTouchEvent:     ACTION_DOWN return false
    activity:     dispatchTouchEvent:     ACTION_DOWN return false
    activity:     dispatchTouchEvent:       ACTION_UP begin
    activity:           onTouchEvent:       ACTION_UP begin
    activity:           onTouchEvent:       ACTION_UP return false
    activity:     dispatchTouchEvent:       ACTION_UP return false
    

    代码分析

    带着上面的结论,看下源码验证。

    上面的分析中我们已经看到,事件传递最关键就是三个函数:onInterceptTouchEvent、dispatchTouchEvent、onTouchEvent

    • onInterceptTouchEvent表示该不该拦截这个事件,如果拦截了,就不会传递给child了。所以只有ViewGroup才有这个方法,因为单纯的View没有child。child可以通过调用
    • dispatchTouchEvent就是具体把事件分发给child。如果上面的onInterceptTouchEvent拦截了事件,就不会走到这里。ViewGroup的默认实现是从后往前遍历child,找到事件的x、y落在其中的view调用它的onTouchEvent
    • onTouchEvent,一般在这里对事件做出响应改变UI,比如滑动内容,触发点击事件回调
    onInterceptTouchEventdispatchTouchEventonTouchEvent
    View
    ViewGroup
    Activity

    ViewGroup的dispatchTouchEvent方法比较长,删去不重要的部分,关键地方加上了注释:

    public boolean dispatchTouchEvent(MotionEvent ev) {
        ...
    
        boolean handled = false;
        if (onFilterTouchEventForSecurity(ev)) {
            final int action = ev.getAction();
    
            ...
            // Check for interception.
            final boolean intercepted;
            if (actionMasked == MotionEvent.ACTION_DOWN
                    || mFirstTouchTarget != null) {
                // 这里的结果受自己的requestDisallowInterceptTouchEvent有没有被调用影响
                final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
                if (!disallowIntercept) {
                    // 调用onInterceptTouchEvent。默认实现是返回false,不拦截
                    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;
            }
    
            // Check for cancelation.
            final boolean canceled = resetCancelNextUpFlag(this)
                    || actionMasked == MotionEvent.ACTION_CANCEL;
    
            // 如果前面没有拦截,下面就尝试分发给合适的child
            if (!canceled && !intercepted) {
    
                    final int childrenCount = mChildrenCount;
                    // 遍历children
                    if (newTouchTarget == null && childrenCount != 0) {
                        final float x = ev.getX(actionIndex);
                        final float y = ev.getY(actionIndex);
                        // Find a child that can receive the event.
                        // Scan children from front to back.
                        final ArrayList<View> preorderedList = buildTouchDispatchChildList();
                        final boolean customOrder = preorderedList == null
                                && isChildrenDrawingOrderEnabled();
                        final View[] children = mChildren;
                        for (int i = childrenCount - 1; i >= 0; i--) {
                            final int childIndex = getAndVerifyPreorderedIndex(
                                    childrenCount, i, customOrder);
                            final View child = getAndVerifyPreorderedView(
                                    preorderedList, children, childIndex);
                            // 这里判断事件是不是落在这个child上,根据坐标判断
                            if (!canViewReceivePointerEvents(child)
                                    || !isTransformedTouchPointInView(x, y, child, null)) {
                                ev.setTargetAccessibilityFocus(false);
                                continue;
                            }
                            // 找到了后续事件应该分发的child,记录起来
                            newTouchTarget = getTouchTarget(child);
                            if (newTouchTarget != null) {
                                // Child is already receiving touch within its bounds.
                                // Give it the new pointer in addition to the ones it is handling.
                                newTouchTarget.pointerIdBits |= idBitsToAssign;
                                break;
                            }
                        }
                        if (preorderedList != null) preorderedList.clear();
                    }
    
                }
            }
    
            // Dispatch to touch targets.
            if (mFirstTouchTarget == null) {
                // No touch targets so treat this as an ordinary view.
                // 前面没有找到合适的分发事件的child,分发给自己
                // 这里的dispatchTransformedTouchEvent最终会调到自身的onTouchEvent
                // 默认不处理事件,返回false
                handled = dispatchTransformedTouchEvent(ev, canceled, null,
                        TouchTarget.ALL_POINTER_IDS);
            } else {
                // Dispatch to touch targets, excluding the new touch target if we already
                // dispatched to it.  Cancel touch targets if necessary.
              
              ...
                // 分发给前面找到的合适的child
                // 这里dispatchTransformedTouchEvent最终会调到child的onTouchEvent
                if (dispatchTransformedTouchEvent(ev, cancelChild,
                        target.child, target.pointerIdBits)) {
                    handled = true;
                }
    
              ...
                    
            }
    
        }
    
        // 向parent报告本次事件的处理结果,true表示消耗了事件,false表示没有消耗
        return handled;
    }
    

    从代码上看大概是这么几个步骤:

    1. 可以看到在最开始就判断自己的requestDisallowInterceptTouchEvent有没有被调用,然后再调用了自己的onInterceptTouchEvent,然后根据返回决定要不要继续分发给child。
    2. 前面没有intercept的话,倒序遍历child,根据touch事件的坐标找到合适的分发child
    3. 如果上一步没有找到合适的child,就把事件分发给自己的onTouchEvent,onTouchEvent的返回就是dispatchTouchEvent的返回。默认返回false
    4. 如果上一步找到了合适的child,就把事件分发给这个child的onTouchEvent,它的onTouchEvent的返回就是dispatchTouchEvent的返回。
    5. 通过返回值向parent回报事件的处理结果。

    实际应用

    RefreshLayout

    app开发中有很多场景要用到列表的形式来展示,往下滑需要不断出来新的内容,在列表回到最顶上的时候再往下滑要把整个列表而不是内容往下拉,松开刷新,也就是下拉刷新。

    分析一下就会发现,列表我们一般用recyclerView来做,要实现下拉刷新,就得把recyclerView放到另一个可以支持滑动的ViewGroup里,有时候需要把滑动事件分发给外层ViewGroup让它把这个recyclerView往下移动,露出上面的loading样式,大部分时候需要把滑动事件分发给recyclerView让列表内容正常滚动。当两个可以滑动的的View有嵌套关系的时候,就需要小心处理滑动冲突。

    不过,在经过上面的分析之后,其实实现一个简陋的下拉刷新控件不难。先看下实现效果:
    refresh

    源码如下:

    import android.animation.ValueAnimator;
    import android.content.Context;
    import android.graphics.Color;
    import android.support.annotation.NonNull;
    import android.support.v7.widget.LinearLayoutManager;
    import android.support.v7.widget.RecyclerView;
    import android.view.Gravity;
    import android.view.MotionEvent;
    import android.view.View;
    import android.view.ViewGroup;
    import android.widget.FrameLayout;
    import android.widget.TextView;
    
    public class RefreshLayout extends FrameLayout {
        private RecyclerView child;
        private TextView loadding;
        private final String RELEASE_TO_REFRESH = "松手刷新";
        private final String REFRESHING = "刷新中";
    
        public RefreshLayout(@NonNull Context context) {
            super(context);
            setBackgroundColor(Color.WHITE);
            child = new RecyclerView(context);
            child.setBackgroundColor(Color.WHITE);
            child.setLayoutManager(new LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false) {
                @Override
                public RecyclerView.LayoutParams generateDefaultLayoutParams() {
                    return new RecyclerView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
                }
            });
            child.setAdapter(new RecyclerView.Adapter() {
                @Override
                public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
                    TextView tv = new TextView(getContext());
                    tv.setGravity(Gravity.CENTER);
                    tv.setTextColor(Color.BLACK);
                    return new RecyclerView.ViewHolder(tv) {};
                }
    
                @Override
                public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
                    ((TextView) holder.itemView).setText(String.valueOf(position));
                }
    
                @Override
                public int getItemCount() {
                    return 100;
                }
            });
            loadding = new TextView(getContext());
            loadding.setTextColor(Color.BLACK);
            loadding.setText(RELEASE_TO_REFRESH);
            loadding.setGravity(Gravity.CENTER);
            addView(loadding, new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 200));
            addView(child, new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
        }
    
        private boolean childAtTop() {
            View firstView = child.getChildAt(0);
            int firstPosition = ((RecyclerView.LayoutParams) firstView.getLayoutParams()).getViewAdapterPosition();
            return firstPosition == 0 && firstView.getTop() >= 0;
        }
    
        int lastY;
        boolean childOnTopWhenDown = false;
        boolean needHandle = false;
        boolean firstMove = true;
        @Override
        public boolean dispatchTouchEvent(MotionEvent event) {
            int action = event.getAction();
            int eventY = (int) event.getY();
            int dy = 0;
            switch (action) {
                case MotionEvent.ACTION_DOWN:
                    lastY = eventY;
                    childOnTopWhenDown = childAtTop();
                    break;
                case MotionEvent.ACTION_MOVE:
                    dy = eventY - lastY;
                    if (firstMove) {
                        needHandle = childOnTopWhenDown && dy > 0;
                    }
                    lastY = eventY;
                    firstMove = false;
                    break;
                case MotionEvent.ACTION_CANCEL:
                case MotionEvent.ACTION_UP:
                    needHandle = false;
                    childOnTopWhenDown = false;
                    firstMove = true;
                    scrollChildBack();
            }
            if (needHandle) {
                if (dy < 0) dy = Math.max(-child.getTop(), dy);
                child.offsetTopAndBottom(dy);
                return true;
            } else {
                return super.dispatchTouchEvent(event);
            }
        }
    
        private void scrollChildBack() {
            if (child.getTop() == 0) return;
            final ValueAnimator animator = ValueAnimator.ofInt(child.getTop(), 0);
            animator.setDuration(200L);
            animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
                @Override
                public void onAnimationUpdate(ValueAnimator animation) {
                    int currentChildTop = (int) animation.getAnimatedValue();
                    child.offsetTopAndBottom(currentChildTop - child.getTop());
                    if (currentChildTop < 10) loadding.setText(RELEASE_TO_REFRESH);
                }
            });
            loadding.setText(REFRESHING);
            postDelayed(new Runnable() {
                @Override
                public void run() {
                    animator.start();
                }
            }, 1000L);
        }
    }
    

    总结

    • 从开发app的角度来看,能最早介入事件分发的节点是Activity的dispatchTouchEvent
    • 事件分发的轨迹是在view hierarchy上先不断往叶子view传递,传到了最内层的view还没有被消耗再沿原路返回
    • 主要关注onInterceptTouchEvent、dispatchTouchEvent、onTouchEvent三个方法
    展开全文
  • Android Touch事件分发

    2020-08-06 16:04:19
    Android Touch事件分发

    目录

    1.Touch事件分发--泛而谈之

    2.Touch事件分类

    3.参与touch事件分发的视图层级

    4.参与touch事件分发的核心类和方法

    5.读源码

    6.关于touch事件分发的几个问题

    6.1 dispatchTouchEvent方法干了啥

    6.2 相关核心方法返回true或者false,各有什么含义?

    6.3 View的enable和clickable的属性值为true或者false有什么区别?

    6.4 什么时候执行onClick方法

    鸣谢


    注意:本文只讨论Android Framework已经实现的Touch事件分发过程,暂不讨论工程师对相关类的继承、方法的重写。

    1.Touch事件分发--泛而谈之

    目前,用户与手机的交互方式以touch(手指触摸屏幕)为主。

    用面向对象的思维解释“touch事件分发”,“touch事件”是一个对象,在Android中就是一个MotionEvent类型的实例;“分发”是方法的调用过程,MotionEvent对象作为方法的参数传递给所调用的方法。

    MotionEvent对象记录了touch事件类型、发生此touch事件的位置坐标等。

    举个例子,当用户触摸手机屏幕上的一个button,然后系统给出一个动态的反馈。从触摸到反馈,泛而谈之,处理流程如下:

    2.Touch事件分类

    类型描述
    ACTION_DOWN手指按下,以下简称down
    ACTION_UP手指抬起,以下简称up
    ACTION_MOVE手指滑动,,以下简称move
    ACTION_POINTER_DOWN多个手指按下
    ACTION_POINTER_UP多个手指抬起
    ACTION_CANCEL取消事件

    手指从按下,再经过移动,最后抬起,走完一个完整的touch事件流程,即down>move>move>move>move>move>move>up。
    手指每触碰到一个位置,都会产生一个MotionEvent对象,并分发下去。

    3.参与touch事件分发的视图层级

    Activity中有一个PhoneWindow实例,PhoneWindow是抽象类Window的唯一子类。

    PhoneWindow中有一个DecorVIew实例,DecorVIew是FrameLayout的子类,本质是一个ViewGroup,DecorVIew是界面的根视图。常见地,在Activity#onCreate()中调用setContentView就是设置DecorVIew的子视图。

    touch事件分发的入口是Activity的dispatchTouchEvent(MotionEvent event)方法。

    视图层级

    4.参与touch事件分发的核心类和方法

    方法
    Activity

    dispatchTouchEvent

    onTouchEvent

    ViewGroup

    dispatchTouchEvent

    onInterceptTouchEvent

    onTouchEvent(继承View的)

    View

    dispatchTouchEvent

    onTouchEvent

    View的onTouch方法需要工程师在外部实现,本文对此不做重点讨论。 

    5.读源码

    了解了touch事件分发的基本概念之后,那就开始读源码吧。

    读源码秘诀:注重主干,选择性忽略细微的实现;带着问题去读源码比较有针对性,始终记得主线,防止读源码过程中,陷得太深,跑偏了。
    读源码目的:理解原理、弥补盲区

    6.关于touch事件分发的几个问题

    6.1 dispatchTouchEvent方法干了啥

    6.1.1 Activity中的dispatchTouchEvent负责分发touch事件给ViewGroup,源码为证:

        public boolean dispatchTouchEvent(MotionEvent ev) {
            if (ev.getAction() == MotionEvent.ACTION_DOWN) {
                onUserInteraction();
            }
            if (getWindow().superDispatchTouchEvent(ev)) {
                return true;
            }
            return onTouchEvent(ev);
        }

    getWindow().superDispatchTouchEvent(ev),顺着调用线,一直追踪这个方法,可以发现,该方法调用的是DecorVIew对象的dispatchTouchEvent方法。

    6.1.2 ViewGroup中的dispatchTouchEvent大体干了三件事:(源码太长,就不全贴了)

    6.1.2.1 判断是否要拦截事件,如果不拦截,则将touch事件传递给其子View处理;如果拦截,则将touch事件传递给自己的onTouch或者onTouchEvent方法处理。

    6.1.2.2 在当前ViewGroup中找到用户实际触摸到的View。

    6.1.2.3 将touch事件分发给View。

    6.1.3 View中的dispatchTouchEvent方法处理touch事件,源码为证:

        public boolean dispatchTouchEvent(MotionEvent event) {
            // If the event should be handled by accessibility focus first.
            if (event.isTargetAccessibilityFocus()) {
                // We don't have focus or no virtual descendant has it, do not handle the event.
                if (!isAccessibilityFocusedViewOrHost()) {
                    return false;
                }
                // We have focus and got the event, then use normal event dispatch.
                event.setTargetAccessibilityFocus(false);
            }
    
            boolean result = false;
    
            if (mInputEventConsistencyVerifier != null) {
                mInputEventConsistencyVerifier.onTouchEvent(event, 0);
            }
    
            final int actionMasked = event.getActionMasked();
            if (actionMasked == MotionEvent.ACTION_DOWN) {
                // Defensive cleanup for new gesture
                stopNestedScroll();
            }
    
            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;
                }
            }
    
            if (!result && mInputEventConsistencyVerifier != null) {
                mInputEventConsistencyVerifier.onUnhandledEvent(event, 0);
            }
    
            // Clean up after nested scrolls if this is the end of a gesture;
            // also cancel it if we tried an ACTION_DOWN but we didn't want the rest
            // of the gesture.
            if (actionMasked == MotionEvent.ACTION_UP ||
                    actionMasked == MotionEvent.ACTION_CANCEL ||
                    (actionMasked == MotionEvent.ACTION_DOWN && !result)) {
                stopNestedScroll();
            }
    
            return result;
        }

     

    6.2 相关核心方法返回true或者false,各有什么含义?

    方法return truereturn false
    Activity # dispatchTouchEvent我重写了该方法,打了log,发现该方法返回true和false,执行结果都是:整个touch事件流中的每一个touch事件都会被分发。见左
    Activity # onTouchEvent该方法的返回值就是Activity # dispatchTouchEvent的返回值见左
    ViewGroup # dispatchTouchEvent该ViewGroup消费了touch事件,其父ViewGroup # dispatchTouchEvent也返回true该ViewGroup消费不了touch事件,touch事件交由其父ViewGroup来处理,并且不再接收down之后的touch事件
    ViewGroup # onInterceptTouchEvent该ViewGroup拦截了touch事件,该touch事件由自己来处理,不将touch事件分发给子View该ViewGroup不拦截touch事件
    ViewGroup # onTouchEvent该方法的返回值就是ViewGroup # dispatchTouchEvent的返回值见左
    View # dispatchTouchEvent该View消费了touch事件,其父ViewGroup # dispatchTouchEvent也返回true该View消费不了touch事件,touch事件交由其父ViewGroup来处理,并且不再接收down之后的touch事件
    View # onTouch该方法消费了touch事件,不再执行View # onTouchEvent,View # dispatchTouchEvent也返回true该方法消费不了touch事件,将touch事件传递给View # onTouchEvent处理
    View # onTouchEvent该方法的返回值就是View # dispatchTouchEvent的返回值见左

    可见,touch事件分发过程中,不仅传递MontionEvent对象,还传递返回值。
    MontionEvent对象从外往内传
    返回值从内往外传

    6.3 View的enable和clickable的属性值为true或者false有什么区别?

    如果enable为true,且注册了OnTouchListener,才会执行重写后的onTouch方法,这与clickable的值无关

    如果执行到了View # onTouchEvent,enable和clickable的值影响onTouchEvent的返回值,关系如下:

    enableclickableView # onTouchEvent的返回值

    false

    true

    true

    false

    false

    false

    true

    true

    true

    true

    false

    false

    6.4 什么时候执行onClick方法

    当 注册了View.OnClickListener
       && enable和clickable均为true
       && 执行到了View#onTouchEvent(onTouchEvent中调用了onClick)
       && 一个完整的touch事件流(down>*>up)都传递到了VIew

    ,才会在up之后执行外部实现的onClick方法。

    鸣谢

    Android 9.0源码

    简书某blog
    Android Touch事件分发超详细解析

    工匠若水 的CSDN博客, 非常详细的3篇分析文章
    Android触摸屏事件派发机制详解与源码分析一(View篇)
    Android触摸屏事件派发机制详解与源码分析二(ViewGroup篇)
    Android触摸屏事件派发机制详解与源码分析三(Activity篇)

     

    展开全文
  • 通过写demo打Log,以ACTION_DOWN事件为例,完整了解整个Android Touch事件分发传递机制。 一步步探索学习Android Touch事件分发传递机制(二) 探索了ACTION_MOVE和ACTION_UP事件分发传递规律。 一步步探索...

    前言

    该系列文章分三篇:

    1. 一步步探索学习Android Touch事件分发传递机制(一)
      通过写demo打Log,以ACTION_DOWN事件为例,完整了解整个Android Touch事件分发传递机制。

    2. 一步步探索学习Android Touch事件分发传递机制(二)
      探索了ACTION_MOVE和ACTION_UP事件的分发传递规律。

    3. 一步步探索学习Android Touch事件分发传递机制(三)
      即本篇,将通过Android源码分析,从本质上认识Android Touch事件分发传递机制。

    1. dispatchTouchEvent()方法源码分析

    1.1 Activity的dispatchTouchEvent()方法
    • 源码:
       /**
         * Called to process touch screen events.  You can override this to
         * intercept all touch screen events before they are dispatched to the
         * window.  Be sure to call this implementation for touch screen events
         * that should be handled normally.
         *
         * @param ev The touch screen event.
         *
         * @return boolean Return true if this event was consumed.
         */
        public boolean dispatchTouchEvent(MotionEvent ev) {
            if (ev.getAction() == MotionEvent.ACTION_DOWN) {
                //当判断是ACTION_DOWN事件,回调onUserInteraction()
                onUserInteraction();
            }
            if (getWindow().superDispatchTouchEvent(ev)) {
                //getWindow()方法拿到的是PhoneWindow的实例
                return true;
            }
            //如果没有找到消费该事件的子View,最终会交给Activity的onTouchEvent()处理
            return onTouchEvent(ev);
        }
    • 分析:

      • 该方法首先判断传递进来的是不是一个ACTION_DOWN事件,if 是,就触发一个叫做 onUserInteraction()的回调方法。

      • onUserInteraction()方法在Activity.java中是个空实现。开发者可以在需要的时候重写它,比如用于判断是不是用户开始做与屏幕交互的事情了。

      • 然后,判断调用了getWindow().superDispatchTouchEvent(ev)。getWindow()拿到的是PhoneWindow的实例。

      • 这里需要简单说明一下Android窗口结构:
        Android Window 结构

      • 继续看源码,看看PhoneWindow.java中的superDispatchTouchEvent(ev)方法。

         @Override
            public boolean superDispatchTouchEvent(MotionEvent event) {
                return mDecor.superDispatchTouchEvent(event);
            }
      • 可以看到,这个方法内部又去调用了DecorView的superDispatchTouchEvent(ev)方法。

      • 一路这么调用,DecorView其实是FrameLayout的子类,它没重写这个方法,所以最后调用到了ViewGroup的dispatchTouchEvent()方法。

    1.2 ViewGroup的dispatchTouchEvent()方法
    • 源码:
        @Override
        public boolean dispatchTouchEvent(MotionEvent ev) {
            if (mInputEventConsistencyVerifier != null) {
                mInputEventConsistencyVerifier.onTouchEvent(ev, 1);
            }
    
            // If the event targets the accessibility focused view and this is it, start
            // normal event dispatch. Maybe a descendant is what will handle the click.
            if (ev.isTargetAccessibilityFocus() && isAccessibilityFocusedViewOrHost()) {
                ev.setTargetAccessibilityFocus(false);
            }
    
            boolean handled = false;
            if (onFilterTouchEventForSecurity(ev)) {
                final int action = ev.getAction();
                final int actionMasked = action & MotionEvent.ACTION_MASK;
    
                // Handle an initial down.
                if (actionMasked == MotionEvent.ACTION_DOWN) {
                    // Throw away all previous state when starting a new touch gesture.
                    // The framework may have dropped the up or cancel event for the previous gesture
                    // due to an app switch, ANR, or some other state change.
                    cancelAndClearTouchTargets(ev);
                    resetTouchState();
                }
    
                // 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;
                }
    
                // If intercepted, start normal event dispatch. Also if there is already
                // a view that is handling the gesture, do normal event dispatch.
                if (intercepted || mFirstTouchTarget != null) {
                    ev.setTargetAccessibilityFocus(false);
                }
    
                // Check for cancelation.
                final boolean canceled = resetCancelNextUpFlag(this)
                        || actionMasked == MotionEvent.ACTION_CANCEL;
    
                // Update list of touch targets for pointer down, if needed.
                final boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0;
                TouchTarget newTouchTarget = null;
                boolean alreadyDispatchedToNewTouchTarget = false;
                if (!canceled && !intercepted) {
    
                    // If the event is targeting accessiiblity focus we give it to the
                    // view that has accessibility focus and if it does not handle it
                    // we clear the flag and dispatch the event to all children as usual.
                    // We are looking up the accessibility focused host to avoid keeping
                    // state since these events are very rare.
                    View childWithAccessibilityFocus = ev.isTargetAccessibilityFocus()
                            ? findChildWithAccessibilityFocus() : null;
    
                    if (actionMasked == MotionEvent.ACTION_DOWN
                            || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
                            || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
                        final int actionIndex = ev.getActionIndex(); // always 0 for down
                        final int idBitsToAssign = split ? 1 << ev.getPointerId(actionIndex)
                                : TouchTarget.ALL_POINTER_IDS;
    
                        // Clean up earlier touch targets for this pointer id in case they
                        // have become out of sync.
                        removePointersFromTouchTargets(idBitsToAssign);
    
                        final int childrenCount = mChildrenCount;
                        if (newTouchTarget == null && childrenCount != 0) {
                            final float x = ev.getX(actionIndex);
                            final float y = ev.getY(actionIndex);
                            // Find a child that can receive the event.
                            // Scan children from front to back.
                            final ArrayList<View> preorderedList = buildTouchDispatchChildList();
                            final boolean customOrder = preorderedList == null
                                    && isChildrenDrawingOrderEnabled();
                            final View[] children = mChildren;
                            for (int i = childrenCount - 1; i >= 0; i--) {
                                final int childIndex = getAndVerifyPreorderedIndex(
                                        childrenCount, i, customOrder);
                                final View child = getAndVerifyPreorderedView(
                                        preorderedList, children, childIndex);
    
                                // If there is a view that has accessibility focus we want it
                                // to get the event first and if not handled we will perform a
                                // normal dispatch. We may do a double iteration but this is
                                // safer given the timeframe.
                                if (childWithAccessibilityFocus != null) {
                                    if (childWithAccessibilityFocus != child) {
                                        continue;
                                    }
                                    childWithAccessibilityFocus = null;
                                    i = childrenCount - 1;
                                }
    
                                if (!canViewReceivePointerEvents(child)
                                        || !isTransformedTouchPointInView(x, y, child, null)) {
                                    ev.setTargetAccessibilityFocus(false);
                                    continue;
                                }
    
                                newTouchTarget = getTouchTarget(child);
                                if (newTouchTarget != null) {
                                    // Child is already receiving touch within its bounds.
                                    // Give it the new pointer in addition to the ones it is handling.
                                    newTouchTarget.pointerIdBits |= idBitsToAssign;
                                    break;
                                }
    
                                resetCancelNextUpFlag(child);
                                if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
                                    // Child wants to receive touch within its bounds.
                                    mLastTouchDownTime = ev.getDownTime();
                                    if (preorderedList != null) {
                                        // childIndex points into presorted list, find original index
                                        for (int j = 0; j < childrenCount; j++) {
                                            if (children[childIndex] == mChildren[j]) {
                                                mLastTouchDownIndex = j;
                                                break;
                                            }
                                        }
                                    } else {
                                        mLastTouchDownIndex = childIndex;
                                    }
                                    mLastTouchDownX = ev.getX();
                                    mLastTouchDownY = ev.getY();
                                    newTouchTarget = addTouchTarget(child, idBitsToAssign);
                                    alreadyDispatchedToNewTouchTarget = true;
                                    break;
                                }
    
                                // The accessibility focus didn't handle the event, so clear
                                // the flag and do a normal dispatch to all children.
                                ev.setTargetAccessibilityFocus(false);
                            }
                            if (preorderedList != null) preorderedList.clear();
                        }
    
                        if (newTouchTarget == null && mFirstTouchTarget != null) {
                            // Did not find a child to receive the event.
                            // Assign the pointer to the least recently added target.
                            newTouchTarget = mFirstTouchTarget;
                            while (newTouchTarget.next != null) {
                                newTouchTarget = newTouchTarget.next;
                            }
                            newTouchTarget.pointerIdBits |= idBitsToAssign;
                        }
                    }
                }
    
                // Dispatch to touch targets.
                if (mFirstTouchTarget == null) {
                    // No touch targets so treat this as an ordinary view.
                    handled = dispatchTransformedTouchEvent(ev, canceled, null,
                            TouchTarget.ALL_POINTER_IDS);
                } else {
                    // Dispatch to touch targets, excluding the new touch target if we already
                    // dispatched to it.  Cancel touch targets if necessary.
                    TouchTarget predecessor = null;
                    TouchTarget target = mFirstTouchTarget;
                    while (target != null) {
                        final TouchTarget next = target.next;
                        if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
                            handled = true;
                        } else {
                            final boolean cancelChild = resetCancelNextUpFlag(target.child)
                                    || intercepted;
                            if (dispatchTransformedTouchEvent(ev, cancelChild,
                                    target.child, target.pointerIdBits)) {
                                handled = true;
                            }
                            if (cancelChild) {
                                if (predecessor == null) {
                                    mFirstTouchTarget = next;
                                } else {
                                    predecessor.next = next;
                                }
                                target.recycle();
                                target = next;
                                continue;
                            }
                        }
                        predecessor = target;
                        target = next;
                    }
                }
    
                // Update list of touch targets for pointer up or cancel, if needed.
                if (canceled
                        || actionMasked == MotionEvent.ACTION_UP
                        || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
                    resetTouchState();
                } else if (split && actionMasked == MotionEvent.ACTION_POINTER_UP) {
                    final int actionIndex = ev.getActionIndex();
                    final int idBitsToRemove = 1 << ev.getPointerId(actionIndex);
                    removePointersFromTouchTargets(idBitsToRemove);
                }
            }
    
            if (!handled && mInputEventConsistencyVerifier != null) {
                mInputEventConsistencyVerifier.onUnhandledEvent(ev, 1);
            }
            return handled;
        }
    • 分析:

      • 这个方法的代码逻辑比较多,有两百多行。我们去繁就简,理出主要的逻辑就行,毕竟我们只关心Touch事件的流向和处理逻辑。

      • 首先来看一下,ViewGroup的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;
                    }
        
      • 这里面我们看到一个很关键的boolean变量disallowIntercept,这个变量是控制是不是不允许父控件去拦截该事件的,取值是看mGroupFlags的取值。

      这里面涉及到一个方法:requestDisallowInterceptTouchEvent()方法。这个方法确定mGroupFlags的取值,控制请求父布局不拦截该事件,而是交给自己去做处理。这个方法在处理滑动冲突等场景时经常用到。但在这里为了整个源码分析的逻辑简洁清晰,不再具体分析该方法的代码。

      • 还有大家可以注意到一个判断条件: if (actionMasked == MotionEvent.ACTION_DOWN|| mFirstTouchTarget != null),也就是说ViewGroup去判断这个事件该不该去拦截,首先是这个事件得是ACTION_DOWN事件或者该事件的mFirstTouchTarget(目标子View)是不为空的才会考虑要不要拦截。

      • 这说明mFirstTouchTarget为空的情况下,ACTION_MOVE和ACTION_UP事件是不会经过这个拦截判断的,而是直接intercepted = true表示事件被直接拦截掉。这一点刚好印证了我在一步步探索学习Android Touch事件分发传递机制(二)中提到的ACTION_MOVE和ACTION_UP事件的分发规律。

      • 那么mFirstTouchTarget(目标子View)这个变量到底是什么呢?什么时候会为空呢?

      • 可以看到ViewGroup的dispatchTouchEvent()方法的后续代码,是一个for循环:

        ...
         for (int i = childrenCount - 1; i >= 0; i--) {
                                            final int childIndex = getAndVerifyPreorderedIndex(
                                                    childrenCount, i, customOrder);
                                            final View child = getAndVerifyPreorderedView(
                                                    preorderedList, children, childIndex);
        
                                            // If there is a view that has accessibility focus we want it
                                            // to get the event first and if not handled we will perform a
                                            // normal dispatch. We may do a double iteration but this is
                                            // safer given the timeframe.
        
                                        }
                                        ...
        • 这个for循环一个一个的遍历子View,寻找看事件的发生坐标在哪个View的范围中,如果找到了,就设置mFirstTouchTarget为child,并且把alreadyDispatchedToNewTouchTarget设置为true。

        • 那么事件最终交给自己处理还是目标子View(mFirstTouchTarget)处理?

        • 很简单,经过上面的分析可以知道,如果遍历完之后mFirstTouchTarget不为null,就传给mFirstTouchTarget(目标子View)处理;如果为null,就自己消费掉。

        • 那其实不管是给目标子View处理还是自己处理,都会跑到View的dispatchTouchEvent()方法。看源码可以知道,当mFirstTouchTarget为null的时候,ViewGoup会调用super.dispatchTouchEvent(event),毕竟ViewGroup本质上是View的子类,所以其实ViewGroup调用的还是View的dispatchTouchEvent()方法。那么我们下面分析View的dispatchTouchEvent()方法。

      1.3 View的dispatchTouchEvent()方法
      • 源码:
          /**
           * Pass the touch screen motion event down to the target view, or this
           * view if it is the target.
           *
           * @param event The motion event to be dispatched.
           * @return True if the event was handled by the view, false otherwise.
           */
          public boolean dispatchTouchEvent(MotionEvent event) {
              // If the event should be handled by accessibility focus first.
              if (event.isTargetAccessibilityFocus()) {
                  // We don't have focus or no virtual descendant has it, do not handle the event.
                  if (!isAccessibilityFocusedViewOrHost()) {
                      return false;
                  }
                  // We have focus and got the event, then use normal event dispatch.
                  event.setTargetAccessibilityFocus(false);
              }
      
              boolean result = false;
      
              if (mInputEventConsistencyVerifier != null) {
                  mInputEventConsistencyVerifier.onTouchEvent(event, 0);
              }
      
              final int actionMasked = event.getActionMasked();
              if (actionMasked == MotionEvent.ACTION_DOWN) {
                  // Defensive cleanup for new gesture
                  stopNestedScroll();
              }
      
              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;
                  }
              }
      
              if (!result && mInputEventConsistencyVerifier != null) {
                  mInputEventConsistencyVerifier.onUnhandledEvent(event, 0);
              }
      
              // Clean up after nested scrolls if this is the end of a gesture;
              // also cancel it if we tried an ACTION_DOWN but we didn't want the rest
              // of the gesture.
              if (actionMasked == MotionEvent.ACTION_UP ||
                      actionMasked == MotionEvent.ACTION_CANCEL ||
                      (actionMasked == MotionEvent.ACTION_DOWN && !result)) {
                  stopNestedScroll();
              }
      
              return result;
          }
    • 分析:

      • 代码量相对于ViewGroup的dispatchTouchEvnet()方法来说少很多。

      • 主要是首先判断这个View本身有没有设置OnTouchListener监听,如果有,就直接跑去调用该接口下的onTouch()方法。该方法如果return true,这事件就是被消费掉了。return false,事件还是会传回给onTouchEvent()方法。

      注意:值得注意的是,ViewGroup本身并没有重写View的onTouchEvnet()方法,所以这里如果回传,也是调用的父类View.java的onTouchEvent()方法。

    2. onTouchEvent()方法源码分析

    2.1 Activity的onTouchEvent()方法
    • 源码:
     /**
         * Called when a touch screen event was not handled by any of the views
         * under it.  This is most useful to process touch events that happen
         * outside of your window bounds, where there is no view to receive it.
         *
         * @param event The touch screen event being processed.
         *
         * @return Return true if you have consumed the event, false if you haven't.
         * The default implementation always returns false.
         */
        public boolean onTouchEvent(MotionEvent event) {
            if (mWindow.shouldCloseOnTouch(this, event)) {
                finish();
                return true;
            }
    
            return false;
        }
    
    • 分析:

      • 这个方法代码很少,默认Activity的onTouchEvent()方法是返回false的,也就是说默认不处理触摸事件。

      • 只有在PhoneWindow的shouldCloseOnTouch()方法返回true才会处理触摸事件,直接finish整个Activity.

    2.2 View的onTouchEvent()方法

    前面说了,ViewGroup没有重写View的onTouchEvent()方法,所以继承VIewGroup时,调用的还是View的onTouchEvent()。

    • 源码:
    /**
         * Implement this method to handle touch screen motion events.
         * <p>
         * If this method is used to detect click actions, it is recommended that
         * the actions be performed by implementing and calling
         * {@link #performClick()}. This will ensure consistent system behavior,
         * including:
         * <ul>
         * <li>obeying click sound preferences
         * <li>dispatching OnClickListener calls
         * <li>handling {@link AccessibilityNodeInfo#ACTION_CLICK ACTION_CLICK} when
         * accessibility features are enabled
         * </ul>
         *
         * @param event The motion event.
         * @return True if the event was handled, false otherwise.
         */
        public boolean onTouchEvent(MotionEvent event) {
            final float x = event.getX();
            final float y = event.getY();
            final int viewFlags = mViewFlags;
            final int action = event.getAction();
    
            final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
                    || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
                    || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;
    
            if ((viewFlags & ENABLED_MASK) == DISABLED) {
                if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
                    setPressed(false);
                }
                mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
                // A disabled view that is clickable still consumes the touch
                // events, it just doesn't respond to them.
                return clickable;
            }
            if (mTouchDelegate != null) {
                if (mTouchDelegate.onTouchEvent(event)) {
                    return true;
                }
            }
    
            if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
                switch (action) {
                    case MotionEvent.ACTION_UP:
                        mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
                        if ((viewFlags & TOOLTIP) == TOOLTIP) {
                            handleTooltipUp();
                        }
                        if (!clickable) {
                            removeTapCallback();
                            removeLongPressCallback();
                            mInContextButtonPress = false;
                            mHasPerformedLongPress = false;
                            mIgnoreNextUpEvent = false;
                            break;
                        }
                        boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
                        if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
                            // take focus if we don't have it already and we should in
                            // touch mode.
                            boolean focusTaken = false;
                            if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {
                                focusTaken = requestFocus();
                            }
    
                            if (prepressed) {
                                // The button is being released before we actually
                                // showed it as pressed.  Make it show the pressed
                                // state now (before scheduling the click) to ensure
                                // the user sees it.
                                setPressed(true, x, y);
                            }
    
                            if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
                                // This is a tap, so remove the longpress check
                                removeLongPressCallback();
    
                                // Only perform take click actions if we were in the pressed state
                                if (!focusTaken) {
                                    // Use a Runnable and post this rather than calling
                                    // performClick directly. This lets other visual state
                                    // of the view update before click actions start.
                                    if (mPerformClick == null) {
                                        mPerformClick = new PerformClick();
                                    }
                                    if (!post(mPerformClick)) {
                                        performClick();
                                    }
                                }
                            }
    
                            if (mUnsetPressedState == null) {
                                mUnsetPressedState = new UnsetPressedState();
                            }
    
                            if (prepressed) {
                                postDelayed(mUnsetPressedState,
                                        ViewConfiguration.getPressedStateDuration());
                            } else if (!post(mUnsetPressedState)) {
                                // If the post failed, unpress right now
                                mUnsetPressedState.run();
                            }
    
                            removeTapCallback();
                        }
                        mIgnoreNextUpEvent = false;
                        break;
    
                    case MotionEvent.ACTION_DOWN:
                        if (event.getSource() == InputDevice.SOURCE_TOUCHSCREEN) {
                            mPrivateFlags3 |= PFLAG3_FINGER_DOWN;
                        }
                        mHasPerformedLongPress = false;
    
                        if (!clickable) {
                            checkForLongClick(0, x, y);
                            break;
                        }
    
                        if (performButtonActionOnTouchDown(event)) {
                            break;
                        }
    
                        // Walk up the hierarchy to determine if we're inside a scrolling container.
                        boolean isInScrollingContainer = isInScrollingContainer();
    
                        // For views inside a scrolling container, delay the pressed feedback for
                        // a short period in case this is a scroll.
                        if (isInScrollingContainer) {
                            mPrivateFlags |= PFLAG_PREPRESSED;
                            if (mPendingCheckForTap == null) {
                                mPendingCheckForTap = new CheckForTap();
                            }
                            mPendingCheckForTap.x = event.getX();
                            mPendingCheckForTap.y = event.getY();
                            postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
                        } else {
                            // Not inside a scrolling container, so show the feedback right away
                            setPressed(true, x, y);
                            checkForLongClick(0, x, y);
                        }
                        break;
    
                    case MotionEvent.ACTION_CANCEL:
                        if (clickable) {
                            setPressed(false);
                        }
                        removeTapCallback();
                        removeLongPressCallback();
                        mInContextButtonPress = false;
                        mHasPerformedLongPress = false;
                        mIgnoreNextUpEvent = false;
                        mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
                        break;
    
                    case MotionEvent.ACTION_MOVE:
                        if (clickable) {
                            drawableHotspotChanged(x, y);
                        }
    
                        // Be lenient about moving outside of buttons
                        if (!pointInView(x, y, mTouchSlop)) {
                            // Outside button
                            // Remove any future long press/tap checks
                            removeTapCallback();
                            removeLongPressCallback();
                            if ((mPrivateFlags & PFLAG_PRESSED) != 0) {
                                setPressed(false);
                            }
                            mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
                        }
                        break;
                }
    
                return true;
            }
    
            return false;
        }
    • 分析:

      • 代码同样很长,那么我们只去理清主要的逻辑。

      • 当事件传递到该方法处,首先会判断这个View是不是enabled状态,是不是clickable状态。

      • 然后会根据Touch事件的类型做出不同的响应。比如View接收到Down事件和up事件等时候的表现效果。

      注意:注意到一段比较重要的代码
      if (!post(mPerformClick)) {
      performClick();
      }

      这段代码在判断是up事件之后调用了performClick()方法,这个方法回去回调onClickListener接口里面的onClick()方法。
      这结合前面dispatchTouchEvent()方法中ACTION_DOWN事件会去调用onTouch,可见onTouch比onClick优先。

    3. onInterceptTouchEvent()方法源码分析

    只有ViewGroup有onInterceptTouchEvent()方法
    • 源码:
      public boolean onInterceptTouchEvent(MotionEvent ev) {
            if (ev.isFromSource(InputDevice.SOURCE_MOUSE)
                    && ev.getAction() == MotionEvent.ACTION_DOWN
                    && ev.isButtonPressed(MotionEvent.BUTTON_PRIMARY)
                    && isOnScrollbarThumb(ev.getX(), ev.getY())) {
                return true;
            }
            return false;
        }
    • 分析:

      • 这个方法默认是返回false,表示不拦截触摸事件的。

      • 只有在
        ev.isFromSource(InputDevice.SOURCE_MOUSE)
        && ev.getAction() == MotionEvent.ACTION_DOWN
        && ev.isButtonPressed(MotionEvent.BUTTON_PRIMARY)
        && isOnScrollbarThumb(ev.getX(), ev.getY())
        这么多条件同时成立时才会拦截。

    • 有需要的话,比如处理滑动冲突的时候,可以重写该方法,retrun true,拦截触摸事件。

    展开全文
  • 详细了解可以查看链接:http://blog.csdn.net/hnulwt/article/details/45847737非常简单的测试小demo,有关于android事件分发机制的。
  • 彻底掌握Android touch事件分发顺序 Android touch事件分发主要由几个方向可以展开深入分析: touch事件是如何从驱动层传递给Framework层的InputManagerService; WMS是如何通过ViewRootImpl将事件传递到目标窗口...
  • android touch事件分发机制

    千次阅读 2015-05-22 09:48:39
    转载请注明出处:http://blog.csdn.net/ZhouLi_CSDN/article/details/45878337看了网上那么多的博客 ,对于android系统的事件处理仍然不是明白的特别透彻,或者一些博客讲解的总是有些歧义或者讲诉的不正确或者不...
  • Android Touch事件分发过程

    万次阅读 多人点赞 2014-08-31 13:38:48
    Android Touch 事件分发过程深度分析
  • 首先,说到Touch事件分发机制,那我们就要弄清楚什么事Touch事件?我们通常说的Touch事件,它其实并不简简单单是一个事件,它是由1个DOWN事件,n个MOVE事件,1个UP事件构成的,当然在这里n可以为零。也就是说,一个...
  • 点击上方“蓝字”关注我们1,touch 事件是如何从驱动层传递给Framework 层的 InputManagerService;2,WMS 是如何通过 ViewRooImple 将事...
  • ——– 写在前面 ————...转载请注明出处:Lshare版权所有概述Android中的事件分发是遵循类似责任链模式的,就是从根节点开始逐层往里分发事件,直到找到责任人(即响应事件的View)或找不到责任人事件“丢弃”为
  • 网上有太多关于Android Touch事件分发和消费博客写得都太过复杂,之前也看的是似懂非懂,于是亲手来过一遍,其实很简单,讲起来也很简单
  • dispatchTouchEvent方法用于事件分发Android中所有的事件都必须经过这个方法的分发,然后决定是自身消费当前事件还是继续往下分发给子控件处理。返回true表示不继续分发事件没有被消费。返回false则继续往下...
  • 三个方法:分发触摸事件dispatchTouchEvent、在触摸事件的时候onTouchEvent、在拦截触摸事件的时候onInterceptTouchEvent。 dispatch是派遣的意思。 就是分发的意思。 分发触摸事件。 intercept 是拦截的意思。 .....
  • 主要讲ViewGroup的 Touch机制
  • Android Touch事件分发详解

    千次阅读 2015-04-10 15:10:02
    Android Touch事件分发详解先说一些基本的知识,方便后面分析源码时能更好理解。 - 所有Touch事件都被封装成MotionEvent对象,包括Touch的位置、历史记录、第几个手指等. 事件类型分为ACTION_DOWN,ACTION_UP,ACTION...
  • 虽然时常写自定义控件,但对Touch事件分发机制只是简单的理解,这次深入研究了它。网上很多这种文章都介绍得太简单,甚至错误。最近也研究了NestedScrolling,顺便说句,传统的Touch机制也能将事件回传给父Layout,...
  • Android的屏幕触摸事件Android官方API中由类MotionEvent来描述,不同的触摸事件对应不同的事件类型。 如:ACTION_DOWN;ACTION_UP;ACTION_MOVE;ACTION_CANCEL 每个事件对应都有自己的传递路径,从产生到传递到最终...
  • Touch 事件分发过程 事件的传递过程:Activity -> Window -> DecorView -> 顶级 View -> 子 View -> ….(顶级 View 即 setContentView 设置的 View,一般为 ViewGroup;DecorView 即顶级 View 的父 View); ...
  • Touch事件分发中只有两个主角:ViewGroup和View。Activity的Touch事件事实上是调用它内部的ViewGroup的Touch事件,可以直接当成ViewGroup处理。 View在ViewGroup内,ViewGroup也可以在其他ViewGroup内,这时候把内部...
  • Android Touch事件分发—拦截—处理

    千次阅读 2016-02-26 16:46:02
    Android Touch事件分发(dispatchTouchEvent)—拦截(onInterceptTouchEvent)—处理(onTouchEvent)转自:http://www.cnblogs.com/linjzong/p/4191891.html(跟人感觉短小精悍有透彻)Touch事件分发中只有两个主角:...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 12,909
精华内容 5,163
关键字:

androidtouch事件分发