前言
事件传递在android中是最基础的知识,但也是最繁琐复杂的知识,在5.0后传递变的更加复杂,网上有很多文章分析事件分发,但是一开始就是在源码里面进行各种讲解这让整个事件传递消费事件的宏观性不强,本文章从最基础的日志开始分析传递事件,如果有耐心把文章认真分析完,我相信事件传递你已经了解了一半了!后面再配合源码进行理解,相信能做到更好!
方法解释
- dispatchTouchEvent:事件分发
- onInterceptTouchEvent:事件拦截
- onTouchEvent:事件处理
- 内向传递:事件传递由外向内传递
- 外向处理:事件处理由内向外传递
注意:viewgroup中包含如上三个方法,但是view只包含其中两个,没有onInterceptTouchEvent方法
自定义组件
为了更好的解释传递与消费的过程,我定义了两个容器两个view,分别是ViewGroupA,ViewGroupB , viewC ,ViewD,并分别重写他们的onInterceptTouchEvent,dispatchTouchEvent,onTouchEvent
代码如下:
public class ViewGroupA extends LinearLayout {
public ViewGroupA(Context context) {
super(context);
}
public ViewGroupA(Context context, AttributeSet attrs) {
super(context, attrs);
}
public ViewGroupA(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
Log.d(LOG_ID, this.getClass().getSimpleName() + " onInterceptTouchEvent -> " + ViewUtils.actionToString(ev.getAction()));
boolean result = super.onInterceptTouchEvent(ev);
Log.d(LOG_ID, this.getClass().getSimpleName() + " onInterceptTouchEvent return super.onInterceptTouchEvent(ev)=" + result);
return result;
}
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
Log.d(LOG_ID, this.getClass().getSimpleName() + " dispatchTouchEvent -> " + ViewUtils.actionToString(ev.getAction()));
boolean result = super.dispatchTouchEvent(ev);
Log.d(LOG_ID, this.getClass().getSimpleName() + " dispatchTouchEvent return super.dispatchTouchEvent(ev)= " + result);
return result;
}
@SuppressLint("ClickableViewAccessibility")
@Override
public boolean onTouchEvent(MotionEvent ev) {
Log.d(LOG_ID, this.getClass().getSimpleName() + " onTouchEvent -> " + ViewUtils.actionToString(ev.getAction()));
boolean result = super.onTouchEvent(ev);
Log.d(LOG_ID, this.getClass().getSimpleName() + " onTouchEvent return super.onTouchEvent(ev)=" + result);
return result;
}
}
public class ViewGroupB extends LinearLayout {
public ViewGroupB(Context context) {
super(context);
}
public ViewGroupB(Context context, AttributeSet attrs) {
super(context, attrs);
}
public ViewGroupB(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
Log.d(LOG_ID, this.getClass().getSimpleName() + " onInterceptTouchEvent -> " + ViewUtils.actionToString(ev.getAction()));
boolean result = super.onInterceptTouchEvent(ev);
Log.d(LOG_ID, this.getClass().getSimpleName() + " onInterceptTouchEvent return super.onInterceptTouchEvent(ev)=" + result);
return result;
}
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
Log.d(LOG_ID, this.getClass().getSimpleName() + " dispatchTouchEvent -> " + ViewUtils.actionToString(ev.getAction()));
boolean result = super.dispatchTouchEvent(ev);
Log.d(LOG_ID, this.getClass().getSimpleName() + " dispatchTouchEvent return super.dispatchTouchEvent(ev)= " + result);
return result;
}
@Override
public boolean onTouchEvent(MotionEvent ev) {
Log.d(LOG_ID, this.getClass().getSimpleName() + " onTouchEvent -> " + ViewUtils.actionToString(ev.getAction()));
boolean result = super.onTouchEvent(ev);
//boolean result = true;
Log.d(LOG_ID, this.getClass().getSimpleName() + " onTouchEvent return super.onTouchEvent(ev)=" + result);
return result;
}
}
public class ViewC extends View {
public ViewC(Context context) {
super(context);
}
public ViewC(Context context, AttributeSet attrs) {
super(context, attrs);
}
public ViewC(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
Log.d(LOG_ID, this.getClass().getSimpleName() + " dispatchTouchEvent -> " + ViewUtils.actionToString(ev.getAction()));
boolean result = super.dispatchTouchEvent(ev);
Log.d(LOG_ID, this.getClass().getSimpleName() + " dispatchTouchEvent return super.dispatchTouchEvent(ev)= " + result);
return result;
}
@Override
public boolean onTouchEvent(MotionEvent ev) {
Log.d(LOG_ID, this.getClass().getSimpleName() + " onTouchEvent -> " + ViewUtils.actionToString(ev.getAction()));
boolean result = super.onTouchEvent(ev);
Log.d(LOG_ID, this.getClass().getSimpleName() + " onTouchEvent return super.onTouchEvent(ev)=" + result);
return result;
}
}
public class ViewD extends View {
public ViewD(Context context) {
super(context);
}
public ViewD(Context context, AttributeSet attrs) {
super(context, attrs);
}
public ViewD(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
Log.d(LOG_ID, this.getClass().getSimpleName() + " dispatchTouchEvent -> " + ViewUtils.actionToString(ev.getAction()));
boolean result = super.dispatchTouchEvent(ev);
Log.d(LOG_ID, this.getClass().getSimpleName() + " dispatchTouchEvent return super.dispatchTouchEvent(ev)= " + result);
return result;
}
@Override
public boolean onTouchEvent(MotionEvent ev) {
Log.d(LOG_ID, this.getClass().getSimpleName() + " onTouchEvent -> " + ViewUtils.actionToString(ev.getAction()));
boolean result = super.onTouchEvent(ev);
Log.d(LOG_ID, this.getClass().getSimpleName() + " onTouchEvent return super.onTouchEvent(ev)=" + result);
return result;
}
}
工具方法:
public class ViewUtils {
public static final int ACTION_DOWN = 0;
/**
* Constant for {@link #getActionMasked}: A pressed gesture has finished, the
* motion contains the final release location as well as any intermediate
* points since the last down or move event.
*/
public static final int ACTION_UP = 1;
/**
* Constant for {@link #getActionMasked}: A change has happened during a
* press gesture (between {@link #ACTION_DOWN} and {@link #ACTION_UP}).
* The motion contains the most recent point, as well as any intermediate
* points since the last down or move event.
*/
public static final int ACTION_MOVE = 2;
/**
* Constant for {@link #getActionMasked}: The current gesture has been aborted.
* You will not receive any more points in it. You should treat this as
* an up event, but not perform any action that you normally would.
*/
public static final int ACTION_CANCEL = 3;
static String actionToString(int i) {
switch (i) {
case 0:
return "ACTION_DOWN";
case 1:
return "ACTION_UP";
case 2:
return "ACTION_MOVE";
case 3:
return "ACTION_CANCEL";
default:
return "未知";
}
}
}
布局搭建
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<com.remote.activity.weight.ViewGroupA
android:layout_marginTop="10dp"
android:id="@+id/viewGroupA"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:background="@color/colorAccent">
<com.remote.activity.weight.ViewGroupB
android:id="@+id/viewGroupB"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_margin="60dp"
android:orientation="vertical"
android:background="@android:color/holo_blue_dark">
<com.remote.activity.weight.ViewC
android:id="@+id/viewC"
android:layout_width="match_parent"
android:layout_height="90dp"
android:layout_margin="60dp"
android:background="@android:color/holo_green_dark"/>
<com.remote.activity.weight.ViewD
android:id="@+id/viewD"
android:layout_width="match_parent"
android:layout_height="90dp"
android:layout_margin="60dp"
android:background="@color/colorPrimary"/>
</com.remote.activity.weight.ViewGroupB>
</com.remote.activity.weight.ViewGroupA>
</LinearLayout>
图解:
image.png
日志分析
不手动消耗事件不手动拦截事件的日志
通过上面的布局完成后,我们什么也不做,直接点击viewC控件日志如下:
D/点击事件: ViewGroupA dispatchTouchEvent -> ACTION_DOWN
D/点击事件: ViewGroupA onInterceptTouchEvent -> ACTION_DOWN
D/点击事件: ViewGroupA onInterceptTouchEvent return super.onInterceptTouchEvent(ev)=false
D/点击事件: ViewGroupB dispatchTouchEvent -> ACTION_DOWN
D/点击事件: ViewGroupB onInterceptTouchEvent -> ACTION_DOWN
D/点击事件: ViewGroupB onInterceptTouchEvent return super.onInterceptTouchEvent(ev)=false
D/点击事件: ViewC dispatchTouchEvent -> ACTION_DOWN
D/点击事件: ViewC onTouchEvent -> ACTION_DOWN
D/点击事件: ViewC onTouchEvent return super.onTouchEvent(ev)=false
D/点击事件: ViewC dispatchTouchEvent return super.dispatchTouchEvent(ev)= false
D/点击事件: ViewGroupB onTouchEvent -> ACTION_DOWN
D/点击事件: ViewGroupB onTouchEvent return super.onTouchEvent(ev)=false
D/点击事件: ViewGroupB dispatchTouchEvent return super.dispatchTouchEvent(ev)= false
D/点击事件: ViewGroupA onTouchEvent -> ACTION_DOWN
D/点击事件: ViewGroupA onTouchEvent return super.onTouchEvent(ev)=false
D/点击事件: ViewGroupA dispatchTouchEvent return super.dispatchTouchEvent(ev)= false
点击viewC后,因为viewC与viewD是在同一个容器中,点击的坐标区域并不涉及到viewD,所以日志中并没有viewD的信息(下文相同)
分析
点击viewC后:
- 事件首先传递到了最外层容器viewgroupA中,并且回调了dispatchTouchEvent()方法,然后又回调了onInterceptTouchEvent()方法,因为viewgroupA中还有其他组件,并且我们没有手动设置返回值,所以这里onInterceptTouchEvent()一定会返回false将事件传递下去
- 事件这时又传递到viewgroupB中,并且回调了dispatchTouchEvent()方法,然后又回调了onInterceptTouchEvent()方法,因为viewgroupB中还有其他组件,并且我们没有手动设置返回值,所以这里一定onInterceptTouchEvent()会返回false将事件传递下去,道理同viewgroupA一样
- 事件又传递到了viewC,同样第一步回调dispatchTouchEvent(),然后回调onTouchEvent(),因为这里没做消耗事件的处理,所以这里又返回false,因为这里到了最底层的view了,并且没有处理这个事件,所以这个事件又会通过dispatchTouchEvent返回false,向上传递给viewgroupB
- 因为viewC没有消耗事件,所以viewgroupB又接受到这个事件并回调onTouchEvent()来处理,因为viewgroupB也没有编写消耗事件的代码,所以他又通过dispatchTouchEvent返回false,向上传递给viewgroupA
- 因为viewgroupB没有消耗事件,所以viewgroupA又接受到这个事件并回调onTouchEvent()来处理,因为viewgroupA也没有编写消耗事件的代码,所以他又通过dispatchTouchEvent返回false,向上传递给他的上层,至于后面我们就可以不用分析了,就是传递给DecorView,PhoneWindow了!
这里日志没有涉及到ACTION_MOVE,ACTION_UP的原因是,第一个ACTION_DOWN事件还没处理,后面的事件是不会进行传递的
手动消耗事件不手动拦截事件的日志
我们在代码中编写这样的一句代码
viewC.setOnClickListener {
toast("我是C消费了此事件")
}
同样点击viewC后得到日志如下:
D/点击事件: ViewGroupA dispatchTouchEvent -> ACTION_DOWN
D/点击事件: ViewGroupA onInterceptTouchEvent -> ACTION_DOWN
D/点击事件: ViewGroupA onInterceptTouchEvent return super.onInterceptTouchEvent(ev)=false
D/点击事件: ViewGroupB dispatchTouchEvent -> ACTION_DOWN
D/点击事件: ViewGroupB onInterceptTouchEvent -> ACTION_DOWN
D/点击事件: ViewGroupB onInterceptTouchEvent return super.onInterceptTouchEvent(ev)=false
D/点击事件: ViewC dispatchTouchEvent -> ACTION_DOWN
D/点击事件: ViewC onTouchEvent -> ACTION_DOWN
D/点击事件: ViewC onTouchEvent return super.onTouchEvent(ev)=true
D/点击事件: ViewC dispatchTouchEvent return super.dispatchTouchEvent(ev)= true
D/点击事件: ViewGroupB dispatchTouchEvent return super.dispatchTouchEvent(ev)= true
D/点击事件: ViewGroupA dispatchTouchEvent return super.dispatchTouchEvent(ev)= true
D/点击事件: ViewGroupA dispatchTouchEvent -> ACTION_UP
D/点击事件: ViewGroupA onInterceptTouchEvent -> ACTION_UP
D/点击事件: ViewGroupA onInterceptTouchEvent return super.onInterceptTouchEvent(ev)=false
D/点击事件: ViewGroupB dispatchTouchEvent -> ACTION_UP
D/点击事件: ViewGroupB onInterceptTouchEvent -> ACTION_UP
D/点击事件: ViewGroupB onInterceptTouchEvent return super.onInterceptTouchEvent(ev)=false
D/点击事件: ViewC dispatchTouchEvent -> ACTION_UP
D/点击事件: ViewC onTouchEvent -> ACTION_UP
D/点击事件: ViewC onTouchEvent return super.onTouchEvent(ev)=true
D/点击事件: ViewC dispatchTouchEvent return super.dispatchTouchEvent(ev)= true
D/点击事件: ViewGroupB dispatchTouchEvent return super.dispatchTouchEvent(ev)= true
D/点击事件: ViewGroupA dispatchTouchEvent return super.dispatchTouchEvent(ev)= true
分析
这个时候可以看到事件的传递也是跟上面那种情况一样的,唯一不同的地方就是viewC中的onTouchEvent()方法返回的是一个true,这里也很明显,我们前面加了点击事件的处理,这里其实就是做了一个事件消耗,并且消耗完了这个事件后dispatchTouchEvent()也返回一个true来通知他的父容器我已经消耗了这个事件了,一直回调到最顶层view,因为这里处理了ACTION_DOWN事件,所以会出现ACTION_UP事件也需要处理,处理的方式跟ACTION_DOWN是一样的!
这里大家应该明白了消耗事件是怎么回事了吧,其实就是我们平时设置的一些点击事件等!
不手动消耗事件手动拦截事件的日志
我们做一个拦截实验,我们修改viewgroupB中onInterceptTouchEvent()的代码,我们手动给他返回一个true:
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
Log.d(LOG_ID, this.getClass().getSimpleName() + " onInterceptTouchEvent -> " + ViewUtils.actionToString(ev.getAction()));
boolean result = true;
Log.d(LOG_ID, this.getClass().getSimpleName() + " onInterceptTouchEvent return super.onInterceptTouchEvent(ev)=" + result);
return true;
}
同样点击viewC后得到日志如下:
D/点击事件: ViewGroupA dispatchTouchEvent -> ACTION_DOWN
D/点击事件: ViewGroupA onInterceptTouchEvent -> ACTION_DOWN
D/点击事件: ViewGroupA onInterceptTouchEvent return super.onInterceptTouchEvent(ev)=false
D/点击事件: ViewGroupB dispatchTouchEvent -> ACTION_DOWN
D/点击事件: ViewGroupB onInterceptTouchEvent -> ACTION_DOWN
D/点击事件: ViewGroupB onInterceptTouchEvent return super.onInterceptTouchEvent(ev)=true
D/点击事件: ViewGroupB onTouchEvent -> ACTION_DOWN
D/点击事件: ViewGroupB onTouchEvent return super.onTouchEvent(ev)=false
D/点击事件: ViewGroupB dispatchTouchEvent return super.dispatchTouchEvent(ev)= false
D/点击事件: ViewGroupA onTouchEvent -> ACTION_DOWN
D/点击事件: ViewGroupA onTouchEvent return super.onTouchEvent(ev)=false
D/点击事件: ViewGroupA dispatchTouchEvent return super.dispatchTouchEvent(ev)= false
分析
从直观的感受似乎日志里面只有viewgroupA与viewgroupB的日志,是的没错
- 按照常规事件传递到viewgroupA回调dispatchTouchEvent,然后回调onInterceptTouchEvent,因为他里面还有子组件所以onInterceptTouchEvent肯定返回为false向下传递
- 接受到viewgroupA传递过来的事件,回调dispatchTouchEvent方法,然后回调onInterceptTouchEvent方法,但是这里我们手动设置的返回为true,所以我们viewgroupB必须调用onTouchEventr去消费这个事件,并且返回为true,但是我们在onTouchEventr中并没有些消费事件的代码,所以onTouchEvent返回false把事件又传递给他上级处理
- viewgroupA接受到viewgroupB没有处理的事件然后调用onTouchEventr去处理,但是也没有些处理的代码,所以继续回调给上级直到phonewindow,
- 因为这个ACTION_DOWN事件没有处理,所以不会收到ACTION_UP等事件了
手动消耗事件手动拦截事件的日志
我们添加消费事件的代码:
viewGroupB.setOnClickListener {
toast("我是B消费了此事件")
}
同时拦截事件让viewGroupB的onInterceptTouchEvent返回为true
public boolean onInterceptTouchEvent(MotionEvent ev) {
Log.d(LOG_ID, this.getClass().getSimpleName() + " onInterceptTouchEvent -> " + ViewUtils.actionToString(ev.getAction()));
boolean result = true;
Log.d(LOG_ID, this.getClass().getSimpleName() + " onInterceptTouchEvent return super.onInterceptTouchEvent(ev)=" + result);
return true;
}
这个时候我们点击viewC按钮日志如下:
D/点击事件: ViewGroupA dispatchTouchEvent -> ACTION_DOWN
D/点击事件: ViewGroupA onInterceptTouchEvent -> ACTION_DOWN
D/点击事件: ViewGroupA onInterceptTouchEvent return super.onInterceptTouchEvent(ev)=false
D/点击事件: ViewGroupB dispatchTouchEvent -> ACTION_DOWN
D/点击事件: ViewGroupB onInterceptTouchEvent -> ACTION_DOWN
D/点击事件: ViewGroupB onInterceptTouchEvent return super.onInterceptTouchEvent(ev)=true
D/点击事件: ViewGroupB onTouchEvent -> ACTION_DOWN
D/点击事件: ViewGroupB onTouchEvent return super.onTouchEvent(ev)=true
D/点击事件: ViewGroupB dispatchTouchEvent return super.dispatchTouchEvent(ev)= true
D/点击事件: ViewGroupA dispatchTouchEvent return super.dispatchTouchEvent(ev)= true
D/点击事件: ViewGroupA dispatchTouchEvent -> ACTION_UP
D/点击事件: ViewGroupA onInterceptTouchEvent -> ACTION_UP
D/点击事件: ViewGroupA onInterceptTouchEvent return super.onInterceptTouchEvent(ev)=false
D/点击事件: ViewGroupB dispatchTouchEvent -> ACTION_UP
D/点击事件: ViewGroupB onTouchEvent -> ACTION_UP
D/点击事件: ViewGroupB onTouchEvent return super.onTouchEvent(ev)=true
D/点击事件: ViewGroupB dispatchTouchEvent return super.dispatchTouchEvent(ev)= true
D/点击事件: ViewGroupA dispatchTouchEvent return super.dispatchTouchEvent(ev)= true
分析
到了这里大家应该都能猜到会发生什么现象了吧
- 因为viewgroupB的onInterceptTouchEvent手动返回为true所以拦截了事件,日志中不会出现viewC的信息了
- 事件传递到viewgroupB中被拦截,所以就要调用他的onTouchEvent方法,由于我们又编写了消耗事件的代码,所以这里会消耗掉事件,并且返回true通知上级我们已经消耗了事件了
- 因为这个ACTION_DOWN事件有处理,所以会收到ACTION_UP等事件了进行下一步的传递
总结
如果认真的看完这里的日志,相信对事件的消耗分发了解了一半了,至于为什么点击的时候没得viewD的事,还有很多不明白的地方,我们会下次继续用源码进行分析!
提醒
需要源码的朋友可以发送请求到邮箱 imkobedroid@gmail.com 文章与代码有待改进!希望可以交流
网友评论