53

Android 8.1 源码_机制篇(一) --消息分发机制

 6 years ago
source link: https://segmentfault.com/a/1190000015983576?amp%3Butm_medium=referral
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

前言

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

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

基础认知

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

事件分发的对象

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

1. 定义

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

【说明】Touch事件的相关细节(发生触摸的位置、时间等)被封装成MotionEvent对象

2. 事件类型

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

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

3. 事件序列

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

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

f63Urme.png!web

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

事件分发的本质

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

【说明】即:事件传递的过程 = 分发过程

事件传递的对象

对象:Activity、ViewGroup、View

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

f63Urme.png!web

事件分发的顺序

顺序:Activity -> ViewGroup -> View

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

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

f63Urme.png!web

事件分发的方法

方法: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方法

f63Urme.png!web

段落总结

f63Urme.png!web

源码分析

还记得我们上节说到的事件分发的 传递顺序 吗?即:1个点击事件发生后,事件先传到Activity、再传到ViewGroup、最终再传到 View

f63Urme.png!web

所以,要想充分理解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(); :boom:
        }
        
        // 若getWindow().superDispatchTouchEvent(ev)的返回true
        // 则Activity.dispatchTouchEvent()就返回true,则方法结束。即 :该点击事件停止往下传递 & 事件传递过程结束
        // 否则:继续往下调用Activity.onTouchEvent
        if (getWindow().superDispatchTouchEvent(ev)) {  :boom:
            return true;
        }
        
        return onTouchEvent(ev);  :boom:
    }

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

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

总结

f63Urme.png!web

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

ViewGroup对点击事件的分发机制

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

说明:

  • 从ViewGroup事件分发机制的源码分析开始,你将会看到一大,大,大堆的代码:grin:,请根据 :boom: 耐心看,细节看不懂,看框架(流程)即可!
  • 小编也是头疼啊,Android从5.0版开始,框架不停的改,改,改:disappointed:!但是原理是一样的(天下武功出少林),所以相信我,读源码,纠其理,方能通精髓!

源码分析

源码路径: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;     :boom:
                if (!disallowIntercept) {
                    // ViewGroup每次事件分发时,都需调用onInterceptTouchEvent()询问是否拦截事件
                    intercepted = onInterceptTouchEvent(ev);     :boom:
                    /// 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;
            
            :boom::boom::boom::boom::boom::boom::boom::boom::boom::boom::boom::boom::boom::boom::boom::boom:重点方法:boom::boom::boom::boom::boom::boom::boom::boom::boom::boom::boom::boom::boom::boom::boom::boom:
            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     :boom:
                        for (int i = childrenCount - 1; i >= 0; i--) {
                            :point_down::point_down::point_down::point_down::point_down::point_down::point_down::point_down::point_down::point_down::point_down::point_down::point_down::point_down::point_down::point_down::point_down::point_down::point_down::point_down::point_down::point_down::point_down::point_down::point_down::point_down::point_down::point_down::point_down::point_down::point_down::point_down::point_down::point_down::point_down:
                            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;
                            }
                            :point_up_2::point_up_2::point_up_2::point_up_2::point_up_2::point_up_2::point_up_2::point_up_2::point_up_2::point_up_2::point_up_2::point_up_2::point_up_2::point_up_2::point_up_2::point_up_2::point_up_2::point_up_2::point_up_2::point_up_2::point_up_2::point_up_2::point_up_2::point_up_2::point_up_2::point_up_2::point_up_2::point_up_2::point_up_2::point_up_2::point_up_2::point_up_2::point_up_2::point_up_2::point_up_2:

                            // 获取新的触摸对象,如果当前的子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)) {     :boom:
                                // 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 点击事件的分发机制的源码流程分析完了, 是不是已经晕了? :dizzy_face::dizzy_face::dizzy_face:

Demo/案例

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

Layout层次:

f63Urme.png!web

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复杂很多,如果你跟踪完上面的源码,我想你已经跟我一样一脸懵逼了:sob:...
  • 此时,我们所需要了解以下几点:

对于任何源码的分析,不可能一遍就能读懂;

对于任何源码的分析,即时你多读几遍也未必就一定能吃透;

对于任何源码的分析,静下心来多坚持学习 & 系统性的归纳整理,相信你总能完全了解它!

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

f63Urme.png!web

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;
            
            :boom::boom::boom::boom::boom::boom::boom::boom::boom::boom::boom::boom:重点方法:boom::boom::boom::boom::boom::boom::boom::boom::boom::boom::boom::boom:
            /*
             * 说明:只有以下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;
            }
            :boom::boom::boom::boom::boom::boom::boom::boom::boom::boom:分别分析上面几个条件:boom::boom::boom::boom::boom::boom::boom::boom::boom::boom:
            
            // 如果上面没有返回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) {
                :boom: // 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();     // 重点分析函数   :boom::boom::boom:
                                }
                            }
                        }
                        ...
                    }
                    break;
                    
                :boom: // b. 若当前的事件 = 按下View
                case MotionEvent.ACTION_DOWN:
                    ... ...
                    break;

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

                :boom: // 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,又到了流程图的展现时候了,如下所示:

f63Urme.png!web

参考Blog

01. https://www.jianshu.com/p/380...

02. https://www.jianshu.com/p/a72...

03. https://blog.csdn.net/salmon_...

04. https://blog.csdn.net/savelov...


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK