Pepsimaxin 阅读(24) 评论(0)

Android 虽然不是四大组件,但其并不比四大组件的地位低(涉及面的广度和深入甚至比四大组件还复杂🔥)。而View的核心知识点“事件分发机制”则是不少刚入门同学的拦路虎(1、项目中处处遇到事件分发机制;2、面试管最喜欢提及的问题)。在实际项目的开发过程中,ScrollView 嵌套 RecyclerView (或者 ListView)的滑动冲突这种老大难的问题的【理论基础】就是“事件分发机制”。

同学,是不是准备摩拳擦掌,埋头苦干了?💪OK,在我们准备由浅入深,由表及里的去分析事件分发机制之前,让我们打开英文单词册,多读几遍Abandon,以便压压惊,然后期待你下次再打开这编博客,重新学习哦~😅(言归正传,还是希望同学你能坚持读完,然后加以归纳学习,肯定能读懂并深入理解事件分发机制的精髓👍,抓好基础,一劳永逸!)。


基础认知


首先,我们了解一下“事件分发机制”中的基础知识点:

🍁 事件分发的对象

事件分发的对象:点击事件(Touch事件)

1. 定义
当用户触摸屏幕时(View 或 ViewGroup派生的控件),将产生点击事件(Touch事件)

2. 事件类型
当用户触摸屏幕时(View 或 ViewGroup派生的控件),将产生点击事件(Touch事件)

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

3. 事件序列
从手指接触屏幕 至 手指离开屏幕,这个过程产生的一系列事件。

一般情况下,事件列都是以DOWN事件开始、UP事件结束,中间有无数的MOVE事件,如下图:

事件列.png
事件列.png

即当一个点击事件(MotionEvent )产生后,系统需把这个事件传递给一个具体的 View 去处理。


🍁 事件分发的本质

本质:将点击事件(MotionEvent)传递到某个具体的View 并 处理的整个过程。


🍁事件传递的对象

对象:Activity、ViewGroup、View

Android的UI界面由Activity、ViewGroup、View 及其派生类组成,如下图:

示意图.png
示意图.png

🍁事件分发的顺序

顺序:Activity -> ViewGroup -> View

事件分发的顺序流程图如下:

传递.png
传递.png

🍁 事件分发的方法

方法:dispatchTouchEvent() 、onInterceptTouchEvent()和onTouchEvent()

dispatchTouchEvent:通过方法名我们不难猜测,它就是事件分发的重要方法。那么很明显,如果一个MotionEvent传递给了View,那么dispatchTouchEvent方法一定会被调用!
返回值:表示是否消费了当前事件。可能是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方法

method.png
method.png

🍁 段落总结

总结.png
总结.png

源码分析


还记得我们上节说到的事件分发的 传递顺序 吗?

即:1个点击事件发生后,事件先传到Activity、再传到ViewGroup、最终再传到 View

流程图.png
流程图.png

所以,要想充分理解Android分发机制,本质上是要理解以下三个部分:
           1、Activity对点击事件的分发机制
           2、ViewGroup对点击事件的分发机制
           3、View对点击事件的分发机制


🍁 Activity的事件分发机制

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

源码分析

dispatchTouchEvent

源码路径:frameworks/base/core/java/android/app/Activity.java

    /**
     * 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) {    // 一般事件列开始都是DOWN事件 = 按下事件,故此处基本是true
            onUserInteraction(); 💥
        }
        
        // 若getWindow().superDispatchTouchEvent(ev)的返回true
        // 则Activity.dispatchTouchEvent()就返回true,则方法结束。即 :该点击事件停止往下传递 & 事件传递过程结束
        // 否则:继续往下调用Activity.onTouchEvent
        if (getWindow().superDispatchTouchEvent(ev)) {  💥
            return true;
        }
        
        return onTouchEvent(ev);  💥
    }

有没有发现,不同的判断条件下会执行不同的函数,深入了解原理的话,我们还需要继续追踪这些重点函数。

onUserInteraction

源码路径:frameworks/base/core/java/android/app/Activity.java

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

superDispatchTouchEvent

源码路径:frameworks/base/core/java/com/android/internal/policy/PhoneWindow.java

  /**
    * 说明:
    *     a. getWindow() = 获取Window类的对象
    *     b. Window类是抽象类,其唯一实现类 = PhoneWindow类;即此处的Window类对象 = PhoneWindow类对象
    *     c. Window类的superDispatchTouchEvent() = 1个抽象方法,由子类PhoneWindow类实现
    */

    @Override
    public boolean superDispatchTouchEvent(MotionEvent event) {
        return mDecor.superDispatchTouchEvent(event);       // mDecor = 顶层View(DecorView)的实例对象
    }

继续跟踪源码,进入DecorView类:

源码路径:frameworks/base/core/java/com/android/internal/policy/DecorView.java

  /**
    * 定义:属于顶层View(DecorView)
    * 说明:
    *     a. DecorView类是PhoneWindow类的一个内部类
    *     b. public class DecorView extends FrameLayout,DecorView继承自FrameLayout,是所有界面的父类           
    *     c. public class FrameLayout extends ViewGroup,FrameLayout是ViewGroup的子类,故DecorView的间接父类=ViewGroup
    */
  
    public boolean superDispatchTouchEvent(MotionEvent event) {
        // 调用父类的方法 = ViewGroup的dispatchTouchEvent()
        // 即 将事件传递到ViewGroup去处理,详细我们在ViewGroup的事件分发机制继续讨论
        return super.dispatchTouchEvent(event);
    }

好了,是不是还剩下一个函数没分析?

onTouchEvent

源码路径:frameworks/base/core/java/android/app/Activity.java

    public boolean onTouchEvent(MotionEvent event) {
        // 当一个点击事件未被Activity下任何一个View接收 / 处理时
        if (mWindow.shouldCloseOnTouch(this, event)) {
            finish();
            return true;
        }

        // 即:只有在点击事件在Window边界外才会返回true,一般情况都返回false
        return false;
    }

跟踪shouldCloseOnTouch方法:

源码路径:frameworks/base/core/java/android/view/Window.java

    /** @hide */
    public boolean shouldCloseOnTouch(Context context, MotionEvent event) {
        // 主要是对于处理边界外点击事件的判断:是否是DOWN事件,event的坐标是否在边界内等
        final boolean isOutside =
                event.getAction() == MotionEvent.ACTION_DOWN && isOutOfBounds(context, event)
                || event.getAction() == MotionEvent.ACTION_OUTSIDE;
        // 返回true:说明事件在边界外,即 消费事件
        // 返回false:未消费(默认)
        if (mCloseOnTouchOutside && peekDecorView() != null && isOutside) {
            return true;
        }
        return false;
    }

总结

Activity.png
Activity.png

至此,我们分析了Activity对点击事件的分发机制处理流程,我们不难发现,Activity的事件走到了ViewGroup进行处理,那么接下来就是分析ViewGroup对点击事件的分发机制了。


🍁 ViewGroup对点击事件的分发机制

从上面Activity事件分发机制可知,ViewGroup事件分发机制从dispatchTouchEvent()开始。

源码分析

源码路径:frameworks/base/core/core/java/android/view/ViewGroup.java

    public boolean dispatchTouchEvent(MotionEvent ev) {

        ... ...  // 分析关键代码
    
        boolean handled = false;  // 这个变量用于记录事件是否被处理完
        
        // 过滤掉一些不合法的事件
        if (onFilterTouchEventForSecurity(ev)) {
            final int action = ev.getAction();
            final int actionMasked = action & MotionEvent.ACTION_MASK;
            
            // Handle an initial down.
            // 判断是不是Down事件,如果是的话,就要做初始化操作
            if (actionMasked == MotionEvent.ACTION_DOWN) {
               /* 
                * 如果是down事件,就要清空掉之前的状态,比如:重置手势判断。
                * 比如:之前在判断是不是一个单点的滑动,但是第二个down来了,就表示不可能是单点的滑动,要重新开始判断触摸的手势
                * 清空掉 mFirstTouchTarget
                */
                cancelAndClearTouchTargets(ev);
                resetTouchState();       // mFirstTouchTarget = null;
            }

        
            // Check for interception,检查是否拦截事件.
            final boolean intercepted;
            if (actionMasked == MotionEvent.ACTION_DOWN
                    || mFirstTouchTarget != null) {            // 如果当前是Down事件,或者已经有处理Touch事件的目标了
                /*
                 * disallowIntercept:是否禁用事件拦截的功能(默认是false)
                 * 可通过调用requestDisallowInterceptTouchEvent()修改
                 * 使用与运算作为判断,可以让我们在flag中,存储好几个标志
                 */
                final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;     💥
                if (!disallowIntercept) {
                    // ViewGroup每次事件分发时,都需调用onInterceptTouchEvent()询问是否拦截事件
                    intercepted = onInterceptTouchEvent(ev);     💥
                    /// M : add log to help debugging
                    if (intercepted == true && ViewDebugManager.DEBUG_TOUCH) {
                        Log.d(TAG, "Touch event was intercepted event = " + ev
                                + ",this = " + this);
                    }
                    // 重新恢复Action,以免action在上面的步骤被人为地改变了
                    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.
                // 如果说,事件已经初始化过了,并且没有子View被分配处理,那么就说明,这个ViewGroup已经拦截了这个事件
                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.
            // 如果需要(不是取消,也没有被拦截)的话,那么在触摸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);
                        
                        // 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循环,遍历了当前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;
                            }

                            if (!canViewReceivePointerEvents(child)
                                    || !isTransformedTouchPointInView(x, y, child, null)) {
                                ev.setTargetAccessibilityFocus(false);
                                continue;
                            }
                            👆👆👆👆👆👆👆👆👆👆👆👆👆👆👆👆👆👆👆👆👆👆👆👆👆👆👆👆👆👆👆👆👆👆👆

                            // 获取新的触摸对象,如果当前的子View在之前的触摸目标的列表当中就返回touchTarget
                            // 子View不在之前的触摸目标列表那么就返回null
                            newTouchTarget = getTouchTarget(child);
                            
                            // 如果新的触摸目标对象不为空,那么就把这个触摸的ID赋予它
                            // 这个触摸的目标对象的id就含有了好几个pointer的ID了
                            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;
                            }

                            // 如果子View不在之前的触摸目标列表中,先重置childView的标志,去除掉CANCEL的标志
                            resetCancelNextUpFlag(child);


                            /*
                             * 调用子View的dispatchTouchEvent,并且把pointer的id赋予进去
                             * 如果说,子View接收并且处理了这个事件,那么就更新上一次触摸事件的信息,
                             * 并且创建一个新的触摸目标对象,并且绑定这个子View和Pointer的ID
                             */
                            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();
                    }

                    /*
                     * 如果newTouchTarget为null,就代表,这个事件没有找到子View去处理它,
                     * 如果之前已经有了触摸对象(比如,我点了一张图,另一个手指在外面图的外面点下去)
                     * 那么就把这个之前那个触摸目标定为第一个触摸对象,并且把这个触摸(pointer)分配给最近添加的触摸目标
                     */
                    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.
                // 那么就表示我们要自己在这个ViewGroup处理这个触摸事件了
                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;
                // 遍历TouchTargt树.分发事件,如果我们已经分发给了新的TouchTarget,那么我们就不再分发给newTouchTarget
                while (target != null) {
                    final TouchTarget next = target.next;
                    if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
                        handled = true;
                    } else {
                        // 是否让child取消处理事件,如果为true,就会分发给child一个ACTION_CANCEL事件
                        final boolean cancelChild = resetCancelNextUpFlag(target.child)
                                || intercepted;
                        // 派发事件
                        if (dispatchTransformedTouchEvent(ev, cancelChild,
                                target.child, target.pointerIdBits)) {
                            handled = true;
                        }

                        // cancelChild:派发给了当前child一个ACTION_CANCEL事件,
                        // 移除这个child
                        if (cancelChild) {
                            if (predecessor == null) {
                                mFirstTouchTarget = next;
                            } else {
                                // 把下一个赋予父节点的上一个,这样当前节点就被丢弃了
                                predecessor.next = next;
                            }
                            
                            // 回收内存
                            target.recycle();
                            
                            // 把下一个赋予现在
                            target = next;

                            // 下面的两行不执行了,因为我们已经做了链表的操作了。
                            // 主要是我们不能执行predecessor=target,因为删除本节点的话,父节点还是父节点
                            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) {
                // 如果是多点触摸下的手指抬起事件,就要根据idBit从TouchTarget中移除掉对应的Pointer(触摸点)
                final int actionIndex = ev.getActionIndex();
                final int idBitsToRemove = 1 << ev.getPointerId(actionIndex);
                removePointersFromTouchTargets(idBitsToRemove);
            }
        }

        if (!handled && mInputEventConsistencyVerifier != null) {
            mInputEventConsistencyVerifier.onUnhandledEvent(ev, 1);
        }
        return handled;
    }

OK,至此,我们 ViewGroup 点击事件的分发机制的源码流程分析完了,是不是已经晕了? 😵😵😵


Demo/案例

前面两节分析了一堆代码,很枯燥啊,要不来个Demo解解乏,看看实际案例中事件分发机制的处理流程,理论实际相结合,印象才会更深刻!

Layout层次:

layout.png
layout.png

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");
            }
        });

    }
}

测试结果:

07-10 16:17:51.877 16250 16250 D TAG     : click the button_01     // 点击按钮 button_01
07-10 16:17:53.875 16250 16250 D TAG     : click the button_02     // 点击按钮 button_02
07-10 16:17:54.758 16250 16250 D TAG     : click the ViewGroup     // 点击空白处

结果说明:

  • 点击Button时,执行 Button.onClick(),但 ViewGroup_Layout 注册的 onTouch() 不会执行
  • 只有点击空白区域时,才会执行 ViewGroup_Layout 的 onTouch()

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


总结

休息片刻,做个小结

  • 相比于Activity的事件分发机制的分析流程,ViewGroup复杂很多,如果你跟踪完上面的源码,我想你已经跟我一样一脸懵逼了😭...
  • 此时,我们所需要了解以下几点:
          对于任何源码的分析,不可能一遍就能读懂;
          对于任何源码的分析,即时你多读几遍也未必就一定能吃透;
          对于任何源码的分析,静下心来多坚持学习 & 系统性的归纳整理,相信你总能完全了解它!

      根据流程图 - 梳理流程来整理思路是最好的方法:(了解整个事件分发机制的流程即可,细节慢慢体会!)

ViewGroup事件分发处理流程.png
ViewGroup事件分发处理流程.png

🍁 View对点击事件的分发机制

从Activity事件分发机制可知,ViewGroup事件分发机制从dispatchTouchEvent()开始,从ViewGroup事件分发机制可知,View事件分发机制从dispatchTouchEvent()开始。

源码分析

dispatchTouchEvent

源码路径:frameworks/base/core/java/android/view/View.java

    /**
     * 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 (onFilterTouchEventForSecurity(event)) {
            if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
                result = true;
            }
            //noinspection SimplifiableIfStatement
            ListenerInfo li = mListenerInfo;
            
            💥💥💥💥💥💥💥💥💥💥💥💥重点方法💥💥💥💥💥💥💥💥💥💥💥💥
            /*
             * 说明:只有以下3个条件都为真,dispatchTouchEvent()才返回true
             *       1. mListenerInfo != null & mListenerInfo.mOnTouchListener != null
             *       2. (mViewFlags & ENABLED_MASK) == ENABLED
             *       3. mListenerInfo.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:mListenerInfo != null & mListenerInfo.mOnTouchListener != null

    /**
      * 条件1:mListenerInfo.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:mListenerInfo.mOnTouchListener.onTouch(this, event)

    /**
      * 条件3:mOnTouchListener.onTouch(this, event)
      * 说明:即 回调控件注册Touch事件时的onTouch();需手动复写设置,具体如下(以按钮Button为例)
      */
    button.setOnTouchListener(new OnTouchListener() {  
        @Override  
        public boolean onTouch(View v, MotionEvent event) {  
     
            return false;  
        }  
    });

接下来讨论onTouchEvent方法:

onTouchEvent

源码路径:frameworks/base/core/java/android/view/View.java

    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();

                            // 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();     // 重点分析函数   💥💥💥
                                }
                            }
                        }
                        ...
                    }
                    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;
    }

performClick

源码路径:frameworks/base/core/java/android/view/View.java

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

        sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);

        notifyEnterOrExitForAutoFillIfNeeded(true);

        return result;
    }

Demo/案例

再来个Demo解解乏:

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
        */
        // 1. 通过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;
            }
        });

        // 2. 通过 OnClickListener()为控件设置点击事件,为mOnClickListener变量赋值(即不为空),从而往下回调onClick()
        button_01.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Log.d("TAG", "run onClick()");
            }
        });
        
        /**
          * 结论验证2:在回调onTouch()里返回true
          */
        // 1. 通过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;
            }
        });

        // 2. 通过 OnClickListener()为控件设置点击事件,为mOnClickListener变量赋值(即不为空),从而往下回调onClick()
        button_01.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Log.d("TAG", "run onClick()");
            }
        });

    }
}

测试结果:

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

结果说明:

通过上面的Demo,怎么样?是不是很清楚了?仔细琢磨一下,手动写个Demo,你会发现原来原理就是这么简单!


总结

OK,又到了流程图的展现时候了,如下所示:

View事件分发机制流程图.png
View事件分发机制流程图.png