Android触摸事件分发机制

用户在操作的时候,不可避免地就会触发触摸事件。Android把触摸过程分成很多个动作(Action),而开发中最常见也最主要考虑的触摸过程就是:从ACTION_DOWN(触摸落下)开始、到ACTION_UP(触摸弹起)/ACTION_CANCEL(触摸取消,譬如在按下控件,将控件移动到外层控件的时候,就会触发,而不是UP)结束,在这之间的是ACTION_MOVE(触摸移动,非必然存在)。

同时,我们在界面上触发触摸事件的时候,同样不可避免地会涉及到这三部分:Activity(可视界面当然还有Fragment,Dialog这些,但它们都依附着Activity),ViewGroup(通常来说做布局容器的LinearLayout、RelativeLayout等都算是),View(Button、ImageView等等)。一个简单的情况就是我们在点击界面(Activity)上在布局(ViewGroup)中的Button(View)的时候,这必然触发了触摸事件,但这具体是怎样的一个过程?我们可以在这些过程中做些什么?这就涉及到了Android触摸事件分发机制,先看一张简略的分发流程图(图片来源于从Android源码的角度理解应用开发(1)-Touch机制,感谢,侵删):

简单的Touch事件传递过程

从流程图可以得知,Touch事件的分发情况是这样的,Activity将事件分发到ViewGroup中,而ViewGroup层层分发直到找到需要处理Touch事件的子元素(可能是View也可能是ViewGroup),将事件传递下去。这里也可以提前告知一个逻辑,就算传递到了,但如果子元素不能处理触摸事件,会将事件交回上一级处理,最后可以到Activity去处理,总之触摸事件最终会被消费掉。
先让我们来了解一下Touch事件分发和处理的三个重要方法。

1.public boolean dispatchTouchEvent(MotionEvent ev)

MotionEvent-手势事件,它里面就包含了上面说的Action。这个方法从字面上的意思都很好理解,调度触摸事件,这个方法是Activity、ViewGroup、View都有的,Touch事件都从它开始,也就是说Touch事件的分发和处理过程中,dispatchTouchEvent()是第一个被调用的方法。

2.public boolean onInterceptTouchEvent(MotionEvent ev)

也是从字面意思上就很好理解了,拦截触摸事件。如果返回值为true,就拦截当前事件,不分发给子元素。很明显这个方法View是没有的,可以说三者中只有ViewGroup才有,因为Activity肯定要把事件分发下去(后面有说到),而View下面是没有子元素的,要么处理触摸事件要么交回ViewGroup处理。

3.public boolean onTouchEvent(MotionEvent ev)

这个方法表示对事件进行处理,在dispatchTouchEvent方法内部调用,如果返回true表示消耗当前事件,如果返回false表示不消耗当前事件。
为了更好地理解整个流程,我们从View——>ViewGroup——>Activity的顺序展开,让我们先来看看是怎么处理Touch事件的,再看具体是怎么分发的。就以上面说到的按钮点击的情况来说一说。

View

上面的按钮点击中,View就是Button,刚刚说到整个过程最先被调用的就是dispatchTouchEvent(),Button的dispatchTouchEvent()是调用View的,我们来看一下这个方法:

View的dispatchTouchEvent()实现

可以看到,if语句的判断条件有三个部分,只要有一部分条件是false,那么就会执行onTouchEvent()。

1.判断是否有注册触摸监听事件;

2.判断控件是否可用(ENABLE),一般控件是默认可用的,除非通过setEnable(false)禁用控件,不然这部分条件一般为true;

3.监听事件里面的onTouch()的返回值。

所以很简单的,控件监听触摸事件让onTouch()返回true,就不会再执行下去了,当然我们是要看下去的,看一下onTouchEvent()里面实现了什么?

public boolean onTouchEvent(MotionEvent event) {
        final int viewFlags = mViewFlags;
        if ((viewFlags & ENABLED_MASK) == DISABLED) {
            // 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));
        }
        if (mTouchDelegate != null) {
            if (mTouchDelegate.onTouchEvent(event)) {
                return true;
            }
        }
        if (((viewFlags & CLICKABLE) == CLICKABLE ||
                (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) {
            switch (event.getAction()) {
                case MotionEvent.ACTION_UP:
                    ...
                                if (mPerformClick == null) {
                                    mPerformClick = new PerformClick();
                                }
                                if (!post(mPerformClick)) {
                                    performClick();
                                }
                            ...
                    break;
                case MotionEvent.ACTION_DOWN:
                    ...
                    mHasPerformedLongPress = false;
                    postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
                    break;
                case MotionEvent.ACTION_CANCEL:
                    ...
                    break;
                case MotionEvent.ACTION_MOVE:
                    ...
                    break;
            }
            return true;
        }
        return false;
    }

这里我们可以简单分析一下,这里面主要有三块if语句:

1.如果控件是禁用(DISABLED)的,控件可以点击(CLICKABLE)或长按点击(LONG_CLICKABLE),返回true,消费事件但不做任何操作,如果控件不可点击,onTouchEvent()返回false;

2.如果存在触摸委托对象(TouchDelegate),交由其onTouchEvent()处理;

3.首先可以看到,只要控件是不可点击的,不满足if语句条件,直接返回false;反之,再处理不同的ACTION,都会返回true。

有意思的是,继续深入查看源码可以发现,控件的onLongClick事件是在ACTION_DOWN里面触发的(postDelayed())、onClick事件是在ACTION_UP里面触发的(performClick()),这里不展开说。

上面说了那么多,好像和分发没什么关系啊?别急,我们可以知道View的Touch事件其实都是为了知道View的dispatchTouchEvent()的返回值是什么,而这个值与分发事件大有关联,请看ViewGroup。

ViewGroup

从上面简略的流程图可以看出,触摸事件由ViewGroup传递给View,很显然,这就是一个事件分发过程,那么,这个过程是怎么做的呢?
ViewGroup首先调用dispatchTouchEvent(),让我们来看一看具体的实现:

public boolean dispatchTouchEvent(MotionEvent ev) {  
    ...
    boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;  
    if (action == MotionEvent.ACTION_DOWN) {  
        if (mMotionTarget != null) {  
            mMotionTarget = null;  
        }  
        if (disallowIntercept || !onInterceptTouchEvent(ev)) {  
           ...
            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);  
                    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;  
                        if (child.dispatchTouchEvent(ev))  {  
                            mMotionTarget = child;  
                            return true;  
                        }  
                    }  
                }  
            }  
        }  
    }  
    ...
    if (target == null) {  
        ...
        return super.dispatchTouchEvent(ev);  
    }  
    if (!disallowIntercept && onInterceptTouchEvent(ev)) {  
        ... 
        return true;  
    }  
    if (isUpOrCancel) {  
        mMotionTarget = null;  
    }  
    ...
    return target.dispatchTouchEvent(ev);  
}  

我们可以看到,首先对ACTION_DOWN做了一大堆处理,来看一下具体在干嘛。

首先清除手势目标;
第二个是重点:
disallowIntercept-禁止拦截,这个变量是一个Boolean值,默认值是false,意味着一般情况下是不禁止拦截功能的;
onIntercepTouchEvent()-这个方法在上面也提到过,拦截事件分发下去,将触摸事件交给ViewGroup自己处理。

很好,那么

if (disallowIntercept || !onInterceptTouchEvent(ev))

这个条件语句的意思是只要不拦截,就进入条件判断内部,它的逻辑运算符是||,只要disallIntercept为true或者!onInterceptTouchEvent(ev)为true,也就是onInterceptTouchEvent(ev)为false就好了。

第一个条件,disallIntercept默认为false,但我们可以通过requestDisallowInterceptTouchEvent(Boolean)使得它的值为true;
第二个条件,看一下onInterceptTouchEvent(ev)的源码:

public boolean onInterceptTouchEvent(MotionEvent ev) {
        return false;
    }

看!它的返回值是默认false的!也就是!onInterceptTouchEvent(ev)为true,那么也就是默认情况下,并不拦截事件,进入条件判断内部。

这一大块代码具体做了什么?
1.获得ViewGroup下的子View
2.for循环判断,手指落下的范围在哪个View之中,接着!

if (child.dispatchTouchEvent(ev))  {  
      mMotionTarget = child;  
      return true;  
}  

上面说了那么多View里面获取dispatchTouchEvent()的返回值,在这里用上了,确定分发目标为dispatchTouchEvent()返回值为true的子View!同时,ViewGroup的dispatchTouchEvent()的返回值也是true,那么ACTION_DOWN就结束了,再次进来就是其它ACTION的执行逻辑了。

所以如果,View的dispatchTouchEvent()返回值为false,target为空,那么可以看到往下的逻辑里面,将自身作为一个View设为target:

 if (target == null) {  
        ···
        return super.dispatchTouchEvent(ev);  
    }  

也就是View的onTouch()返回值为true,View禁用,View的onTouchEvent()返回false等种种情况都会导致把事件重新交回给ViewGroup,然后ViewGroup执行super.dispatchTouchEvent(ev),ViewGroup的超类是View,又回到了前面说的View的内容了。

总结(来自于从Android源码的角度理解应用开发(1)-Touch机制,很精准简练的总结,感谢,侵删):
1.在Down并且不拦截的时候会多出一个寻找Target的过程,在这个过程中遍历子View,如果子View的dispatchTouch为true,则这个子View就是当前ViewGroup的Target。找Target是处理Down事件时候特有的,其他事件不会触发找Target;

2.如果没有Target,则发送把自己当做一个View去处理这个事件(super.dispatchTouch());

3.如果有Target并且拦截,则发送Cancel给子View ;

4.如果有Target并且不拦截,则调用Target的dispatchTouch;

5.可以利用requestDisallowInterceptTouchEvent(boolean)来强制viewparent不拦截事件。但是作用域限于一个Touch的过程(Down->Up/Cancel)。

接着就剩下最后一块了,我们来看一下Activity又是怎么样把事件分发给ViewGroup的。

Activity

还是先看dispatchTouchEvent():

public boolean dispatchTouchEvent(MotionEvent ev) {
        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
            onUserInteraction();
        }
        if (getWindow().superDispatchTouchEvent(ev)) {
            return true;
        }
        return onTouchEvent(ev);
    }

代码很简单,但是其实经过一系列的步骤之后,传递过程是这样的:
Activity->Window->DecorView->ViewGroup,同理,如果最后Touch事件没有被消费,也会交回由Activity的onTouchEvent()里面去处理。

补充: ACTION的传递过程:
对于在onTouchEvent消费事件的情况:
在哪个View的onTouchEvent 返回true,那么ACTION_MOVE和ACTION_UP的事件从上往下传到这个View后就不再往下传递了,而直接传给自己的onTouchEvent 并结束本次事件传递过程。

对于ACTION_MOVE、ACTION_UP总结:
ACTION_DOWN事件在哪个控件消费了(return true), 那么ACTION_MOVE和ACTION_UP就会从上往下(通过dispatchTouchEvent)做事件分发往下传,就只会传到这个控件,不会继续往下传,如果ACTION_DOWN事件是在dispatchTouchEvent消费,那么事件到此为止停止传递,如果ACTION_DOWN事件是在onTouchEvent消费的,那么会把ACTION_MOVE或ACTION_UP事件传给该控件的onTouchEvent处理并结束传递。

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

推荐阅读更多精彩内容