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

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

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

一、引入

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

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

二、事件分发机制

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

概述

在事件分发机制中,涉及到的几个关键部分分别是: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的事件分发机制就介绍的差不多了,欢迎指正批评。

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

推荐阅读更多精彩内容