Android里的事件处理是很复杂但也是非常重要的,一般看别人写的开源控件里,基本上都会涉及到对事件的处理,所以理解这一部分是非常有必要的。曾经看过很多关于Android事件处理的文章,很多都是围绕着onInterceptTouchEvent()和onTouchEvent()两个方法来说的,一般的解释是说Android的View层次结构是递归的,如果这些方法返回了true就代表消费了事件,如果返回false,就把事件传递给它的父亲。我很迷惑这些事件是怎样从子控件传递到父控件的,难道当中做了什么特殊处理?反正看例子程序是越看越迷糊。最后还是拿到了一份源码,以及找到了几篇不错的文章,才把这些疑惑给消除掉。
我相信现在有很多人也是对这一块的知识点很迷惑,写这篇文章也算把我看到的一些和自己理解的一些分享给大家,而一些别人已经写过的我也不再重复了,别人写的也比我写的详细多了。下面就是我推荐的几篇文章,如果大家希望很好的了解这一部分,这几篇文章非常值得一读。
Android事件分发机制完全解析,带你从源码的角度彻底理解(上)
Android事件分发机制完全解析,带你从源码的角度彻底理解(下)
Andriod 从源码的角度详解View,ViewGroup的Touch事件的分发机制
希望大家在阅读下面的内容前,可以先阅读这几篇文章。
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;
final Rect frame = mTempRect;
// 默认为false,即默认是允许拦截事件的
boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (action == MotionEvent.ACTION_DOWN) {
/**
* 对于每一次的点击事件(down-move-...-move-up),
* 它都会去找一个接收事件的target view,而且此次找到的target view
* (如果能找到的话)不会影响下一次点击事件的target view,因为在每次事件
* 的一开始,都将target view置为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
// 如果我们的ViewGroup不允许拦截事件,或者没有成功拦截下来,就执行到if语句里面的内容
if (disallowIntercept || !onInterceptTouchEvent(ev)) {
// reset this event's action (just to protect ourselves)
// 万一在onInterceptTouchEvent()中将这个action给变了呢,是吧?
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.
final int scrolledXInt = (int) scrolledXFloat;
final int scrolledYInt = (int) scrolledYFloat;
final View[] children = mChildren;
final int count = mChildrenCount;
/**
* 也只有在ACTION_DOWN事件的时候,才会对该ViewGroup的所有子View进行
* 一一探测,在其他事件的时候,会直接根据target view来处理事件,所以一个
* View有可能接收到ACTION_DOWN事件,但不一定能接收到ACTION_MOVE或者
* ACTION_UP事件。这一点要切记!!!
*/
for (int i = count - 1; i >= 0; i--) {
final View child = children[i];
if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE
|| child.getAnimation() != null) {
child.getHitRect(frame);
if (frame.contains(scrolledXInt, scrolledYInt)) {
// offset the event to the view's coordinate system
final float xc = scrolledXFloat - child.mLeft;
final float yc = scrolledYFloat - child.mTop;
ev.setLocation(xc, yc);
child.mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;
if (child.dispatchTouchEvent(ev)) {
// Event handled, we have a target now.
/**
* OK,找到了target view,即有view接收并处理了该事件.
* 注意:这里的target view是该ViewGroup的直接子View,
* 而该ViewGroup也会成为它父ViewGroup的target view.
*
* 所以,我们在执行ACTION_DOWN事件时候,就已经确定了该次
* 事件的target view.
*/
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.
}
}
}
}
}
boolean isUpOrCancel = (action == MotionEvent.ACTION_UP) ||
(action == MotionEvent.ACTION_CANCEL);
if (isUpOrCancel) {
// Note, we've already copied the previous state to our local
// variable, so this takes effect on the next event
// 恢复默认值,即允许ViewGroup拦截掉事件,所以我们如果在执行一次事件之前
// 设置了不允许拦截事件,不用担心它会影响下一次事件的执行。
mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
}
// The event wasn't an ACTION_DOWN, dispatch it to our target if
// we have one.
/**
* 如果执行到这里的事件是:
* 1. ACTION_DOWN事件:
* 如果target view为null,则说明该ViewGroup的所有子View都没法成为target view,
* 那么就试试该ViewGroup自身能否成为target view。ViewGroup的父类是View,所以
* 这里super.dispatchTouchEvent(ev)就是调用View类中onTouchEvent()等方法。所以
* 如果该ViewGroup设置了TouchListener并且返回true,或者在onTouchEvent()中返回
* true,那么该ViewGroup就成为了target view。
* 此时对于该ViewGroup的Parent View来说,它正在执行的是上面的for循环,如果它
* 发现该ViewGroup成为了target view,那么循环就终止了,否则继续循环。
*
* 2. ACTION_MOVE或ACTION_UP事件,这里会出现三种情况:
* a. 所有的ACTION_MOVE和ACTION_UP事件执行到这的时候target view都为null,说明
* 该ViewGroup就是整个事件的target view,交给它处理这些事件即可。
* b. 与上面相反,所以事件执行到这的时候target view都不为null,我们就需要把事件
* 交给当前ViewGroup的target view来处理。
* c. 参考下面的onInterceptTouchEvent()方法,即在确定了target view的情况下,该
* ViewGroup把事件给拦截下来了,会先向当前ViewGroup的target view发送一个
* ACTION_CANCEL动作,然后设置target view为NULL,所以以后的事件都交给当前
* ViewGroup本身来执行。
*/
final View target = mMotionTarget;
if (target == null) {
// We don't have a target, this means we're handling the
// event as a regular view.
ev.setLocation(xf, yf);
if ((mPrivateFlags & CANCEL_NEXT_UP_EVENT) != 0) {
ev.setAction(MotionEvent.ACTION_CANCEL);
mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;
}
return super.dispatchTouchEvent(ev);
}
/**
* 如果是ACTION_DOWN动作,执行到这里一定会执行完了。
* 接下来的处理只有ACTION_MOVE、ACTION_UP等动作能执行到了
* (这都是在该ViewGroup的target view不为null前提下执行的)
*/
// if have a target, see if we're allowed to and want to intercept its
// events
// 如果我们设置了允许拦截事件,并且成功拦截下来了
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);
/*
* 既然当前的ViewGroup把事件拦截下来了,那么它的target view就不应该再收到
* 这个事件,我们给target view发送一个CANCAL事件。
*/
if (!target.dispatchTouchEvent(ev)) {
// target didn't handle ACTION_CANCEL. not much we can do
// but they should have.
}
// clear the target
// 注意这里:清空了当前ViewGroup的target view,但是对于它的父View或者整个事件来说,
// target view 仍然是存在的。
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;
}
if (isUpOrCancel) {
mMotionTarget = null;
}
// finally offset the event to the target's coordinate system and
// dispatch the event.
final float xc = scrolledXFloat - (float) target.mLeft;
final float yc = scrolledYFloat - (float) target.mTop;
ev.setLocation(xc, yc);
if ((target.mPrivateFlags & CANCEL_NEXT_UP_EVENT) != 0) {
ev.setAction(MotionEvent.ACTION_CANCEL);
target.mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;
mMotionTarget = null;
}
// 一般的情况,我们直接把这个事件传递给我们的target view,既然有这个传递过程,
// 那么肯定是经过了当前ViewGroup的onInterceptTouchEvent()的,这从上面代码
// 中也可得知。
// 并且,此时target view的返回值并不会对整个事件造成影响了。
return target.dispatchTouchEvent(ev);
}