谈谈你对从事件分发的理解

一、面试场景

面试官:讲讲 Android 的事件分发机制?

菜鸟:基本会遵从 Activity => ViewGroup => View 的顺序进行事件分发,然后通过调用 onTouchEvent() 方法进行事件的处理。我们在项目中一般会对 MotionEvent.ACTION_DOWN,MotionEvent.ACTION_UP,MotionEvent.ACTION_MOVE,MotionEvent.ACTION_CANCEL 分情况进行操作。

面试官:有去查看源码中的事件拦截方法吗?或者说在进行事件分发的时候如何让正常的分发方式进行拦截?

菜鸟:我知道有个拦截事件的方法叫...叫,onInterceptEvent()?应该是,不过由于平时项目较多,确实没时间去关注太多源码。

面试官:厄,那你觉得在一个列表中,同时对父 View 和子 View 设置点击方法,优先响应哪个?为什么会这样?

菜鸟:肯定是优先响应子 View 的,至于为什么这样,平时知道这个结论,所以没去太深入研究

面试官:哦,这样子呀。那谈谈你对滑动冲突的理解,平时是如何处理滑动冲突的

菜鸟:滑动冲突就是父View和子View在处理MOVE事件上出现了冲突,但是由于平时遇到的比较少,所以并没有自己处理过滑动冲突的经验。

面试官:嗯,你的情况我基本了解了。您先回家等我们的结果吧。

我们可能经常会遇到上面的这种情况,面试官希望了解我们知识的深入情况,或者说是平时学习欲望到底怎样。可很不幸的是,我搞 模拟面试 以来,80% 的小伙伴都属于开发能力不错,可对类似事件分发这样的基础问题一概不知。究其原因,除去忙以外,大多数小伙伴还是觉得平时开发也用不上什么,即使用到了,直接 百度一下便能得到正确答案。这其中除了我们平时面向百度编程的习惯外,更重要的是没有去真正的理解这个事件分发机制的原理,更没有去扣源码的习惯,也没有自定义View的经验。于是尴尬的面试场景就来了。

而实际上要回答上面的几个问题,只要我们弄明白Android的事件分发机制就能够回答出80%了。

扫描二维码关注公众号,回复: 9458971 查看本文章

二、事件分发

1、事件分发的对象是谁

答案:点击事件(Touch事件)

而Touch事件又分为如下四种

事件

简介

ACTION_DOWN

手指 初次接触到屏幕 时触发

ACTION_MOVE

手指 在屏幕上滑动时触发,会多次触发

ACTION_UP

手指 离开屏幕 时触发

ACTION_CANCEL

事件 被上层拦截 时触发

一般情况下,事件列都是以DOWN事件开始、UP事件结束,中间有无数的MOVE事件,如下图:

即当一个点击事件(MotionEvent )产生后,系统需把这个事件传递给一个具体的 View 去处理。

而我们可以将事件传递这个过程分成三层:Activity、ViewGroup、View。

 

同时这个过程中,事件每经过一层都会触发如下三个方法。

  • dispatchTouchEvent()
  • onTouchEvent()
  • onInterceptTouchEvent()

类型

相关方法

Activity

ViewGroup

View

事件分发

dispatchTouchEvent

事件拦截

onInterceptTouchEvent

X

X

事件消费

onTouchEvent

X

 2、Activity事件分发

没源码没真相,首先我们来对源码进行分析


    public boolean dispatchTouchEvent(MotionEvent ev) {
        if (ev.getAction() == MotionEvent.ACTION_DOWN) {//事件分发都是从用户按下那一刻,产生DOWN事件开始的
            onUserInteraction();//一般都是用来做屏保功能的,但是平时开发中我们基本不会重写该方法
        }
        if (getWindow().superDispatchTouchEvent(ev)) {//将事件传递给PhoneWindow,再有PhoneWindow通过DecodView分发给ViewGroup
		//如果Activity下的控件消费了事件则直接返回,停止事件传递
            return true;
        }
        return onTouchEvent(ev);//没有控件消费事件则直接调用onTouchEvent
    }

3、ViewGroup事件分发

上面的源码我们说ViewGroup的事件是从Activity通过PhoneWindow再通过DecodeView传递的。

 

直接调用了 DecorView 的 superDispatchTouchEvent() 方法。DecorView 继承于 FrameLayout,作为顶层 View,是所有界面的父类。而 FrameLayout 作为 ViewGroup 的子类,所以直接调用了 ViewGroup 的 dispatchTouchEvent()。

而ViewGroup对事件分发的所有处理逻辑都在dispatchTouchEvent方法中,所以dispatchTouchEvent方法是我们必须要看的源码之一。

首先我们来看知识点:如图中红色框的内容,定义了一个标志位来判断该ViewGroup是否拦截事件,其中就调用了 onInterceptTouchEvent(ev) 对 intercept 进行赋值。大多数情况下,onInterceptTouchEvent() 返回值为 false,但我们完全可以通过重写 onInterceptTouchEvent(ev) 来改变它的返回值来完成对事件的拦截。

 当ViewGroup不对事件进行拦截的时候,就需要将事件分发到子View了,但是并不是所有的子View都要分发这个事件,而是通过如红框中所示根据用户的点击区域找到对应的子View。当找到在点击区域内的子View剩下的就是将事件分发给View了。

那么这个dispatchTransformedTouchEvent又干了什么呢?

 除了cancel事件还有其他的几个事件呀,其他事件怎么处理呢?我们继续往下看:

这里需要注意啦,这里说的子View本身可能也是一个ViewGroup。这就是著名的责任链模式。

 3、View事件分发

ViewGroup 说到底还是一个 View,而且ViewGroup包含着其他的子View,并可能将事件分发给这些子View。所以View 的 dispatchTouchEvent()方法同样是我们需要关注的重点

 红框中的三个条件,第一个我就不用说了。

  • (mViewFlags & ENABLED_MASK) == ENABLED

该条件是判断当前点击的控件是否为 enable,但由于基本 View 都是 enable 的,所以这个条件基本都返回 true。而handleScrollBarDragging()则是判断当前View是否正在处理拖拽。

  • mOnTouchListener.onTouch(this, event)

即我们调用 setOnTouchListener() 时必须覆盖的方法 onTouch() 的返回值。

  • 当上面两个判断执行后只要事件没有被消费,即result依旧为false。就会继续调用该View自身的onTouchEvent()方法。

从上述的分析,终于知道OnTouchListener的onTouch() 方法优先级高于View自身的 onTouchEvent(event) 方法是怎么来的了吧。

三、事件拦截

都在说事件拦截是ViewGroup独有的特性,因为它有一个抽象的onInterceptTouchEvent()方法供开发者去实现拦截功能。但如果自己做过滑动冲突的朋友应该明白,实际上事件拦截是分为两种的:父控件拦截、子控件拦截。这里我们就简单的说一下这两种拦截的方式。

1、父控件拦截

既然是父控件,那必然是ViewGroup了。所以重写onInterceptTouchEvent来实现拦截是必然的了。那么我们就跟着源码分析一下,重写这个onInterceptTouchEvent方法后,是如何实现拦截的?并且是否只要拦截一个事件其他事件同样不会往下传递了呢?

 2、子控件拦截

有人就奇怪了,View并没有onInterceptTouchEvent的方法,如何实现拦截呢?这里说的子控件拦截实质上是说子控件请求父类不要拦截事件,将事件分发到子View,由子View来消费该事件。

 当子View想要获取事件,但是事件又被父控件把持着不下发的时候就需要子View调用ViewGroup的requestDisallowInterceptTouchEvent(true)来请求事件(实际上就是设置一下mGroupFlags这个标志位的值)。但是,是否只要子View调用了这个方法就必然能够获取到事件呢?比如父控件的onInterceptTouchEvent()方法直接返回true的情况下。是否能够通过这种方法请求到事件下发呢?答案是不能!答案就在每次Down事件时,ViewGroup都会重置这个标志位,这就导致了子View发起的请求无效了。

 那么如何解决这个问题呢?既然在Down事件的时候都会被重置标志,那么如果跳过这个事件,让Down事件正常下发,然后再设置不就可以了吗?所以父控件只要拦截除了Down事件之外的事件就行了,而Down事件继续分发到子View。子View接收到Down事件后再调用requestDisallowInterceptTouchEvent(true)方法请求其他事件的下发。如果子View不需要某个事件了再调用requestDisallowInterceptTouchEvent(false)将事件拦截的权利重新交给父控件。

实际上我们在后续解决滑动冲突的时候遵循的就是这两种方式。

四、事件消费

我们都知道ViewGroup实质上也是继承自View,它同样有着View的属性。而在事件分发过程中,不管该事件是继续向子View下发还是拦截下来自己处理,最终都会执行View的dispatchTouchEvent()方法。

而事件的消费就从View的dispatchTouchEvent()开始。

 public boolean onTouchEvent(MotionEvent event) {
       .......
// 若该控件可点击,则进入switch判断中
        if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
            switch (action) {
 // a. 若当前的事件 = 抬起View(主要分析)
                case MotionEvent.ACTION_UP:
                   	...// 经过种种判断,此处省略

                            // Only perform take click actions if we were in the pressed state
                            if (!focusTaken) {
                                // Use a Runnable and post this rather than calling
                                // performClick directly. This lets other visual state
                                // of the view update before click actions start.
                                if (mPerformClick == null) {
                                    mPerformClick = new PerformClick();
                                }
// 执行performClickInternal() ->>分析1
                                if (!post(mPerformClick)) {
                                    performClickInternal();
                                }
                            }
                        }
 // b. 若当前的事件 = 按下View
                case MotionEvent.ACTION_DOWN:
                  ....
                    if (!clickable) {
//执行长按事件
                        checkForLongClick(
                                ViewConfiguration.getLongPressTimeout(),
                                x,
                                y,
                                TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__LONG_PRESS);
                        break;
                    }
               // c. 若当前的事件 = 结束事件(非人为原因)
                case MotionEvent.ACTION_CANCEL:
               	......
                    break;
 // d. 若当前的事件 = 滑动View
                case MotionEvent.ACTION_MOVE:
                   ......
                    break;
            }
 // 若该控件可点击,就一定返回true
            return true;
        }
 // 若该控件不可点击,就一定返回false
        return false;
    }
分析一
 private boolean performClickInternal() {
        // Must notify autofill manager before performing the click actions to avoid scenarios where
        // the app has a click listener that changes the state of views the autofill service might
        // be interested on.
        notifyAutofillManagerOnClick();

//分析二:执行performClick
        return performClick();
   }
分析二
 public boolean performClick() {
      ...
        if (li != null && li.mOnClickListener != null) {
            playSoundEffect(SoundEffectConstants.CLICK);
// 只要我们通过setOnClickListener()为控件View注册1个点击事件
            // 那么就会给mOnClickListener变量赋值(即不为空)
            // 则会往下回调onClick() & performClick()返回true
            li.mOnClickListener.onClick(this);
            result = true;
        } else {
            result = false;
        }
.....
        return result;
    }

 我们可以看到当View的dispatchTouchEvent返回true即表示该View消费该事件。

发布了29 篇原创文章 · 获赞 3 · 访问量 887

猜你喜欢

转载自blog.csdn.net/LVEfrist/article/details/103482052