开发笔记-自定义View(十)-View的事件分发机制

前言

关于自定义View系列的文章,好久没有写了。今天抽空看了下Android开发艺术探索。正好看到了View的事件分发机制,所以将它写成笔记记录下来。
关于View的事件分发,我起初是学习郭神的2篇文章。感觉其实也没有什么。大致也就了解下。不过看完其他很多优秀的文章和书籍后,才知道自己too young too simple。下面我们就一起来分析下Android的时间分发机制。

关于事件分发机制,其实网上的文章已经有很多了。我简单的看了几篇,发现写的都很好。之所以写这篇文章,主要是记录自己的学习过程,其次也想帮助和我一样的的初学者更加理解与掌握,才是本篇的目的。
注:本文源码 API=25 同时本文较长,可以先收藏再好好阅读

概念

在学习事件分发之前我们先来了解下,什么是事件分发。所谓点击事件(Touch)的事件分发,其实就是对MotionEvent(Touch的封装)事件的分发过程,即当一个MotionEvent产生以后,系统需要把这这个事件传递给那个具体的View。这个传递的过程就是事件分发过程

1.MotionEvent

那么MotionEvent又是什么呢?
这个类就是记录手指接触屏幕后所产生的一系列的事件(也就是说我们事件分发其实就是分发MotionEvent这个对象)。这个类里包含了一系列的事件。事件的类型与含义如下:

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

下面列举2个我们常见的点击事件序列:

  1. 事件序列 : DOWN->UP 点击屏幕后松开 (常见的点击事件)
  2. 事件序列 : DOWN->MOVE->MOVE->...->MOVE->UP 点击屏幕滑动一会 (常见的滑动屏幕)

用图来概括如下:

事件系列

                图片来源

2. 事件分发的顺序

事件分发的顺序是Activity->ViewGroup->View。也就是说在默认情况下。最后消费事件的都是View。虽然我们现在还没有开始深入讲解。但是结合我们日常开发的情况我们可以想到下面这张流程图:

事件分发概括.png

这张流程图就算我们没有了解事件分发,通过我们一直的使用规则来看,也是非常容易理解的。细心的小伙伴会发现。为什么Activity向下分发第一个就是ViewGroup,如果我们布局中只有一个简单View控件(如TextView)呢?还记得我们在讲View的绘制流程中介绍的吗?我们布局加载中的顶级View是DecorView(继承FrameLayout),他本是就是一个ViewGroup。不了解的可以回头看下这篇文章

2. 事件分发的核心方法

在对事件分发机制概念,以及结合平时我们经验总结出来的原理后。下面我们就来通过源码来去将我们的想法串联起来。不过在看源码之前,我们要先讲下在事件分发机制中三个至关重要的方法。如下:

方法 作用 调用时刻 返回结果
dispatchTouchEvent(MotionEvent event) 用来进行事件分发 在三个方法中第一个被调用。 如果事件能够传递给当前View/ViewGroup,那么此方法一定会调用 表示是否消耗当前事件
onInterceptTouchEvent(MotionEvent ev) 用来判断是否拦截某个事件(ViewGroup有此方法,View没有) 在dispatchTouchEvent()方法中调用 如果当前View拦截了某个事件,那么同一事件序列将不再会调用此方法。 表示是否拦截当前事件
onTouchEvent(MotionEvent event) 用来处理点击事件 在dispatchTouchEvent()方法中调用,不消耗当前事件,那么当前View在同一事件序列中无法再接受到事件 表示是否消耗当前事件

这三个方法就是事件分发机制中的核心三个方法,也是我们下面在源码中重要去分析的三个方法。他们三者之间的关系可以概述如下(注意这是一段伪代码。在任何类中并没有此方法。只是为了对解释三个方法关系):


/**
  * 点击事件产生后 
  */ 
  // 步骤1:调用dispatchTouchEvent()
  public boolean dispatchTouchEvent(MotionEvent ev) {

    boolean consume = false; //代表 是否会消费事件

    // 步骤2:判断是否拦截事件
    if (onInterceptTouchEvent(ev)) {
      // a. 若拦截,则将该事件交给当前View进行处理
      // 即调用onTouchEvent ()方法去处理点击事件
        consume = onTouchEvent (ev) ;

    } else {

      // b. 若不拦截,则将该事件传递到下层
      // 即 下层元素的dispatchTouchEvent()就会被调用,重复上述过程
      // 直到点击事件被最终处理为止
      consume = child.dispatchTouchEvent (ev) ;
    }

    // 步骤3:最终返回通知 该事件是否被消费(接收 & 处理)
    return consume;


三个方法的解释在加上这段伪代码,就很好理解三者的关系了:对于一个跟ViewGroup,点击事件产生后,首先会传给它,这时它的dispatchTouchEvent就会被调用,开始进行事件的分发,首先会进行判断,判断当前ViewGroup是否进行了拦截。如果进行拦截,那么ev(点击事件)就会交给ViewGroup去处理。不再向下传递。分发结束。如果没设置拦截。那么就会调用ViewGroup中所包含的子控件的dispatchTouchEvent (ev)方法,并将事件ev向下传递。如果子控件还是ViewGroup继续上面的循环。知道将事件最终被处理消费掉。这么一看,这不正好对应了我们前面总结的流程图嘛。看来我们将事件分发的大致流程已经都搞清楚了。

源码分析

从上面来看,好像事件分发机制也就这些东西了。好像我们都掌握了。其实不然,不过如果你上面的都理解了,说你对Android事件分发机制了个整体认识,那就一点都不为过了。不过事件分发还远不止这么简单。里面还是有很多需要注意的点和事件在分发过程中的一些规则。下面我们就从源码的角度来一一探索。

1. Activity事件分发

上面我们说了当一个事件的产生首先是传递个Activity。由Activity来进行事件的分发。那么我就看下Activity#dispatchTouchEvent():


    public boolean dispatchTouchEvent(MotionEvent ev) {
        // 一般事件列开始都是DOWN事件 = 按下事件,故此处基本是true
        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
            //实现屏保功能
            //是一个空方法 
            //当此activity在栈顶时,触屏点击按home,back,menu键等都会触发此方法
            onUserInteraction();
        }
        if (getWindow().superDispatchTouchEvent(ev)) {
            return true;
        }
        //当一个点击事件未被Activity下任何一个View接收 / 处理
        //或是发生在Window边界外的触摸事件就会调用
        return onTouchEvent(ev);
    }

这个方法非常短。这里我们重点看第二个if语句。这里的getWindow().superDispatchTouchEvent(ev)点进去是Window#superDispatchTouchEvent()。这是一个抽象方法。不过相信看过我前面文章的小伙伴一定知道这个方法的实现实在PhoneWindow中。那么找到这个方法如下:

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

    /**
     * 这里调用了mDecor.superDispatchTouchEvent(event);方法。这个mDecor就* 是DecorView 下面我们继续跟踪
     */
    //DecorView#superDispatchTouchEvent()
    public boolean superDispatchTouchEvent(MotionEvent event) {
        //DecorView继承FrameLayout 那么他本是就是一个ViewGroup
        //那么这个方法最后就会调用到ViewGroup#dispatchTouchEvent()
        return super.dispatchTouchEvent(event);
    }

可以看到最后Activity的分发过程最后就是将事件交给顶级DecorView去进行事件分发。然后它又会调用ViewGroup#dispatchTouchEvent()。OK!到这里我们就将我们的事件由Activity->ViewGroup的传递。并将返回值设置成true。表示这个事件已经被我们消耗掉了。

这里还有一点需要注意。从源码中我们可以看到,假设Activity下的所有View或者我们点击了Window边界以外,那么就会调用Activity#onTouchEvent(ev);这个方法:

public boolean onTouchEvent(MotionEvent event) {
        if (mWindow.shouldCloseOnTouch(this, event)) {
            finish();
            return true;
        }

        return false;
    }

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

这部分了解即可,并不是我们的重点。

2. ViewGroup事件分发

通过上面的分析,现在事件已经从Activity->ViewGroup。那么我们就分析ViewGroup#dispatchTouchEvent()方法:

    @Override
    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.
            if (actionMasked == MotionEvent.ACTION_DOWN) {
                // Throw away all previous state when starting a new touch gesture.
                // The framework may have dropped the up or cancel event for the previous gesture
                // due to an app switch, ANR, or some other state change.
                cancelAndClearTouchTargets(ev);
                //清空mFirstTouchTarget
                resetTouchState();
            }

            // Check for interception.
            /**
            *讲解一
            */
            //检查是否拦截事件  
            //1.当事件为ACTION_DOWN时 
            //2. 当ViewGroup不拦截事件交给子元素处理 条件成立 即mFirstTouchTarget != null
            final boolean intercepted;
            if (actionMasked == MotionEvent.ACTION_DOWN
                    || mFirstTouchTarget != null) {
                final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
                if (!disallowIntercept) {
                    //调用事件拦截方法
                    intercepted = onInterceptTouchEvent(ev);
                    ev.setAction(action); // restore action in case it was changed
                } else {
                    intercepted = false;
                }
            } else {
                // There are no touch targets and this action is not an initial down
                // so this view group continues to intercept touches.
                intercepted = true;
            }

           ......

            // Check for cancelation.
            final boolean canceled = resetCancelNextUpFlag(this)
                    || actionMasked == MotionEvent.ACTION_CANCEL;

            // Update list of touch targets for pointer down, if needed.
            final boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0;
            TouchTarget newTouchTarget = null;
            boolean alreadyDispatchedToNewTouchTarget = false;
            //非MotionEvent.ACTION_CANCEL并且没有拦截事件
            //进入if语句,对ViewGroup的子元素进行遍历
            /**
            *讲解三
            */
            if (!canceled && !intercepted) {

                // If the event is targeting accessiiblity focus we give it to the
                // view that has accessibility focus and if it does not handle it
                // we clear the flag and dispatch the event to all children as usual.
                // We are looking up the accessibility focused host to avoid keeping
                // state since these events are very rare.
                View childWithAccessibilityFocus = ev.isTargetAccessibilityFocus()
                        ? findChildWithAccessibilityFocus() : null;

                if (actionMasked == MotionEvent.ACTION_DOWN
                        || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
                        || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
                    final int actionIndex = ev.getActionIndex(); // always 0 for down
                    final int idBitsToAssign = split ? 1 << ev.getPointerId(actionIndex)
                            : TouchTarget.ALL_POINTER_IDS;

                    // Clean up earlier touch targets for this pointer id in case they
                    // have become out of sync.
                    removePointersFromTouchTargets(idBitsToAssign);

                    final int childrenCount = mChildrenCount;
                    if (newTouchTarget == null && childrenCount != 0) {
                        final float x = ev.getX(actionIndex);
                        final float y = ev.getY(actionIndex);
                        // Find a child that can receive the event.
                        // Scan children from front to back.
                        final ArrayList<View> preorderedList = buildTouchDispatchChildList();
                        final boolean customOrder = preorderedList == null
                                && isChildrenDrawingOrderEnabled();
                        final View[] children = mChildren;
                        //对子元素进行遍历
                        for (int i = childrenCount - 1; i >= 0; i--) {
                            //1.子元素是否在做动画
                            final int childIndex = getAndVerifyPreorderedIndex(
                                    childrenCount, i, customOrder);
                            //2.事件左边是否落在子元素区域内
                            final View child = getAndVerifyPreorderedView(
                                    preorderedList, children, childIndex);
                            //接受点击事件的View根据1.2条件判断  
                            //是否能够接受点击事件
                            if (!canViewReceivePointerEvents(child)
                                    || !isTransformedTouchPointInView(x, y, child, null)) {
                                ev.setTargetAccessibilityFocus(false);
                                //不符合要求 执行下一个循环
                                continue;
                            }
                            ......
                            //调用此方法进行事件分发处理 如果有子元素
                            //那么参数中的child!=null
                             /**
                              *讲解四
                              */
                            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;
                                            //条件满足跳出循环
                                            //这里是跳出内部for循环不是外部的
                                            //其实就是break的用法
                                            break;
                                        }
                                    }
                                } else {
                                    mLastTouchDownIndex = childIndex;
                                }
                                mLastTouchDownX = ev.getX();
                                mLastTouchDownY = ev.getY();
                            /**
                              *讲解五
                              */
                                //对mFirstTouchTarget 进行赋值
                                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);
                        }
                        
                        ......
                    }
                }
            }

            /**
            *讲解六
            */
            // Dispatch to touch targets.
            if (mFirstTouchTarget == null) {
                // No touch targets so treat this as an ordinary view.
                handled = dispatchTransformedTouchEvent(ev, canceled, null,
                        TouchTarget.ALL_POINTER_IDS);
            } else {
                ......
            }

            // Update list of touch targets for pointer up or cancel, if needed.
            if (canceled
                    || actionMasked == MotionEvent.ACTION_UP
                    || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
                //MotionEvent.ACTION_UP 后 清空mFirstTouchTarget
                /**
                *讲解七
                */
                resetTouchState();
            } else if (split && actionMasked == MotionEvent.ACTION_POINTER_UP) {
                final int actionIndex = ev.getActionIndex();
                final int idBitsToRemove = 1 << ev.getPointerId(actionIndex);
                removePointersFromTouchTargets(idBitsToRemove);
            }
        }

     ......
    }

关于ViewGroup的事件分发源码(即dispatchTouchEvent()方法),还是比较长的,同时也是难点,下面我们将上面的代码拆分来看,来具体分析。

part1
·
        boolean handled = false;
        if (onFilterTouchEventForSecurity(ev)) {
            final int action = ev.getAction();
            final int actionMasked = action & MotionEvent.ACTION_MASK;
             
             /**
             * 讲解二
             */
            // Handle an initial down.
            if (actionMasked == MotionEvent.ACTION_DOWN) {
                // Throw away all previous state when starting a new touch gesture.
                // The framework may have dropped the up or cancel event for the previous gesture
                // due to an app switch, ANR, or some other state change.
                cancelAndClearTouchTargets(ev);
                //清空mFirstTouchTarget
                resetTouchState();
            }

            // Check for interception.
            /**
            *讲解一
            */
            //检查是否拦截事件  
            //1.当事件为ACTION_DOWN时 
            //2. 当ViewGroup不拦截事件交给子元素处理 条件成立 即mFirstTouchTarget != null
            final boolean intercepted;
            if (actionMasked == MotionEvent.ACTION_DOWN
                    || mFirstTouchTarget != null) {
                final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
                if (!disallowIntercept) {
                    //调用事件拦截方法
                    intercepted = onInterceptTouchEvent(ev);
                    ev.setAction(action); // restore action in case it was changed
                } else {
                    intercepted = false;
                }
            } else {
                // There are no touch targets and this action is not an initial down
                // so this view group continues to intercept touches.
                intercepted = true;
            }

  • 讲解一
    这个方法首先会判断,是否拦截当前事件。这个条件第一个好理解。一个事件的系列一定是由DOWN开始的,那么就会进入条件语句。调用onInterceptTouchEvent(ev)开始进行事件拦截。关于第二个代码中有解释。这个方法是在那里还原初始值与赋值请看下面。
  • 讲解二
    事件开始时会调用 resetTouchState();清空mFirstTouchTarget
part2
//非MotionEvent.ACTION_CANCEL并且没有拦截事件
            //进入if语句,对ViewGroup的子元素进行遍历
            /**
            *讲解三
            */
            if (!canceled && !intercepted) {

                        ......省略
                        
                        //对子元素进行遍历
                        for (int i = childrenCount - 1; i >= 0; i--) {
                            //1.子元素是否在做动画
                            final int childIndex = getAndVerifyPreorderedIndex(
                                    childrenCount, i, customOrder);
                            //2.事件左边是否落在子元素区域内
                            final View child = getAndVerifyPreorderedView(
                                    preorderedList, children, childIndex);
                            //接受点击事件的View根据1.2条件判断  
                            //是否能够接受点击事件
                            if (!canViewReceivePointerEvents(child)
                                    || !isTransformedTouchPointInView(x, y, child, null)) {
                                ev.setTargetAccessibilityFocus(false);
                                //不符合要求 执行下一个循环
                                continue;
                            }
                            ......
                            //调用此方法进行事件分发处理 如果有子元素
                            //那么参数中的child!=null
                             /**
                              *讲解四
                              */
                            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;
                                            //条件满足跳出循环
                                            //这里是跳出内部for循环不是外部的
                                            //其实就是break的用法
                                            break;
                                        }
                                    }
                                } else {
                                    mLastTouchDownIndex = childIndex;
                                }
                                mLastTouchDownX = ev.getX();
                                mLastTouchDownY = ev.getY();
                            /**
                              *讲解五
                              */
                                //对mFirstTouchTarget 进行赋值
                                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语句,判断条件为没有对事件进行拦截,同时事件没有结束。对ViewGroup的子元素进行遍历
  • 讲解️四
    通过判断,将ViewGroup的子元素进行遍历,找到能够处理点击事件的子元素并调用dispatchTransformedTouchEvent()方法,进行事件的分发。
  • 讲解五
    当子元素能够处理点击事件,就调用addTouchTarget()方法,对mFirstTouchTarget()方法进行赋值。那么下次再进入讲解一方法。
part3

            /**
            *讲解六
            */
            // Dispatch to touch targets.
            if (mFirstTouchTarget == null) {
                // No touch targets so treat this as an ordinary view.
                handled = dispatchTransformedTouchEvent(ev, canceled, null,
                        TouchTarget.ALL_POINTER_IDS);
            } else {
                ......
            }

            // Update list of touch targets for pointer up or cancel, if needed.
            if (canceled
                    || actionMasked == MotionEvent.ACTION_UP
                    || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
                //MotionEvent.ACTION_UP 后 清空mFirstTouchTarget
                /**
                *讲解七
                */
                resetTouchState();
            } else if (split && actionMasked == MotionEvent.ACTION_POINTER_UP) {
                final int actionIndex = ev.getActionIndex();
                final int idBitsToRemove = 1 << ev.getPointerId(actionIndex);
                removePointersFromTouchTargets(idBitsToRemove);
            }
        }

  • 讲解六
    讲解三中的if语句不成立表示对事件进行拦截。那么直接走到了讲解六,并且mFirstTouchTarget == null没有在子元素的遍历中赋值,即条件成立。执行dispatchTransformedTouchEvent()方法。
  • 讲解七
    在同一事件系列结束后调用resetTouchState();对mFirstTouchTarget清空还原。

这样我们就将ViewGroup#dispatchTouchEvent()方法分析完成了。

在上面的讲解中我们多此提到mFirstTouchTarget与dispatchTransformedTouchEvent()方法。前者已经说明了他的作用与赋值及清空还原的位置。对于后者,这个方法其实就是ViewGroup对事件分发。看下他的源码:

   private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
            View child, int desiredPointerIdBits) {
        final boolean handled;
        final int oldAction = event.getAction();
        if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
            event.setAction(MotionEvent.ACTION_CANCEL);
            if (child == null) {
                handled = super.dispatchTouchEvent(event);
            } else {
                handled = child.dispatchTouchEvent(event);
            }
            event.setAction(oldAction);
            return handled;
        }
        ......
    }

是ViewGroup#dispatchTransformedTouchEvent()方法,其他方法省略。可以可看到,如果ViewGroup有子元素同时子元素可以处理点击事件。那么就会调用子元素的child.dispatchTouchEvent(event);方法。如果是child是ViewGroup继续上面的循环,如果子元素是View,那么就或调用View.dispatchTouchEvent(event)方法。关于这个方法我们后面分析。如果child为空(即讲解六),那么就会调用super.dispatchTouchEvent(event)方法,那么就会调用ViewGroup父类的,即View.dispatchTouchEvent(event)方法,ViewGroup自己处理点击事件。最后都会默认(是默认不是一定) 调用onTounchEvent()方法。

通关对源码与这七个重要部分的讲解。我们可以总结如下几点:

  1. 一个事件序列只能一个View进行拦截且消耗。由讲解三我们知道。如果拦截事件,就不会进入if语句对子元素进行遍历与事件分发。同时又讲解六我们知道,如果拦截了某一事件(如MOVE)那么统一事件序列内的所有事件都交给它处理。
  2. 某个View一旦拦截事件,那么这一事件序列只能有它来处理(由1可知)。同时我们知道既然拦截就无法进入讲解三中,那么mFirstTouchTarget就无法被赋值,那么讲解一中的条件就不成立。所以调用onInterceptTouchEvent()不会再被调用。其实通过1.我们也都理解,如果拦截那么同一事件序列的所有事件都间给当前View处理。你拦截就说明你必须全都处理。那我还问你干啥。
  3. 。dispatchTransformedTouchEvent()方法中,当dispatchTouchEvent()的返回值与dispatchTransformedTouchEvent()返回值相同,由讲解六得知。这样会直接影响ViewGroup#dispatchTouchEvent()返回值(两者相同)。也就是说:View一旦开始处理事件,如果它不消耗ACTION_DOWN事件(onTounchEvent()返回false)。那么统一事件序列的其他事件都不会交给他处理。并会重新交给父元素(注意是父元素,不是父类)去处理,即父元素onTounchEvent()会被调用。
  4. onInterceptTouchEvent()默认返回false,即默认不拦截任何事件。
  5. View没有onInterceptTouchEvent()。一旦事件传递给他,那么他的onTouchEvent就会调用。

关于ViewGroup的源码分析我们也就到这里了。有的啰嗦。不过详细才能跟好的理解与全面

3. View事件分发

由上面dispatchTransformedTouchEvent()方法可知,最后方法无论是ViewGroup消耗还是View消耗都会调用View#dispatchTouchEvent()方法。那么我们就来看这个方法:

 public boolean dispatchTouchEvent(MotionEvent event) {
       
        ......
        
        boolean result = false;

        ......

        if (onFilterTouchEventForSecurity(event)) {
            if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
                result = true;
            }
            //noinspection SimplifiableIfStatement
            ListenerInfo li = mListenerInfo;
            if (li != null && li.mOnTouchListener != null
                    && (mViewFlags & ENABLED_MASK) == ENABLED //是否是enable可点击的 按钮默认都是可点击的 ImageView 不可点击
                    && li.mOnTouchListener.onTouch(this, event)) {
                result = true;
            }
            //调用onTouchEvent(event)方法 返回值直接影响此方法的返回值
            //返回值与次方法相同
            if (!result && onTouchEvent(event)) {
                result = true;
            }
        }

        ......

        return result;
    }

View的dispatchTouchEvent比较简单。首先判断mOnTouchListener!=null同时li.mOnTouchListener.onTouch(this, event)返回为true那么result = true;。那么下面的 if (!result && onTouchEvent(event)) 中的第一个条件就不会成立所以onTouchEvent(event)永远不会得到执行。有此可见onTouch()优先级要高于onTouchEvent(event)。
下面我们看下默认情况进入onTouchEvent(event)方法中:

    public boolean onTouchEvent(MotionEvent event) {
        final float x = event.getX();
        final float y = event.getY();
        final int viewFlags = mViewFlags;
        final int action = event.getAction();
        
        /**
        *讲解一
        */
        //首先判断当前View是不是DISABLED不可用状态
        if ((viewFlags & ENABLED_MASK) == DISABLED) {
            if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
                setPressed(false);
            }
            // A disabled view that is clickable still consumes the touch
            // events, it just doesn't respond to them.
            //如果不可用 同时当前控件的clickable与long_clickable
            //与CONTEXT_CLICKABLE全是false
            //那么才返回false
            return (((viewFlags & CLICKABLE) == CLICKABLE
                    || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
                    || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE);
        }
        //如果View有代理会执行这个方法
        if (mTouchDelegate != null) {
            if (mTouchDelegate.onTouchEvent(event)) {
                return true;
            }
        }
        /**
        *讲解二
        */
        //只要控件的clickable与long_clickable
        //与CONTEXT_CLICKABLE 有一个为true 就进入次循环
        if (((viewFlags & CLICKABLE) == CLICKABLE ||
                (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) ||
                (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE) {
            switch (action) {
                case MotionEvent.ACTION_UP:
                        ......
                  
                        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)) {
                                 /**
                                  *讲解三
                                  */
                                //onClickListener监听在此方法中
                                    performClick();
                                }
                            }
                        }

                       ......
                    }
                    break;

                ......
            }
            //默认返回 true
            /**
            *讲解四
            */
            return true;
        }

        return false;
    }

这部分代码比较简单。主要的都有注释。如果控件!=DISABLED,那么就会进入同时讲解二判断有一个成立。就会进入switch语句。当接收到MotionEvent.ACTION_UP是。最后执行performClick()方法.这个方法代码如下:

    public boolean performClick() {
        final boolean result;
        final ListenerInfo li = mListenerInfo;
        if (li != null && li.mOnClickListener != null) {
            playSoundEffect(SoundEffectConstants.CLICK);
            li.mOnClickListener.onClick(this);
            result = true;
        } else {
            result = false;
        }

        sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
        return result;
    }

可以看到我们的设置setOnclickListener就是在这个赋值,li.mOnClickListener.onClick(this);就会调用我们的onClik方法。
那么有的同学会问View的longClickable默认是false,同时TextView的clickable也为false,那么为何我们给TextView设置setOnclickListener也能生效。我们下来看下TextView源码其他默认clickable=false的控件是一样的。:

    public void setOnClickListener(@Nullable OnClickListener l) {
        if (!isClickable()) {
        //设置clickable
            setClickable(true);
        }
        getListenerInfo().mOnClickListener = l;
    }
    
public void setOnLongClickListener(@Nullable OnLongClickListener l) {
        if (!isLongClickable()) {
        //设置longclickable
            setLongClickable(true);
        }
        getListenerInfo().mOnLongClickListener = l;
    }

可以看到在设置监听时,方法内部已经帮我们设置了。

下面我们在针对onTouchEvent(MotionEvent event)方法来拆分分析下:

part1

        /**
        *讲解一
        */
        //首先判断当前View是不是DISABLED不可用状态
        if ((viewFlags & ENABLED_MASK) == DISABLED) {
            if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
                setPressed(false);
            }
            // A disabled view that is clickable still consumes the touch
            // events, it just doesn't respond to them.
            //如果不可用 同时当前控件的clickable与long_clickable
            //与CONTEXT_CLICKABLE全是false
            //那么才返回false
            return (((viewFlags & CLICKABLE) == CLICKABLE
                    || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
                    || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE);
        }
        //如果View有代理会执行这个方法
        if (mTouchDelegate != null) {
            if (mTouchDelegate.onTouchEvent(event)) {
                return true;
            }
        }
  • 讲解一
    由代码可知即使控件是DISABLED状态,只要clickable与longclickable有一个返回true那么此方法就返回true,即事件被消费。但是不会执行onClick()方法。这点通过代码很容易理解。
part2
/**
        *讲解二
        */
        //只要控件的clickable与long_clickable
        //与CONTEXT_CLICKABLE 有一个为true 就进入次循环
        if (((viewFlags & CLICKABLE) == CLICKABLE ||
                (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) ||
                (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE) {
            switch (action) {
                case MotionEvent.ACTION_UP:
                        ......
                  
                        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)) {
                                 /**
                                  *讲解三
                                  */
                                //onClickListener监听在此方法中
                                    performClick();
                                }
                            }
                        }

                       ......
                    }
                    break;

                ......
            }
            //默认返回 true
            /**
            *讲解四
            */
            return true;
        }


  • 讲解二
    讲解二中只要有一个条件满足。就会进入switch语句。当接收到MotionEvent.ACTION_UP时(前提MotionEvent.ACTION_DOWN也接收到了)会经过判断最后执行 performClick();方法。
  • 讲解三
    performClick()方法内部会执行我们设置的监听,即onClick()方法。
  • 讲解四
    由代码可知只要讲解二中的if语句成立,不管进入switch中的任何ACTION或是都不进入,返回值都是true,即事件消费了。同时讲解四也证明默认情况下是返回true

总结

下面我们用流程图在来总结下:

总体流程总结

                 图片来源 侵权即删

流程图真的懒得画了。一篇文章学习得3.4天。写出来又得很长时间,所以大家勿怪。同时这张图结合文章理解起来简直是so easy。
其实关于Android事件分发机制优秀的文章由很多。如果观看一篇文章无法完全掌握,就多看几篇文章。然后自己总结,结合。反正最后能理解成自己的就算成功了。

结语

本人是个菜鸟,如果文章哪里有错误,欢迎指出。有问题也可以留言。最后如果文章对您有帮助。感谢支持。

优秀干货

Android事件分发机制
Android事件分发机制详解:史上最全面、最易懂
《Android开发艺术探索》

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,362评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,330评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,247评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,560评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,580评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,569评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,929评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,587评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,840评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,596评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,678评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,366评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,945评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,929评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,165评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,271评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,403评论 2 342

推荐阅读更多精彩内容