前言
在Android中,View主要负责界面的绘制和事件的分发、处理,它是所有控件Widgets的基类。通过源码分析View的事件分发,我们可以更加深刻地理解Android系统中View的工作原理。不仅如此,在日常的开发中,当我们遇到View事件冲突、滑动冲突时,处理起来将会游刃有余。
基础知识
当我们的手指触摸手机屏幕时,手机中的应用会对我们的触摸动作做出响应,确切地说是应用里的控件Widgets响应了触摸事件。在Android中,使用MotionEvent来描述触摸事件,我们可以通过getAction()方法来获取当前的事件类型。通常,一次手势动作会产生一系列的事件,下面列举了4个主要事件:
-
ACTION_DOWN事件
当手指第一次触摸到屏幕时将产生此事件。ACTION_DOWN事件表示一系列事件的开始。 -
ACTION_UP事件
当手指离开屏幕时将产生此事件。与ACTION_DOWN事件对应,ACTION_UP事件表示一系列事件的结束。 -
ACTION_MOVE事件
当手指有在屏幕上滑动时将产生此事件。 -
ACTION_CANCEL事件
表示当前的手势被中止了。如果一个View收到了ACTION_CANCEL事件,那么它不会再收到其它任何事件,包括ACTION_UP事件。
通过getX(), getY()方法可以获取到当前事件在屏幕上的坐标。注意,这个坐标是相对于父容器左上角的坐标。通过getRawX(), getRawY()方法可以获取到当前事件在屏幕上的原始坐标。通过前后两个ACTION_MOVE事件的坐标我们就可以知道当前手势动作的方向了。
在具体分析之前,先提一下View的事件分发的3个核心方法:
-
dispatchTouchEvent()方法
主要负责事件的分发。 -
onInterceptTouchEvent()方法
主要负责事件的拦截,ViewGroup专有。 -
onTouchEvent()方法
主要负责事件的处理。
dispatchTouchEvent()和onTouchEvent()方法都有返回值,如果返回值为true,表示当前事件被处理了或者被消费了。另外再提一个ViewGroup的requestDisallowInterceptTouchEvent()方法,子控件通过调用这个方法可以控制是否允许父容器拦截事件,它具体影响了父容器的FLAG_DISALLOW_INTERCEPT标志位。
下面我们开始具体的源码分析。
Activity的事件分发
在Android中,底层的触摸事件最开始是传递到Activity中的,从Activity的dispatchTouchEvent()方法开始分发事件。
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}
从上面的代码可以知道,Activity将事件交给Window来负责分发到具体的页面布局中。如果Window的superDispatchTouchEvent()方法返回了true,即事件被消费了,那么直接退出。反之,如果没有任何一个View消费事件,那么最终Activity的onTouchEvent()方法将被调用,即Activity自己来处理事件。
Activity的Window是个抽象类,它的具体实现类是PhoneWindow。下面来看PhoneWindow的superDispatchTouchEvent()方法。
@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
return mDecor.superDispatchTouchEvent(event);
}
PhoneWindow的superDispatchTouchEvent()方法比较简单,它直接将事件传递给了DecorView。DecorView是Android系统中所有Activity页面布局的顶级父容器。平常我们在Activity的onCreate()方法中调用setContentView()方法来设置页面布局,其实页面布局是被添加到DecorView这个父容器中。下面来看DecorView的superDispatchTouchEvent()方法。
public boolean superDispatchTouchEvent(MotionEvent event) {
return super.dispatchTouchEvent(event);
}
DecorView的superDispatchTouchEvent()方法比较简单,它将事件传递给了父类的dispatchTouchEvent()方法。在Android中,所有的父容器都是继承自ViewGroup,而ViewGroup继承自View。ViewGroup重写了View的dispatchTouchEvent()方法,所以事件开始从ViewGroup中进行分发。
ViewGroup的事件分发
ViewGroup的dispatchTouchEvent()方法比较复杂,我们分段来分析。
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
...
boolean handled = false;
if (onFilterTouchEventForSecurity(ev)) {
final int action = ev.getAction();
final int actionMasked = action & MotionEvent.ACTION_MASK;
// Handle an initial down.
if (actionMasked == MotionEvent.ACTION_DOWN) {
// Throw away all previous state when starting a new touch gesture.
// The framework may have dropped the up or cancel event for the previous gesture
// due to an app switch, ANR, or some other state change.
cancelAndClearTouchTargets(ev);
resetTouchState();
}
...
}
...
return handled;
}
前面说过,ACTION_DOWN事件表示一次手势动作产生的一系列事件的起始事件。在dispatchTouchEvent()方法的开始,如果是ACTION_DOWN事件,那么ViewGroup会做一些复位、重置操作。
private void cancelAndClearTouchTargets(MotionEvent event) {
if (mFirstTouchTarget != null) {
...
for (TouchTarget target = mFirstTouchTarget; target != null; target = target.next) {
resetCancelNextUpFlag(target.child);
dispatchTransformedTouchEvent(event, true, target.child, target.pointerIdBits);
}
clearTouchTargets();
...
}
}
ViewGroup使用mFirstTouchTarget变量来存储消费了事件的子控件。mFirstTouchTarget变量将所有消费了事件的子控件以链表的形式存储在一起。但是,通常要么没有子控件消费事件,要么只有一个子控件消费了事件。在cancelAndClearTouchTargets()方法中,如果之前有子控件消费了事件,那么ViewGroup将通过dispatchTransformedTouchEvent()方法向它们分发ACTION_CANCEL中止事件以便开始一轮新的事件传递。接着在clearTouchTargets()方法中将mFirstTouchTarget变量重置为null。
private void resetTouchState() {
clearTouchTargets();
resetCancelNextUpFlag(this);
mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
mNestedScrollAxes = SCROLL_AXIS_NONE;
}
resetTouchState()方法中复位了一些标志位,包括了不允许父容器拦截事件的标志位FLAG_DISALLOW_INTERCEPT。接着往下看dispatchTouchEvent()方法。
@Override
public boolean dispatchTouchEvent(MotionEvent ev