Android触摸事件分发机制

一 概述

触摸事件的分发机制是安卓开发中的基础知识,但这块知识又有点绕,总是让人觉得似懂非懂。其实安卓事件传递就是把用户触摸屏幕时的touch事件封装成MotionEvent对象在Activity、ViewGroup和View中传递并处理该touch事件的过程。

二 触摸事件分发的方法

现在我们知道触摸事件是在Activity、ViewGroup和View中进行传递的,对应的方法如下:

  1. Activity
    Activity不对触摸事件进行拦截,收到触摸事件后直接分发给ViewGroup,如果所有的view最后都没有处理该触摸事件,会调用Activity的onTouchEvent方法进行处理,因此Activity处理触摸事件的方法为:
    dispatchTouchEvent
    onTouchEvent
  2. ViewGroup
    当ViewGroup收到触摸事件后,它可以分发给自己的子View但在分发之前可以判断是否需要拦截该触摸事件,也可以调用自己的onTouchEvent方法处理触摸事件,因此ViewGroup处理触摸事件的方法有三个:
    dispatchTouchEvent
    onInterceptTouchEvent
    onTouchEvent
  3. View
    View和Activity一样可以接收和处理触摸事件但不能拦截触摸事件,毕竟View下面也没有子View存在了,拦截没有意义。故View中的方法为:
    dispatchTouchEvent
    onTouchEvent

三 触摸事件的传递机制

  1. 在Activity中
    当用户点击屏幕时Activity最先收到触摸事件此时会调用Activity的dispatchTouchEvent方法,源码如下:
public boolean dispatchTouchEvent(MotionEvent ev) {
    if (ev.getAction() == MotionEvent.ACTION_DOWN) {
        onUserInteraction();
    }
    if (getWindow().superDispatchTouchEvent(ev)) {
        return true;
    }
    return onTouchEvent(ev);
}

可以看到Activity调用getWindow().superDispatchTouchEvent(ev)方法继续把触摸事件传递给包含的View,如果有View处理了该事件则返回true,事件传递结束;如果没有View处理该事件则调用Activity的onTouchEvent(ev)方法处理该事件,无论在Activity的onTouchEvent(ev)方法中是否消费该事件,该事件的传递都结束了。

  1. 在ViewGroup中
    当在Activity中调用getWindow().superDispatchTouchEvent(ev)方法时,Touch事件会被传递给Activity包含的最外层ViewGroup,然后层层向下传递。我们分析ViewGroup是如何处理Touch事件的。
    当Touch事件传递到ViewGroup会先调用ViewGroup的dispatchTouchEvent方法:
 /**
  * 源码分析:ViewGroup.dispatchTouchEvent()
  */ 
    public boolean dispatchTouchEvent(MotionEvent ev) { 

    ... // 仅贴出关键代码

    // ViewGroup每次事件分发时,都需调用onInterceptTouchEvent()询问是否拦截事件
    if (disallowIntercept || !onInterceptTouchEvent(ev)) {  

    // 判断值1:disallowIntercept = 是否禁用事件拦截的功能(默认是false),可通过调用requestDisallowInterceptTouchEvent()修改
    // 判断值2: !onInterceptTouchEvent(ev) = 对onInterceptTouchEvent()返回值取反
            // a. 若在onInterceptTouchEvent()中返回false(即不拦截事件),就会让第二个值为true,从而进入到条件判断的内部
            // b. 若在onInterceptTouchEvent()中返回true(即拦截事件),就会让第二个值为false,从而跳出了这个条件判断

        ev.setAction(MotionEvent.ACTION_DOWN);  
        final int scrolledXInt = (int) scrolledXFloat;  
        final int scrolledYInt = (int) scrolledYFloat;  
        final View[] children = mChildren;  
        final int count = mChildrenCount;  

    // 通过for循环,遍历了当前ViewGroup下的所有子View
    for (int i = count - 1; i >= 0; i--) {  
        final View child = children[i];  
        if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE  
                || child.getAnimation() != null) {  
            child.getHitRect(frame);  

            // 判断当前遍历的View是不是正在点击的View,从而找到当前被点击的View
            // 若是,则进入条件判断内部
            if (frame.contains(scrolledXInt, scrolledYInt)) {  
                final float xc = scrolledXFloat - child.mLeft;  
                final float yc = scrolledYFloat - child.mTop;  
                ev.setLocation(xc, yc);  
                child.mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;  

                // 条件判断的内部调用了该View的dispatchTouchEvent()
                // 即 实现了点击事件从ViewGroup到子View的传递(具体请看下面的View事件分发机制)
                if (child.dispatchTouchEvent(ev))  { 

                mMotionTarget = child;  
                return true; 
                // 调用子View的dispatchTouchEvent后是有返回值的
                // 若该控件可点击,那么点击时,dispatchTouchEvent的返回值必定是true,因此会导致条件判断成立
                // 于是给ViewGroup的dispatchTouchEvent()直接返回了true,即直接跳出
                // 即把ViewGroup的点击事件拦截掉

                        }  
                    }  
                }  
            }  
        }  
    }  
}

/**
* 作用:是否拦截事件
* 说明:
*     a. 返回true = 拦截,即事件停止往下传递(需手动设置,即复写onInterceptTouchEvent(),从而让其返回true)
*     b. 返回false = 不拦截(默认)
*/
public boolean onInterceptTouchEvent(MotionEvent ev) {  
    return false;
} 

从代码中可以看出在ViewGroup的dispatchTouchEvent方法中先判断该ViewGroup是否拦截该Touch事件,如果拦截了,Touch事件就不会往下传递而是直接调用ViewGroup的onTouchEvent方法处理事件;如果没有拦截则遍历所有子View找到正在点击的那个View并把Touch事件传递给它。
3.在View中
View收到Touch事件后会先调用View的dispatchTouchEvent方法,在dispatchTouchEvent方法中调用该View的onTouchEvent方法去处理该Touch事件,如果该View的onTouchEvent方法返回true则表示该View消费了该事件,否则会继续调用其父View的onTouchEvent方法去处理该事件,直到某个View的onTouchEvent方法消费了该事件,或者传递到Activity的onTouchEvent方法,则事件传递结束。
在View的onTouchEvent方法中如果接收并消费了ACTION_DOWN事件,则该View会接收到后续的ACTION_MOVE、ACTION_UP等事件;反之,如果该View没有消费ACTION_DOWN事件则后续的事件不会再传递给该View。

四 注意事项

  1. ViewGroup的onInterceptTouchEvent方法默认返回false,ViewGroup进行事件分发都会调用该方法,但是一旦onInterceptTouchEvent方法返回true则表示该ViewGroup拦截了触摸事件,后续进行事件分发不再调用onInterceptTouchEvent方法。
    举个栗子:我们在onInterceptTouchEvent方法中判断是ACTION_MOVE事件就返回true,在该ViewGroup的子View可以收到ACTION_DOWN事件,如该子View消费了ACTION_DOWN事件,则在第一个ACTION_MOVE事件到来时,ViewGroup会拦截该事件,但是并不会调用ViewGroup的onTouchEvent方法,同时把ACTION_CANCEL事件传递给子View。后续的事件都不会传递给子View了,而是直接调用ViewGroup的onTouchEvent方法去处理。
  2. View的onTouch方法会先于onTouchEvent方法执行,如下所示(onClick在onTouchEvent方法中执行):
button1.setOnTouchListener(new View.OnTouchListener() {
    @Override
    public boolean onTouch(View v, MotionEvent event) {
        Log.e("onTouch","touch:button1");
        //返回true不执行onClick方法
        //返回false接着执行onClick方法
        return false;
    }
});


button1.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        Log.e("button1","点击了:button1");
    }
});
  1. View的onClick方法是在ACTION_UP事件之后执行的,并不是在ACTION_DOWN事件到来时就执行。因此如果在父View中拦截了ACTION_MOVE或者ACTION_UP事件,是不会执行该方法的。
  2. 可以调用getParent().requestDisallowInterceptTouchEvent(true)方法请求父View不要拦截Touch事件。注意这个方法不能在子View初始化时调用(无效),最好在子View接收到Touch事件也就是在子View的dispatchTouchEvent方法中调用。调用完该方法后,父View以及父View的父View就不会再调用onInterceptTouchEvent方法去判断是否拦截了。

好了,触摸事件的传递机制就讲到这里啦,有不对的地方欢迎留言指正。

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

推荐阅读更多精彩内容