事件分发的逻辑理清了吗

一、背景

        在Android开发过程,不管你乐意与否一定会接触到自定义View,而自定View的时候一定会遇到各种各样的坑等着你。而滑动冲突就是其中的一个坑。而填坑的解决方案就在源码中。所以理清楚事件分发的逻辑至关重要。

二、事件分发

1、事件分发的对象是谁

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

而Touch事件又分为如下四种

事件

简介

ACTION_DOWN

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

ACTION_MOVE

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

ACTION_UP

手指 离开屏幕 时触发

ACTION_CANCEL

事件 被上层拦截 时触发

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

事件在传递的过程会经历三层如下图:

这个过程中,事件每经过一层都会触发一个最关键的核心方法:

  • dispatchTouchEvent()

 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的disPatchTouchEvent中首先会做如下逻辑判断:

  • 是Down事件:
    • 拦截事件:执行ViewGroup自身的supper.dispatchTouchEvent()方法来执行事件处理
    • 不拦截事件:向下分发,根据Event事件的坐标信息,判断点击是否在子View的范围内,在子View的范围内分发给子View。如果子View消费了事件(返回true),则记录消费事件的子View(firstTargetTouch)。
  • 非Down事件:
    • 之前的事件 有子View消费(firstTargetTouch != null):判断是否拦截事件,如果拦截执行ViewGroup自身的supper.dispatchTouchEvent()方法来执行事件处理,如果不拦截则直接分发给firstTargetTouch处理。
    • 之前的事件无子View消费(firstTargetTouch == null):直接拦截, 执行ViewGroup自身的supper.dispatchTouchEvent()方法来执行事件处理                 

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

 3、View事件处理

ViewGroup 将事件传递到子View后,如果子View不是ViewGroup就会执行子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,由子View来消费该事件。

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

所以子控件拦截的前提条件就是父控件不能拦截Down事件,父控件只会拦截除了Down事件之外的事件,而Down事件继续分发到子View。子View接收到Down事件后再调用requestDisallowInterceptTouchEvent(true)方法请求其他事件的下发。如果子View不需要某个事件了再调用requestDisallowInterceptTouchEvent(false)将事件拦截的权利重新交给父控件。

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

四、事件消费

我们都知道ViewGroup实质上也是继承自View,它同样有着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;
    }

当dispatchTouchEventt返回true即表示View消费了该事件,他的父View会记录消费了事件的子View。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值