您的位置:首页 > 移动开发 > Android开发

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

2016-06-01 18:01 981 查看

介绍

上一篇博客职责链/责任链模式(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左上角的横纵坐标。

重要的方法

public boolean dispatchTouchEvent(MotionEvent ev)

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

  

public boolean onInterceptTouchEvent(MotionEvent ev)

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

  

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。



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

分析:

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

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

public boolean onInterceptTouchEvent(MotionEvent ev) {
return false;
}


因为ViewGroup不拦截点击事件,事件开始分发,子控件View有机会得到事件,调用内部的两个方法处理事件,因为我默认没有做任何处理,View也不会处理事件,返回false。

最后因为Down事件没有控件响应。如果不消耗当前事件的话,那么对于在同一个事件序列当中,当前View就不会再次接收到事件。

所以手指在触摸到屏幕之后滑动,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。

同一事件序列是从手指触摸屏幕开始算起,手指离开屏幕结束。也就是Down事件开始+不定数目的Move事件+Up事件

正常情况下,一个事件序列只能被一个VIew拦截且消耗。因为一旦某个View拦截了此事件,那么同一事件序列内的所有事件都会直接交给它处理,所以同一事件序列中事件不能分发给两个View同时处理。

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

某个View一旦决定拦截事件之后,它的onInterceptTouchEvent不会再调用,所以的后继事件都直接给它处理而不再询问是否拦截。这在上的打印结果可以得到验证。

某个View一旦开始处理事件,如果它不消耗Down事件(onTouchEvent返回了false)那么同一事件序列中的其他事件都不会再交给它处理,并且事件将重新交由父View处理,即父View的onTouchEvent会被调用。也就是一旦事件交给了View而它没有消耗掉事件,之后的事件序列都不会再分发给它,父控件会开始尝试处理事件。这点在第一个张实验结果图可以得到验证。

如果View不消耗除Down以外的事件,那么这个点击事件会消失,并且父View的onTouchEvent不会调用,并且当前View可以持续收到后续事件,最终这些消失的点击事件会传递给Activity处理。

ViewGroup默认不拦截任何事件,具体请看上文。

真正的View没有onInterceptTouchEvent方法,一旦有事件发给它,它的onTouchEvent就会调用。

onClick会发生的前提是当前View可点击,并且它收到了Down事件和Up事件。

事件传递过程是由外向内传递的。即事件总是先传给父View,然后由父View决定分发。通过requestDisallowInterceptTouchEvent方法可以在子View中干预父View的事件分发过程,但是Down事件除外。

总结

本文部分内容来自《Android开发艺术探索》,书里面有具体的源码分析,感兴趣的可以去看。

本文主要以实验论证部分书中结论,并且提出滑动冲突的一个解决方案使用NestedScrollView,并且Android源码中很多控件都实现了NestedScrollView方法。

本文还结合职责链模式分析View事件分发机制。通过更高层次的抽象分析帮助理解实现原理。结合部分源码分析,并没有陷入源码中不能自拔。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息