Android事件分发机制——几行伪代码就够了

1. 概述

事件分发机制在开发或面试中常常被提及,而其又是自定义view点击事件的处理、滑动冲突等问题的理论基础。如果想写出酷炫的自定义View,理解该机制是必不可少的功课。

但是发现往往在开发过程中,一动手写事件逻辑,常常出现一些无法理解的错误,如果还停留在“onTouchEvent 返回true拦截事件,返回false不拦截事件”表层理论,远远无法满足开发需求的。不得不翻出曾经收藏的博文或笔记,对于一个开发人员来说,太浪费时间了。 这个问题原因是,对分发机制不甚了解和对其中细节不予关注所导致的, 那么我们来梳理一下事件分发机制的结构和一些不能被忽视的细节。

本文不会涉及到源码分析,但会从源码中剥离出重要的逻辑。

2. 分发机制

事件分发机制优秀博文非常多,推荐如下几篇:

一文读懂Android View事件分发机制:https://www.jianshu.com/p/238d1b753e64
Android事件分发机制完全解析,带你从源码的角度彻底理解(郭霖大神):https://blog.csdn.net/guolin_blog/article/details/9097463/

首先从整体结构上去了解整个事件分发机制,然后深究事件分发机制中一些细节。

U形图

讲述该机制时,常常提及流程走向的“U形图”:
image
上图ACTION_DOWN事件流转过程,注意标红的地方,因为View默认情况下,onTouchEvent() 返回false,但部分系统View会返回false,比如ImageView,具体原因请关注下面的终极伪代码逻辑。

一个完整的事件流程包括按下(ACTION_DOWN),移动(ACTION_MOVE), 抬起(ACTION_UP)三个事件。理解事件分发机制,必须要将ACTION_DOWN事件区别于其他事件来分析,其他事件(ACTION_MOVE 和 ACTION_UP)能否继续流转往往取决处理ACTION_DOWN事件的返回结果,我们直接来总结一下:

  • ACTION_DOWN:

    • dispatchTouchEvent/onTouchEvent:
      • Return false: 后续无法接收到后续其他事件(MOVE & UP),回传给父控件处理,至于父控件是否处理可以套用同样的逻辑;
      • Return true:事件被消费,不会往下传递,包括后续其他事件;
      • Super method: 事件继续往下传递;
    • onInterceptTouchEvent(ViewGroup独有):
      • return false: 不拦截事件,事件往下传递;
      • return true: 拦截事件,交给自己onTouchEvent处理,后续其他事件能否传递到该ViewGroup,取决于自身onTouchEvent返回值(默认为false),可套用上面的逻辑,一旦返回true,后续将不会再被调用;
  • ACTION_MOVE/ACTION_UP:

    • 自身必须在处理ACTION_DOWN事件时Return true,否则无法接收到后续的ACTION_MOVE/ACTION_UP 事件;
    • dispatchTouchEvent/onTouchEvent:
      • Return false/true/Super method: 无论返回什么值,都能收到后续事件;
    • onInterceptTouchEvent(ViewGroup独有):
      • return false: 不拦截事件,事件往下传递;
      • return true:会将当前事件置为ACTION_CANCEL,后续事件将不会往下传递,进行拦截;

终极伪代码

以上的结论依旧很难记忆和理解,结合源码我们可以把上面的逻辑写成伪代码:

ViewGroup:

// mFirstTouchTarget 可以理解为存储可以处理Touch事件的子View(不包括自身)的数据结构
private TouchTarget mFirstTouchTarget;

public boolean dispatchTouchEvent(MotionEvent ev) {
    // 是否中断
    final boolean intercepted;
    // 仅在ACTION_DOWN 和 已确定处理的子View时 调用,一旦onInterceptTouchEvent返回true,
    //则后续将不会在被调用和接收事件。后面会讲返回true后,mFirstTouchTarget会被为null;
    if (action == MotionEvent.ACTION_DOWN
                    || mFirstTouchTarget != null) {
        intercepted = onInterceptTouchEvent(ev);
    }
    
    // 如果不被拦截,在ACTION_DOWN事件处理中遍历所有的子View,找寻可以处理Touch事件的目标子View
    // 然后封装到mFirstTouchTarget,如果子View的dispatchTouchEvent返回true,则认为是目标子View;
    if(!intercepted){
        if (action == MotionEvent.ACTION_DOWN) {
            if(child.dispatchTouchEvent(MotionEvent ev)){
                mFirstTouchTarget = addTouchTarget(child);
                break;
            }
        }
    }
    boolean handled;
    // 如果mFirstTouchTarget == null,调用自身onTouchEvent()
    if(mFirstTouchTarget == null){
        handled=onTouchEvent(ev);
    }else{
        // 应上面的逻辑,如果ACTION_MOVE传递过程中被拦截,则将mFirstTouchTarget置为null,并传递一个cancel事件,
        // 告诉目标子View当前动作被取消了,后续事件将不会再次被传递;
        if (intercepted){
            ev.action=MotionEvent.ACTION_CANCEL;
            handled=mFirstTouchTarget.child.dispatchTouchEvent(ev);
            mFirstTouchTarget=null;
        }else {
            // 调用目标子view的dispatchTouchEvent,这也是为什么,上面结论所述的,dispatchTouchEvent/onTouchEvent 
            // 在ACTION_DOWN事件返回true,不管子View返回什么值,都能收到后续事件,会出现所谓控制“失效”的现象。
            handled=mFirstTouchTarget.child.dispatchTouchEvent(ev);
        }
    }
    retrun handled;
}

以上伪代码囊括了整个事件机制的逻辑(包括所有事件的处理,都可以套用上面的逻辑进行分析)。再来归纳几个关键点:

  • 每个ViewGroup对象存储一个处理Touch事件的目标子View的封装:mFirstTouchTarget
  • mFirstTouchTarget 仅仅在ACTION_DOWN事件中被赋值,可以这样理解为ACTION_DOWN目的为了找寻目标子View(mFirstTouchTarget),即也是为什么要将ACTION_DOWN和其他事件区分来理解
  • onInterceptTouchEvent一旦返回true,后续将不再被调用,因为mFirstTouchTarget被置为null;

View:

public boolean dispatchTouchEvent(MotionEvent event) {
    boolean result;
    if(mOnTouchListener !=null 
          && ENABLE 
          && mOnTouchListener.onTouch(this, event)){
        result = true;
    }
    if (!result && onTouchEvent(event)) {
        result = true;
    }
    
    return result;
}

public boolean onTouchEvent(MotionEvent event) {
    final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
                || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
                || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;
    
    if(DISABLED){
        return clickable;
    }
                
    if(clickable){
        switch (action) {
            case MotionEvent.ACTION_UP:
                onClick(this);//onLongClick(this)
                break;
        }
    }            
}

通过View 的 dispatchTouchEvent和onTouchEvent 伪代码可以发现非常多有意思的事情,在下一节重点聊一聊。
请注意:伪代码是基于源码抽离出来的逻辑骨架而写成,为了方便阅读往往忽略部分细节,源码中并非如此,详细逻辑可以参考上面郭霖大神的源码解读或自行查看源码。

3. 不能忽视的细节

onTouch(),onTouchEvent()与onClick()

一般情况下,三者的调用顺序为:onTouch()>onTouchEvent()>onClick()。通过上面的伪代码发现,有几种特殊情况:

  • 如果View DISABLE(即setEnabled(false)), onTouch()与onClick() 将不会被调用,但是onTouchEvent()会调用,即如下这种情况
    mBt.setOnTouchListener(new View.OnTouchListener() {
            @Override
            public boolean onTouch(View v, MotionEvent event) {
                return false;
            }
        });
    mBt.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                
            }
        });    
    mBt.setEnabled(false);
  • 如果View ENABLE,onTouch()返回true,onTouchEvent()和onClick()都将不会被调用;
    mBt.setOnTouchListener(new View.OnTouchListener() {
            @Override
            public boolean onTouch(View v, MotionEvent event) {
                return true;
            }
        });
  • 如果View ENABLE, 但NOT CLICKABLE, onTouch()和onTouchEvent() 都可以执行,onClick() 将不会被执行。而这里所说的NOT CLICKABLE, 要CLIICABLE,LONG_CLICKABLE与 CONTEXT_CLICKABLE 同时为false,否则onClick() 依旧会被回调,比如如下情况:
    mBt.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Log.i(TAG, "onClick");
            }
        });
    mBt.setOnLongClickListener(new View.OnLongClickListener() {
            @Override
            public boolean onLongClick(View v) {
                Log.i(TAG, "onLongClick");
                return false;
            }
        });
    mBt.setClickable(false);

4. 小结

跟着源码,以及结合众多优秀的博文(郭霖大神),尝试把逻辑架构抽取出来写成上述的伪代码,对理解事件分发机制有着莫大的好处。以及其中细节也加多加留意。如有错误请多多指正,不胜感激。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值