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

事件分发机制,是Android提供的一套完善的对触摸事件进行处理的机制,熟悉整个事件分发流程很有必要,因为它也是Android中常见的滑动冲突问题解决的理论基础。这几天阅读了《Android开发艺术探索》等书籍,总结如下。

一、引入
二、事件分发机制
   1.概述
   2.详细
三、源码解析
   1.ViewGroup事件分发
   2.View事件分发
四、滑动冲突解决
五、总结

一、引入

在介绍Android事件分发机制之前,我们先看生活中的一个例子。公司里有三个角色,老板,项目经理,程序员。有一天老板接到一个任务,他将任务分配给项目经理完成,项目经理又把任务分给程序员。程序员完成任务后,告诉项目经理任务完成了,项目经理再向老板报告任务完成了。从老板接到任务,到老板最终去交付任务,这是个完整的过程。

在这个过程中,可能会有其它情况。假如在一开始老板接到任务时,决定自己完成,不需要把任务往下分配,那么老板就自己做,项目经理和程序员就没事。同样,如果项目经理决定自己去做,那么就没有程序员的事。上面的这个例子其实就是任务在老板、项目经理和程序员这三个角色间的传递过程,Android中屏幕上的触摸事件就相当于这个任务,事件分发就类似于这个传递过程。

二、事件分发机制

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

Android学习笔记12 事件分发机制完全解析_第1张图片

概述

在事件分发机制中,涉及到的几个关键部分分别是:TouchEvent(触摸事件)、ViewGroup(视图组合)、View(视图)。下面先对这几个部分做个介绍。

  • TouchEvent(触摸事件)

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

  • View

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

详解

上面介绍了一些事件分发的基本概念,下面对分发流程有个总体的把握。Android中事件分发机制主要涉及到三个重要方法,如下:

  • dispatchTouchEvent ( MotionEvent event ) 事件分发
  • 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和View两种情况下的分发过程,下面我们结合源码来分析,因为View的过程相对来说较为简单,我们先看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。第二种情况是指,ViewGroup是否不拦截事件并把事件交由子View处理,如果是,那么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处理。

                        
//子View
final 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就自己处理事件。


**综上,触摸事件传递到ViewGroup时,会执行方法dispatchTouchEvent()进行事件分发,如果事件是Down类型(或者同一事件序列没被拦截已经交由子元素处理),那么就调用方法onInterceptTouchEvent进行拦截判断,默认情况下不会拦截事件。ViewGroup不拦截的话,那么就会遍历它的子View,判断能否接收到事件,如果接收到那么就调用子View的dispatchTouchEvent方法继续进行分发。如果遍历子View后都没处理事件,那么ViewGroup自己处理事件。
**


View事件分发

View的事件分发比ViewGroup简单,因为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,那么它就会消耗事件。


**综上,触摸事件传递到View时,会执行方法dispatchTouchEvent()进行事件分发,这里会判断有没有设置OnTouchListener,如果OnTouchListener的onTouch方法返回true,那么onTouchEvent就不会被调用。View的onTouchEvent默认都会消耗事件,除非它是不可点击的(clickable和longClickable同时为false),而View的enable属性并不影响onTouchEvent的返回值。
**


四、滑动冲突解决

上面主要主要介绍了View的事件分发机制的整个过程,在平常的开发中,在熟悉整个分发过程后,滑动冲突问题应该就不再是难题了。下面主要以一个典型的例子,介绍下滑动冲突问题的解决。

滑动冲突的产生主要是因为界面中内外两层都可以滑动,比如一个界面外部可以左右滑动,内部可以上下滑动。这时就可以采取外部拦截法,前面我们提到分发过程中方法onInterceptTouchEvent主要是用于判断是否拦截,那么外部拦截中我们可以重写父容器的onInterceptTouchEvent方法,根据需要决定是否拦截。

public boolean onInterceptHoverEvent(MotionEvent event) {

        boolean intercepted = false;

        int x = (int) event.getX();
        int y = (int) event.getY();

        switch (event.getAction()) {

            case MotionEvent.ACTION_DOWN:
                break;

            case MotionEvent.ACTION_MOVE:
                if(父容器需要当前点击事件){
                    intercepted = true;
                }else {
                    intercepted = false;
                }
                break;

            case MotionEvent.ACTION_UP:
                break;

            default:
                break;
        }
        
        mLastXIntercept = x;
        mLastYIntercept = y;
        
        return intercepted;
    }

五、总结

到这里关于Android中View的事件分发机制就介绍的差不多了,欢迎指正批评。

你可能感兴趣的:(Android学习笔记12 事件分发机制完全解析)