Android开发-分析ViewGroup、View的事件分发机制、结合职责链模式

介绍

上一篇博客职责链/责任链模式(Chain of Responsibility)分析理解和在Android的应用

介绍了职责链模式,作为理解View事件分发机制的基础。

套用职责链模式的结构分析,当我们的手指在屏幕上点击或者滑动,就是一个事件,每个显示在屏幕上的View或者ViewGroup就是职责对象,它们通过Android中视图层级组织关系,层层传递事件,直到有职责对象处理消耗事件,或者没有职责对象处理导致事件消失。

关键概念介绍

要理解有关View的事件分发,先要看几个关键概念

MotionEvent

当手指接触到屏幕以后,所产生的一系列的事件中,都是由以下三种事件类型组成。

  1. ACTION_DOWN: 手指按下屏幕

  2. ACTION_MOVE: 手指在屏幕上移动

  3. ACTION_UP: 手指从屏幕上抬起

  例如一个简单的屏幕触摸动作触发了一系列Touch事件:ACTION_DOWN->ACTION_MOVE->…->ACTION_MOVE->ACTION_UP

  对于Android中的这个事件分发机制,其中的这个事件指的就是MotionEvent。而View的对事件的分发也是对MotionEvent的分发操作。可以通过getRawX和getRawY来获取事件相对于屏幕左上角的横纵坐标。通过getX()和getY()来获取事件相对于当前View左上角的横纵坐标。

重要的方法

  1. public boolean dispatchTouchEvent(MotionEvent ev)

      这是一个对事件分发的方法。如果一个事件传递给了当前的View,那么当前View一定会调用该方法。对于dispatchTouchEvent的返回类型是boolean类型的,返回结果表示是否消耗了这个事件,如果返回的是true,就表明了这个View已经被消耗,不会再继续向下传递。  

      

  2. public boolean onInterceptTouchEvent(MotionEvent ev)

      该方法存在于ViewGroup类中,对于View类并无此方法。表示是否拦截某个事件,ViewGroup如果成功拦截某个事件,那么这个事件就不在向下进行传递。对于同一个事件序列当中,当前View若是成功拦截该事件,那么对于后面的一系列事件不会再次调用该方法。返回的结果表示是否拦截当前事件,默认返回false。由于一个View它已经处于最底层,它不会存在子控件,所以无该方法。

      

  3. public boolean onTouchEvent(MotionEvent event)

      这个方法被dispatchTouchEvent调用,用来处理事件,对于返回的结果用来表示是否消耗掉当前事件。如果不消耗当前事件的话,那么对于在同一个事件序列当中,当前View就不会再次接收到事件。

上文部分内容来自《Android开发艺术探索》

代码实验

为了验证和理解实际的运行状态,重写View和ViewGroup这些关键方法,打印方法调用。

代码

继承View重写方法,加入结果打印。

注:View作为子控件,不存在内部子控件,所以传入事件就视图处理,而不存在拦截子控件的事件,所以没有onInterceptTouchEvent方法

public class MyView extends View {
    public MyView(Context context) {
        super(context);
    }

    public MyView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public MyView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        boolean result=super.dispatchTouchEvent(ev);
        Logger.d("result= "+result+" info="+MotionEvent.actionToString(ev.getAction()));
        return result;
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        boolean result=super.onTouchEvent(event);
        Logger.d("result= "+result+" info="+MotionEvent.actionToString(event.getAction()));
        return result;
    }
}

继承ViewGroup重写方法,加入结果打印。

注:ViewGroup没有实现onLayout布置控件位置,所以继承LinearLayout,对分发不影响

public class MyViewGroup extends LinearLayout {
    public MyViewGroup(Context context) {
        super(context);
    }

    public MyViewGroup(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public MyViewGroup(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        boolean result=super.dispatchTouchEvent(ev);
        Logger.d("result= "+result+" info="+MotionEvent.actionToString(ev.getAction()));
        return result;
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        boolean result=super.onTouchEvent(event);
        Logger.d("result= "+result+" info="+MotionEvent.actionToString(event.getAction()));
        return result;
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        boolean result=super.onInterceptTouchEvent(ev);
        Logger.d("result= "+result+" info="+MotionEvent.actionToString(ev.getAction()));
        return result;
    }

}

最后把这两个控件加入布局文件就可以了。

<com.demo.licola.HttpDemo.view.MyViewGroup
        android:layout_width="200dp"
        android:layout_height="200dp"
        android:id="@+id/ll_group"
        android:background="@color/saffron"
        >
        <com.demo.licola.HttpDemo.view.MyView
            android:layout_width="100dp"
            android:layout_height="100dp"
            android:id="@+id/view_child"
            android:background="@color/colorAccent"
            />

    </com.demo.licola.HttpDemo.view.MyViewGroup>

测试

直接运行

首先运行上面的代码后,可以看到两个色块。用手机点击里面的MyViewGroup的子控件MyView。

相信我,不论怎么滑动或者点击都是一样的结果,下面会分析这样的情况发生原因。

分析:

  1. 手指点击在MyViewGroup中方法onInterceptTouchEvent开始调用,判断是否拦截这个点击事件。

    ViewGroup2717行代码,源码中ViewGroup默认是不拦截事件的:

public boolean onInterceptTouchEvent(MotionEvent ev) {
        return false;
    }
  1. 因为ViewGroup不拦截点击事件,事件开始分发,子控件View有机会得到事件,调用内部的两个方法处理事件,因为我默认没有做任何处理,View也不会处理事件,返回false。
  2. 最后因为Down事件没有控件响应。如果不消耗当前事件的话,那么对于在同一个事件序列当中,当前View就不会再次接收到事件。
  3. 所以手指在触摸到屏幕之后滑动,View也就接受不到Move事件。所以手指怎么滑动都没有其他的日志结果打印。除非抬起,再按下生成新的事件,又看到同样的打印结果。

让子控件响应事件

修改代码,给View添加点击事件,也就是使用setOnClickListener简单的处理。

然后手指点击迅速抬起,要不然打印结果太多了。

分析:产生点击事件,子控件onTouchEvent可以处理事件,得到Down事件,同时影响ViewGroup父控件dispatchTouchEvent返回ture可以向下分发,后继的UP事件也相继的传进来。

让父控件拦截事件

在原基础上,再次修改代码,给ViewGroup父控件的onInterceptTouchEvent方法返回true,表示拦截事件。然后给ViewGroup添加点击监听回调setOnClickListener

分析:有了上面的基础,这里就什么好说的了,因为父控件拦截了事件,同时能够响应事件,所有的事件都发送到ViewGroup上。

总结

通过上面的3个实验的结果,可以大概对ViewGourp的事件分发有个基本的认识。

所以通过抽象源码提取关键实现,可以有下面的大概处理逻辑。

在ViewGroup中有如下逻辑:

 public boolean dispatchTouchEvent(MotionEvent ev) {

        boolean consume =false;//默认不处理
        if (onInterceptTouchEvent(ev)){//首先判断是否拦截
            consume=onTouchEvent(ev);//看是否能够处理
        }else {
        //如果不拦截,遍历子控件,这里省略掉
        //调用子控件的分发方法,下发事件。
            consume=getChildView.dispatchTouchEvent(ev);
        }
        return consume;
    }

结合职责链模式分析

我在上篇博客介绍分析了职责链模式,用该模式的思想来分析。

可以画出这样的UML图:

当然这是简略的画法,实际会复杂得多,而且View和VIewGorup采用设计模式组合模式的思想构造。

  • Cilent:表示发出请求的对象,在这里应该对应的是Activity。
  • ViewHandler:内部以数组的方式持有后继者,使用的时候采用从最后一位遍历。dispatchTouchEvent方法表示统一的事件请求方法
  • View和ViewGourp:都是实际的实现职责类,内部通过调用其他的方法判断是否能够处理请求事件。

    链的构造:

    很明显链的构造是在ViewHandler中组装的,内部链的实现方式。onTouchEvent和onInterceptTouchEvent都会影响链的构造。动态的生成职责链。

ViewGroup/View的事件分发机制总结

最后提出一些结论,给大家一些对事件分发的提示和总结。说不定面试的时候就用上了呢。

提示:ViewGroup在继承关系上继承View,所以下文可以用View指代ViewGroup。具体的原因自行Google。

  1. 同一事件序列是从手指触摸屏幕开始算起,手指离开屏幕结束。也就是Down事件开始+不定数目的Move事件+Up事件
  2. 正常情况下,一个事件序列只能被一个VIew拦截且消耗。因为一旦某个View拦截了此事件,那么同一事件序列内的所有事件都会直接交给它处理,所以同一事件序列中事件不能分发给两个View同时处理。

这个就是我们在处理一些嵌套滑动时候遇到的主要问题。子控件拦截了事件,View对这个滑动事件不想要处理的时候,只能抛弃这个事件,而不会把这些传给父view去处理。这就是滑动的嵌套的父子控件同方向滑动不流畅的原因。好消息时NestedScrollView的出现很好的解决了这个问题。

  1. 某个View一旦决定拦截事件之后,它的onInterceptTouchEvent不会再调用,所以的后继事件都直接给它处理而不再询问是否拦截。这在上的打印结果可以得到验证。
  2. 某个View一旦开始处理事件,如果它不消耗Down事件(onTouchEvent返回了false)那么同一事件序列中的其他事件都不会再交给它处理,并且事件将重新交由父View处理,即父View的onTouchEvent会被调用。也就是一旦事件交给了View而它没有消耗掉事件,之后的事件序列都不会再分发给它,父控件会开始尝试处理事件。这点在第一个张实验结果图可以得到验证。
  3. 如果View不消耗除Down以外的事件,那么这个点击事件会消失,并且父View的onTouchEvent不会调用,并且当前View可以持续收到后续事件,最终这些消失的点击事件会传递给Activity处理。
  4. ViewGroup默认不拦截任何事件,具体请看上文。
  5. 真正的View没有onInterceptTouchEvent方法,一旦有事件发给它,它的onTouchEvent就会调用。
  6. onClick会发生的前提是当前View可点击,并且它收到了Down事件和Up事件。
  7. 事件传递过程是由外向内传递的。即事件总是先传给父View,然后由父View决定分发。通过requestDisallowInterceptTouchEvent方法可以在子View中干预父View的事件分发过程,但是Down事件除外。

总结

  • 本文部分内容来自《Android开发艺术探索》,书里面有具体的源码分析,感兴趣的可以去看。
  • 本文主要以实验论证部分书中结论,并且提出滑动冲突的一个解决方案使用NestedScrollView,并且Android源码中很多控件都实现了NestedScrollView方法。
  • 本文还结合职责链模式分析View事件分发机制。通过更高层次的抽象分析帮助理解实现原理。结合部分源码分析,并没有陷入源码中不能自拔。
时间: 2024-10-21 20:16:48

Android开发-分析ViewGroup、View的事件分发机制、结合职责链模式的相关文章

Android View的事件分发机制

准备了一阵子,一直想写一篇事件分发的文章总结一下.这个知识点实在是太重要了. 一个应用的布局是丰富的,有TextView,ImageView,Button等.这些子View的外层还有ViewGroup.如RelativeLayout.LinearLayout.作为一个开发人员,我们会思考.当点击一个button,Android系统是如何确定我点的就是button而不是TextView的?然后还正确的响应了button的点击事件. 内部经过了一系列什么过程呢? 先铺垫一些知识能更加清晰的理解事件分

View的事件分发机制解析

引言 Android事件构成 在Android中,事件主要包含点按.长按.拖拽.滑动等,点按又包含单击和双击,另外还包含单指操作和多指操作.全部这些都构成了Android中的事件响应.总的来说.全部的事件都由例如以下三个部分作为基础: 按下(ACTION_DOWN) 移动(ACTION_MOVE) 抬起(ACTION_UP) 全部的操作事件首先必须运行的是按下操作(ACTION_DOWN).之后全部的操作都是以按下操作作为前提,当按下操作完毕后.接下来可能是一段移动(ACTION_MOVE)然后

13.View的事件分发机制——dispatchTouchEvent详解

在前面的第二篇文章中,我们提过,View的事件分发是一种委托思想:上层委托下层,父容器委托子元素来处理这个流程.接下来,我们就将深入去学习View的事件分发机制. 1.事件的传递流程 事件,在Android中对应的类是MotionEvent,因此,我们要分析的就是MotionEvent这个类.对点击事件的分发,其实就是对MotionEvent的对象进行处分发.所以,当一个MotionEvent产生以后(从驱动读取),系统需要把这个事件传递给一个具体的View,这个传递的过程就是分发过程,点击事件

Android 开发艺术探究V第三章之view的事件分发机制

在介绍点击事件的传递机制,首先我们要分析的对象就是MOtionEvent,即点击事件,(当点击屏幕时由硬件传递过来,关于MotionEvent在View的基础知识中做了介绍),所谓的点击事件的分发就是MotionEvent的分发过程.即当一个MoTionEvent产生以后,系统需要把这个事件具体传递给一个具体的View,而这个传递过程就是分发过程,点击事件传递过程有三个很重要的方法,下面先来介绍这几个方法.  public boolean dispatchTouchEvent(MOtionEve

Android View的事件分发机制探索

概述 Android事件传递机制也是Android系统中比较重要的一块,事件类型有很多种,这里主要讨论TouchEvent的事件在framework层的传递处理机制.因为对于App开发人员来说,理解framework层的事件传递机制,就差不多了. 带着问题来思考整个事件分发过程. 1.为什么要有事件分发过程? 当Android设备的屏幕,接收到触摸的动作时,屏幕驱动把压力信号(包括压力大小,压力位置等)传递给系统底层,然后操作系统经过一系列的处理,然后把触摸事件一层一层的向上传递,最终事件会被准

Android view 的事件分发机制

1 事件的传递顺序是 Activity -> Window -> 顶层View touch 事件产生后,最先由 activity 的 dispatchTouchEvent 处理 /** * Called to process touch screen events. You can override this to * intercept all touch screen events before they are dispatched to the * window. Be sure to

Android View体系(五)从源码解析View的事件分发机制

相关文章 Android View体系(一)视图坐标系 Android View体系(二)实现View滑动的六种方法 Android View体系(三)属性动画 Android View体系(四)从源码解析Scroller 前言 三年前写过事件分发机制的文章但是写的不是很好,所以重新再写一篇,关于事件分发机制的文章已经有很多,但是希望我这篇是最简洁.最易懂的一篇. 1.处理点击事件的方法 View的层级 我们知道View的结构是树形的结构,View可以放在ViewGroup中,这个ViewGro

Android中View的事件分发机制

简介 事件也称MotionEvent,事件分发机制就是对MotionEvent事件的分发过程,即当一个MotionEvent发生之后,系统需要把这个事件传递给一个具体的View. 点击事件的分发过程由三个函数共同完成: dispatchTouchEvent(DTE) - 进行事件的分发,如果时间能够传递给当前View,该方法会被调用,返回结果受当前view的onTouchEvent, 子View的dispatchTouchEvent影响,表示是否消耗当前事件. onInterceptTouchE

Android中View的事件分发机制——Android开发艺术探索笔记

欢迎转载,转载请注明出处http://blog.csdn.net/l664675249/article/details/50738102 介绍 点击事件的事件分发就是对MotionEvent事件的分发过程,当一个MotionEvent产生了以后,系统需要把这个事件传递给一个具体的View,而这个传递的过程就是分发的过程. 涉及到的三个方法 dispatchTouchEvent:用来进行事件的分发,如果事件能够传递给当前View,那么此方法一定会被调用,返回结果受当前View的onTouchEve