首先,我们要明白事件的定义『当用户触摸屏幕时,将产生的触摸行为』
其实,我们需要处理的就是把一个MotionEvent对象处理掉,而能处理它的其实只有三个方法,dispatchTouchEvent(MotionEvent event)、onInterceptTouchEvent(MotionEvent event)、onTouchEvent(MotionEvent event),但是如果搭配上ViewGroup、View和Activity处理的流程可能就要变的复杂一点了,下面我们来具体分析。
来源与传递
首先我们一定要先了解,Activity和View是什么样的关系,为什么Activity中的事件都能传递给View去处理。
本质上Activity其实只是一个『容器』,但是这个容器并不是直接承载了View,而是通过Window,这里我们不深究,你只需要把Activity也看成是一个ViewGroup就好了。
我们可以认为,MotionEvent就是从Activity来的(其实并不是,具体参看)而它调用的第一个方法就是dispatchTouchEvent,我们看源码它将这个事件交给了谁。
//1. Activity.dispatchTouchEvent()
public boolean dispatchTouchEvent(MotionEvent ev) {
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}
随后,我们找到Window的实现类PhoneWindow,查看superDispatchTouchEvent方法。
//2. PhoneWindow.superDispatchTouchEvent()
public boolean superDispatchTouchEvent(MotionEvent event) {
return mDecor.superDispatchTouchEvent(event);
}
很明显,MotionEvent进入到了DecorView
//3. DecorView.superDispatchTouchEvent()
public boolean superDispatchTouchEvent(MotionEvent event) {
return super.dispatchTouchEvent(event);
}
而DecorView继承于FrameLayout,但是FrameLayout中并没有这个方法,我们继续往上层找即ViewGroup中,这里事件的分发才真正开始。
传递开始
在这里我们要先明白一个概念,在一次点击事件中其实是有多MotionEvent的,MotionEvent也根据Action的不同来表达不同的动作,比如DOWN、MOVE、UP、CANCEL。
一次快速的点击包含一个DOWN和UP,而点击屏幕、拖动、抬起手指却是包含了一个DOWN、一个UP和若干个MOVE,再明白了这一系列动作之后,我们开始去传递事件了。
在ViewGroup的dispatchTouchEvent方法中这个方法的作用是去分发事件,分发——就是要找到真正需要处理事件的子View,根据我们上面说的,难道一系列的事件来了之后,我们要一个一个的去寻找处理它的子View吗?当然没有必要,我们只需要通过Down事件找到那个View,之后的后续事件都交给它处理就好了,如果没有子View去『吃掉』这个事件,再考虑自己处理或者交给上层。
public boolean dispatchTouchEvent(MotionEvent ev) {
if (actionMasked == MotionEvent.ACTION_DOWN) {
//清空上一次的DOWN事件遗留
cancelAndClearTouchTargets(ev);
resetTouchState();
}
// 检查是否需要拦截
final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
// 从字面意思上我们可以猜出,不允许拦截,如果为false,就去调用onInterceptTouchEvent()方法去拦截事件
// 这个字段非常有用,子View可以通过requestDisallowInterceptTouchEvent()方法来控制父ViewGroup是否去拦截点击事件
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (!disallowIntercept) {
intercepted = onInterceptTouchEvent(ev);
} 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) {
final View[] children = mChildren;
for (int i = childrenCount - 1; i >= 0; i--) {
final int childIndex = getAndVerifyPreorderedIndex(
childrenCount, i, customOrder);
// 获取到每个child view
final View child = getAndVerifyPreorderedView(
preorderedList, children, childIndex);
// 这两个方法也很关键,canViewReceivePointerEvents()表示:view是否可见或者是否正在执行动画
// isTransformedTouchPointInView() 表示:此次滑动或者点击的范围是否在View的范围内
// 也就是说,如果这个子View正在执行动画或者不可见,或者不在滑动范围内,是不能处理点击事件的。
if (!canViewReceivePointerEvents(child)
|| !isTransformedTouchPointInView(x, y, child, null)) {
continue;
}
// 这里其实就是调用了child.dispatchTouchEvent(event);
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
// 为mFirstTouchTarget赋值,就是找到了处理事件的那个View
newTouchTarget = addTouchTarget(child, idBitsToAssign);
break;
}
}
}
// 把事件传递给目标View
if (mFirstTouchTarget == null) {
// 没有找到目标View,交给子View处理或者自己调用TouchEvent处理
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
} else {
// 处理MOVE、UP等后续事件(因为上面都是针对DOWN事件的,也因为找到了mFirstTouchTarget,后续事件可以不参与上面的判断而直接来分发)
TouchTarget target = mFirstTouchTarget;
while (target != null) {
final TouchTarget next = target.next;
if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
handled = true;
} else {
final boolean cancelChild = resetCancelNextUpFlag(target.child)
|| intercepted;
if (dispatchTransformedTouchEvent(ev, cancelChild,
target.child, target.pointerIdBits)) {
handled = true;
}
}
}
}
return handled;
}
这里是ViewGroup中事件的分发方法,省去了很多不必要的行,大家可以对照注释去理解。
ViewGroup如果找到了处理的View,就会调用子View的dispatchTouchEvent()方法,这个方法要相对简单很多,但是对于各种listener的调用会对大家的今后的使用有一定帮助。
public boolean dispatchTouchEvent(MotionEvent event) {
final int actionMasked = event.getActionMasked();
if (actionMasked == MotionEvent.ACTION_DOWN) {
stopNestedScroll();
}
if (onFilterTouchEventForSecurity(event)) {
// 各种listener的合集(OnClickListener、OnLongClickListener、OnTouchListener等等)
ListenerInfo li = mListenerInfo;
// 如果我们设置了OnTouchListener监听,同时OnTouchListener的onTouch方法返回的是true,并且是可用状态,
// 那么,这个View就不会调用自己的onTouchEvent方法了
if (li != null && li.mOnTouchListener != null
&& (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {
result = true;
}
// 如果上面不调用result = true,这里去调用onTouchEvent方法。注意:View里面没有onInterceptTouchEvent方法
if (!result && onTouchEvent(event)) {
result = true;
}
}
return result;
}
于是,我们的事件传递到了TouchEvent中。
但是有一个问题,我们收到这个MotionEvent之后该做什么?回想我们在开发中用到的东西,最多的难道不是setOnClickListener或者setOnLongClickListener,再或者setOnTouchListener吗?而这里就是处理这些方法的中心。
public boolean onTouchEvent(MotionEvent event) {
// 这里主要看viewFlags这个参数,如果设置了Clicklistener或者LongClickener都会把这个值置位相应的标志位,也就是说是可点击的。
final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
|| (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
|| (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;
// 这里注意,虽然有的组件设置为了不可用,但是也是会『吃掉』点击事件的,只不过没有回应。
if ((viewFlags & ENABLED_MASK) == DISABLED) {
return clickable;
}
if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
switch (action) {
case MotionEvent.ACTION_UP:
// 把各种回调都移除,因为一次点击已经结束
if (!clickable) {
removeTapCallback();
removeLongPressCallback();
mInContextButtonPress = false;
mHasPerformedLongPress = false;
break;
}
boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
// View是否获取了焦点
boolean focusTaken = false;
if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {
focusTaken = requestFocus();
}
// 这里其实判断OnClick事件的一个重要分支,mHasPerformedLongPress顾名思义就是是否触发的长按事件,但是如果
// OnLongClick方法返回false,依然是会触发OnClick事件
if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
// Only perform take click actions if we were in the pressed state
if (!focusTaken) {
// 这里其实就是触发OnClick方法的触发点,但是触发的时候不是直接调用,而是用post的方式,
// 这样可以在点击事件的反馈效果之前,让其他的视觉效果也能相继触发
if (mPerformClick == null) {
mPerformClick = new PerformClick();
}
if (!post(mPerformClick)) {
performClickInternal();
}
}
}
}
break;
case MotionEvent.ACTION_DOWN:
// 开启触发LongClick的触发点,如果是在可滚动的容器内,需要延迟100ms判断,如果不在开始触发,触发也是通过post执行,delay的时间是500ms
if (isInScrollingContainer) {
postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
} else {
checkForLongClick(0, x, y);
}
break;
case MotionEvent.ACTION_CANCEL:
// 取消各种回调
removeTapCallback();
removeLongPressCallback();
mInContextButtonPress = false;
mHasPerformedLongPress = false;
break;
case MotionEvent.ACTION_MOVE:
if (clickable) {
// 绘制各个热点区域,比如水波纹效果
drawableHotspotChanged(x, y);
}
// 是否已经移出了View中
if (!pointInView(x, y, mTouchSlop)) {
// 移除tap和LongPress的触发回调
removeTapCallback();
removeLongPressCallback();
}
break;
}
return true;
}
return false;
}
由源码我们可以看出,View的TouchEvent方法就是一个大型的回调调用方法,里面判断各种条件,去把合适的回调方法调用,来满足我们开发的需求。比如我们可以明显的看出Onclick方法是在UP事件中触发的,LongClick可以制约Click的触发。
结论
- 一个事件序列从手指接触屏幕到手指离开屏幕,在这个过程中产生一系列的事件,以DOWN事件为开始,包含若干个MOVE,以UP事件为结尾。
- 正常情况下,一个事件序列只能被一个View拦截并且消耗。
- 某个View一旦决定拦截,那么这个事件序列都将由它的onTouchEvent处理,并且它的onInterceptTouchEvent调用。
- 某个View一旦开始处理事件,如果它不消耗ACTION_DOWN事件,那么同一事件序列中其他事件都不会再交给它处理。并且重新交由它的父元素处理。
- 事件传递的过程是由外向内的,即事件总是先传递给父元素,然后再由父元素分发给子View,通过reqeustDisallowInterceptTouchEvent方法可以在子View中干预父元素的事件分发过程,但ACTION_DOWN事件除外。
- ViewGroup默认不拦截任何事件,onInterceptTouchEvent默认返回false。View没有onInterceptTouchEvent方法,一旦有事件传递给它,那么它的onTouchEvent方法就会被调用。
- View的onTouchEvent默认会消耗事件(返回true),除非它是不可点击的(clickable和longClickable同时为false)。View的longClickable默认都为false,clickable要分情况,不如Button的clickable默认为true,TextView的clickable默认为false。
- View的enable属性不影响onTouchEvent的返回值。哪怕一个View为disable状态,只要它的clickable或者LongClickable有一个为true,那么它的onTouchEvent就返回true。
- onClick事件会响应的前提是当前View是可点击的,并且收到了ACTION_DOWN的ACTION_UP的事件,并且受长按事件的影响,当长按事件返回true时,onClick不会响应。
- onLongClick在ACTION_DOWN里判断是否进行响应,要想执行长按事件该View必须是longClickable的并且设置了OnLongClickListener。
总结
事件的分发其实并不复杂,但是这个设计思路以及细节的设计特别值得我们推敲,我一直在想,如果让我们自己设计一个事件分发模型,我们会怎么做呢?