View的事件分发机制

来源:互联网 发布:二次元动态桌面软件 编辑:程序博客网 时间:2024/05/16 14:31

View的事件分发机制,也称为View的事件拦截机制,在说事件分发机制之前,需要对MotionEvent对象就行分析,也就是点击事件,MotionEvent是手指接触屏幕后所产生的一系列事件,典型的事件类型有如下几种:

ACTION_DOWN——手指刚接触屏幕;

ACTION_MOVE——手指在屏幕上移动:

ACTION_UP——手指在屏幕上松开的一瞬间。

点击屏幕后松开,事件顺序 DOWN->UP

点击屏幕滑动一会再松开,事件顺序为DOWN->MOVE->.....MOVE->UP

所谓点击事件的分发过程,其实就是对MotionEvent事件的分发过程,即当一个MotionEvent产生了以后,系统需要把这个事件传递给一个具体的View,而这个传递的过程就是分发过程。分发过程由三个很重要的方法来共同完成;分别是dispatchTouchEvent、onInterceptTouchEvent、onTouchEvent。


public boolean dispatchTouchEvent(MotionEvent event)

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


public boolean onInterceptTouchEvent(MotionEvent ev)

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


public boolean onTouchEvent(MotionEvent event)

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

我们知道View结构是树形结构,也就是说,View可以放在ViewGroup里面,通过不同的组合来实现不同的样式。

现在假设一个情况:

一个View放在ViewGroup1中,这个ViewGroup1又放在另外一个ViewGroup2中,现在来分析点击事件的分发过程。


布局如上图所示。

ViewGroup1最外层的ViewGroup    ViewGroup2中间的ViewGroup。View最底层的View。

他们代码都很简单,都只是重写了时间拦截核处理的几个方法。

代码贴上:

package com.qian;import android.annotation.SuppressLint;import android.content.Context;import android.util.AttributeSet;import android.util.Log;import android.view.MotionEvent;import android.widget.LinearLayout;public class ViewGroup1  extends LinearLayout{private String tag = "ViewGroup1";@SuppressLint("NewApi") public ViewGroup1(Context context, AttributeSet attrs, int defStyle) {super(context, attrs, defStyle);}public ViewGroup1(Context context, AttributeSet attrs) {super(context, attrs);}public ViewGroup1(Context context) {super(context);}@Overridepublic boolean onTouchEvent(MotionEvent event) {Log.i(tag, "onTouchEvent");return super.onTouchEvent(event);}@Overridepublic boolean onInterceptTouchEvent(MotionEvent ev) {Log.i(tag, "onInterceptTouchEvent");return super.onInterceptTouchEvent(ev);}@Overridepublic boolean dispatchTouchEvent(MotionEvent ev) {Log.i(tag, "dispatchTouchEvent");return super.dispatchTouchEvent(ev);}}
package com.qian;import android.annotation.SuppressLint;import android.content.Context;import android.util.AttributeSet;import android.util.Log;import android.view.MotionEvent;import android.widget.LinearLayout;public class ViewGroup2  extends LinearLayout{private String tag = "ViewGroup2";@SuppressLint("NewApi") public ViewGroup2(Context context, AttributeSet attrs, int defStyle) {super(context, attrs, defStyle);}public ViewGroup2(Context context, AttributeSet attrs) {super(context, attrs);}public ViewGroup2(Context context) {super(context);}@Overridepublic boolean onTouchEvent(MotionEvent event) {Log.i(tag, "onTouchEvent");return super.onTouchEvent(event);}@Overridepublic boolean onInterceptTouchEvent(MotionEvent ev) {Log.i(tag, "onInterceptTouchEvent");return super.onInterceptTouchEvent(ev);}@Overridepublic boolean dispatchTouchEvent(MotionEvent ev) {Log.i(tag, "dispatchTouchEvent");return super.dispatchTouchEvent(ev);}}

最后View1的代码:

package com.qian;import android.content.Context;import android.util.AttributeSet;import android.util.Log;import android.view.MotionEvent;import android.widget.TextView;public class View1 extends TextView{private String tag = "View1";public View1(Context context, AttributeSet attrs, int defStyleAttr) {super(context, attrs, defStyleAttr);}public View1(Context context, AttributeSet attrs) {super(context, attrs);}public View1(Context context) {super(context);}@Overridepublic boolean onTouchEvent(MotionEvent event) {Log.i(tag , "onTouchEvent");return super.onTouchEvent(event);}@Overridepublic boolean dispatchTouchEvent(MotionEvent ev) {Log.i(tag , "dispatchTouchEvent");return super.dispatchTouchEvent(ev);}}

layout布局:

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"    xmlns:tools="http://schemas.android.com/tools"    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=".MainActivity" >    <com.qian.ViewGroup1     android:layout_width="match_parent"    android:layout_height="match_parent"    android:background="#000000"    ><com.qian.ViewGroup2     android:layout_width="200dp"    android:layout_height="200dp"    android:background="#ff0000"    android:layout_gravity="center"    android:gravity="center"    >    <com.qian.View1         android:layout_width="100dp"    android:layout_height="100dp"    android:background="#00ff00"    android:layout_gravity="center"    android:gravity="center"        /></com.qian.ViewGroup2>    </com.qian.ViewGroup1>    </RelativeLayout>
布局效果如图所示:


从上面的代码可以看出,ViewGroup级别比View的级别要高一点,比View多了方法。onInterceptTouchEvent,这个方法看名字就能猜到是事件拦截机制的核心方法,由于View中已经不能再放子元素了,所以它没有这个方法。

上面的代码只是添加了一些Log而已,布局改了一下北京颜色,主要是方便观察。现在点击一下中间的View1所在的绿色的区域,打印的Log如下:




从打印的Log可以得出事件分发或者说事件传递的顺序为,从dispatchTouchEvent和onInterceptTouchEvent方法看:

ViewGroup1——>ViewGroup2——>View1

而事件的处理顺序可以从onTouchEvent方法看出:

View1——>ViewGroup2——>ViewGroup1

事件传递的返回值表示   true,拦截  false 不拦截继续往下传递  默认为false

事件处理的返回值也类似,  true 处理了  不用审核了  false 给上级处理 默认值false

事件传递过程,这里暂时先不管dispatchTouchEvent这个方法,这个方法一般不太会改这个,所以这里主要关注onInterceptTouchEvent方法,它的返回值就表示是否拦截事件。所以可以可以把上面的整个事件过程整理为下面一张图:


下面再改一下代码,加深理解.

在ViewGroup1中修改onInterceptTouchEvent,返回true

@Overridepublic boolean onInterceptTouchEvent(MotionEvent ev) {Log.i(tag, "onInterceptTouchEvent");return true;//return super.onInterceptTouchEvent(ev);}

再来看一下Log:


跟我们设想的一样,ViewGroup1拦截了事件,那么ViewGroup2和View1就不能收到事件。所以Log中只能看到ViewGroup1的Log。ViewGroup1拦截事件的过程如下图所示:


同样,我们修改ViewGroup2的onInterceptTouchEvent,返回true ViewGroup1改回默认的false。打印的Log如下:



可以看到,这次是ViewGroup2拦截了事件。ViewGroup2拦截事件的过程如下图所示:


这样对事件的分发,拦截应该就比较清楚了,下面再看看对事件的处理,也就是onTouchEvent方法。这里最底层的View1,每次处理完任务也就是事件,都需要向上级报告,需要上级的确认,所以事件处理需要返回false。那么如果返回true,又是怎么样的呢?直接看Log打印。

@Overridepublic boolean onTouchEvent(MotionEvent event) {Log.i(tag , "onTouchEvent");return true;//return super.onTouchEvent(event);}



此时View1处理事件的过程如下:


如果如果ViewGroup的2onTouchEvent方法true。


总结一下,用一段伪代码

       @Overridepublic boolean dispatchTouchEvent(MotionEvent ev) {   boolean isConsume = false;   if(onInterceptTouchEvent(ev)) {   isConsume = onTouchEvent(ev);   } else {   isConsume = dispatchTouchEvent(ev);   }   return isConsume;   }



github源代码:https://github.com/dandelion133/viewDispatch



通过这一段伪代码,也理解了dispaTouchEvent方法的原理,虽然上面还说暂时不管,dispaTouchEvent方法从全局上把我整个传递过程。对于一个根ViewGroup来说,点击事件产生后,首先会传递给它,这时它的dispaTouchEvent就会被调用,如果这个ViewGroup的onInterceptTouchEvent方法返回true就表示他要拦截当前事件,接着事件就会交给这个ViewGroup处理,即它的onTouchEvent就会被调用,如果这个ViewGroup的onInterceptTouchEvent方法返回false返回false就表示它不拦截这个事件,这时当前事件会继续传递给它的子元素,接着子元素的dispaTouchEvent方法就会被调用,如此反复直到事件被最终处理。

顺便提一下onTouchListener,当一个View需要处理事件时,如果它设置了onTouchListener,那么onTouchListener中的onTouch方法会被调用,这时事件如何处理还要看onTouch的返回值,如果onTouch返回false,当前View的onTouch方法会被调用,反之不会被调用,由此可见,onTouchListener的优先级比onTouchEvent优先级要高。


再说onClickListener,在onTouchEvent中,如果当前View设置了onClickListener,那么他的onClick方法会被调用,前提是onTouchEvent被调用,平时经常用得onClickListener,优先级最低,处于事件传递的尾端。


当一个点击事件产生后,它的传递过程遵循如下顺序:

Activity——>Window——>View

即事件总是先传递给Activity,Activity再传递给Window,最后Window再传递给顶级View。顶级View接受事件后,就会按照事件分发机制去分发事件。考虑一种情况,如果一个View的onTouchEvent返回false,那么它的父容器的onTouchEvent方法就会被调用,一次类推,如果所有元素的onTouchEvent都返回false,那么最终这个事件会传递给Activity去处理。这就是Activity为什么也有onTouchEvent方法和dispatchTouchEvent方法,但是它没有onInterceptTouchEvent方法,因为它处在整个事件传递的最高层,不需要onInterceptTouchEvent拦截事件这个方法。


总结一些View的分发机制的结论。

1)同一个事件序列是指触摸屏幕的那一刻起,到手指离开屏幕的那一刻结束,在这个过程中所产生的一系列事件,这个事件徐柳以down事件开始,中间含有数量不定的move事件,最终以up事件结束。

2)正常情况下,一个事件序列只能被一个View兰家且消耗,原因参考3),因为一旦一个元素拦截了某些事件,那么同一个事件序列的所有事件都会交给它处理,但不是绝对的,可以强制。

3)某个View一旦绝对拦截,那么这个一个事件序列都只能由它来处理,并且它的onInterceptTouchEvent方法不会被再次调用。

4)某个View一旦开始处理事件,如果不消耗down,onTouchEvent返回false,那么这个一个事件序列都不会交给它来处理,并且事件将重新交给它的父容器去处理。

5)如果所有的View都不处理事件,这个事件最终会小时。

6)ViewGroup默认不拦截任何事件。

7)View没有onInterceptTouchEvent方法,也就是不拦截事件,只要有时间传递给它,它的onTouchEvent方法就会被调用。

8)View的onTouchEvent默认都会消耗事件,也就是onTouchEvent方法返回true。除非它是不可点击的(clickable和longclickable同时为false)。View的longclickable默认为false,clickable要看情况,比如Button默认是true,也就是Button默认会消耗事件,onTouchEvent方法返回true。但是TextView不是不可点击的,onTouchEvent方法返回false

这里说明一下为什么最上面View1采用继承TextView,因为TextView不可点击,方便演示。

9)View的enable属性不影响onTouchEvent的默认返回值。哪怕一个View是disable状态的。主要clickable和longclickable有一个为true,那么它的onTouchEvent方法就会返回true。

10)onClick会发生的前提是当前View是可点击的,并且它收到了down和up事件。

11)事件传递过程是由外向内的,即事件总是先传递给父元素,然后再由父元素传递给子View,但是子元素可以通过requestDisallowTouchEvent方法干扰父元素的分发过程,但是down事件除外。








4 0