天天看点

Android事件分发全面解析(基础篇)-夯实基础

这是一个老生常谈的话题了,也是一个初级开发者必须掌握的技能,但有多少人真正明白呢,夯实基础,方能长远。

本文不过多涉及基础性问题,建议有点了解再来阅读,或者直接硬干。
Android事件分发全面解析(基础篇)-夯实基础

首先,事件分发对象是谁?

事件。

当用户触摸屏幕时( View 或 ViewGroup 派生的控件),将产生点击事件(Touch事件)

Touch事件相关细节,比如触摸位置,时间,手势等等,会被封装成 MotionEvent 对象。

Touch 事件主要有以下几种:

事件 简介
ACTION_DOWN 手指 初次接触到屏幕 时触发。
ACTION_MOVE 手指 在屏幕上滑动 时触发,会会多次触发。
ACTION_UP 手指 离开屏幕 时触发。
ACTION_CANCEL 事件 被上层拦截 时触发。

事件列:从手指接触屏幕至手指离开屏幕,这个过程产生一系列时间,任何时间都是以Down事件开始,UP事件结束,中间会有无数Move事件。

[外链图片转存失败(img-CBqGwtSj-1562061405691)(…/…/assets/944365-79b1e86793514e99.png)]

也就是说,当一个 MotionEvent 产生后,系统需要把这个事件传递给一个具体View去处理。

什么是事件分发?

要知道什么是事件分发,其实也就是对MotionEvent 事件的分发过程,也就是当一个 手指 按下之后,系统需要把这个事件传递给一个具体的 View,而这个传递的过程就是分发过程。

分发顺序也就是 Activity(Window)-> ViewGroup -> View.

三大方法: - >(入门)

Android事件分发全面解析(基础篇)-夯实基础

说到分发过程,就不得不说这三个方法了:

  1. dispatchTouchEvent,
  2. onInterceptTouchEvent
  3. onTouchEvent

先介绍一下这三个方法分别是干啥的:

dispatchTouchEvent

用来进行事件的分发,如果事件能够传递给当前View,那么此方法一定会被调用,返回结果受当前View 的 onTouchEvent 和 下级View的 dispatchTouchEvent 方法的影响,表示是否消耗当前事件。

onInterceptTouchEvent

在 dispatchTouchEvent内部调用,用来判断是否拦截某个事件,如果当前View拦截了某个事件,那么在同一个事件序列中,此方法不会被再次调用,返回结果表示是否拦截当前事件。

onTouchEvent

在 'dispatchTouchEvent '方法中调用,用来处理点击事件,返回结果表示是否消耗当前事件,如果不消耗,则在同一个事件序列中,当前View 无法再次接受到事件。

用一个图来解释

表示有该方法。

X

表示没有该方法。
类型 相关方法 ViewGroup View
事件分发 dispatchTouchEvent
事件拦截 onInterceptTouchEvent X
事件消费 onTouchEvent

看到这,不知道你们有没有这个疑问,为啥View会有 dispatchTouchEvent 方法?

一个View 可以注册很多监听器吧,例如单击,长按,触摸事件(onTouch),并且View 本身也有 onTouchEvent 方法,那么问题来了,这么多事件相关的方法应该由谁管理,所以View也会有这个 方法。

他们之间的关系我们可以通过以下的伪代码来看:

// 点击事件产生后,会直接调用dispatchTouchEvent()方法
public boolean dispatchTouchEvent(MotionEvent ev) {

    //代表是否消耗事件
    boolean consume = false;


    if (onInterceptTouchEvent(ev)) {
    //如果onInterceptTouchEvent()返回true则代表当前View拦截了点击事件
    //则该点击事件则会交给当前View进行处理
    //即调用onTouchEvent ()方法去处理点击事件
      consume = onTouchEvent (ev) ;

    } else {
      //如果onInterceptTouchEvent()返回false则代表当前View不拦截点击事件
      //则该点击事件则会继续传递给它的子元素
      //子元素的dispatchTouchEvent()就会被调用,重复上述过程
      //直到点击事件被最终处理为止
      consume = child.dispatchTouchEvent (ev) ;
    }

    return consume;
   }
           

通过上面的伪代码我们可以发现,对于一个根 ViewGroup 来说,点击事件产生后,首先会传递给它,这时他的 dispatchTouEvent 就会调用,如果这个 ViewGroup 的 onInterceptTouchEvent方法返回true,就表示它要拦截当前事件,接着事件就会交给这个 ViewGroup 处理,即它的 onTouchEvent 方法会被调用,如果这个 ViewGroup 的onInterceptTouchEvent 方法返回false 就表示它不拦截当前事件,这时当前事件就会继续传递给它的 子元素,接着子元素 dispatchTouEvent 方法就会被调用。如此反复知道事件最终被处理。

用一张搬运过来的事件分发流程图来说明一下:

Android事件分发全面解析(基础篇)-夯实基础

当一个View需要处理事件时,如果它设置了 OnTouchListener, 那么 OnTouchListener 中的 onTouch 方法会被回调。这时事件如何处理还要看 onTouch 的返回值,如果返回false,则当前View 的onTouchEvent 方法会被调用;如果返回 true,那么 onTouchEvent 方法将不会被调用。由此可见,给View设置 onTouchListener,其优先级比 onTouchEvent 还要高。在 onTouchEvent 方法中,如果当前设置有 onClickListener,那么它的 onClick 方法会被调用。可以看出,平时我们常用的 onClickListener,其优先度最低,即处于事件传递的尾端.

通过以下源码即可看出:

public boolean dispatchTouchEvent(MotionEvent event) {
        ......
        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;
}
           

关于事件传递机制,我们可以总结出以下结论,根据这些结论能更好的理解整个传递机制:(摘录自Android开发艺术探索)

  1. 同一个事件序列是指从手指接触屏幕的那一刻起,到手指离开屏幕的那一刻结束,在这个过程中所产生的的一系列时间,这个事件序列以 down 事件开始,中间含有数量不定的 move 事件,最终以 up 事件结束。
  2. 正常情况下,一个事件序列只能被一个 View 拦截且消耗。这一条原因可以参考(3),因为一旦一个元素拦截了某次事件,那么同一个事件序列内的所有事件都会直接交给它处理,因此同一个事件序列中的事件不能分别由两个 View同时处理,但是通过特殊手段可以做到,比如一个 View 将本该自己处理的事件 通过 onTouchEvent 强行传递给其他View处理。
  3. 某个View一旦决定拦截,那么这一个事件序列都只能由它来处理(如果事件序列能够传递给它的话),并且它的 onInterceptTouchEvent 不会再被调用。这条也很好理解,就是说当一个 View 决定拦截一个事件后,那么系统会把同一个事件序列内的其他方法都直接交给它来处理,因此就不再调用这个View的 onInterceptTouchEvent 去询问它是否要拦截了。
  4. 某个View 一旦开始处理事件,如果它不消耗 ACTION_DOWN 事件( onTOuchEvent 返回了 false ) ,那么同一事件序列中的其他事件都不会交给 它来处理,并且事件将重新交由它的父元素去处理,即父元素的 onTouchEvent 会被调用。意思就是事件一旦交给一个 View 处理,那么它就必须消耗掉,否则同一事件序列中剩下的事件就不再交给它来处理了,这就好比上级交给程序猿一件事,如果事情没有处理好,短期内上级就不敢再把事情交给程序猿去做了。二者道理差不多。
  5. 如果View 不消耗除 ACTION_DOWN 以外的其他事件,那么这个点击事件会消失,此时父元素的 onTouchEvent 并不会被调用,并且当前 View 可以持续收到后续的事件,最终这些消失的点击事件会传递给 Activity 处理。
  6. ViewGroup 默认不拦截任何事件。Android 源码中ViewGroup 的 onInterceptTouchEvent 方法默认返回false.
  7. View 没有 onInterceptTouchEvent 方法,一旦有点击事件传递给它,那么它的 onTouchEvent 方法就会被调用。
  8. View 的 onTouchEvent 默认都会消耗事件(返回true),除非它是不可点击的(clickable和longClickable同时为false).View 的 longClickable 属性默认为 false,clickable 属性要分情况,比如Button 的clickable 属性默认为true,而 TextView 的 clickable 属性默认为 false.
  9. View 的 enable 属性不受影响 onTouchEvent 的 默认返回值,哪怕一个 View 是 disable 状态的,只要它的 clickable 或者 longClickable 有一个 为 true,那么它的 onTouchEvent 就返回 true.
  10. onClick 会发生的前提是当前View 是可点击的,并且它收到了 down 和 up 的事件。
  11. 事件传递过程是由外而向的,即事件总是先传递给父元素,然后再由父元素分发给子View,通过requestDisallowInterceptTouchEvent 方法可以在子元素中干预父元素的事件分发过程,但是 ACTION_DOWN 事件除外。

实例: ->(实践)

Android事件分发全面解析(基础篇)-夯实基础

结合上面的结论,我们来用实例来演示一下

首先用这样一个图来看

Android事件分发全面解析(基础篇)-夯实基础

这是一个简单的布局,Activity里面一个LinearLayout,用来代替ViewGroup,内部是一个Button,没什么说的。

我们要从如下几个方面入手,探究上面那些结论。

  1. 默认情况下的事件分发
  2. 拦截时的分发情况
  3. 如果viewagroup 不拦截 Down事件,即还是 Button 来处理,但它拦截接下来的move事件(也就是半路拦截),那么接下来情况又会是咋样?
  4. 如果全部都不消费事件,事件最终由谁来安排。
  5. onTouch中返回 true或者false,对onTouchEvent有什么影响吗。

上代码啦:

首先继承自 LinearLayout,为了重写相应的 三大方法。

/**
 * @author Petterp on 2019/7/2
 * Summary:重写LinearLayout相应方法
 * 邮箱:[email protected]
 */
public class LinearLayoutView extends LinearLayout {
    public LinearLayoutView(Context context) {
        super(context);
    }

    public LinearLayoutView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    /**
     * 事件分发
     *
     * @param ev
     * @return 是否消费当前事件
     * true-> 消费事件,后续事件继续分发到该view
     * false->
     */
    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        return super.dispatchTouchEvent(ev);
    }

    /**
     * 事件拦截,即自己处理事件
     *
     * @param ev
     * @return 是否拦截当前事件
     * true->事件停止传递,执行自己onTouchEvent,该方法后续不再调用
     * false -> 事件继续向下传递,调用子view.dispatchTouchEvent(),该方法后续仍被调用
     */
    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        Log.e("demo", "viewgroup-onInterceptTouchEvent: "+ ViewActivity.mode);
        return ViewActivity.mode;
    }

    /**
     * 处理点击事件
     *
     * @param event
     * @return 是否消费当前事件
     * true-> 事件停止传递,后续事件由其处理
     * false-> 不处理,事件交由父 onTouchEvent()处理,此view不再接受此事件列的其他事件
     */
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        return super.onTouchEvent(event);
    }
}
           

接着是Activity:

public class ViewActivity extends AppCompatActivity {
    //mode标记位
    public static boolean mode=false;


    @SuppressLint("ClickableViewAccessibility")
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_view);
        Button btn_1=findViewById(R.id.btn_t1);
        LinearLayoutView linearLayout=findViewById(R.id.ln_group);

        //LinearLayout 即viewGroup
        linearLayout.setOnClickListener(v -> Log.e("demo","viewgroup-onClick"));
        linearLayout.setOnTouchListener((v, event) -> {
            switch (event.getAction()){
                case MotionEvent.ACTION_DOWN:
                    Log.e("demo","viewgroup-手指按下");
                    break;
                case MotionEvent.ACTION_UP:
                    Log.e("demo","viewgroup-手指放开");
                    break;
                case MotionEvent.ACTION_MOVE:
                    Log.e("demo","viewgroup-手指移动");
                    break;
            }
            return false;
        });



        //button 即子view
        btn_1.setOnClickListener(v -> Log.e("view-demo","onClick"));
        btn_1.setOnTouchListener((v, event) -> {
            switch (event.getAction()){
                case MotionEvent.ACTION_DOWN:
                    Log.e("demo","view-手指按下");
                    break;
                case MotionEvent.ACTION_UP:
                    Log.e("demo","view-手指放开");
                    break;
                case MotionEvent.ACTION_MOVE:
                    Log.e("demo","view-手指移动");
                    break;
                case MotionEvent.ACTION_CANCEL:
                    Log.e("demo","view-被父view截断了");
                    break;
            }
            return false;
        });
    }

    /**
     * 重写Activity onTouchEvent,
     * 模拟子view不消费事件时的处理情况
     * @param event 
     * @return
     */
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        Log.e("demo","Activity-自己消化");
        return super.onTouchEvent(event);
    }
}
           

相应的注释简洁明了,不用多说了吧。

xml

<?xml version="1.0" encoding="utf-8"?>
<com.petterp.studybook.View.LinearLayoutView xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:custom="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/ln_group"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context=".View.ViewActivity">

    <Button
        android:id="@+id/btn_t1"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Button"
        tools:ignore="HardcodedText" />
</com.petterp.studybook.View.LinearLayoutView>
           
Android事件分发全面解析(基础篇)-夯实基础

首先看看默认情况下的事件分发过程:

手指-> 点击Button,再点击空白处(即Viewgroup)。观察日志打印:

Android事件分发全面解析(基础篇)-夯实基础

结论:默认情况下,viewgroup 拦截器返回false,事件会传递到子view的 dispatchTouchEvent 然后继续分发,直到最后被 子view 的onTouchEvent 所消费,这时候会调用 onclick方法,所以 onclick处于优先级最低。

拦截情况下的事件分发,比如,ViewGroup 的onInterceptTouchEvent返回true呢?

更改代码

//mode标记位
public static boolean mode=true;
           

默认这里是false,当然这个是我自己写的一个标记位,真实情况肯定不是这样。只是为了模拟效果。现在改为true,这样的话LinearLayout 的onInterceptTouchEvent 将返回true,也就是ViewGroup 消费了此事件。

手指 -> 点击Button,再点击 空白处(即ViewGroup),观察日志打印:

Android事件分发全面解析(基础篇)-夯实基础

可以发现,viewGroup已经消费了此次事件,这时无论点击什么位置,事件都不会传递到子view。

结论:当 dispatchTouchEvent 拦截了此次事件,那么接下来的事件序列都不会向下传递。都由此view处理。

如果我们先不拦截,当点击之后再拦截子view事件,这时候又会是什么情况?

修改代码:

//mode标记位
public static boolean mode=false;

...
btn_1.setOnTouchListener((v, event) -> {
      switch (event.getAction()){
          case MotionEvent.ACTION_DOWN:
              Log.e("demo","view-手指按下");
              mode=true;
              break;
        ...
      }
      return false;
  });
           

手指 -> button,然后稍微移动一下松开:

Android事件分发全面解析(基础篇)-夯实基础

是不是发现此次事件序列在被拦截时传递了一个 ACTION_CANCEL 给子view,而以后后续事件都不会再向下传递。

结论:当一个事件 被 onInterceptTouchEvent 返回true 中途拦截时,会传递 ACTION_CANCEL 给view的 onTouchEvent方法。后续的 move等事件,会直接传递给 拦截者 的 onTouchEvent( ) 方法。而且后续事件不会在传递给 其 onInterceptTouchEvent 方法,该方法一旦返回一次true,就再也不会被调用了。

如果都不处理,消费此次事件,会是怎样的呢?

修改代码:

这里为了演示,因为默认的 onInterceptTouchEvent 返回true,懒得修改Button,所以我们尝试用ViewGroup拦截事件,然后不消费它,也就是 onTOuchEvent 返回false。

//mode标记位
public static boolean mode=false;

LinearLayoutView ->更改代码
  public boolean onTouchEvent(MotionEvent event) {
        Log.e("demo","viewgroup-我不消费");
        return false;
    }
           

手指-> button 按下轻轻移动:

Android事件分发全面解析(基础篇)-夯实基础

结论:这也就是我们常说的责任链模式,层层传递事件,决定分发dispatchTouchEvent,最终由onTouchEvent接收,如果子不消费,就继续向上,直到Activity自己消费。Activity这里,其实无论返回true还是false,都会消费事件。

onTouch中返回 true或者false,对onTouchEvent有什么影响吗?

先将代码恢复如初,然后更改

Activity: 
linearLayout.setOnTouchListener((v, event) -> {
            switch (event.getAction()){
                case MotionEvent.ACTION_DOWN:
                    Log.e("demo","viewgroup-手指按下");
                    break;
                case MotionEvent.ACTION_UP:
                    Log.e("demo","viewgroup-手指放开");
                    break;
                case MotionEvent.ACTION_MOVE:
                    Log.e("demo","viewgroup-手指移动");
                    break;
            }
            return true;
        });

LinearLayout:
  @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        boolean mode=super.onInterceptTouchEvent(ev);
        Log.e("demo", "viewgroup-onInterceptTouchEvent: "+ mode);
        return false;
    }


 @Override
    public boolean onTouchEvent(MotionEvent event) {
        boolean mode=super.onTouchEvent(event);
        Log.e("demo","viewgroup-onTouchEvent");
        return mode;
    }
           

手指-> 按下空白区域再松开:

Android事件分发全面解析(基础篇)-夯实基础

接着修改代码

Activity: 
 linearLayout.setOnTouchListener((v, event) -> {
         	...
            return false;
        });
           

手指-> 按下空白区域再松开:

Android事件分发全面解析(基础篇)-夯实基础

结论:可以发现 onTouch方法优先于 onTouchEvent执行。具体的原因可以看我下一篇Android事件分发全面解析(源码篇)

更多Android开发知识请访问—— Android开发日常笔记,欢迎Star,你的小小点赞,是对我的莫大鼓励。

参阅:

  • GcsSloop
  • Android开发艺术探索

继续阅读