介绍
上一篇博客职责链/责任链模式(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事件分发机制。通过更高层次的抽象分析帮助理解实现原理。结合部分源码分析,并没有陷入源码中不能自拔。