View的事件分发和滑动冲突学习总结
前言
本文分两个部分,第一部分会先过一遍事件分发机制的流程并做一些结论性的总结,然后从源码层面分析这些流程。第二部分会介绍滑动冲突问题的一些解决方案。查了比较多的资料,也有一些自己的看法,由于知识有限,差错之处希望各位不吝指出。
View 的事件分发机制
简介
当一个MotionEvent产生了以后,系统需要把这个事件传递给一个具体的View,这个传递过程就是分发过程。这个过程由三个方法共同完成:
public boolean dispatchTouchEvent(MotionEvent ev)
这个方法用来进行事件的分发,如果事件能够传递给当前View,那么这个方法就一定会被调用。他的返回结果受到当前View的onTouchEvent和下级的dispatchTouchEvent的影响,表示的是当前View(包括其子View)是否消耗这个事件。
public boolean onInterceptTouchEvent(MotionEvent event)
这个方法在该ViewGroup的dispatchTouchEvent方法中调用,用来判断该View是否拦截某个事件,若拦截,那么该View将直接拦截与该事件同一事件序列的剩余事件,对这一事件序列不再调用onInterceptTouchEvent判断是否拦截。返回值表示是否拦截当前事件。这个方法在View的子类ViewGroup中而不在View中。
public boolean onTouchEvent(MotionEvent event)
在dispatchTouchEvent中调用,用来处理点击事件,返回结果表示是否消耗当前事件,如果不消耗,则不能接受同一事件序列内的剩余事件。
图解–来自Kelin
模拟流程的伪代码
public boolean dispatchTouchEvent(MontionEvent ev){
boolean consume = false;
if(onInterceptTouchEvent(ev)){
consume = onTouchEvent(ev);
}else{
consume = child.dispatchTouchEvent(ev);
}
return consume;
}
结论
- 事件序列是指从手指接触屏幕开始,到手指离开屏幕结束这个过程中产生的一系列事件,这个事件序列以down开始,中间有数量不定的move,最终以up结束。
- 某个ViewGroup被判定到拦截某一事件M,那么M所在序列中M之后的事件,都会被这个ViewGroup处理(如果事件能传递给它),而且这个ViewGroup不会再调用其onInterceptTouchEvent方法去判断是否拦截,而是默认拦截。也就是说,onInterceptTouchEvent这个方法并不会总是被调用。
- 如果某个View不消耗ACTION_DOWN事件,那么同一事件序列中的其他事件都不会再给它处理,而是给它的父元素处理。
- 如果View不消耗ACTION_DOWN以外的其他事件,那么这个点击事件会消失,此时父元素的onTouchEvent不会被调用,并且当前View可以持续接收到后续事件,消失的事件会传递给Activity处理。
- ViewGroup默认不拦截任何事件。
- View没有onInterceptTouchEvent方法,一旦有事件传递给它,它的onTouchEvent方法就会被调用。
- View的onTouchEvent默认会消耗事件,除非它是不可点击的(clickable和longClickable同时为false)。View的longClickable属性默认为false。
- 一个disable的View依然可能会消耗事件,只要clickable或longClickablec中有一个为true。但并不运行onClickListener的onClick方法和onLongClickListener中的onLongClick方法甚至是onTouchListener中的onTouch方法。
- 事件的传递过程是由内向外的,即事件总是先传递给父元素,然后再由父元素分发给子View,通过requestDisallowInterceptTouchEvent方法可以在子元素中干预父元素的分发过程,当ACTION_DOWN事件除外。
分析
接下来就该上源码了,首先是ViewGroup的dispatchTouch方法(分析在注释里)
public boolean dispatchTouchEvent(MotionEvent ev) {
if (mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onTouchEvent(ev, 1);
}
// If the event targets the accessibility focused view and this is it, start
// normal event dispatch. Maybe a descendant is what will handle the click.
if (ev.isTargetAccessibilityFocus() && isAccessibilityFocusedViewOrHost()) {
ev.setTargetAccessibilityFocus(false);
}
boolean handled = false;
if (onFilterTouchEventForSecurity(ev)) {
final int action = ev.getAction();
final int actionMasked = action & MotionEvent.ACTION_MASK;
//第一步,进行初始化操作
/*判断是否为ACTION_DOWN,如果是,
就将一些标志位进行重置等操作,包括
disallowIntercept,所以不能被不允许拦截
注意,在cancelAndClearTouchTargets(ev)中
会将mFirstTouchTarget设置为null*/
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();
}
// 第二步,检查是否拦截
final boolean intercepted;
/* 判断是否为ACTION_DOWN或mFirstTouchTarget是否为null
当满足其中一个的时候,进入并调用
onInterceptTouchEvent(ev)
来判断是否拦截。这里需要注意的是,在ViewGroup的子View处理
事件成功的时候,mFirstTouchTarget会被赋值并指向子元素。
也就是说,当事件不为ACTION_DOWN时,如果想调用
onInterceptTouchEvent(ev)判断是否拦截,
就必须让mFirstTouchTarget != null,
而这个条件必须是前一个事件没有被拦截且
ACTION_DWON能被子View消耗(如果ACTION_Down不能被消耗,
则mFirstTouchTarget是不会被赋值的。而如果是其他事件不
被消耗,由于子View消耗ACTION_DOWN时对
mFirstTouchTarget
赋了值,所以还是会进入调用onInterceptTouchEvent)。*/
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
/* 如果子View调用了requestDisallowInterceptTouchEvent方法,
则disallowIntercept为true,那么除了ACTION_DOWN,其他事件都不允许被拦截*/
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 intercepted, start normal event dispatch. Also if there is already
// a view that is handling the gesture, do normal event dispatch.
if (intercepted || mFirstTouchTarget != null) {
ev.setTargetAccessibilityFocus(false);
}
//第三步:检查cancel
// 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 the event is targeting accessiiblity focus we give it to the
// view that has accessibility focus and if it does not handle it
// we clear the flag and dispatch the event to all children as usual.
// We are looking up the accessibility focused host to avoid keeping
// state since these events are very rare.
View childWithAccessibilityFocus = ev.isTargetAccessibilityFocus()
? findChildWithAccessibilityFocus() : null;
//处理ACTION_DOWN事件,如果子View处理成功,
//那么mFirstTouchTarget会被赋值
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) {
// 依据Touch坐标寻找子View来接收Touch事件
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 ArrayList<View> preorderedList = buildTouchDispatchChildList();
final boolean customOrder = preorderedList == null
&& isChildrenDrawingOrderEnabled();
final View[] children = mChildren;
// 遍历子View判断哪个子View接受Touch事件
for (int i = childrenCount - 1; i >= 0; i--) {
final int childIndex = getAndVerifyPreorderedIndex(
childrenCount, i, customOrder);
final View child = getAndVerifyPreorderedView(
preorderedList, children, childIndex);
// If there is a view that has accessibility focus we want it
// to get the event first and if not handled we will perform a
// normal dispatch. We may do a double iteration but this is
// safer given the timeframe.
if (childWithAccessibilityFocus != null) {
if (childWithAccessibilityFocus != child) {
continue;
}
childWithAccessibilityFocus = null;
i = childrenCount - 1;
}
if (!canViewReceivePointerEvents(child)
|| !isTransformedTouchPointInView(x, y, child, null)) {
ev.setTargetAccessibilityFocus(false);
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();
if (preorderedList != null) {
// childIndex points into presorted list, find original index
for (int j = 0; j < childrenCount; j++) {
if (children[childIndex] == mChildren[j]) {
mLastTouchDownIndex = j;
break;
}
}
} else {
mLastTouchDownIndex = childIndex;
}
mLastTouchDownX = ev.getX();
mLastTouchDownY = ev.getY();
newTouchTarget = addTouchTarget(child, idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true;
break;
}
// The accessibility focus didn't handle the event, so clear
// the flag and do a normal dispatch to all children.
ev.setTargetAccessibilityFocus(false);
}
if (preorderedList != null) preorderedList.clear();
}
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.
/*mFirstTouchTarget == null有两种可能,
一是ACTION_DOWN被拦截或没有被处理,二是前一个事件被拦截。
不管是一还是二,当前事件和同一序列后续事件都不会被子View处理*/
if (mFirstTouchTarget == null) {
// No touch targets so treat this as an ordinary view.
// 向子View传递一个cancel事件,
//dispatchTransformedTouchEvent()可以将事件分发给子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 {
//若当前事件被拦截,cancelChild则为true
final boolean cancelChild = resetCancelNextUpFlag(target.child)
|| intercepted;
//如果cancelChild为true,那么向子View分发一个cancel事件
//从这里可以看出,如果拦截一个子View的事件,则会向
//它分发一个cancel事件使得它状态重置
if (dispatchTransformedTouchEvent(ev, cancelChild,
target.child, target.pointerIdBits)) {
handled = true;
}
if (cancelChild) {
if (predecessor == null) {
//如果intercepted为true
//mFirstTouchTarget最后会被赋值为null
mFirstTouchTarget = next;
} else {
predecessor.next = next;
}
target.recycle();
target = next;
continue;
}
}
predecessor = target;
target = next;
}
}
//处理ACTION_UP和ACTION_CANCEL,主要是还原操作
// 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;
}
然后是View的onTouchEvent:
public boolean onTouchEvent(MotionEvent event) {
final float x = event.getX();
final float y = event.getY();
final int viewFlags = mViewFlags;
final int action = event.getAction();
//判断其属性是否为DISABLED
if ((viewFlags & ENABLED_MASK) == DISABLED) {
//如果当前事件为ACTION_UP且该View的状态为Pressed
if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
//清除掉Pressed状态
setPressed(false);
}
// A disabled view that is clickable still consumes the touch
// events, it just doesn't respond to them.
//如果这个View是clickable或longClickable,
//则返回true,即消费该事件
return (((viewFlags & CLICKABLE) == CLICKABLE
|| (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
|| (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE);
}
//如果设置有代理,那就执行代理的onTouchEvent
//一般是由于View太小不好按,才会设置代理。
if (mTouchDelegate != null) {
if (mTouchDelegate.onTouchEvent(event)) {
return true;
}
}
//按照逻辑的连贯性,接下来我们先看ACTION_DOWN,最后再看ACTION_UP
if (((viewFlags & CLICKABLE) == CLICKABLE ||
(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) ||
(viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE) {
switch (action) {
case MotionEvent.ACTION_UP:
/*检测到ACTION_UP的时候,
不管是Pressed还是PrePressed状态,只要期间没有
ACTION_MOVE,即Pressed和PrePressed状态没有被取消,
就可以执行onClick方法,不同的是,由于PrePerssed
状态还没有被转换为Pressed状态的(mPendingCheckForTap
进程未被执行),所以在这里要setPressed(true, x, y);*/
boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
// take focus if we don't have it already and we should in
// touch mode.
boolean focusTaken = false;
if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {
focusTaken = requestFocus();
}
if (prepressed) {
// The button is being released before we actually
// showed it as pressed. Make it show the pressed
// state now (before scheduling the click) to ensure
// the user sees it.
// 当前并不是Pressed状态,所以在这里setPressed
setPressed(true, x, y);
}
//如果不是长按事件且下个ACTION_UP事件不被忽略
if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
// This is a tap, so remove the longpress check
/*移除等待长按的线程,这个线程做的事情其实就是
等待一段时间后调用longClick方法,如果你按下时间
足够,那就会执行这个方法。如果你中途移动或抬起,
那这个线程就会被停止*/
removeLongPressCallback();
// Only perform take click actions if we were in the pressed state
if (!focusTaken) {
// Use a Runnable and post this rather than calling
// performClick directly. This lets other visual state
// of the view update before click actions start.
if (mPerformClick == null) {
mPerformClick = new PerformClick();
}
if (!post(mPerformClick)) {
//在performClick方法里面会判断
//onClickListener是否为null,并执行onClick方法
performClick();
}
}
}
//下面都是一些还原状态的操作
if (mUnsetPressedState == null) {
mUnsetPressedState = new UnsetPressedState();
}
if (prepressed) {
postDelayed(mUnsetPressedState,
ViewConfiguration.getPressedStateDuration());
} else if (!post(mUnsetPressedState)) {
// If the post failed, unpress right now
mUnsetPressedState.run();
}
removeTapCallback();
}
mIgnoreNextUpEvent = false;
break;
case MotionEvent.ACTION_DOWN:
mHasPerformedLongPress = false;//初始化长按标志为false
if (performButtonActionOnTouchDown(event)) {
break;
}
// Walk up the hierarchy to determine if we're inside a scrolling container.
boolean isInScrollingContainer = isInScrollingContainer();
// For views inside a scrolling container, delay the pressed feedback for
// a short period in case this is a scroll.
/* 如果是在可滑动的容器中,接到ACTION_DOWN事件时,
不能直接将View设置为Pressed状态,得先等一下
(让手指保持当前状态115ms,即ViewConfiguration.getTapTimeout()),
这是为了避免将外部的滑动当作点击。如果不设置这个状态,
那么即使用户想滑动,当一碰到就会显示Pressed的状态,这是
不合理的。在对ACTION_MOVE的处理我们也可以看到,
如果滑出了View的范围,那这个PrePressed
状态会被去除,如果不是在可滑动的容器中,则直接设置为
Pressed状态。*/
if (isInScrollingContainer) {
mPrivateFlags |= PFLAG_PREPRESSED;
if (mPendingCheckForTap == null) {
mPendingCheckForTap = new CheckForTap();
}
mPendingCheckForTap.x = event.getX();
mPendingCheckForTap.y = event.getY();
postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
} else {
// Not inside a scrolling container, so show the feedback right away
setPressed(true, x, y);
checkForLongClick(0, x, y);
}
break;
//处理取消点击事件,将状态还原
case MotionEvent.ACTION_CANCEL:
setPressed(false);
removeTapCallback();
removeLongPressCallback();
mInContextButtonPress = false;
mHasPerformedLongPress = false;
mIgnoreNextUpEvent = false;
break;
/*如果手指移出了View的范围,则取消
“延迟115ms并设置为Pressed”
这一操作,也就是说如果在115ms
你的手指移动出这一范围,就不算是Pressed。
如果已经是Pressed状态,则进一步把
"等待500ms,并设置为longPressed”
这一操作也取消了,并setPressed(false)。*/
case MotionEvent.ACTION_MOVE:
drawableHotspotChanged(x, y);
// Be lenient about moving outside of buttons
if (!pointInView(x, y, mTouchSlop)) {
// Outside button
removeTapCallback();
if ((mPrivateFlags & PFLAG_PRESSED) != 0) {
// Remove any future long press/tap checks
removeLongPressCallback();
setPressed(false);
}
}
break;
}
return true;
}
return false;
}
private final class CheckForTap implements Runnable {
public void run() {
/*取消mPrivateFlags的PREPRESSED,
然后设置PRESSED标识,刷新背景,
如果View支持长按事件,则再发一个延时消息,检测长按;*/
mPrivateFlags &= ~PREPRESSED;
mPrivateFlags |= PRESSED;
refreshDrawableState();
if ((mViewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) {
postCheckForLongClick(ViewConfiguration.getTapTimeout());
}
}
}
class CheckForLongPress implements Runnable {
private int mOriginalWindowAttachCount;
public void run() {
// 1、如果此时设置了长按的回调,则执行长按时的回调,且如果长按的回调返回true;才把mHasPerformedLongPress置为ture;
// 2、否则,如果没有设置长按回调或者长按回调返回的是false;则mHasPerformedLongPress依然是false;
if (isPressed() && (mParent != null)
&& mOriginalWindowAttachCount == mWindowAttachCount) {
if (performLongClick()) {
mHasPerformedLongPress = true;
}
}
}
}
滑动冲突的处理
外部拦截法
让在点击事件都先经过父容器的拦截处理,若父容器需要此事件就拦截,若不需要就不拦截。这个方法需要重写父容器的onInterceptTouchEvent方法,在内部做相应的拦截即可,
伪代码如下:
public boolean onInterceptTouchEvent(MotionEvent event){
boolean intercepted = false;
switch(event.getAction()){
case MotionEvent.ACTION_DOWN:
intercepted = false;
break;
case MotionEvent.ACTION_MOVE:
if(父容器需要当前事件){
intercepted = false;
}else{
intercepted = false;
}
break;
case MotionEvent.ACTION_UP:
intercepted = false;
break;
default:
break;
}
return intercepted;
}
这里的ACTION_DOWN必须返回false,否则事件就没法在传递给子元素了,而ACTION_UP在这里意义不大,但考虑到ACTION_UP如果被拦截,那子元素的onClick事件就无法触发,所以也让它返回false。
内部拦截法
内部拦截罚是指父容器不拦截任何事件,所以的事件都传递给子元素,如果子元素选哟此事件就直接消耗掉,否则则由父容器来处理,这种方法需要配合requestDisallowInterceptTouchEvent方法才能正常工作。伪代码如下:
public boolean dispatchTouchEvent(MotionEvent event){
switch(event.getAction()){
case MotionEvent.ACTION_DOWN:
parent.requestDisallowInterceptTouchEvent(true);
break;
case MotionEvent.ACTION_MOVE:
if(父容器需要此类点击事件){
parent.requestDisallowIntercepTouchEvent(false);
}
break;
case MotionEvent.ACTION_UP:
break;
case default:
break;
}
return super.dispatchTouchEvent(event);
}