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

Posted 李可乐

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Android开发-分析ViewGroupView的事件分发机制结合职责链模式相关的知识,希望对你有一定的参考价值。

介绍

上一篇博客职责链/责任链模式(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。

First

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

分析:

  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简单的处理。
然后手指点击迅速抬起,要不然打印结果太多了。
View响应事件

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

让父控件拦截事件

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

ViewGorup拦截事件

分析:有了上面的基础,这里就什么好说的了,因为父控件拦截了事件,同时能够响应事件,所有的事件都发送到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图:
ViewGroupUML图

当然这是简略的画法,实际会复杂得多,而且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事件分发机制。通过更高层次的抽象分析帮助理解实现原理。结合部分源码分析,并没有陷入源码中不能自拔。

以上是关于Android开发-分析ViewGroupView的事件分发机制结合职责链模式的主要内容,如果未能解决你的问题,请参考以下文章

Android开发学习总结——Android应用目录结构分析(转)

Android应用开发项目结构分析

Android开发学习之路-Handler消息派发机制源码分析

android 网络框架 源码分析

Android开发艺术探索——第七章:Android动画深入分析

Android开发学习------项目结构分析