Android事件分发机制(View篇)

来源:互联网 发布:淘宝所有分类怎么去掉 编辑:程序博客网 时间:2024/06/06 00:06
纸上得来终觉浅,看了很多别人写的有关View的事件分发机制的博客,但别人的终究是别人的,把自己的理解写下来,才是自己的,但万变不离其宗。本篇将从另外一个角度带你理解View的事件分发机制。

序言

关于 ViewViewGroup 的事件分发机制 我打算用两篇博客来写,
本篇主要讲述 View 的事件分发机制

View 的事件分发和领导派发任务是很相似的,所以我们先通过这个生活中常见的场景引入,以便大家更好的理解View的事件分发

场景描述:

当老板有一个问题需要解决的时候,他可以自己直接解决,也可以将问题交给经理去解决,当然老板不可能所有问题都要自己解决,那样还要经理和员工干什么,所以老板一般会将任务派发给经理,一般经理也不会自己解决,他会将问题派发给员工去解决,员工嘛就是干活的,否则就得走人了,当然不排除有些问题员工没有能力解决那只能将问题返回给经理,经理有能力解决就解决了,如果经理也解决不了,那就把这个问题扔回给老板,最后只能由老板自己解决了。

类似地:

我们的View的事件分发也是这样的,当我们手接触到手机屏幕的时候,屏幕接收到一个Touch事件,系统会将这Touch事件封装成一个对象MotionEvent,之后所有的事件处理都将与这个MotionEvent相关,和领导派发任务一样,我们的Touch事件会首先到达最外层的ViewGroup(Activity),然后再一层一层地向子View派发,最终会到达最内层的View,最内层的View可以处理这个Touch事件,也可以将这个Touch事件扔回给自己的父View

对于View和ViewGroup我们需要关注下面的个方法:

View 两个方法:

  • dispatchTouchEvent (MotionEvent event)//负责事件分发
  • onTouchEvent(MotionEvent event)//当前View自己处理当前事件

ViewGroup 三个方法:

  • dispatchTouchEvent (MotionEvent event)//负责事件分发
  • onInterCeptTouchEvent(MotionEvent event)//处理是否拦截当前事件
  • onTouchEvent(MotionEvent event)//当前View自己处理当前事件

我们注意到上面的方法都参数都是一个 MotionEvent 对象,ViewViewGroup少了一个onInterCeptTouchEvent()方法,这时因为对于View来讲,它是不能有子View的,所以不需要拦截事件的方法

正文:

我们先从Android中两种不同类型的控件开始介绍

  • 可被点击的: Button、ImageButton... //即本身CLICKABLE为true
  • 不可被点击的: ImageView、TextView...//即本身CLICKABLE为false

    注意:上面两点很重要,大家一定要记在心里。也可能你会有疑问我们平常用的ImageViewTextView都能点击呀,请注意我说的是本身可被点击的,ImageViewTextView能被点击是因为我们给他们设置了setOnClickListener(),这样就把这两种ViewCLICKABLE置为true了,关于这一点大家可以通过调用这个方法前后添加log日志去验证。

我们先新建一个project: ViewDemo

ViewDemo 很简单:在布局文件中添加一个 ImageView 和一个 Button 控件
MainActivity中初始化并给两个控件都设置setOnTouchListener

先看布局文件

activity_main.xml

<?xml version="1.0" encoding="utf-8"?>    <LinearLayout    xmlns:android="http://schemas.android.com/apk/res/android"    android:layout_width="match_parent"    android:layout_height="match_parent"    android:gravity="center"    android:orientation="horizontal">    <ImageView        android:id="@+id/imageView"        android:layout_width="wrap_content"        android:layout_height="wrap_content"        android:src="@mipmap/ic_launcher"        />    <Button        android:id="@+id/button"        android:layout_width="wrap_content"        android:layout_height="wrap_content"        android:text="按钮"/></LinearLayout>

MainActivity中初始化ImageViewButton并设置setOnTouchListener
onTouch()方法中添加log日志

MainActivity主要代码

@Overrideprotected void onCreate(Bundle savedInstanceState) {    super.onCreate(savedInstanceState);    setContentView(R.layout.activity_main);    mImageView = findViewById(R.id.imageView);    mButton = findViewById(R.id.button);    mImageView.setOnTouchListener(new View.OnTouchListener() {        @Override        public boolean onTouch(View view, MotionEvent motionEvent) {            Log.e(TAG,"----mImageView-----onTouch---->"+motionEvent.getAction());            return false;        }    });    mButton.setOnTouchListener(new View.OnTouchListener() {        @Override        public boolean onTouch(View view, MotionEvent motionEvent) {            Log.e(TAG,"----mButton--------onTouch---->"+motionEvent.getAction());            return false;        }    });}

运行程序依次点击 ImageViewButton

其中 motionEvent.getAction() 的值代表的含义:

0:ACTION_DOWN2:ACTION_MOVE1:ACTION_UP

打印log如下:

10-30 11:50:06.439 10517-10517/com.marco.viewdemo E/MainActivity: ----mImageView-----onTouch---->010-30 11:50:07.276 10517-10517/com.marco.viewdemo E/MainActivity: ----mButton--------onTouch---->010-30 11:50:07.285 10517-10517/com.marco.viewdemo E/MainActivity: ----mButton--------onTouch---->210-30 11:50:07.313 10517-10517/com.marco.viewdemo E/MainActivity: ----mButton--------onTouch---->1

我们可以看到点击ImageView的时候日志打印了一次,点击Button的时候日志打印了三次
如果我们把两个onTouch()方法的返回值都返回true即:

mImageView.setOnTouchListener(new View.OnTouchListener() {    @Override    public boolean onTouch(View view, MotionEvent motionEvent) {        Log.e(TAG,"----mImageView-----onTouch---->"+motionEvent.getAction());        return true;    }});mButton.setOnTouchListener(new View.OnTouchListener() {    @Override    public boolean onTouch(View view, MotionEvent motionEvent) {        Log.e(TAG,"----mButton--------onTouch---->"+motionEvent.getAction());        return true;    }});}

分别点击 ImageViewButton 再来看日志:

10-30 12:55:16.427 20779-20779/com.marco.viewdemo E/MainActivity: ----mImageView-----onTouch---->010-30 12:55:16.442 20779-20779/com.marco.viewdemo E/MainActivity: ----mImageView-----onTouch---->210-30 12:55:16.487 20779-20779/com.marco.viewdemo E/MainActivity: ----mImageView-----onTouch---->110-30 12:55:19.514 20779-20779/com.marco.viewdemo E/MainActivity: ----mButton--------onTouch---->010-30 12:55:19.533 20779-20779/com.marco.viewdemo E/MainActivity: ----mButton--------onTouch---->210-30 12:55:19.574 20779-20779/com.marco.viewdemo E/MainActivity: ----mButton--------onTouch---->1

此时可以看到两个控件的日志打印都是三次,DOWNMOVEUP

一分钟时间思考一下这是为什么?

下面让我带大家从源码角度分析一下为什么会这样:
前面我们提到过对于View来讲我们关注两个方法:

  • dispatchTouchEvent (MotionEvent event)//负责事件分发
  • onTouchEvent(MotionEvent event)//当前View自己处理当前事件

我们需要知道,当一个View接收到Touch事件的时候,首先被调用的是当前ViewdispatchTouchEvent()方法:
下面我们看一下View源码中的这个方法:dispatchTouchEvent()

为了便于理解,我省略了部分干扰代码,只保留了有效的部分

dispatchTouchEvent(MotionEvent event)

/** * Pass the touch screen motion event down to the target view, or this * view if it is the target. * * @param event The motion event to be dispatched. * @return True if the event was handled by the view, false otherwise. */public boolean dispatchTouchEvent(MotionEvent event) {    boolean result = false;    ......        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;}

从方法的注释我们可以知道:这个方法是处理事件分发的,如果return true说明当前事件被消费了,便不再继续分发,否则继续分发

我们主要看方法中的两个if判断:先看第一个

li != null && li.mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED

&& li.mOnTouchListener.onTouch(this, event)

因为我们给控件设置了onTouchLishener所以mListenerInfo不为空,li != nulltrue,li.mOnTouchListener != null也为true,这个mOnTouchListener就是我们mImageView.setOnTouchListener(new View.OnTouchListener())的时候new出来的,第三个条件主要取决于mViewFlags,在Android系统中所有控件默认都是enable的,除非我们对一个控件设置view.setEnable(false),所以(mViewFlags & ENABLED_MASK) == ENABLED也为true,那么能不能进入到if()内部关键看li.mOnTouchListener.onTouch(this, event),这个方法的返回值就是我们最开始setOnTouchListener中重写的onTouch()方法的返回值,默认为false,后来被我们改成了true

onTouch()方法中默认false,也就是说此处的第一个if()条件不成立,那么将到第二个if()判断:!resulttrue,那么我们主要看onTouchEvent(event)的返回值,那么这个onTouchEvent(event)有时什么东东呢?我们进入到这个方法中(干扰代码已省略):

onTouchEvent(MotionEvent event)

 /*  * @param event The motion event.  * @return True if the event was handled, false otherwise.  */public boolean onTouchEvent(MotionEvent event) {   ......    if (((viewFlags & CLICKABLE) == CLICKABLE ||            (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) {        switch (event.getAction()) {            case MotionEvent.ACTION_UP:                  ......                    if (!post(mPerformClick)) {                         performClick();                     }                  ......                break;            case MotionEvent.ACTION_DOWN:                ......                break;            case MotionEvent.ACTION_CANCEL:                ......                break;            case MotionEvent.ACTION_MOVE:                ......                break;        }        return true;    }    return false;}

从方法中我们可以看到这个方法的返回值也是 boolean 类型的,看到这里不知道大家能不能想起我在开头说的Android中两种不同类型,一种是默认可以点击的,一种是默认不可以点击,我们的 ImageViewButton 的可点击与否的作用就在这个方法中的到了体现,让我们来看一下这个 if() 条件判断吧

先说默认条件下两个Touch方法都返回false

- ImageView

如果是 ImageView不用想,默认就是不可点击的,也就是说CLICKABLELONG_CLICKABLE都为false,那么好,这个条件我们是进不去的,再看最后return值是false,也就是说只要if()判断进不去,全都会返回false,回到我们之前的dispatchTouchEvent()中,既然onTouchEvent()也返回了false,那么最终dispatchTouchEvent()的返回值也是false,说明这个TouchDOWN事件没有被消费,既然DOWN事件没有被消费,也就没有后面的MOVEUP,所以最初的ImageView值打印了一行log,

- Button

如果是Button默认就是可点击的CLICKABLELONG_CLICKABLE都为true,也就是说if()判断能够进入,那么就说明,里面的DOWNMOVEUP事件都会响应,另外我们再看,进入if()后最后一句: return true;这句返回了true,也就是说,只要进入了这个if()判断,就回返回true,也就是我们的dispatchTouchEvent()中的第二个if()判断会返回true,那么我们的dispatchTouchEvent()最终也就回返回true,也就是说我们的Touch事件被消费了。当然DOWNMOVEUP事件都会响应,所以我们的Button打印了三行log

再说之后我们把两个Touch方法都返回true

当这两个onTouch()方法都返回true的时候,就更简单了,还是看我们的dispatchTouchEvent(),即第一个if()判断中mOnTouchListener.onTouch(this, event)true,前面我们已经判断了,if()中其他的都为true,所以第一个if()判断可以进入,最终返回了truedispatchTouchEvent()的返回值为true,根本就走不到第二个if()条件判断,所以跟后面的onTouchEvent()就没什么关系了。所以最终这个Touch事件在dispatchTouchEvent()中的回调的onTouch()方法中得消费掉了。

注意:不知道有没有同学发现我上面onTouchEvent()方法中除了省略的代码之外,还留下了一行这个方法就与我们的Click事件又关了,我们先看一下这个方法:performClick();

performClick()

/** * Call this view's OnClickListener, if it is defined.  Performs all normal * actions associated with clicking: reporting accessibility event, playing * a sound, etc. * * @return True there was an assigned OnClickListener that was called, false *         otherwise is returned. */public boolean performClick() {    final boolean result;    final ListenerInfo li = mListenerInfo;    if (li != null && li.mOnClickListener != null) {        playSoundEffect(SoundEffectConstants.CLICK);        li.mOnClickListener.onClick(this);        result = true;    } else {        result = false;    }    sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);    return result;}

不过要想执行到 performClick() 这个方法要想执行到,首先得能进入到上面 onTouchEvent() 方法中的 if() 条件判断

下面我们在MainActivity的onCreate方法中给ImageViewButton设置两个click监听事件,把onTouch事件还原到默认的false返回值

    mImageView.setOnTouchListener(new View.OnTouchListener() {        @Override        public boolean onTouch(View view, MotionEvent motionEvent) {            Log.e(TAG,"----mImageView-----onTouch---->"+motionEvent.getAction());            return false;        }    });    mButton.setOnTouchListener(new View.OnTouchListener() {        @Override        public boolean onTouch(View view, MotionEvent motionEvent) {            Log.e(TAG,"----mButton--------onTouch---->"+motionEvent.getAction());            return false;        }    });    mImageView.setOnClickListener(new View.OnClickListener() {        @Override        public void onClick(View view) {            Log.e(TAG,"----mImageView--------onClick---->");        }    });    mButton.setOnClickListener(new View.OnClickListener() {        @Override        public void onClick(View view) {            Log.e(TAG,"----mButton--------onClick---->");        }    });

先点击ImageView打印log:

10-30 14:17:01.255 11719-11719/com.marco.viewdemo E/MainActivity: ----mImageView-----onTouch---->010-30 14:17:01.272 11719-11719/com.marco.viewdemo E/MainActivity: ----mImageView-----onTouch---->210-30 14:17:01.305 11719-11719/com.marco.viewdemo E/MainActivity: ----mImageView-----onTouch---->110-30 14:17:01.309 11719-11719/com.marco.viewdemo E/MainActivity: ----mImageView-----onClick---->

再点击Button打印log:

10-30 14:18:00.624 11719-11719/com.marco.viewdemo E/MainActivity: ----mButton--------onTouch---->010-30 14:18:00.641 11719-11719/com.marco.viewdemo E/MainActivity: ----mButton--------onTouch---->210-30 14:18:00.671 11719-11719/com.marco.viewdemo E/MainActivity: ----mButton--------onTouch---->110-30 14:18:00.674 11719-11719/com.marco.viewdemo E/MainActivity: ----mButton--------onClick---->

咦?竟然一样?你可能要问了,对于Button打印的结果我们可以理解,但是我们只给ImageViewButton添加了Click监听事件,其他都没变,两个onTouch()方法返回的竟然也都一样,按照我们最初只添加两个setOnTouchListener方法,ImageView应该只打印一次才对呀,怎么添加了一个setOnClickListener,就打印三次了呢?而切如果按照我们最开始的推论,既然ImageView是默认不可点击的,那么在onTouchEvent方法中的if()判断条件不可能进入呀,更不会执行到performClick()方法,怎么会也打印出除了最后一行onClicklog呢?

猜想:上面log既然显示执行了performClick(),也就是说onTouchEvent()中的if()判断条件进入了,而我们只设置了setOnClickListener方法,莫非这个方法做了什么手脚?是不是把ImageViewCLICKABLE的值改变了呢?
我们进入到setOnClickListener中一探究竟:

/** * Register a callback to be invoked when this view is clicked. If this view is not * clickable, it becomes clickable. * * @param l The callback that will run * * @see #setClickable(boolean) */public void setOnClickListener(OnClickListener l) {    if (!isClickable()) {        setClickable(true);    }    getListenerInfo().mOnClickListener = l;}

天呐,setClickable(true);看到了吧?我们的猜想是对的,一旦我们设置了setOnClickListener当前ViewCLICKABLE的值就会被置为true,这就不难理解为什么ImageView也能进入到if()判断中,而且执行了performClick()方法。

通过上面例子我们也的出来一个结论:ViewonTouch()方法是先于onClick()方法执行的,如果onTouch()方法返回true即被消费了,就不会执行onTouchEvent()方法,也就执行不到onClick()方法,
这点我们也可以验证一下,就是把上面两个setOnTouchListener中的onTouch方法都置为true然后分别点击ImageViewButton然后看log
点击ImageViewhe

10-30 14:43:54.142 29165-29165/com.marco.viewdemo E/MainActivity: ----mImageView-----onTouch---->010-30 14:43:54.150 29165-29165/com.marco.viewdemo E/MainActivity: ----mImageView-----onTouch---->210-30 14:43:54.202 29165-29165/com.marco.viewdemo E/MainActivity: ----mImageView-----onTouch---->1

点击Button

10-30 14:44:13.478 29165-29165/com.marco.viewdemo E/MainActivity: ----mButton--------onTouch---->010-30 14:44:13.494 29165-29165/com.marco.viewdemo E/MainActivity: ----mButton--------onTouch---->210-30 14:44:13.641 29165-29165/com.marco.viewdemo E/MainActivity: ----mButton--------onTouch---->1

看到了吧?即使我们给ImageViewButton也都设置了setOnClickListener,但是在onTouch()方法中返回了true,所以onclick都没有执行

总结

  • Android中分为两种类型的View,即默认可点击的和默认不可点击的(重点)

  • 当一个Touch事件发出的时候,View中最先执行的是dispatchTouchEvent()

  • 在dispatchTouchEvent()方法内部先判断onTouch()的返回值,根据返回值确定是否执行onTouchEvent()方法

  • 一个View的onTouch事件是优先于onClick执行的

  • onTouch()和onTouchEvent()的共同点是,都在dispatchTouchEvent中执行判断;区别是,onTouch()优先于onTouchEvent()执行,而且其返回值决定了onTouchEvent()能不能够得到执行

好了以上就是我对View的事件分发机制的全部理解,不足之处或者又不理解的地方请在评论区留下您的评论,大家共同进步
最后附上ViewDemo源码:github下载

下一篇:Android事件分发机制(ViewGroup篇)

原创粉丝点击