一、概述
之前写了很多项目,多多少少会使用到Android View 和 ViewGroup的事件分发机制。
即:View Group的3兄弟,dispatchTouchEvent,onInterceptTouchEvent,onTouchEvent;
和View 的事件分发2兄弟:dispatchTouchEvent,onTouchEvent。
首先说明,本文此次只重点说明,dispatchTouchEvent 和 onInterceptTouchEvent这两个事件分发的方法,至于onTouchEvent,相信大家都已经很熟悉,也用过很多,具体操作也没什么难点。
二、ViewGroup 的事件分发
1、viewgroup 的事件分发 dispatchTouchEvent 源码分析
/**
* {@inheritDoc}
*/
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
if (mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onTouchEvent(ev, 1);
}
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();
}
// Check for interception.
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;
}
// Check for cancelation.
final boolean canceled = resetCancelNextUpFlag(this)
|| actionMasked == MotionEvent.ACTION_CANCEL;
// Update list of touch targets for pointer down, if needed.
final boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0;
TouchTarget newTouchTarget = null;
boolean alreadyDispatchedToNewTouchTarget = false;
if (!canceled && !intercepted) {
if (actionMasked == MotionEvent.ACTION_DOWN
|| (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
final int actionIndex = ev.getActionIndex(); // always 0 for down
final int idBitsToAssign = split ? 1 << ev.getPointerId(actionIndex)
: TouchTarget.ALL_POINTER_IDS;
// Clean up earlier touch targets for this pointer id in case they
// have become out of sync.
removePointersFromTouchTargets(idBitsToAssign);
final int childrenCount = mChildrenCount;
if (newTouchTarget == null && childrenCount != 0) {
final float x = ev.getX(actionIndex);
final float y = ev.getY(actionIndex);
// Find a child that can receive the event.
// Scan children from front to back.
final View[] children = mChildren;
final boolean customOrder = isChildrenDrawingOrderEnabled();
for (int i = childrenCount - 1; i >= 0; i--) {
final int childIndex = customOrder ?
getChildDrawingOrder(childrenCount, i) : i;
final View child = children[childIndex];
if (!canViewReceivePointerEvents(child)
|| !isTransformedTouchPointInView(x, y, child, null)) {
continue;
}
newTouchTarget = getTouchTarget(child);
if (newTouchTarget != null) {
// Child is already receiving touch within its bounds.
// Give it the new pointer in addition to the ones it is handling.
newTouchTarget.pointerIdBits |= idBitsToAssign;
break;
}
resetCancelNextUpFlag(child);
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
// Child wants to receive touch within its bounds.
mLastTouchDownTime = ev.getDownTime();
mLastTouchDownIndex = childIndex;
mLastTouchDownX = ev.getX();
mLastTouchDownY = ev.getY();
newTouchTarget = addTouchTarget(child, idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true;
break;
}
}
}
if (newTouchTarget == null && mFirstTouchTarget != null) {
// Did not find a child to receive the event.
// Assign the pointer to the least recently added target.
newTouchTarget = mFirstTouchTarget;
while (newTouchTarget.next != null) {
newTouchTarget = newTouchTarget.next;
}
newTouchTarget.pointerIdBits |= idBitsToAssign;
}
}
}
// Dispatch to touch targets.
if (mFirstTouchTarget == null) {
// No touch targets so treat this as an ordinary view.
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
} else {
// Dispatch to touch targets, excluding the new touch target if we already
// dispatched to it. Cancel touch targets if necessary.
TouchTarget predecessor = null;
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;
}
if (cancelChild) {
if (predecessor == null) {
mFirstTouchTarget = next;
} else {
predecessor.next = next;
}
target.recycle();
target = next;
continue;
}
}
predecessor = target;
target = next;
}
}
// Update list of touch targets for pointer up or cancel, if needed.
if (canceled
|| actionMasked == MotionEvent.ACTION_UP
|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
resetTouchState();
} else if (split && actionMasked == MotionEvent.ACTION_POINTER_UP) {
final int actionIndex = ev.getActionIndex();
final int idBitsToRemove = 1 << ev.getPointerId(actionIndex);
removePointersFromTouchTargets(idBitsToRemove);
}
}
if (!handled && mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onUnhandledEvent(ev, 1);
}
return handled;
}
.
·
代码较长,我们直接看这一段,比较重点的代码:
// Check for interception.(确认是否拦截)
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;
}
google给的这段代码的解释是
// Check for interception.(确认是否拦截)
那么说明,这段代码是拦截ViewGroup的事件分发的核心。再来看看下面的代码,默认拦截变量final boolean intercepted;为false,接下来就是第一个if判断,需要2个条件触发,
1 是actionMasked == MotionEvent.ACTION_DOWN,想必大家很熟悉,意思是当前点击按下,一般当有触摸事件时,都会先触发ACTION_DOWN,所以当有点击事件时,所以1为true;
2 是mFirstTouchTarget != null,我们不知道mFirstTouchTarget具体是什么意思,我们先看,如果第一个if失败,进入else是什么:
else {
// There are no touch targets and this action is not an initial down
// so this view group continues to intercept touches.
intercepted = true;
}
google的注释意思是,没有触摸事件的目标而且一开始不是down,简单说就是viewgroup的子view没有注册触摸事件的监听,或者触摸事件不是down开始(这个我也不是很明白,意思就是说非正常的触摸事件)所以,viewgroup会继续拦截此触摸事件,看看返回值,true。好的,到此我们应该已经知道了,若想拦截子view 的触摸事件只需要viewGroup的dispatchTouchEvent事件,返回true即可。
那么我们回到第一个if为true的方法内(即正常的触摸事件执行流程),这里又定义一个变量disallowIntercept ,顾名思义是指“不允许拦截”的意思,那么看看,赋值的代码如下:
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
这里我不清楚 mGroupFlags 是多少,但是不要紧,我们继续往下看,接下来就出现第二个if判断,而且条件就是 disallowIntercept 变量的取反是否为 true,如果 disallowIntercept = true;意思是不允许viewgroup,拦截触摸事件,好的,刚才我们说 viewgroup会拦截此触摸事件就会赋值 intercepted = true,那么此时应该赋值false,看看第二个else方法,果然赋值 false。
intercepted = false;
现在我们直接看第二个if里的代码
if (!disallowIntercept) {
intercepted = onInterceptTouchEvent(ev);
ev.setAction(action); // restore action in case it was changed
}
此时有对intercepted的赋值代码为
intercepted = onInterceptTouchEvent(ev);
其实,第二个if内的代码,才是我们日常编程中接触的,触摸事件的传递处理方法,其他只是我们不知道出什么问题时的紧急处理。个人觉得,此处的代码才是我们前期 重点分析的地方。继续往下看
onInterceptTouchEvent方法的返回值赋值给 intercepted ,决定着分发事件的结果,此方法也正是 ViewGroup事件分发3兄弟中的一员,那么我们直接进入onInterceptTouchEvent方法看看:
2、viewgroup 的事件分发 dispatchTouchEvent 源码分析
public boolean onInterceptTouchEvent(MotionEvent ev) {
return false;
}
看到这里,顿时心里微微一笑,哈哈,只有一行,而且返回值固定为 false,那么我们回头再看看,第二个if的条件判断,也就是说,不管if条件是真是假都会赋值 intercepted = false; 好的,那么我们如何修改intercepted 的赋值呢? 很简单,直接修改 onInterceptTouchEvent的返回值就好了,而且它只有一句话,是不是觉得 是gooogle故意留给开发者修改返回值,从而达到开发者想要的事件拦截的目的呢(我猜的O(∩_∩)O~)
不过事实证明,修改viewGroup的事件传递确实只需要修改 onInterceptTouchEvent的返回值,就可以了,人为返回true后,子view的触摸事件将不会执行,这里就不将测试结果贴出来了了。
那么我们现在对viewGroup的3兄弟已经处理好2个了,而且 dispatchTouchEvent根本不需要开发者重写。还有一个 onTouchEvent事件,其实一般来说,ViewGroup 的onTouchEvent事件都不会写些什么,主要是把事件如何分发给子view 的 onTouchEvent去处理。就是处理的话,和View的onTouchEvent处理方法类似。
三、View的事件分发
1、view 的事件分发 dispatchTouchEvent 源码分析
/**
* 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) {
if (mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onTouchEvent(event, 0);
}
if (onFilterTouchEventForSecurity(event)) {
//noinspection SimplifiableIfStatement
ListenerInfo li = mListenerInfo;
if (li != null && li.mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {
return true;
}
if (onTouchEvent(event)) {
return true;
}
}
if (mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onUnhandledEvent(event, 0);
}
return false;
}
View 的 dispatchTouchEvent代码相对较少,分析起来也相对容易。
我们直接看重点代码第二个if的判断,因为我们可以看到,除此之外就没有修改返回值的地方了
`if (onFilterTouchEventForSecurity(event))
我们来看看 onFilterTouchEventForSecurity 具体操作是什么
/**
* Filter the touch event to apply security policies.(过滤触摸事件应用的安全策略。)
*
* @param event The motion event to be filtered.
* @return True if the event should be dispatched, false if the event should be dropped.
*
* @see #getFilterTouchesWhenObscured
*/
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.(Window 被遮盖了,终止触摸事件)
return false;
}
return true;
}
google的官方解释是过滤触摸事件应用的安全策略,就是说为了防止出现一些紧急情况,系统帮助我们处理了,再看看里面的if条件 成立时返回false,google解释是,Window 被遮盖了,终止触摸事件就是说,返回false时是系统的应急处理,正常情况返回true,所以,就像之前我们说过的,我们前期只需要关心,正常的触摸事件分发即可
再回到View 的dispatchTouchEvent 方法的 if (onFilterTouchEventForSecurity(event))此处if判断为我们正常情况为true,进入方法内部,看此段代码:
if (onTouchEvent(event)) {
return true;
}
由此我们可以看出,关系到View 的 dispatchTouchEvent 方法的返回值,是和 View 的onTouchEvent(event)方法相关的,且返回值的结果 和 onTouchEvent 的返回值一致。
正如我们所知,当View 的 onTouchEvent返回true时,表示此View消费了当前的触摸事件,故我们得出如下结论:
dispatchTouchEvent作用是:
将touch事件向下传递直到遇到被触发的目标view,如果返回true,表示当前view就是目标view,事件停止向下分发。否则返回false,表示当前view不是目标view,需要继续向下分发寻找目标view.这个方法也可以被重载,手动分配事件。
所以,如果我们想手动分发View 的触摸事件,就重写 View 的 dispatchTouchEvent 方法,人为返回true 则不会向下分发,返回false,则本身,不能监听到触摸事件。
不管ViewGroup 还是 View 最终只会有一个 控件/View 能处理当次的触摸事件。
对于继承ViewGroup的一些控件,默认onInterceptTouchEvent 的返回值可能有所不同,如ViewPage重写了此方法,根据具体的滑动距离等不同,返回true或者false。(我之前因为这个卡了)
总结:
现在我们来总结一下,ViewGroup 和 View 的 事件分发机制。
1、 Android事件分发是先传递到ViewGroup,再由ViewGroup传递到View的。
2、ViewGroup 的dispatchTouchEvent 方法,开发者可以不处理,仅处理onInterceptTouchEvent即可。
3、在ViewGroup中我们可以通过onInterceptTouchEvent方法对事件传递进行拦截,方法返回true代表不允许事件继续向子View传递,返回false代表不对事件进行拦截,默认返回false。(有些继承ViewGroup的控件可能重写了此方法,会有不同)。
4、子View中如果将传递的事件消费掉,ViewGroup中将无法接收到任何事件,即一个触摸事件只能有一个控件消费。
5、View 的 dispatchTouchEvent 方法返回值决定View 是否处理改触摸事件,返回true,事件由当前View处理不向下传递,返回false,则本身不处理继续向下传递。
PS:附一张 Android 事件分发机制流程图。