android的事件分发,大多数人都是似懂非懂,很多时候就卡在事件冲突这一步。比如在按钮上不能滑动出侧边栏,比如说ViewPager和banner冲突。我之前也是这样,然后狠下心去看了一遍源码,并且看了很多大神的博客,然后以我自身的理解配合源码来查看一个事件的传递过程。源码用的是API-8的,因为版本越高,健壮性越好,代码阅读性越差。
因为篇幅比较长,所以更底层的代码我也不准备写了,日后有机会再研究。在看博客之前,我们需要先来了解一些事件分发的基本流程,然后再一步步的深入去研究。
这其中有三个关键方法,首先我们先来理解几个方法的方法名和它们的返回值所代表的意义。
细心的网友发现了,dispatch无论return true还是false,都不往下继续分发,不对吧!
dispatchTouchEvent一般不会直接return true或false。而是将事件抛给onInterceptTouchEvent和onTouchevent处理,问它们需不需要,最后再由dispatchTouchEvent进行最终的返回。这是很多新手开发者的理解误区,包括以前的我……
事件是怎么产生的这种底层问题我们先不管,我们只从知道的地方开始说起——Activity。
当一个事件开始传递后,最先接收到的是当前的Activity。然后Activity调用public boolean dispatchTouchEvent(MotionEvent ev)
开始分发事件。内部又会调用PhoneWindow的内部对象DecorView的superdispatchKeyEvent,也就是ViewGroup的dispatchTouchEvent开始事件分发,DecorView是最顶层的View。
在dispatchTouchEvent方法中,ViewGroup会遍历自身的child(move事件和up事件有点不同,不会遍历child,下面会另外说明)
,再去调用子child的dispathTouchEvent方法,直到该事件被消费。传递途中还有onInterceptTouchEvent和onTouchEvent方法参与。
事件是由最外层的View开始传递,然后结果从最底层往外层返回。
如下图:ViewGroupA,ViewGroupB ,View的关系: A 是B的parent, B是C的parent。
图中的call是“调用”
我们再用代码来看下大致流程。
我们先建一个项目,然后写几个类继承FrameLayout,重写dispatchEvnet、onInterceptTouchEvent,onTouchEvent几个方法,看log输出。
ViewGroupA,B一样。
package com.aitsuki.touchevent;
import android.content.Context;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.widget.FrameLayout;
/** * Created by AItsuki on 2015/12/30. */
public class ViewGroupA extends FrameLayout {
public ViewGroupA(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
Log.e("Event","===ViewGroup:A======onTouchEvent===============Down");
break;
case MotionEvent.ACTION_MOVE:
Log.e("Event","===ViewGroup:A======onTouchEvent===============Move");
break;
case MotionEvent.ACTION_UP:
Log.e("Event","===ViewGroup:A======onTouchEvent===============Up");
break;
}
return super.onTouchEvent(event);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
Log.e("Event","===ViewGroup:A======onInterceptTouchEvent======Down");
break;
case MotionEvent.ACTION_MOVE:
Log.e("Event","===ViewGroup:A======onInterceptTouchEvent======Move");
break;
case MotionEvent.ACTION_UP:
Log.e("Event","===ViewGroup:A======onInterceptTouchEvent======Up");
break;
}
return super.onInterceptTouchEvent(ev);
}
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
Log.e("Event","===ViewGroup:A======dispatchTouchEvent========Down");
break;
case MotionEvent.ACTION_MOVE:
Log.e("Event","===ViewGroup:A======dispatchTouchEvent========Move");
break;
case MotionEvent.ACTION_UP:
Log.e("Event","===ViewGroup:A======dispatchTouchEvent========Up");
break;
}
return super.dispatchTouchEvent(ev);
}
}
MyView
package com.aitsuki.touchevent;
import android.content.Context;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
/** * Created by AItsuki on 2015/12/30. */
public class MyView extends View {
public MyView(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
Log.e("Event","===View=============onTouchEvent===============Down");
break;
case MotionEvent.ACTION_MOVE:
Log.e("Event","===View=============onTouchEvent===============Move");
break;
case MotionEvent.ACTION_UP:
Log.e("Event","===View=============onTouchEvent===============Up");
break;
}
return super.onTouchEvent(event);
}
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
Log.e("Event","===View=============dispatchTouchEvent=========Down");
break;
case MotionEvent.ACTION_MOVE:
Log.e("Event","===View=============dispatchTouchEvent=========Move");
break;
case MotionEvent.ACTION_UP:
Log.e("Event","===View=============dispatchTouchEvent=========Up");
break;
}
return super.dispatchTouchEvent(event);
}
}
布局activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.aitsuki.touchevent.ViewGroupA
android:layout_width="300dp"
android:layout_height="300dp"
android:background="#FF6A6A"
android:layout_gravity="center">
<com.aitsuki.touchevent.ViewGroupB
android:layout_width="200dp"
android:layout_height="200dp"
android:layout_gravity="center"
android:background="#9ACD32">
<com.aitsuki.touchevent.MyView
android:layout_width="100dp"
android:layout_height="100dp"
android:background="#1E90FF"
android:layout_gravity="center"/>
</com.aitsuki.touchevent.ViewGroupB>
</com.aitsuki.touchevent.ViewGroupA>
</FrameLayout>
布局预览:
红色:ViewGroupA
绿色:ViewGroupB
蓝色:View
现在我点击一下蓝色区域(View),看Log输出。
我们给ViewGroupA加上拦截之后再看看(让onInterceptTouchEvent返回true)
好了,基本流程和我们的图是一致的。
但是要注意 一、前言 的那段红字
大概理解了事件的传递过程之后,我们来看一下源码。
为什么先看View的源码而不看ViewGroup的原因有两点:
1. View是ViewGroup的父类,ViewGroup的onTouchEvent方法继承自View, 并没有重写。
2. View没有child,事件传递简单,不会打消各位的阅读源码积极性。
那么,开始吧。
先来看View的dispatchTouchView方法,我直接在上面注释。
mViewFlags:一种通过位运算记录开关的方式。mViewFlags一个32位的int值,用每一位的0或1记录属性。比如第1位的0和1记录focusable(是否可以获取焦点)
/** * Pass the touch screen motion event down to the target view, or this * view if it is the target. * * @param event The motion event to be dispatched. * @return True if the event was handled by the view, false otherwise. */
public boolean dispatchTouchEvent(MotionEvent event) {
// 如果这个View设置了触摸监听onTouchListener并且View是可用的,并且onTouch返回的是true
// 那么事件就消费掉了,传递结束。
if (mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED &&
mOnTouchListener.onTouch(this, event)) {
return true;
}
// 否则,交给onTouchEvent处理(View中没有onInterceptTouchEvent方法,因为它没有
// child了,不需要有拦截方法)
return onTouchEvent(event);
}
// 注意看谷歌工程师的注释,它们称消费事件的View为target view,如果这里的dispatch或者说
// onTouchEvent返回true,那么这个View就是target View了。先记下来
继续看onTouchEvent
/** * Implement this method to handle touch screen motion events. * * @param event The motion event. * @return True if the event was handled, false otherwise. */
public boolean onTouchEvent(MotionEvent event) {
final int viewFlags = mViewFlags;
// 如果View不可用,但它却是可点击的(clickable属性),那么仍然消费这个事件(但是不执行任何
// 操作,也就是不会响应)。也就是说,只要有clickable属性,那么这个点击事件就必然被消费掉。
if ((viewFlags & ENABLED_MASK) == DISABLED) {
// A disabled view that is clickable still consumes the touch
// events, it just doesn't respond to them.
return (((viewFlags & CLICKABLE) == CLICKABLE ||
(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE));
}
// 这个是触摸代理,就是点击另一个View,这个view会响应点击事件。默认是null,开发者可以通过
// setTouchDelegate设置。详情请自行查看TouchDelegate
if (mTouchDelegate != null) {
if (mTouchDelegate.onTouchEvent(event)) {
return true;
}
}
// 如果View是可点击的,那么消费掉这个事件,否则返回给上层处理(parent)
if (((viewFlags & CLICKABLE) == CLICKABLE ||
(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) {
//...... 此处省略N行代码,没有return语句,我们pass。
return true;
}
return false;
}
好了,View处理事件的源码就这么点。一会就看完了,挺简单的。
从上面这两段代码可以得出的结论:
结论1:onTouch优先于onTouchEvent执行,并且onTouch消费掉事件后,onTouEvent不会再执行。
结论2:如果View是可点击的(clickable),那么事件一定会被消费掉,不会再继续传递。
我们来验证一下结论,用的还是 二、事件分发的基本过程 的那个项目。
我们给MyView设置touchListener并return true,然后点击蓝色区域看看log输出
activity代码:
public class MainActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
MyView view = (MyView) findViewById(R.id.view);
view.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
Log.e("Event","===View=============onTouch====================Down");
break;
case MotionEvent.ACTION_MOVE:
Log.e("Event","===View=============onTouch====================Move");
break;
case MotionEvent.ACTION_UP:
Log.e("Event","===View=============onTouch====================Up");
break;
}
return true;
}
});
}
}
将MainActivity设置侦听的代码注释掉,给View设置clickable属性,测试结论3
public class MainActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
MyView view = (MyView) findViewById(R.id.view);
view.setClickable(true);
// view.setOnTouchListener(new View.OnTouchListener() {
// @Override
// public boolean onTouch(View v, MotionEvent event) {
// switch (event.getAction()) {
// case MotionEvent.ACTION_DOWN:
// Log.e("Event","===View=============onTouch====================Down");
// break;
// case MotionEvent.ACTION_MOVE:
// Log.e("Event","===View=============onTouch====================Move");
// break;
// case MotionEvent.ACTION_UP:
// Log.e("Event","===View=============onTouch====================Up");
// break;
// }
// return true;
// }
// });
}
}
View执行了onTouchEvent,消费掉了事件,不再传递,结论3验证正确
请各位网友理解了View的事件传递的结论消化之后再继续往下看,因为ViewGroup中多次调用到super.dispatchTouchEvent, 其实也就是调用View的dispatchTouchEvent,因为View就是ViewGroup的父类。
因为注释比较多,所以有点影响阅读性,最好可以配合没有注释的源码(api-8 Android2.2)一起阅读。同时思考一下我的分析是否和你的一样,有什么错误也可以在评论中指出。
我们只需要看dispatch的源码就可以了,onInterceptTouchEvent默认都是return false,没有onTouchEvent。
和View一样,也是使用注释的方式来说明。
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
// 获取事件的类型和触摸坐标
final int action = ev.getAction();
final float xf = ev.getX();
final float yf = ev.getY();
final float scrolledXFloat = xf + mScrollX;
final float scrolledYFloat = yf + mScrollY;
// mTempRect初始是null的,这个Rect对象主要是用来记录child的可点击范围。
final Rect frame = mTempRect;
// mGroupFlags和mViewFlags一样是记录当前控件的状态。这里记录的是“是否允许拦截”这个属性。
// 可以通过requestDisallowInterceptTouchEvent这个方法进行设置,如果设置了这个属性,
// 那么ViewGroup就不会拦截child的事件了。
boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
// 如果是down事件,就遍历child进行分发。
if (action == MotionEvent.ACTION_DOWN) {
// 两位谷歌工程师在聊天么=。=,大概意思就是:
// 为什么一个View响应完down事件后还没有消失,继续响应了第二次down事件。
// xxx: 我们可能应该发送一个up事件,而不是down……
// target:在之前也说过了,响应了down事件的那个View就是target,而在up或者cancel
// 事件执行后,这个target应该会被重置为null。
// 但是这里居然不是空的。博主我也不知道什么回事=。=
// 如果不是null,那么就让它重置为null。
if (mMotionTarget != null) {
// this is weird, we got a pen down, but we thought it was
// already down!
// XXX: We should probably send an ACTION_UP to the current
// target.
mMotionTarget = null;
}
// If we're disallowing intercept or if we're allowing and we didn't
// intercept
// 这里判断disallowIntercept有点多余,因为执行up或者cancel之后,这个属性就会被重
// 置为false(往下找)。而requestDisallowInterceptTouchEvent方法一般在child的
// dispatchTouchEvent中调用,但是事件还没有传到chid,那么这个方法也就不会执行,
// disallowIntercept肯定也只能是false。
// 我们无视掉这里的disallowIntercept,这里判断是否拦截child的事件。
if (disallowIntercept || !onInterceptTouchEvent(ev)) {
// reset this event's action (just to protect ourselves)
// 重置这个事件为donw事件,只是为了保证健壮性。上面那个disallowIntercept也是
// 为了健壮性么=。=
ev.setAction(MotionEvent.ACTION_DOWN);
// We know we want to dispatch the event down, find a child
// who can handle it, start with the front-most child.
// 不得不说谷歌工程师的注释也是很详细的=。=
// 我们想要分发这个down事件,遍历子View看谁可以持有它,从最上层的子View开始。
final int scrolledXInt = (int) scrolledXFloat;
final int scrolledYInt = (int) scrolledYFloat;
final View[] children = mChildren;
final int count = mChildrenCount;
// 这里开始遍历所有child
for (int i = count - 1; i >= 0; i--) {
final View child = children[i];
// 如果child是可见的,或者child正在进行动画(动画中的View这个我没细看
// ,无视掉好了)。
if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE
|| child.getAnimation() != null) {
// 获得child的有效点击范围,判断点击事件是否点在此child中。
child.getHitRect(frame);
if (frame.contains(scrolledXInt, scrolledYInt)) {
// offset the event to the view's coordinate system
// 获取到事件的坐标是屏幕的绝对坐标,要转成child的相对坐标。
final float xc = scrolledXFloat - child.mLeft;
final float yc = scrolledYFloat - child.mTop;
ev.setLocation(xc, yc);
// 如果点击事件在此child中,重置属性:CANCEL_NEXT_UP_EVENT
// 该属性的描述:Indicates whether the view is temporarily
// detached。
// 标记哪一个View是暂时和parent分离,就是和当前这个ViewGroup分
// 离。
// 有什么应用场景我也不知道,不过abslistView中是用到了,那边的源码
// PS:位运算没忘吧=。=,比如CANCEL_NEXT_UP_EVENT = 0000 1000
// 取反 1111 0111。 与上这个数就是取消 CANCEL_NEXT_UP_EVENT
// 这个属性……
child.mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;
// 然后这里调用child的事件分发
// 这里会出现几种情况
// 1:child是View,那么回想一下View的源码吧(三、View源码走读)
// 如果child消费了此down事件,那么这个child就是target了。
// 如果不消费,那么返回给当前的ViewGroup的onTouchEvent消费。
// 2:child是ViewGroup,那么继续调用child的dispatch,
// 继续遍历child的child(噗,孙子),直到有响应这个事件的。
// 如果最底层的child也是ViewGroup,那么请直接跳过这个遍历,
// 往下看…… if(target == null)那里。
// 调用super.dispatchEvent。
// 也就是说,如果最底层的child是ViewGroup,那么将它作为View处理。
if (child.dispatchTouchEvent(ev)) {
// Event handled, we have a target now.
// 如果响应了down事件,那么这个child就是target
mMotionTarget = child;
return true;
}
// The event didn't get handled, try the next view.
// Don't reset the event's location, it's not
// necessary here.
}
}
}
}
}
// 思考:dispatch中调用child的dispatch方法,这看起来是不是像递归遍历。直到有child响应down事件,
// 否则将所有child遍历完后这个事件流产。
// 注意:child有可能是View,也有可能是ViewGroup。它们两个的dispatch方法是不同的。如果是View,
// 就代表这个事件分发已经到了最底下的View了。
//===================================================================================
// 判断事件是否是up或者cancel
boolean isUpOrCancel = (action == MotionEvent.ACTION_UP) ||
(action == MotionEvent.ACTION_CANCEL);
// 如果是up或者cancel事件,将FLAG_DISALLOW_INTERCEPT(disallowIntercept是通过
// 这个值计算的)这个属性移除。
// 就是说,既然是up事件和cancel事件已经接收到,那么就代表这在target上的事件结束了,
// 重置这个属性,不然会影响到下一个事件。
if (isUpOrCancel) {
// Note, we've already copied the previous state to our local
// variable, so this takes effect on the next event
// 我们已经复制了上一个的状态到变量,所以这里就影响了下一个事件。
mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
}
// The event wasn't an ACTION_DOWN, dispatch it to our target if
// we have one.
// 如果traget存在,并且这个事件不是down,那么就将事件交给target处理
// (谁响应了down事件,后续事件就交给谁处理)
final View target = mMotionTarget;
if (target == null) {
// We don't have a target, this means we're handling the
// event as a regular view.
// 如果target为null,那么代表down事件没有被响应(也可能是target的后续事件被拦截,
// 那么target也会被重置为null)
ev.setLocation(xf, yf);
// 如果当前的View或者说ViewGroup已经从parent中分离,那么将事件改为cancel
if ((mPrivateFlags & CANCEL_NEXT_UP_EVENT) != 0) {
ev.setAction(MotionEvent.ACTION_CANCEL);
mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;
}
// 这里关键了,如果这个ViewGroup没有child,那么它就会走到这里,然后调用View的事件
// 分发, 将自己作为一个View处理。
// 当返回dispatch返回true之后,这个ViewGroup本身就是target了,不过这个target引用
// 是在parent中,不是当前的=。=
return super.dispatchTouchEvent(ev);
}
// 代码能走到这里的话就代表,这不是一个down事件。
// if have a target, see if we're allowed to and want to intercept its
// events
// 如果我们有一个target,我们就会将后续的事件都交给target处理(跳过这个if判断直接往
// 下看)。
// 结合前面分析,从这里看出,如果某个child请求了requestDisallow,那么child就肯定能
// 接收到move和up事件,前提是没有拦截down事件。
// 假设我们拦截了后续事件(不拦截child的down事件,但是拦截了child的其他事件)
if (!disallowIntercept && onInterceptTouchEvent(ev)) {
final float xc = scrolledXFloat - (float) target.mLeft;
final float yc = scrolledYFloat - (float) target.mTop;
mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;
ev.setAction(MotionEvent.ACTION_CANCEL);
ev.setLocation(xc, yc);
// 将事件改成cancel,然后交给target的dispatch处理。(这里可以看出,如果有
// target响应的down事件,但是target后续事件被拦截,那么就会传一个cancel给target)
// 不管它有没有响应,我们都将target置为空,就是说,后续事件当做没有target来处理。
if (!target.dispatchTouchEvent(ev)) {
// target didn't handle ACTION_CANCEL. not much we can do
// but they should have.
}
// clear the target
mMotionTarget = null;
// Don't dispatch this event to our own view, because we already
// saw it when intercepting; we just want to give the following
// event to the normal onTouchEvent().
return true;
}
// 如果事件是up或者cancel,那么将target重置为null。因为响应down事件的就是target,
// 既然已经抬起手指了,那target就没了。
if (isUpOrCancel) {
mMotionTarget = null;
}
// finally offset the event to the target's coordinate system and
// dispatch the event.
// 将坐标转成target的相对坐标。
final float xc = scrolledXFloat - (float) target.mLeft;
final float yc = scrolledYFloat - (float) target.mTop;
ev.setLocation(xc, yc);
// 如果target已经被分离出去(相当于remove),那么将事件改成cancel。
if ((target.mPrivateFlags & CANCEL_NEXT_UP_EVENT) != 0) {
ev.setAction(MotionEvent.ACTION_CANCEL);
target.mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;
mMotionTarget = null;
}
// 将所有后续事件都交给target处理。
return target.dispatchTouchEvent(ev);
}
注释太多看起来有点乱,不过没关系,大家配合无注释的代码一起看就行了。我只是将每一行的代码的作用注释出来,各种结论还是需要推测。
现在我们来总结一下分析源码得出的一些结论:
结论1:onTouch优先于onTouchEvent执行,并且onTouch消费掉事件后,onTouEvent不会再执行。
结论2:如果View是可点击的(clickable),那么事件一定会被消费掉,不会再继续传递。
结论3: 如果ViewGroup在onInterceptTouchEvent中拦截了child的事件,那么这个事件会交给ViewGroup的onTouchEvent处理。
结论4:onInterceptTouchEvent方法在一次事件序列(down到up或者cancel的过程)中,只要返回true就不会再调用,或者说只要拦截过一次之后就不会再调用,直到下一次down事件开始前。
结论5:响应了down事件的View被称为target,Android会将后续事件都交给target处理。
结论6:在结论5的基础上,如果onInterceptTouchEvent中拦截了target的其他事件,比如move或up,那么target就会接受到一个cancel事件,并且将target置为null,后续的事件交给target的parent处理。(比如,我可以让child响应down事件,然后只拦截它的move事件,那么我就可以接受到除了down的所有后续事件了,而child则会接收到一个cancel事件,这样就可以解决滑动事件和按钮冲突的问题了。)
结论7:在结论6的基础上,child可以通过requestDisallowInterceptTouchEvent请求parent不拦截他的事件,前提是child能响应到down事件。(例如:parent在onIntercept中拦截了事件,child就没机会请求了。再例如:parent不拦截down事件,但是拦截了move和up事件,这时候requestDisallowInterceptTouchEvent就派上用场了)
结论8: 如果一个View在处理一个事件序列(down到up或者cancel的过程)的时候,parent将他remove掉了,那么这个View会接收到一个cancel事件。
结论的验证过程都很简单,因为文章篇幅已经太长了不打算贴出来,请自行验证结论的正确性,也可惜选择相信我的验证结果。
这博客真的很难写,花了两天多的时间。不知道应该怎么写才能更加简洁易懂,最后干脆以注释的方式了。
虽然写了七八个结论,但是这并不是全部,我也很难将所有结论一个一个列出来。
分析源码,读懂源码的好处就是,当你发现事件冲突的时候,不会像无头苍蝇一样在百度胡乱搜方案,最后还搞得一头雾水。而是让你自己有能力解决冲突,能找到冲突的源头。
至于解决事件冲突的一些案例,我有空的话可能会整理几个出来,但是写着博客有种身心疲惫的感觉_(:з」∠)_
,休息一段时间再说,就这样,下次见。