前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >开发笔记-自定义View(十)-View的事件分发机制

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

作者头像
g小志
发布2018-09-11 17:25:23
6370
发布2018-09-11 17:25:23
举报
文章被收录于专栏:Android常用基础
前言

关于自定义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在同一事件序列中无法再接受到事件

表示是否消耗当前事件

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

代码语言:javascript
复制
/**
  * 点击事件产生后 
  */ 
  // 步骤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():

代码语言:javascript
复制
    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中。那么找到这个方法如下:

代码语言:javascript
复制
    //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);这个方法:

代码语言:javascript
复制
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()方法:

代码语言:javascript
复制
    @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
代码语言:javascript
复制
·
        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
代码语言:javascript
复制
//非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
代码语言:javascript
复制
            /**
            *讲解六
            */
            // 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对事件分发。看下他的源码:

代码语言:javascript
复制
   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()方法。那么我们就来看这个方法:

代码语言:javascript
复制
 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)方法中:

代码语言:javascript
复制
    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()方法.这个方法代码如下:

代码语言:javascript
复制
    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的控件是一样的。:

代码语言:javascript
复制
    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
代码语言:javascript
复制
        /**
        *讲解一
        */
        //首先判断当前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
代码语言:javascript
复制
/**
        *讲解二
        */
        //只要控件的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开发艺术探索》

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2018.02.11 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 前言
  • 概念
    • 1.MotionEvent
      • 2. 事件分发的顺序
        • 2. 事件分发的核心方法
        • 源码分析
          • 1. Activity事件分发
            • 2. ViewGroup事件分发
              • part1
                • part2
                  • part3
                    • 3. View事件分发
                      • part1
                        • part2
                        • 总结
                        • 结语
                        领券
                        问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档