1.为什么要有事件分发机制?
因为安卓上面的View是树形结构,View可能会重叠在一起,当我们点击的地方有多个View的时候,事件应该分发给谁呢?
这时候就有了事件分发机制。
2.View的结构。
3.事件分发,消费与拦截
4.事件分发的流程
前面了解到View是树形的结构,所以我们的事件也是有序的分发的。
事件收集之后最先传给Activity,然后依次向下传递,最后传入到底层的View。前提是事件没有发生拦截
Activity->photoWindow-DecorView->ViewGroup->.....->View
如果View消费了事件的话,则会返回true表示消费了该事件。
也会依次传递给上层的结构。
如果View没有消费事件的话,事件也不会浪费,则View会返回false,按照反方向回传,最终传会给Activity,如果Activity 也没有处理,则该事件就会被抛弃。
Activity <- PhoneWindow <- DecorView <- ViewGroup <- ... <- View
由此可见,这种事件传递机制是一种非常经典的责任链模式: 如果我能处理就拦截下来自己干,如果自己不能处理或者不确定就交给责任链中下一个对象。
这种设计是非常精巧的,上层View既可以直接拦截该事件,自己处理,也可以先询问(分发给)子View,如果子View需要就交给子View处理,如果子View不需要还能继续交给上层View处理。既保证了事件的有序性,又非常的灵活。
5.实例讲解
1.首先要明确View和ViewGroup中的方法
2.创建一个Demo演示。
①自定义一个ViewGroup
继承RelativeLayout是为了让RelativeLayout帮助我们将子控件布局好。
并重写ViewGroup的onInterceptTouchEvent,dispatchTouchEvent,onTouchEvent这三个方法。
并将其方法的返回值定义成一个变量,然后将这个变量返回,然后在通过Log将变量打印出来。
通过这个Log打印方式可以清楚的知道,这些方法的调用顺序
代码如下:
public class MyViewGroup extends RelativeLayout {
public MyViewGroup(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
Log.v("view",this.getClass().getName()+"---onInterceptTouchEvent 开始"+ev.getAction());
boolean onInterceptTouchEvent = super.onInterceptTouchEvent(ev);
Log.v("view",this.getClass().getName()+"---onInterceptTouchEvent 结束:"+ev.getAction() +":返回:"+onInterceptTouchEvent);
return onInterceptTouchEvent;
}
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
Log.v("view",this.getClass().getName()+"---dispatchTouchEvent 开始:"+ev.getAction());
boolean dispatchTouchEvent = super.dispatchTouchEvent(ev);
Log.v("view",this.getClass().getName()+"---dispatchTouchEvent 结束:"+ev.getAction() +":返回:"+dispatchTouchEvent);
return dispatchTouchEvent;
}
@Override
public boolean onTouchEvent(MotionEvent event) {
Log.v("view",this.getClass().getName()+"---onTouchEvent 开始:"+event.getAction());
boolean onTouchEvent = super.onTouchEvent(event);
Log.v("view",this.getClass().getName()+"---onTouchEvent 结束:"+event.getAction() +":返回:"+onTouchEvent);
return onTouchEvent;
}
}
②自定义一个View
代码如下:
重写其dispatchTouchEvent,与onTouchEvent方法,而且Log的方式与ViewGroup一致,就不做多解释。
public class MyView extends View {
public MyView(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
Log.v("view",this.getClass().getName()+"---dispatchTouchEvent 开始:"+ev.getAction());
boolean dispatchTouchEvent = super.dispatchTouchEvent(ev);
Log.v("view",this.getClass().getName()+"---dispatchTouchEvent 结束:"+ev.getAction() +":返回:"+dispatchTouchEvent);
return dispatchTouchEvent;
}
@Override
public boolean onTouchEvent(MotionEvent event) {
Log.v("view",this.getClass().getName()+"---onTouchEvent 开始:"+event.getAction());
boolean onTouchEvent = super.onTouchEvent(event);
Log.v("view",this.getClass().getName()+"---onTouchEvent 结束:"+event.getAction() +":返回:"+onTouchEvent);
return onTouchEvent;
}
}
③将其布局到MainActivity中
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/activity_main"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.testproject.testproject.MyViewGroup
android:layout_width="200dp"
android:layout_height="200dp"
android:layout_centerInParent="true"
android:background="@color/colorPrimary"
>
<com.testproject.testproject.MyView
android:layout_width="100dp"
android:layout_height="100dp"
android:layout_centerInParent="true"
android:background="@color/colorAccent"
/>
</com.testproject.testproject.MyViewGroup>
</RelativeLayout>
④MainActivity代码,我们也将MainActivity的dispatchTouchEvent,onTouchEvent重写,并Log方式与上文一致。
因为事件传递的源头是Activity,而事件传递的终结点也是Activity,所以此处一定要Log,用于判断事件传递的流程。
我们先将其注释,等需要用到的时候在使用它。
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
/* @Override
public boolean dispatchTouchEvent(MotionEvent ev) {
Log.v("view",this.getClass().getName() + "---dispatchTouchEvent 开始:" + ev.getAction());
boolean dispatchTouchEvent = super.dispatchTouchEvent(ev);
Log.v("view",this.getClass().getName()+"---dispatchTouchEvent 结束:"+ev.getAction() +":返回:"+dispatchTouchEvent);
return dispatchTouchEvent;
}
@Override
public boolean onTouchEvent(MotionEvent event) {
Log.v("view",this.getClass().getName()+"---onTouchEvent 开始:"+event.getAction());
boolean onTouchEvent = super.onTouchEvent(event);
Log.v("view",this.getClass().getName()+"---onTouchEvent 结束:"+event.getAction() +":返回:"+onTouchEvent);
return onTouchEvent;
}*/
}
⑤则实际效果如下
ViewGroup 包裹着View
1.当我们点击view的时候,则会产生一系列的Log。
tips:
触摸事件有三种形式:分别由0,1,2表示
Action.DOWN 对应 0
Action.MOVE 对应 1
Action.UP 对应 2
通过Log可以得知,此时我们是点击事件,所以Action =0;
则由Log可以得到
此时事件分发机制的顺序是:
①当产生点击事件时
②先传递到最上层的MyViewGroup中,调用其dispatchTouchEvent 方法,然后在调用其onInterceptTouchEvent方法,如果不拦截则onInterceptTouchEvent结束,并返回false。此时MyViewGroup中的dispatchTouchEvent方法并没有结束,因为我们没有此时还没有方法结束的Log,所以是在dispatchTouchEvent方法中调用了自身的onInterceptTouchEvent方法,来判断是否拦截,不拦截则返回fasle,拦截返回true。
tips:ViewGroup中的onInterceptTouchEvent方法默认是返回false,不拦截。
③则ViewGroup中的onInterceptTouchEvent方法结束之后,会调用View的dispatchTouchEvent,则此时View的dispatchTouchEvent开始,dispatchTouchEvent方法会调用View的onTouchEvent方法,如果onTouchEvent消费事件的话则返回true,不消费的话则会返回false.
因为我们在View的onTouchEvent方法的返回值是默认实现的,所以其返回值是false.则是不消费事件,则事件会回传。
④当View的onTouchEvent返回false,说明事件View不消费,此时会将事件会传到上层,当onTouchEvent返回false后,会调用View的dispatchTouchEvent,因为没有消费事件,所以View的dispatchTouchEvent会结束,并返回false,开始向上一层传递。此时事件开始在View中的处理就结束了,事件开始上传。
⑤当View的dispatchTouchEvent返回false,会上传到ViewGroup的onTouchEvent方法,判断ViewGroup是否消费该事件,返回false,则说明ViewGroup不消费该事件,则会onTouchEvent返回false之后,ViewGroup的dispatchTouchEvent就结束了,然后返回了false。此时Log就结束了。
⑥我们只打印了View和ViewGroup,但是事件分发机制的顺序也差不多就是这样。因为这个事件都没有控件消费,所以最终源头会会传到Activity,然后浪费掉此事件。通过一张图来表示其顺序,
注意: 上图显示分发流程仅仅是一个示意流程,并不代表实际情况,如果按照实际情况绘制,会导致流程图非常复杂和混乱。
上面的流程中存在部分不合理内容,请大家选择性接受。
事件返回时 dispatchTouchEvent
直接指向了父View的 onTouchEvent
这一部分是不合理的,实际上它仅仅是给了父View的 dispatchTouchEvent
一个 false 返回值,父View根据返回值来调用自身的onTouchEvent
。
ViewGroup 是根据 onInterceptTouchEvent
的返回值来确定是调用子View的 dispatchTouchEvent
还是自身的 onTouchEvent
, 并没有将调用交给 onInterceptTouchEvent
。
ViewGroup 的事件分发机制伪代码如下,可以看出调用的顺序。
public boolean dispatchTouchEvent(MotionEvent ev) { boolean result = false; // 默认状态为没有消费过 if (!onInterceptTouchEvent(ev)) { // 如果没有拦截交给子View result = child.dispatchTouchEvent(ev); } if (!result) { // 如果事件没有被消费,询问自身onTouchEvent result = onTouchEvent(ev); } return result;}
2.上一个流程View没有消费事件,因为onTouchEvent的返回值为true,当View消费掉事件时,则Log如下:
当View消费掉事件后,则会返回true,当事件回传时,也不会去询问ViewGroup是否消费,则会直接回传,并返回true.
3.当ViewGroup中的onInterceptTouchEvent拦截事件时
当ViewGroup拦截时,则会直接询问是否消费事件,如果不消费事件则返回false,事件会依旧上传。
4.当我们也启用Activity中的dispatchTouchEvent和onTouchEvent时,且其余的事件返回值都是默认。
事件传递和上面的 《1.》是差不多的,不过是又多了一层Activity的Log,说明事件传递的方法调用顺序和我们预想的是一样的。
但是特殊的是,UP事件并没有和DOWN事件一样,有那么多的方法的调用。
这是因为安卓自身的问题,因为:
事件传递过程中,如果一个控件在onTouchEvent方法中没有消费DOWN事件,以后父容器不再给你传递任何事件。
所以需要我们如果要消费事件的话一定要返回true,不然后续的事件也会接收不到。
5.和《3》一样,当ViewGroup拦截事件时。
则事件的传递和《3》是大同小异的,不过是多了回传给Activity。
而且DOWN事件因为没有消费,则UP事件也没有向下传递,更验证了我们《4》的结论。
事件传递过程中,如果一个控件在onTouchEvent方法中没有消费DOWN事件,以后父容器不再给你传递任何事件。
通过以上5个小例子,相信大家对事件传递的机制有了一些了解。
通过这些我们也得到了一些结论:
事件是一组一组的,以DOWN开始,0到n个MOVE,以UP结尾
事件是从外出ViewGroup往内层传递,如果过程中有控件消费(onTouchEvent),事件不再往下传递,如果过程中没有被消费,事件再依次回传,最终回传到Activity中 ViewGroup可以拦截事件,默认5大布局都不拦截事件,如果拦截了事件,调用自己的onTouchEvent方法,事件不往下传递
事件传递过程中,如果一个控件在onTouchEvent方法中没有消费DOWN事件,以后父容器不再给你传递任何事件
其他情况
事件分发机制设计到到情形非常多,这里就不一一列举了,记住以下几条原则就行了。
1.如果事件被消费,就意味着事件信息传递终止。
2.如果事件一直没有被消费,最后会传给Activity,如果Activity也不需要就被抛弃。
3.判断事件是否被消费是根据返回值,而不是根据你是否使用了事件。
总结
View的事件分发机制实际上就是一个非常经典的责任链模式,如果你了解责任链模式,那么事件分发对你来说并不是什么难题,如果你不了解责任链模式,刚好借此机会学习一下啦。
责任链模式:当有多个对象均可以处理同一请求的时候,将这些对象串联成一条链,并沿着这条链传递改请求,直到有对象处理它为止。