关于 " 事件分发 " 的原理


# 核心源码

关键类 路径
Activity.java frameworks/base/core/java/android/app/Activity.java
DecorView.java frameworks/base/core/java/com/android/internal/policy/DecorView.java
PhoneWindow.java frameworks/base/core/java/com/android/internal/policy/PhoneWindow.java
View.java frameworks/base/core/java/android/view/View.java
ViewGroup.java frameworks/base/core/core/java/android/view/ViewGroup.java
Window.java frameworks/base/core/java/android/view/Window.java


# 基础认知

Android “ View “ 虽然不是四大组件,但其并不比四大组件的地位低。而 View 的核心知识点 “ 事件分发机制 “ 则是不少刚入门工程师的拦路虎(1、项目中处处遇到事件分发机制 - - 必须掌握;2、面试管最喜欢提及的问题 - - 必须了解)。

(1)事件分发的 “ 对象 “ - - 点击事件(Touch 事件)

事件定义

当用户触摸屏幕时(View 或 ViewGroup 派生的控件),将产生点击事件(Touch事件),Touch 事件的相关细节(发生触摸的位置、时间等)被封装成了 MotionEvent 对象。

事件类型

事件类型 具体动作
MotionEvent.ACTION_DOWN 按下 View(所有事件的开始)
MotionEvent.ACTION_UP 抬起 View(与DOWN对应)
MotionEvent.ACTION_MOVE 滑动 View
MotionEvent.ACTION_CANCEL 结束事件(非人为原因)

事件序列

所谓事件序列就是指:从手指接触屏幕至手指离开屏幕,这个过程产生的一系列事件。一般情况下,事件列都是以 DOWN 事件开始、UP 事件结束,中间有无数的 MOVE 事件

(2)事件分发的 “ 本质 “ - - 将点击事件传递到某个具体的 View 并处理的整个过程

即:事件传递的过程 = 事件分发过程

(3)事件分发的 “ 对象 “ - - Activity、ViewGroup、View

(4)事件分发的 “ 顺序 “ - - Activity -> ViewGroup -> View

(5)事件分发的 “ 方法 “ - - dispatchTouchEvent() / onInterceptTouchEvent() / onTouchEvent()

dispatchTouchEvent(事件分发):当监听到有触发事件时,首先由 Activity 进行捕获,然后事件就进入事件分发的流程。Activity 本身没有事件拦截,从而将事件传递给最外层的 View 的 dispatchTouchEvent(MotionEvent ev),该方法将对事件进行分发。

返回值:表示是否消费了当前事件。可能是 View 本身的 onTouchEvent() 消费,也可能是子 View 的 dispatchTouchEvent() 中消费。返回 true 表示事件被消费,本次的事件终止。返回 false 表示 View 以及子 View 均没有消费事件,将调用父 View 的 onTouchEvent()。

onInterceptTouchEvent(事件拦截):当一个 ViewGroup 在接到 MotionEvent 事件序列时候,首先会调用此方法判断是否需要拦截。特别注意,这是 ViewGroup 特有的方法,View 并没有拦截方法。

返回值:是否拦截事件传递,返回 true 表示拦截了事件,那么事件将不再向下分发而是调用 View 本身的 onTouchEvent()。返回 false 表示不做拦截,事件将向下分发到子 View 的 dispatchTouchEvent()。

onTouchEvent(事件响应):真正对 MotionEvent 进行处理或者说消费的方法,在 dispatchTouchEvent() 中进行调用。

返回值:返回 true 表示事件被消费,本次的事件终止。返回 false 表示事件没有被消费,将调用父 View 的 onTouchEvent(),直到返回 true 为止。

我们可以做个总结:

        ✎  Activity 的点击事件事实上是调用它内部的 ViewGroup 的点击事件,可以直接当成 ViewGroup 处理。

        ✎  ViewGroup 的相关事件方法有三个:onInterceptTouchEventdispatchTouchEventonTouchEvent

        ✎  View 的相关事件方法只有两个:dispatchTouchEventonTouchEvent



要想充分理解 Android 事件分发机制,本质上是要理解以下三个部分:

    ✯ Activity - Touch 事件分发     ✯ ViewGroup - Touch 事件分发      ✯ View - Touch 事件分发



一、Touch 事件分发 - - Activity

当一个点击事件发生时,事件最先传到 Activity 的 dispatchTouchEvent()。

1.1 Activity.dispatchTouchEvent()

// frameworks/base/core/java/android/app/Activity.java

public class Activity extends ContextThemeWrapper ... {

    public boolean dispatchTouchEvent(MotionEvent ev) {
        // 一般事件序列开始都是 DOWN 事件,即按下事件,所以此处基本是 true
        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
            onUserInteraction();                          // 1
        }
        /**
          * getWindow():获取 Window 类的对象,Window 类是抽象类,
          * 其唯一实现类是 PhoneWindow 类;即此处的 Window 类对象 = PhoneWindow 类对象。
          */
        if (getWindow().superDispatchTouchEvent(ev)) {    // 2
            return true;
        }        
        return onTouchEvent(ev);                          // 3
    }

}

Activity 的 dispatchTouchEvent() 方法很简单,主要涉及三个方法:onUserInteraction()superDispatchTouchEvent()onTouchEvent()

1.2 Activity.onUserInteraction()

// frameworks/base/core/java/android/app/Activity.java

public class Activity extends ContextThemeWrapper ... {

   /**
    * 说明:
    *    a. 该方法为空方法,主要实现屏保功能
    *    b. 当此 activity 在栈顶时,触屏点击按 home,back,menu 键等都会触发此方法
    */
    public void onUserInteraction() {
    }

}

1.3 PhoneWindow.superDispatchTouchEvent()

// frameworks/base/core/java/com/android/internal/policy/PhoneWindow.java

public class PhoneWindow extends Window implements MenuBuilder.Callback {

    // This is the top-level view of the window, containing the window decor.
    private DecorView mDecor;

    @Override
    public boolean superDispatchTouchEvent(MotionEvent event) {
        return mDecor.superDispatchTouchEvent(event);
    }

}

ActivitysuperDispatchTouchEvent() 方法走到了 DecorViewsuperDispatchTouchEvent() 方法。

1.4 DecorView.superDispatchTouchEvent()

// frameworks/base/core/java/com/android/internal/policy/DecorView.java

public class DecorView extends FrameLayout implements RootViewSurfaceTaker, WindowCallbacks {

  /**
    * public class DecorView extends FrameLayout -- DecorView 继承自 FrameLayout,是所有界面的父类。         
    * public class FrameLayout extends ViewGroup -- FrameLayout 是 ViewGroup 的子类,故 DecorView 的间接父类是 ViewGroup。
    */

    public boolean superDispatchTouchEvent(MotionEvent event) {
        // 调用父类的方法:ViewGroup 的 dispatchTouchEvent(),
        // 即:将事件传递到 ViewGroup 去处理,我们在 ViewGroup 的事件分发机制继续讨论。
        return super.dispatchTouchEvent(event);
    }

}

所以不难发现,ActivitydispatchTouchEvent() 方法最终会走到 ViewGroupdispatchTouchEvent() 方法。

1.5 Activity.onTouchEvent()

// frameworks/base/core/java/android/app/Activity.java

public class Activity extends ContextThemeWrapper ... {

    public boolean onTouchEvent(MotionEvent event) {
        // 一般返回 true,除非 Touch 事件在 Window 边界外,所以这边我们不再继续跟踪。
        if (mWindow.shouldCloseOnTouch(this, event)) {
            finish();
            return true;
        }

        return false;
    }

}

1.6 小结

看下 Activity 对点击事件的处理流程图:

Activity 事件分发流程.png

至此,我们不难发现 Activity 的事件走到了 ViewGroup 进行处理。


二、Touch 事件分发 - - ViewGroup

2.1 ViewGroup Demo

我们先通过一个 Demo 来看看 ViewGroup 对点击事件的处理。

Layout

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/my_layout"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <Button
        android:id="@+id/button_01"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Button_01"
        tools:layout_editor_absoluteX="94dp"
        tools:layout_editor_absoluteY="106dp" />

    <Button
        android:id="@+id/button_02"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Button_02"
        tools:layout_editor_absoluteX="94dp"
        tools:layout_editor_absoluteY="211dp" />
</android.support.constraint.ConstraintLayout>

Activity

package com.example.marco.myapplication;

import android.support.constraint.ConstraintLayout;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;

public class MainActivity extends AppCompatActivity {

    private Button button_01;
    private Button button_02;
    private ViewGroup myLayout;


    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        button_01 = (Button) findViewById(R.id.button_01);
        button_02 = (Button) findViewById(R.id.button_02);
        myLayout = (ConstraintLayout) findViewById(R.id.my_layout);

        // 1. ViewGroup: myLayout 布局设置监听事件
        myLayout.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Log.d("TAG", "点击了ViewGroup");
            }
        });

        // 2. View: button_01 设置监听事件
        button_01.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Log.d("TAG", "点击了button_01");
            }
        });

        // 3. View: button_02设置监听事件
        button_02.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Log.d("TAG", "点击了button_02");
            }
        });

    }
}

测试结果

09-26 16:17:51.877 16250 16250 D TAG : 点击了button_01     // 点击按钮 button_01
09-26 16:17:53.875 16250 16250 D TAG : 点击了button_02     // 点击按钮 button_02
09-26 16:17:54.758 16250 16250 D TAG : 点击了ViewGroup     // 点击空白处

结果说明

  • 点击 Button 时,执行 Button.onClick(),但 ViewGroup_Layout 注册的 onClick() 不会执行;

  • 只有点击空白区域时,才会执行 ViewGroup_Layout 的 onClick() 方法。

结论: Button 的 onClick() 将事件消费掉了,因此事件不会再继续向下传递。

所以,这样的结果是不是说明 Touch 事件是 先传递到 View,再传递到 ViewGroup 的?但从前面的分析,事件是从 Activity 传递到 ViewGroup 进行处理,这不是矛盾吗?我们通过源码来看看怎么回事。

2.2 ViewGroup.dispatchTouchEvent()

ViewGroup 对点击事件的分发流程就复杂很多了:

// frameworks/base/core/java/android/view/ViewGroup.java

public abstract class ViewGroup extends View implements ViewParent, ViewManager {

    // First touch target in the linked list of touch targets.
    private TouchTarget mFirstTouchTarget;

    public boolean dispatchTouchEvent(MotionEvent ev) {
        ... ...

        boolean handled = false;                      // 这个变量用于记录事件是否被处理完

        if (onFilterTouchEventForSecurity(ev)) {      // 过滤掉一些不合法的事件
            final int action = ev.getAction();
            final int actionMasked = action & MotionEvent.ACTION_MASK;

            // 判断是不是 Down 事件,如果是的话,就要做初始化操作
            if (actionMasked == MotionEvent.ACTION_DOWN) {
                cancelAndClearTouchTargets(ev);       // 如果是 Down 事件,就要清空掉之前的状态
                resetTouchState();
            }

            final boolean intercepted;                // 是否拦截事件的标志

            // 如果当前是 Down 事件,或者已经有处理 Touch 事件的目标了
            if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) {
                /**
                  * disallowIntercept:是否禁用事件拦截的功能,默认为 false,我们也可以通过调用
                  * requestDisallowInterceptTouchEvent() 方法对这个值进行修改。
                  */
                final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;                          
                // disallowIntercept 默认为 false 所以会走以下流程
                if (!disallowIntercept) {
                    // 每次事件分发时,都需调用 onInterceptTouchEvent() 询问是否拦截事件
                    intercepted = onInterceptTouchEvent(ev);
                    // 重新恢复 Action,以免 Action 在上面的步骤被人为地改变了
                    ev.setAction(action); // restore action in case it was changed
                } else {
                    // 如果禁用了事件拦截功能,则 intercepted 肯定为 false
                    intercepted = false;
                }
            } else {
                // 如果事件已经初始化过了,并且没有子 View 被分配处理,那么就说明,这个 ViewGroup 已经拦截了这个事件。
                intercepted = true;
            }

            if (intercepted || mFirstTouchTarget != null) {
                ev.setTargetAccessibilityFocus(false);
            }

            // Check for cancelation,标志着取消事件
            final boolean canceled = resetCancelNextUpFlag(this) || actionMasked == MotionEvent.ACTION_CANCEL;

            // 如果需要(不取消,也没有被拦截),那么在触摸 Down 事件的时候更新触摸目标列表
            // split:代表当前的 ViewGroup 是不是支持分割 MotionEvent 到不同的 View 当中
            final boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0;

            TouchTarget newTouchTarget = null;                    // 新的触摸对象
            boolean alreadyDispatchedToNewTouchTarget = false;    // 是否把事件分配给了新的触摸

            ★ ★ ★ ★ ★ ★ ★ ★ 重点方法 ★ ★ ★ ★ ★ ★ ★ ★ 
            // 如果事件不是取消事件,也没有拦截,那么进入此函数
            if (!canceled && !intercepted) {      
                View childWithAccessibilityFocus = ev.isTargetAccessibilityFocus()
                        ? findChildWithAccessibilityFocus() : null;

                // 如果是个全新的 Down 事件,或者是有新的触摸点,或者是光标来回移动事件
                if (actionMasked == MotionEvent.ACTION_DOWN
                        || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
                        || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {

                    // 事件的索引,Down 事件的 index:0
                    final int actionIndex = ev.getActionIndex(); // always 0 for down

                    // 获取分配的 ID 的 bit 数量
                    final int idBitsToAssign = split ? 1 << ev.getPointerId(actionIndex)
                                : TouchTarget.ALL_POINTER_IDS;

                    // 清理之前触摸这个指针标识,以防它们的目标变得不同步
                    removePointersFromTouchTargets(idBitsToAssign);

                    final int childrenCount = mChildrenCount;
                    // 如果新的触摸对象为 null & 当前 ViewGroup 有子元素
                    if (newTouchTarget == null && childrenCount != 0) {
                        final float x = ev.getX(actionIndex);
                        final float y = ev.getY(actionIndex);

                        final ArrayList<View> preorderedList = buildTouchDispatchChildList();
                        final boolean customOrder = preorderedList == null
                                && isChildrenDrawingOrderEnabled();
                        final View[] children = mChildren;

                        // 通过 for 循环,遍历了当前 ViewGroup 下的所有子 View
                        for (int i = childrenCount - 1; i >= 0; i--) {
                            final int childIndex = getAndVerifyPreorderedIndex(childrenCount, i, customOrder);
                            final View child = getAndVerifyPreorderedView(preorderedList, children, childIndex);

                            if (childWithAccessibilityFocus != null) {
                                if (childWithAccessibilityFocus != child) {
                                    continue;
                                }
                                childWithAccessibilityFocus = null;
                                i = childrenCount - 1;
                            }
                            ... ...    // 无关紧要的代码我们这边暂且省略

                            // 派发事件到子 View 处理
                            if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) { 
                                // Child wants to receive touch within its bounds.
                                mLastTouchDownTime = ev.getDownTime();
                                if (preorderedList != null) {
                                    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;
                                // 如果子 View 处理了事件,则 break,ViewGroup 不再处理事件(事件被拦截)
                                // 例子说的 Button.onClick() 处理了之后,ViewGroup.onClick() 不会再执行
                                break;     
                            }

                            ev.setTargetAccessibilityFocus(false);
                        }
                        if (preorderedList != null) preorderedList.clear();
                    }
                    ... ... 

                }
            }
            ... ...

        }
        return handled;
    }

}

其实 ViewGroup 的 dispatchTouchEvent() 处理流程,我们只需要关注两个重点方法:onInterceptTouchEvent()dispatchTransformedTouchEvent()

2.3 ViewGroup.onInterceptTouchEvent()

// frameworks/base/core/java/android/view/ViewGroup.java

public abstract class ViewGroup extends View implements ViewParent, ViewManager {

    public boolean onInterceptTouchEvent(MotionEvent ev) {
        // 判断条件很多,我们只需要知道一点,一般 Touch 事件默认返回 false
        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;
    }

}

2.4 ViewGroup.dispatchTransformedTouchEvent()

dispatchTransformedTouchEvent 方法的作用,主要就是把事件下发给子 View 进行处理

// frameworks/base/core/java/android/view/ViewGroup.java

public abstract class ViewGroup extends View implements ViewParent, ViewManager {

    private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
            View child, int desiredPointerIdBits) {
        final boolean handled;
        ... ...

        final MotionEvent transformedEvent;
        if (newPointerIdBits == oldPointerIdBits) {
            if (child == null || child.hasIdentityMatrix()) {
                if (child == null) {
                    // 调用父类的 dispatchTouchEvent 方法,也就是 ViewGroup.dispatchTouchEvent()
                    handled = super.dispatchTouchEvent(event);
                } else {
                    final float offsetX = mScrollX - child.mLeft;
                    final float offsetY = mScrollY - child.mTop;
                    event.offsetLocation(offsetX, offsetY);
                    // 有子 View,则调用子 View 的 dispatchTouchEvent() 方法
                    handled = child.dispatchTouchEvent(event);

                    event.offsetLocation(-offsetX, -offsetY);
                }
                return handled;
            }
            transformedEvent = MotionEvent.obtain(event);
        } else {
            transformedEvent = event.split(newPointerIdBits);
        }
        ... ...

        // Done.
        transformedEvent.recycle();
        return handled;
    }

}

当存在子 View 的时候,会调用 View.dispatchTouchEvent() 方法,如果没有则会向上调用 super.dispatchTouchEvent() 方法。

   ✒ 1、Android 事件分发是先传递到 ViewGroup,再由 ViewGroup 传递到 View 的。

   ✒ 2、在 ViewGroup 中可以通过 onInterceptTouchEvent() 方法对事件传递进行拦截,onInterceptTouchEvent() 方法返回 true 代表不允许事件继续向子 View 传递,返回 false 代表不对事件进行拦截,默认返回 false

   ✒ 3、子 View 中如果将传递的事件消费掉,ViewGroup 中将无法接收到任何事件。

2.5 小结

看下 ViewGroup 对点击事件的处理流程图:

ViewGroup 事件分发流程


三、Touch 事件分发 - - View

这边我们先做个说明:其实,只要你触摸了任何控件,就一定会调用该控件的 dispatchTouchEvent() 方法!

严格一点来说:当你点击了某个控件,首先会去调用该控件所在 布局dispatchTouchEvent() 方法,然后在 布局dispatchTouchEvent() 方法中找到被点击的相应控件,再去调用该 控件dispatchTouchEvent()方法。

3.1 View Demo

我们先通过一个 Demo 来看看 View 对点击事件的处理,这边我们只设置一个简单的 Button。

Layout

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/my_layout"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context=".MainActivity">

    <Button
        android:id="@+id/button_01"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Button_01"
        tools:layout_editor_absoluteX="94dp"
        tools:layout_editor_absoluteY="106dp" />

</LinearLayout>

Activity

package com.example.marco.myapplication;

import android.support.constraint.ConstraintLayout;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.LinearLayout;

public class MainActivity extends AppCompatActivity {

    private Button button_01;


    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        button_01 = (Button) findViewById(R.id.button_01);

        /**
          * 结论验证1:在回调 onTouch() 里返回 false
        */

        // 通过 OnTouchListener() 复写 onTouch(),从而手动设置返回 false
        button_01.setOnTouchListener(new View.OnTouchListener() {
            @Override
            public boolean onTouch(View v, MotionEvent event) {
                Log.d("TAG", "run onTouch(), action:" + event.getAction());
                return false;
            }
        });

        button_01.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Log.d("TAG", "run onClick()");
            }
        });

        /**
          * 结论验证2:在回调 onTouch() 里返回 true
          */

        // 通过 OnTouchListener()复写 onTouch(),从而手动设置返回 true
        button_01.setOnTouchListener(new View.OnTouchListener() {
            @Override
            public boolean onTouch(View v, MotionEvent event) {
                Log.d("TAG", "run onTouch(), action:" + event.getAction());
                return true;
            }
        });

        button_01.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Log.d("TAG", "run onClick()");
            }
        });

    }
}

测试结果

// 通过 OnTouchListener() 复写 onTouch(),从而手动设置返回 false
09-26 18:14:19.299 23350 23350 D TAG : run onTouch(), action:0    // ACTION_DOWN
09-26 18:14:19.327 23350 23350 D TAG : run onTouch(), action:2    // ACTION_MOVE
09-26 18:14:19.343 23350 23350 D TAG : run onTouch(), action:2    // ACTION_MOVE
09-26 18:14:19.383 23350 23350 D TAG : run onTouch(), action:2    // ACTION_MOVE
09-26 18:14:19.384 23350 23350 D TAG : run onTouch(), action:1    // ACTION_UP
09-26 18:14:19.385 23350 23350 D TAG : run onClick()
// 通过 OnTouchListener() 复写 onTouch(),从而手动设置返回 true
09-26 18:16:29.758 23847 23847 D TAG : run onTouch(), action:0    // ACTION_DOWN
09-26 18:16:29.773 23847 23847 D TAG : run onTouch(), action:2    // ACTION_MOVE
09-26 18:16:29.856 23847 23847 D TAG : run onTouch(), action:2    // ACTION_MOVE
09-26 18:16:29.858 23847 23847 D TAG : run onTouch(), action:1    // ACTION_UP

结果说明

   ✒ 1、我们发现 onTouch() 是优先于 onClick() 执行的;

   ✒ 2、onTouch() 方法是有返回值的,如果返回 true,事件被消费,那么 onClick() 将不再执行;

3.2 View.dispatchTouchEvent()

案例看了,但是为什么会出现上面的结果,还是需要从源码角度去深入分析。首先,我们需要知道的是:只要你触摸到了任何一个控件,就一定会调用该控件的 dispatchTouchEvent() 方法。所以,当我们点击按钮的时候,就会去调用 Button 类里的 dispatchTouchEvent()方法,可是你会发现 Button 类里并没有这个方法,那么就到它的父类 TextView 里去找一找,但是 TextView 里也没有这个方法,继续往上找,最终我们在 View 里找到了这个方法,那么接下来我们就要查看 View.dispatchTouchEvent() 源码。

// frameworks/base/core/java/android/view/View.java

public class View implements Drawable.Callback, KeyEvent.Callback, AccessibilityEventSource {

    public boolean dispatchTouchEvent(MotionEvent event) {
        ... ...

        if (onFilterTouchEventForSecurity(event)) {
            if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
                result = true;
            }

            ListenerInfo li = mListenerInfo;

            ★ ★ ★ ★ ★ ★ ★ ★ 重点方法 ★ ★ ★ ★ ★ ★ ★ ★ 
            /*
             * 注意:只有以下3个条件都为真,dispatchTouchEvent() 才返回 true
             *       1. li != null & li.mOnTouchListener != null
             *       2. (mViewFlags & ENABLED_MASK) == ENABLED
             *       3. li.mOnTouchListener.onTouch(this, event)
             */
            if (li != null && li.mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED
                           && li.mOnTouchListener.onTouch(this, event)) {
                result = true;
            }

            // 如果上面没有返回 true,那么执行 onTouchEvent()
            if (!result && onTouchEvent(event)) {    // 下面再分析
                result = true;
            }
        }
        ... ...

        return result;
    }

}

我们来分析一下重点方法中提到的几个关键条件:

条件 1:li != null & li.mOnTouchListener != null

mOnTouchListener 这个变量是在哪里赋值的?

    /**
      * 条件 1:li.mOnTouchListener != null
      * 说明:mOnTouchListener 变量在 View.setOnTouchListener() 方法里赋值
      */
    public void setOnTouchListener(OnTouchListener l) {
        // 所以,只要我们给控件注册了 Touch 事件,mOnTouchListener 就一定被赋值(不为空)
        getListenerInfo().mOnTouchListener = l;
    }

条件 2:(mViewFlags & ENABLED_MASK) == ENABLED

    /**
      * 条件 2:(mViewFlags & ENABLED_MASK) == ENABLED
      * 说明:
      *     a. 该条件是判断当前点击的控件是否 enable
      *     b. 一般 View 默认 enable,故该条件恒定为 true
      */

条件 3:li.mOnTouchListener.onTouch(this, event)

mListenerInfo.mOnTouchListener.onTouch(this, event),其实也就是去回调控件注册 touch 事件时的 onTouch() 方法。

也就是说如果我们在 onTouch() 方法里返回 true,就会让这三个条件全部成立,从而整个方法直接返回 true。如果我们在 onTouch 方法里返回 false,就会再去执行 onTouchEvent(event) 方法。

    /**
      * 条件 3:mOnTouchListener.onTouch(this, event)
      * 说明:回调控件注册 Touch 事件时的 onTouch()
      */
    button.setOnTouchListener(new OnTouchListener() {  
        @Override  
        public boolean onTouch(View v, MotionEvent event) {  
            // return true;
            // return false;
        }  
    });

3.3 View.onTouchEvent()

接下来,我们看看 onTouch() 返回 false 后,onTouchEvent() 所做的事情。

// frameworks/base/core/java/android/view/View.java

public class View implements Drawable.Callback, KeyEvent.Callback, AccessibilityEventSource {

    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;
        ... ...

        // 若该控件可点击,则进入 switch 判断中
        if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
            switch (action) {
                // a. 若当前的事件 = 抬起 View
                case MotionEvent.ACTION_UP:
                    ... ...

                    boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
                    if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
                        ... ...

                        if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
                            // This is a tap, so remove the longpress check
                            removeLongPressCallback();

                            if (!focusTaken) {
                                if (mPerformClick == null) {
                                    mPerformClick = new PerformClick();
                                }
                                if (!post(mPerformClick)) {
                                    performClick();    // 重点分析函数
                                }
                            }
                        }
                        ...
                    }
                    break;

                // b. 若当前的事件 = 按下 View
                case MotionEvent.ACTION_DOWN:
                    ... ...

                    break;

                // c. 若当前的事件 = 结束事件(非人为原因)
                case MotionEvent.ACTION_CANCEL:
                    ... ...

                    break;

                // d. 若当前的事件 = 滑动 View
                case MotionEvent.ACTION_MOVE:
                    ... ...

                    break;
            }

            // 若该控件可点击,就一定返回true
            return true;
        }

        // 若该控件不可点击,就一定返回false
        return false;
    }

}

3.4 View.performClick()

// frameworks/base/core/java/android/view/View.java

public class View implements Drawable.Callback, KeyEvent.Callback, AccessibilityEventSource {

    public boolean performClick() {
        notifyAutofillManagerOnClick();

        final boolean result;
        final ListenerInfo li = mListenerInfo;
        /*
         * 只要我们通过 setOnClickListener() 为控件 View 注册 1 个点击事件,那么就会给 li.mOnClickListener 
         * 变量赋值(即不为空),则会往下回调 onClick()。
         */
        if (li != null && li.mOnClickListener != null) {
            playSoundEffect(SoundEffectConstants.CLICK);
            li.mOnClickListener.onClick(this);    // 执行 onClick()
            result = true;
        } else {
            result = false;
        }
        ... ...

        return result;
    }

}

根据以上源码的分析,从原理上解释了我们前面 Demo 的运行结果。

这边我们来看个很有意思的现象:如果我们把 Button 换成 ImageView,然后给它注册一个 Touch 事件,结果会怎样?

imageView.setOnTouchListener(new OnTouchListener() {  
    @Override  
    public boolean onTouch(View v, MotionEvent event) {  
        Log.d("TAG", "run onTouch(), action:" + event.getAction());
        return false;  
    }  
});  

运行一下程序,点击ImageView,你会发现结果如下:

// 通过 OnTouchListener() 复写 onTouch(),从而手动设置返回 true
09-26 18:16:29.758 23847 23847 D TAG : run onTouch(), action:0    // ACTION_DOWN

ACTION_DOWN 执行完后,后面的一系列 action都不会得到执行了!怎么会出现这个结果?

因为 ImageView 和按钮不同,它是 默认不可点击的,因此在 onTouchEvent() 的方法中进行 if 判断 的时候是进不去的,所以直接返回了 false,也就导致后面其它的 action 都无法执行了。

关于事件分发有个重要的知识点:

就是 Touch 事件的层级传递。我们都知道如果给一个控件注册了 touch 事件,每次点击它的时候都会触发一系列的 ACTION_DOWN,ACTION_MOVE,ACTION_UP 等事件。这里需要注意,如果你在执行 ACTION_DOWN 的时候返回了 false,后面一系列其它的 action 就不会再得到执行了。简单的说,就是当 dispatchTouchEvent() 在进行事件分发的时候,只有前一个 action 返回 true,才会触发后一个 action。

3.5 小结

看下 View 对点击事件的处理流程图:

View 事件分发流程.png

四、总结

       ✒ 1、Android 事件分发机制主要由【“事件分发” —> “事件拦截” —> “事件响应”】这三步来进行逻辑控制的。

       ✒ 2、ViewGroup 默认不拦截任何事件。

       ✒ 3、onInterceptTouchEvent 返回 true 表示事件拦截,onTouchEvent 返回 true 表示事件消费。

       ✒ 4、点击事件的分发过程如下:dispatchTouchEvent —> onTouchListener 的 OnTouch 方法 —> onTouchEvent —> onClickListener 的 onClick 方法。从而也可以看出 onTouch 是优先于 onClick 执行,事件传递的顺序是先经过 onTouch,再传递到 onClick。

       ✒ 5、Android 中的事件 onClick、onLongClick、onScroll 等,都是由多个 Touch 事件(一个 ACTION_DOWN,多个 ACTION_MOVE,一个 ACTION_UP)组成。

       ✒ 6、子 View 可以通过使用 getParent().requestDisallowInterceptTouchEvent(true) ,阻止 ViewGroup 对其 MOVE 或 UP 事件进行拦截。

       ✒ 7、MotionEvent 对象的四种状态:MotionEvent.ACTION_DOWN:手指按下屏幕的瞬间

                                                                    MotionEvent.ACTION_MOVE:手指在屏幕上移动

                                                                    MotionEvent.ACTION_UP:手指离开屏幕瞬间

                                                                    MotionEvent.ACTION_CANCEL:取消手势

       ✒ 8、点击某个控件,首先会去调用该控件所在布局的 dispatchTouchEvent 方法,然后在布局的 dispatchTouchEvent 方法中找到被点击的相应控件,再去调用该控件的 dispatchTouchEvent方法。

       ✒ 9、如果 View 没有消费 ACTION_DOWN 事件,则之后的 ACTION_MOVE 等事件都不会再接收。

       ✒ 10、事件在从 Activity.dispatchTouchEvent 往下分发的过程中:

如果中间的 ViewGroup 都不拦截,进入最底层的 View 后,由 View.onTouchEvent 处理,如果 View 也没有消费事件,最后会返回到 Activity.onTouchEvent。
如果中间任何一层 ViewGroup 拦截事件,则事件不再往下分发,交由拦截的 ViewGroup 的 onTouchEvent 来处理。

最后,我们用一张图看下“事件分发机制”的处理流程:

事件分发流程图.png