完全理解Android TouchEvent事件分发机制

Posted 单灿灿

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了完全理解Android TouchEvent事件分发机制相关的知识,希望对你有一定的参考价值。

本文能给你带来和解决一些你模糊的Touch事件概念及用法

  • 1.掌握View及ViewGroup的TouchEvent事件分发机制
  • 2.为解决View滑动冲突及点击事件消费提供支持
  • 3.为你解决面试中的一些问题。

Touch事件分发中只有两个主角:ViewGroup和View。

Activity的Touch事件事实上是调用它内部的ViewGroup的Touch事件,可以直接当成ViewGroup处理。

Activity、ViewGroup、View都关心Touch事件,其中ViewGroup的关心的事件有三个:onInterceptTouchEvent、dispatchTouchEvent、onTouchEvent。

Activity和View关心的事件只有两个:dispatchTouchEvent、onTouchEvent。

只有ViewGroup可以对事件进行拦截。

android中Touch**触摸事件**主要包括点击(onClick)、长按(onLongClick)、拖拽(onDrag)、滑动(onScroll)等,

其中Touch的第一个状态是 ACTION_DOWN,表示按下了屏幕后,touch将会有后续事件,比如移动、抬起等。

一个Action_DOWN,一个ACTION_UP,许多个ACTION_MOVE,构成了Android中众多的Touch交互事件。

安卓里经常会有多个布局嵌套,View重叠,View的Visibility设置等等,还有ViewGroup包含View的情况。
这个时候点击到子View时,其实也是同时点到ViewGroup这个父控件的,那是把这个点击事件应该是怎么分发的呢(有没有遇到过listview或recyclerview的item事件或者是item中的控件是不是没反应撒)?

触摸事件分发机制涉及的三个重要方法:

 public boolean dispatchTouchEvent(MotionEvent event)

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

public boolean onInterceptTouchEvent(MotionEvent event)

onInterceptTouchEvent是ViewGroup提供的方法,用来判断是否拦截某个事件,如果当前View拦截了某个事件,
那么在同一个事件序列当中,此方法不会被再次调用,返回结果表示是否拦截当前事件。默认返回false,返回true表示拦截。

  public boolean onTouchEvent(MotionEvent event)

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

今天所使用的Demo目录结构及Activity如图所示:

首先我们来看一下dispatchTouchEvent(MotionEvent event)

布局activity_touch_test.xml

<?xml version="1.0" encoding="utf-8"?>
<com.shanlovana.rcimageview.touchviews.GrandPaViewGroup
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/activity_touch_test"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    tools:context="com.shanlovana.rcimageview.TouchTestActivity">

    <com.shanlovana.rcimageview.touchviews.FatherViewGroup
        android:layout_width="match_parent"
        android:gravity="center"
        android:layout_height="match_parent">

        <com.shanlovana.rcimageview.touchviews.LogImageView
            android:layout_width="300dp"
            android:layout_height="200dp"
            android:src="@drawable/damimi"/>


    </com.shanlovana.rcimageview.touchviews.FatherViewGroup>


</com.shanlovana.rcimageview.touchviews.GrandPaViewGroup>

下面是三层布局及预览情况:

点击一下图片:看一下打印:

03-31 09:02:40.554 10898-10898/com.shanlovana.rcimageview E/ShanCanCan: GrandPaViewGroup  dispatchTouchEvent  Event 0
03-31 09:02:40.555 10898-10898/com.shanlovana.rcimageview E/ShanCanCan: GrandPaViewGroup  onInterceptTouchEvent  Event 0
03-31 09:02:40.555 10898-10898/com.shanlovana.rcimageview E/ShanCanCan: FatherViewGroup  dispatchTouchEvent  Event 0
03-31 09:02:40.555 10898-10898/com.shanlovana.rcimageview E/ShanCanCan: FatherViewGroup  onInterceptTouchEvent  Event 0
03-31 09:02:40.555 10898-10898/com.shanlovana.rcimageview E/ShanCanCan: LogImageView  dispatchTouchEvent  Event 0
03-31 09:02:40.556 10898-10898/com.shanlovana.rcimageview E/ShanCanCan: LogImageView  onTouchEvent  Event 0
03-31 09:02:40.558 10898-10898/com.shanlovana.rcimageview E/ShanCanCan: FatherViewGroup  onTouchEvent  Event 0
03-31 09:02:40.559 10898-10898/com.shanlovana.rcimageview E/ShanCanCan: GrandPaViewGroup  onTouchEvent  Event 0

源码中0,1,2,3,4所代表的Action

   public static final int ACTION_DOWN = 0;


    public static final int ACTION_UP   = 1;


    public static final int ACTION_MOVE  = 2;


    public static final int ACTION_CANCEL = 3;


    public static final int ACTION_OUTSIDE = 4;

为什么是这样一个从父级到子级再到父级的顺序呢?

来,follow me进入源码查看,所有的核心在于ViewGroup的dispatchTouchEvent方法:

boolean dispatchTouchEvent() 
    // 是否拦截
     final boolean intercepted;
    intercepted = onInterceptTouchEvent(ev);

    // final boolean canceled = resetCancelNextUpFlag(this)
                 //   || actionMasked == MotionEvent.ACTION_CANCEL;
    if( !intercepted) 
        // 如果不拦截遍历所有child,判断是否有分发
        boolean handled;
        if (child == null) 
            // 等同于handled = onTouchEvent()
            handled = super.dispatchTouchEvent();
         else 
            // 如果有child,再调用child的分发方法
            handled = child.dispatchTouchEvent();
        

        if(handled) 
            touchTarget = child;
            break;
           
    

    if(touchTarget == null) 
        // 如果所有child中都没有消费掉事件
        // 那么就把自己作为没child的普通View
        handled = super.dispatchTouchEvent();
    

    return handled;


  public boolean onInterceptTouchEvent(MotionEvent ev) 
        if (ev.isFromSource(InputDevice.SOURCE_MOUSE)
                && ev.getAction() == MotionEvent.ACTION_DOWN
                && ev.isButtonPressed(MotionEvent.BUTTON_PRIMARY)
                && isOnScrollbarThumb(ev.getX(), ev.getY())) 
            return true;
        
        return false;
    

dispatchTouchEvent方法的作用是将屏幕点击事件进行向下分发(子一级)传递到目标控件上,或者传递给自己。

如果事件被(自己或者下面某一层的子控件)处理掉了的话,就返回true,否则返回false

那问题来了,如果我没有child了,或者我就是一个View,那我的dispatchTouchEvent返回值要如何获取呢?

这种情况下就会使用父类的dispatchTouchEvent方法,
也就是调用View类中的实现,简化代码如下:

boolean dispatchTouchEvent() 
    // 实质上就是调用onTouchEvent用其返回值
    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;

由此可见,只要是enable=false或者没有设置过touchListener, 那么他一定会调用onTouchEvent,且dispatchTouchEvent的返回值就是onTouchEvent的返回值。

ViewGroup进行事件的分发,一直到自己或者是最底层的View,逻辑图如下。

现在我们基本知道了事件的分发dispatchTouchEvent,最终调用了onTouchEvent方法

接着我们来理解和讲解onInterceptTouchEvent拦截方法

该方法用于拦截事件向下分发

当返回值为true时,就会拦截TouchEvent不再向下传递,直接交给自己的onTouchEvent方法处理。返回false则不拦截。

把**Demo中的Parent层的onInterceptTouchEvent返回值改为tru**e。

运行一下,点View,看下输出结果:

03-31 11:45:39.953 23170-23170/com.shanlovana.rcimageview E/ShanCanCan: GrandPaViewGroup  dispatchTouchEvent  Event 0
03-31 11:45:39.953 23170-23170/com.shanlovana.rcimageview E/ShanCanCan: GrandPaViewGroup  onInterceptTouchEvent  Event 0
03-31 11:45:39.953 23170-23170/com.shanlovana.rcimageview E/ShanCanCan: FatherViewGroup  dispatchTouchEvent  Event 0
03-31 11:45:39.953 23170-23170/com.shanlovana.rcimageview E/ShanCanCan: FatherViewGroup  onInterceptTouchEvent  Event 0
03-31 11:45:39.954 23170-23170/com.shanlovana.rcimageview E/ShanCanCan: FatherViewGroup  onTouchEvent  Event 0
03-31 11:45:39.955 23170-23170/com.shanlovana.rcimageview E/ShanCanCan: GrandPaViewGroup  onTouchEvent  Event 0

即当事件一层层向下传递到parent时,被他就拦截了下来然后自己消费使用。

intercepted为true,没有进入FatherViewGroup的条件,就跳过了child.dispatchTouchEvent的向下事件分发(结合我的demo看比较直观)。

最后我们来讲解 onTouchEvent方法

方法的主体内容其实是处理具体操作逻辑的,是产生一次点击还是一次横纵向的滑动等

而他的返回值才会影响整个事件分发机制,
意义在于通知父级的ViewGroup们是否已经消费找到目标Target了。

把示例中的Parent的TouchEvent返回值改为true。拦截方法不变
点一下View(小伙子,如果你不是点一下,会出现不同的结果哦),则输出日志为:

03-31 12:01:21.661 2596-2596/com.shanlovana.rcimageview E/ShanCanCan: GrandPaViewGroup  dispatchTouchEvent  Event 0
03-31 12:01:21.674 2596-2596/com.shanlovana.rcimageview E/ShanCanCan: GrandPaViewGroup  onInterceptTouchEvent  Event 0
03-31 12:01:21.676 2596-2596/com.shanlovana.rcimageview E/ShanCanCan: FatherViewGroup  dispatchTouchEvent  Event 0
03-31 12:01:21.677 2596-2596/com.shanlovana.rcimageview E/ShanCanCan: FatherViewGroup  onInterceptTouchEvent  Event 0
03-31 12:01:21.677 2596-2596/com.shanlovana.rcimageview E/ShanCanCan: LogImageView  dispatchTouchEvent  Event 0
03-31 12:01:21.678 2596-2596/com.shanlovana.rcimageview E/ShanCanCan: LogImageView  onTouchEvent  Event 0
03-31 12:01:21.681 2596-2596/com.shanlovana.rcimageview E/ShanCanCan: FatherViewGroup  onTouchEvent  Event 0
03-31 12:01:21.723 2596-2596/com.shanlovana.rcimageview E/ShanCanCan: GrandPaViewGroup  dispatchTouchEvent  Event 1
03-31 12:01:21.723 2596-2596/com.shanlovana.rcimageview E/ShanCanCan: GrandPaViewGroup  onInterceptTouchEvent  Event 1
03-31 12:01:21.723 2596-2596/com.shanlovana.rcimageview E/ShanCanCan: FatherViewGroup  dispatchTouchEvent  Event 1
03-31 12:01:21.723 2596-2596/com.shanlovana.rcimageview E/ShanCanCan: FatherViewGroup  onTouchEvent  Event 1

先看Down的逻辑,对应的源码执行顺序如下

Father调用super.dispatchTouchEvent实际上是调用了onTouchEvent方法,

这里因为我们修改成了true,所以dispatchTouchEvent最终也返回true。

所以返回到GrandPa中,touchTarget 就非空了,

因此GrandPa的onTouchEvent也没有执行~

可以看出来,事件一旦被某一层消费掉,其它层就不会再消费了

到这里其实对事件分发的机制就有个大概了解了看了源码也知道里面的原理是怎么回事。

突然有事,明天接着更新。

以上是关于完全理解Android TouchEvent事件分发机制的主要内容,如果未能解决你的问题,请参考以下文章

Android——滑动事件冲突解决

Android Touch事件传递机制解析

Android事件分发机制完全解析,带你从源码的角度彻底理解

Android事件分发机制完全解析,带你从源码的角度彻底理解(下)

Android事件分发机制完全解析,带你从源码的角度彻底理解(上)

(转) Android事件分发机制完全解析,带你从源码的角度彻底理解(上)