一般在实际开发中,我们很少主动去处理相关滑动处理,所以就很少关注事件分发相关机制。因为系统已经帮我们处理好了,如:ScrollView、ViewPage、ListView 等。
这里我们就以事件分发为入口,来分析一下事件分发机制与滑动冲突的解决方法。
学习 Android 开发过程中,或多或少都有听过事件分发机制,就算没听过,如果有面试经历,也大有几率被问到过吧。毕竟在现在,这个分发机制以及算是烂大街的题目了。
当用户触摸屏幕时,产生的一系列行为( Touch 事件)。
系统源码中通过 MotionEvent
这个类来描述这一系列相关行为。这个类中也定义了许多与事件相关的常量与变量,如事件类型、事件的相关属性等。这里就只贴出主要的四个相关类型
public final class MotionEvent extends InputEvent implements Parcelable {
//按下事件:手指刚触碰屏幕
public static final int ACTION_DOWN = 0;
//松开事件:手指从屏幕松开
public static final int ACTION_UP = 1;
//移动事件:手指在屏幕上滑动
public static final int ACTION_MOVE = 2;
//取消事件:非人为因素取消
public static final int ACTION_CANCEL = 3;
}
正常情况下,一次手指触摸屏幕的行为会发出一系列点击事件,可以分为如下两种情况:
我们在从宏观角度了解一下事件的分发总流程,总体来说就是:U型模型
事件先从 Activity
出发,然后传递给 DecorView(ViewGroup)
,经过 ViewGroup
然后在传递给相应的子 View 进行处理。如果不处理,则按照原路径逐级向上返回,直至传递到 Activity
结束。
如下流程图表述应该比较容易理解。
通过上面的分发序列和分发模型,可以看出,一个事件的分发主要涉及三个对象:Activity、ViewGroup和View。
而这三个分发对象都有相应的事件处理方法
到这里我们对整个大致流程已经有了相关的印象了,接下来我们在从源码的角度来看看,事件是怎么传递的。代码省略了不重要的部分,并且加上了相关注释,就不过多进行解释了。
事件从 Activity
的 dispatchTouchEvent
方法开始向下分发进行处理。
public class Activity{
public boolean dispatchTouchEvent(MotionEvent ev) {
...
//调用 window 实现类 PhoneWindow 的 superDispatchTouchEvent 方法
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
//如果上面不进行处理,则调用 Activity 的 onTouchEvent 进行处理
return onTouchEvent(ev);
}
public boolean onTouchEvent(MotionEvent event) {
//如果点击区域外,则销毁当前界面,否则不进行任何处理
if (mWindow.shouldCloseOnTouch(this, event)) {
finish();
return true;
}
return false;
}
}
接下来在看看 PhoneWindow
怎么处理
public class PhoneWindow extends Window implements MenuBuilder.Callback {
// This is the top-level view of the window, containing the window decor.
//窗口的顶层视图,相当于上面模型中的 跟ViewGroup
private DecorView mDecor;
@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
return mDecor.superDispatchTouchEvent(event);
}
}
public class DecorView extends FrameLayout{
public boolean superDispatchTouchEvent(MotionEvent event) {
//最终会调用 ViewGroup 的 dispatchTouchEvent 方法
return super.dispatchTouchEvent(event);
}
}
先看看 ViewGroup
中的拦截逻辑
public abstract class ViewGroup extends View{
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
...
boolean handled = false;
...
//进行安全校验和过滤,一般都为 true
if (onFilterTouchEventForSecurity(ev)) {
...
// 检测是否需要拦截
final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (!disallowIntercept) {
intercepted = onInterceptTouchEvent(ev);
ev.setAction(action); // restore action in case it was changed
} else {
intercepted = false;
}
} else {
// There are no touch targets and this action is not an initial down
// so this view group continues to intercept touches.
intercepted = true;
}
...
if (!canceled && !intercepted) {
...
if (newTouchTarget == null && childrenCount != 0) {
for (int i = childrenCount - 1; i >= 0; i--) {//倒叙遍历 子View
...
//将事件分发给 子View
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
...
//返回 true 表示该 view 处理事件,然后将该 view 赋值给 目标view
newTouchTarget = addTouchTarget(child, idBitsToAssign);
...
}
...
}
}
...
}
}
...
return handled;
}
//进行一些安全性校验和过滤
public boolean onFilterTouchEventForSecurity(MotionEvent event) {
//noinspection RedundantIfStatement
if ((mViewFlags & FILTER_TOUCHES_WHEN_OBSCURED) != 0
&& (event.getFlags() & MotionEvent.FLAG_WINDOW_IS_OBSCURED) != 0) {
// Window is obscured, drop this touch.
return false;
}
return true;
}
//是否拦截事件,在 ViewGroup 中一般返回的 false
public boolean onInterceptTouchEvent(MotionEvent ev) {
if (ev.isFromSource(InputDevice.SOURCE_MOUSE)
&& ev.getAction() == MotionEvent.ACTION_DOWN
&& ev.isButtonPressed(MotionEvent.BUTTON_PRIMARY)
&& isOnScrollbarThumb(ev.getX(), ev.getY())) {
return true;
}
return false;
}
}
在看看 ViewGroup
的分发逻辑
public abstract class ViewGroup extends View{
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
View child, int desiredPointerIdBits) {
final boolean handled;
...
if (child == null) {
//调用 View 的 dispatchTouchEvent 方法
handled = super.dispatchTouchEvent(event);
} else {
//调用 子View 的 dispatchTouchEvent 方法
handled = child.dispatchTouchEvent(event);
}
event.setAction(oldAction);
...
return handled
}
}
上面事件已经从 Activity
分发到了 ViewGroup
,再从 ViewGroup
的 dispatchTransformedTouchEvent
方法调用了子 View
的 dispatchTouchEvent
方法,接下来在看看 View 中对于事件是怎么处理的。
public class View implements Drawable.Callback, KeyEvent.Callback,
AccessibilityEventSource {
public boolean dispatchTouchEvent(MotionEvent event) {
...
boolean result = false;
//当前View是否可见(未被其他窗口遮盖住,且未隐藏)
if (onFilterTouchEventForSecurity(event)) {
...
ListenerInfo li = mListenerInfo;
if (li != null && li.mOnTouchListener != null
&& (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {
//如果设置了 OnTouchListener
//则先响应 OnTouchListener.onTouch 方法
result = true;
}
// 当 onTouch 返回 false,在执行 onTouchEvent 处理相关事件,如果处理,则返回 true
if (!result && onTouchEvent(event)) {
result = true;
}
...
}
...
return result;
}
public boolean onTouchEvent(MotionEvent event) {
...
final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
|| (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
|| (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;
...
//如果 View 可点击 或者 可长按,则最终一定 return true
if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
switch (action) {
//抬起,判断是否处理点击事件
case MotionEvent.ACTION_UP:
break;
//按下,处理长按事件
case MotionEvent.ACTION_DOWN:
break;
//移动,检测触摸是否划出了控件,移除响应事件
case MotionEvent.ACTION_MOVE:
break;
}
return true;
}
...
return false;
}
}