Android学习笔记之事件分发机制完全解析

事件分发机制,作为Android中必须要掌握的重点,同时也是难点,想要解决开发中的一些问题,必须对它的整个流程有着较好的掌握。这几天阅读了《Android开发艺术探索》等书籍,结合自己的理解,记录下自己的所学,这里只是初步学习总结,想要完全地搞明白事件分发,还需要进一步学习。文中如果有错误,欢迎大家指出。

一、引入

想象生活中的一个例子,老板,项目经理,程序员。老板接到一个任务,分配给项目经理完成,项目经理又把任务分给程序员。程序员完成任务,告诉项目经理任务完成了,项目经理再向老板报告任务完成了。从老板接到任务,到老板最终去交付任务,这是个完整的过程。

但是在这个过程中,会有其它情况。假如在一开始老板接到任务时,决定自己完成,不需要把任务再分配,那么老板就自己做,项目经理和程序员就没事。同样,如果项目经理决定自己去做,那么就没有程序员的事。

这个例子大家肯定都能听懂,下面我们来看Android中的事件分发。

二、事件分发机制概述

1、概念

我们知道,Android的界面可能是由多个视图层层嵌套构成,当一个触摸事件发生时,系统需要把这个事件传递给一个具体的View,由它来完成处理。从事件发生,到传递给具体的View去完成,这个传递的过程就是View 事件分发。

2、相关

触摸事件,简单来说就是是触摸屏幕产生的动作事件,比如常见的手指按下,移动,抬起等等,Android为我们提供了一个专门的MotionEvent类,它包含了发生的动作事件以及相关坐标信息,利用MotionEvent,我们可以处理很多与动作相关的工作。

这里的View其实指的是View以及ViewGroup,我们知道View是Android中所有控件的基类,而ViewGroup翻译为视图组,它是继承自View的,可以包含子控件。我们在接下来的讨论中,会把ViewGroup和View分开讨论。

3、详解

事件分发过程主要涉及到三个重要方法,主要介绍如下:

dispatchTouchEvent 用于事件分发

onInterceptTouchEvent 用于拦截事件

onTouchEvent 用于处理事件

上面三个方法之间的关系大概如下,当事件传递到某个View时,先执行dispatchTouchEvent方法进行事件分发,在这个方法内会调用方法onInterceptTouchEvent来决定是否拦截,如果返回true表示拦截,则调用onTouchEvent进行事件处理,否则继续往下传递,执行子View的dispatchTouchEvent。

需要注意一点,View没有onInterceptTouchEvent方法,一旦有事件传递给它,那么它的onTouchEvent方法就会被调用。ViewGroup默认不拦截任何事件,因为从源码中可以看到ViewGroup的onInterceptTouchEvent方法默认返回false.

我们知道,四大组件中,Activity通常提供界面用于交互,我们会通过setContentView来设置界面布局,一般如果我们不希望布局顶部出现一个标题栏,我们可能会调用requestWindowFeature(Window.FEATURE_NO_TITLE);方法,这里我们简单了解一下Android的界面架构。

界面上一个点击事件发生时,它最先被传递的是给当前的Activity,由Activity的dispatchTouchEvent来进行事件分发,而Activity内部其实是包含一个Window的,这个抽象Window的实现是PhoneWindow,Activity把事件传递给PhoneWindow,PhoneWindow里又包含DecorView,PhoneWindow继续把事件传递给DecorView,DecorWindow里包含有我们设置的布局,DecorView继承自FrameLayout,事件最终传递给我们设置的布局,一般来说设置的布局是一个ViewGroup。下面介绍ViewGroup的事件分发过程。

三、ViewGroup 的事件分发

ViewGroup事件分发过程简述主要如下,事件到达ViewGroup后会调用方法dispatchTouchEvent,在其中会调用onInterceptTouchEvent进行判断是否拦截,如果返回true表示拦截则事件由ViewGroup处理,如果返回false不拦截,则事件会传递给子View,子View的dispatchTouchEvent会被调用。默认情况下,onInterceptTouchEvent返回false.

下面我们看下源码。

1、首先是dispatchTouchEvent方法里判断是否拦截。

final boolean intercepted;

//判断是否拦截if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) {

    //默认是false 允许拦截

    final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;

    if (!disallowIntercept) {

        intercepted = onInterceptTouchEvent(ev);

        ev.setAction(action);

    } else {

        intercepted = false;

    }

}else {

    intercepted = true;

}

这里可以看到,ViewGroup会在两种情况下判断是否拦截事件,第一种是发生ACTION_DOWN事件,第二种是mFirstTouchTarget != null。判断是否拦截时,会看disallowIntercept,默认是false不允许拦截,所以!disallowIntercept为true,然后调用onInterceptTouchEvent为false,即不拦截。有种情况,如果ACTION_DOWN判断时被ViewGroup拦截,那么mFirstTouchTarget != null就不成立,那么同一事件序列中的剩余事件ACTION_MOVE或者ACTION_UP来临时,不进行判断,直接拦截。

得出两条结论,某个View一旦决定拦截一个事件后,那么系统会把同一个事件序列的其它方法都交给这个View处理。某个View如果不消耗ACTION_DOWN事件交给了子View处理,那么同一个事件序列的其它方法都不会交给它处理。

2、当ViewGroup不拦截事件,事件分发给子View处理。

//子Viewfinal View[] children = mChildren;//循环遍历for (int i = childrenCount - 1; i >= 0; i--) {

     ... ...

     //如果子View接收不到事件 或者 不在播动画 就不分发

     if (!canViewReceivePointerEvents(child)

           || !isTransformedTouchPointInView(x, y, child, null)) {

        ev.setTargetAccessibilityFocus(false);

        continue;

     }

     //分发事件给子View

     newTouchTarget = getTouchTarget(child);

     if (newTouchTarget != null) {

        newTouchTarget.pointerIdBits |= idBitsToAssign;

        break;

     }

     resetCancelNextUpFlag(child);

     //调用子元素的dispatchTouchEvent

     if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {

        // Child wants to receive touch within its bounds.

        mLastTouchDownTime = ev.getDownTime();

        if (preorderedList != null) {

        // childIndex points into presorted list, find original index

           for (int j = 0; j < childrenCount; j++) {

                if (children[childIndex] == mChildren[j]) {

                    mLastTouchDownIndex = j;

                    break;

                }

           }

        } else {

            mLastTouchDownIndex = childIndex;

        }

        mLastTouchDownX = ev.getX();

        mLastTouchDownY = ev.getY();

        newTouchTarget = addTouchTarget(child, idBitsToAssign);

        alreadyDispatchedToNewTouchTarget = true;

        break;

     }

     ev.setTargetAccessibilityFocus(false);

}

可以看到大概流程如下,循环遍历子View,判断子元素能否接收到点击事件。如果子元素满足条件,则事件传递给子View处理。dispatchTransformedTouchEvent方法里调用了子View的dispatchTouchEvent方法。

如果子View的dispatchTouchEvent返回true,那么终止子元素的遍历,如果返回false,则继续分发给下个子元素。如果遍历所有的子元素后事件都没处理,那么ViewGroup就自己处理事件。

四、View 的事件分发

除了ViewGroup之外的View进行事件分发时,因为View不包含子View,所以它只能自己处理事件。

下面是它的dispatchTouchEvent方法内的部分源码。

public boolean dispatchTouchEvent(MotionEvent event) {

        ...

        boolean result = false;

        if (onFilterTouchEventForSecurity(event)) {

            //noinspection SimplifiableIfStatement

            ListenerInfo li = mListenerInfo;

            if (li != null && li.mOnTouchListener != null

                    && (mViewFlags & ENABLED_MASK) == ENABLED

                    && li.mOnTouchListener.onTouch(this, event)) {

                result = true;

            }

            if (!result && onTouchEvent(event)) {

                result = true;

            }

        }

        ...

        return result;

    }

View对点击事件的处理,首先会判断有没有设置OnTouchListener,如果OnTouchListener的优先级高于onTouchEvent。

onTouchEvent中,即使View处于不可用状态,照样会消耗点击事件。下面代码可以看出来。

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.

            return (((viewFlags & CLICKABLE) == CLICKABLE

                    || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)

                    || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE);

        }

A disabled view that is clickable still consumes the touch events, it just doesn't respond to them,一个不可用的View仍然可以消耗事件,只是不做任何响应。

onTouchEvent中对点击事件的具体处理流程大概如下,只要View的CLICKABLE和LONG_CLICKABLE有一个为true,那么它就会消耗事件,返回true。

总的来说,View的可不可用不影响是否消耗事件,只要clickable或者longClickable有一个为true,那么它就会消耗事件。

五、总结

事件分发机制的初步学习就到这里,了解了这些原理,对于遇到的常见的滑动冲突问题应该都能解决了,之后还要更进一步的了解事件分发,做到了然于胸。

the end

评论(0)