图解+源代码 全面理解Android View事件分发
Posted TellH
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了图解+源代码 全面理解Android View事件分发相关的知识,希望对你有一定的参考价值。
View事件分发是android里面非常重要的知识点,我查阅了很多资料,新建demo分析Log日志加以验证,总结成这篇博文。
宏观的角度
View 事件分发涉及到三种角色,三个方法,三种重点事件,分别是Activity,ViewGroup,View;dispatchTouchEvent, onInterceptTouchEvent,onTouchEvent和ACTION_DOWN,ACTION_MOVE,ACTION_UP。
三个方法的关系可概括为如下伪代码:
public boolean dispatchTouchEvent(MotionEvent ev)
boolean consume = false;
if (onInterceptTouchEvent(ev))
consume = onTouchEvent(ev);
else
consume = child.dispatchTouchEvent(ev);
return consume;
值得记住的几个结论:
- activity的dispatchTouchEvent无论返回true or false都没有意义,但当View or ViewGroup的dispatchTouchEvent返回true时,表示它要消耗了该事件,并且该事件不会再向下传递。
- onTouchEvent返回ture表示它要消耗该事件,并且该事件不会再往下或往上传递。同一个View or ViewGroup的dispatchTouchEvent的返回值会与onTouchEvent的返回值同步,也就是说当onTouchEvent返回true,它的dispatchTouchEvent也会返回true。
- 因此,第一个View or ViewGroup要消耗一个事件,除了可以在onTouchEvent中返回true,还可以在dispatchTouchEvent中返回true。而前者是后者的充分非必要条件,
- 一个事件序列包含若干个事件,一般地,ACTION_DOWN是一个事件序列的开头,ACTION_UP是一个事件序列的结尾。
- 一个事件序列只能被一个VIew拦截且消耗。同一个事件序列中的事件不能分别由两个View同时处理。某个View一旦消耗了ACTION_DOWN事件,那么这一个事件序列的事件,都会从上往下一直传递到这个View为止(但往下传递的过程中上层的onInterceptTouchEvent仍会被调用,上层仍可以拦截事件),而且它的onInterceptTouchEvent不会再被调用了(如果有的话)。
- 如果一个事件序列本来要交给下层某一View or ViewGroup(代号A)来处理,但某个事件从上往下传递的过程中被上层某一个ViewGroup(代号B)拦截(onInterceptTouchEvent return true),那么A会收到一个ACTION_CANCEL事件,而原事件也消失了。此事件序列以后的事件就不会再传到A而是传给B,B获得了事件序列处理权。
ACTION_DOWN的事件分发图解
图源:http://www.jianshu.com/p/e99b5e8bd67b#rd
- 带箭头的虚线上的super表示在起点方法内调用父类的重写方法后事件的流向,也就是说带super的虚线标识着事件默认的流动。
- 带箭头的虚线上的true or false标识起点方法返回值,方法返回后事件的流动,
- 令dispatchTouchEvent 直接返回false时,系统会回调父容器的onTouchEvent,事件会回流给父容器。
@Override
public boolean dispatchTouchEvent(MotionEvent ev)
return false;
- ACTION_MOVE,ACTION_UP与ACTION_DOWN的事件分发的异同可以概括为:ACTION_MOVE,ACTION_UP跟ACTION_DOWN从上往下传递,只不过当ACTION_MOVE,ACTION_UP传到拥有事件序列接管权的那一层View or ViewGroup时,不再调用该层的onInterceptTouchEvent 了,而是直接调用该层的onTouchEvent,不管它返回true or false,事件不再往别处传递。举个例子:Activity中依次套着ViewGroup1(ViewGroup2(View)) ,ACTION_UP在ViewGroup的onTouchEvent中被消费(红线表示),那么之后同一个事件序列的ACTION_MOVE,ACTION_UP的流向都会像蓝线那样。
图源:http://www.jianshu.com/p/e99b5e8bd67b#rd
源代码的角度
Activity对事件的分发过程
当一个点击操作发生时,事件最先传递到当前的Activity,由Activity的dispatchTouchEvent进行事件分发。
public boolean dispatchTouchEvent(MotionEvent ev)
if (ev.getAction() == MotionEvent.ACTION_DOWN)
onUserInteraction();
if (getWindow().superDispatchTouchEvent(ev))
return true;
return onTouchEvent(ev);
事件会先传递给PhoneWindow中的DecorView,如果DecorView不能处理,则调用自己的onTouchEvent。
ViewGroup对事件的分发过程
在ViewGroup的dispatchTouchEvent中,有如下代码判断是否拦截点击事件。
// 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;
当事件由VIewGroup的子元素消耗时,mFirstTouchTarget会指向该元素。mFirstTouchTarget其实是一种单链表结构,如果mFirstTouchTarget==null,那么ViewGroup就默认拦截接下来同一事件序列的事件。
- 当
(actionMasked == MotionEvent.ACTION_DOWN|| mFirstTouchTarget != null)==false
时,ViewGroup的onInterceptTouchEvent不会再被调用,并且同一个事件序列的其他事件都会默认交给它处理,因为此时的传过来的事件为ACTION_MOVE or ACTION_UP,而且没有一个子元素能处理,那就这能交给自己来处理了。 - 当
(actionMasked == MotionEvent.ACTION_DOWN|| mFirstTouchTarget != null)==true
,这时,是否拦截事件取决于标记位FLAG_DISALLOW_INTERCEPT,它是通过requestDisallowInterceptTouchEvent方法来设置,一般用于子View请求让父容器不要拦截除了ACTION_DOWN以外的事件。可以用于解决滑动冲突。
接着根据点击坐标定位候选子元素,并将事件逐个分发给子元素来处理。
for (...)
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;
如果某一子元素成功处理,mFirstTouchTarget会在addTouchTarget内被赋值。
View对事件的分发过程
在View的dispatchTouchEvent方法中进行事件分发
public boolean dispatchTouchEvent(MotionEvent event)
boolean result = false;
if (onFilterTouchEventForSecurity(event))
//noinspection SimplifiableIfStatement
ListenerInfo li = mListenerInfo;
if (li != null && li.mOnTouchListener != null
&& (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event))
result = true;
if (!result && onTouchEvent(event))
result = true;
首先会判断是否设置了OnTouchListener,如果OnTouchListener#onTouch返回true,那么onTouchEvent就不会被调用了。由此可见,OnTouchListener#onTouch的优先级高于onTouchEvent。
在View中的onTouchEvent中会对点击事件做处理
if (((viewFlags & CLICKABLE) == CLICKABLE ||(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE))
...
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();
return true;
else
return false;
只要VIew的CLICKABLE和LONG_CLICKABLE有一个为true,那么它就会消耗这个事件。在ACTION_UP事件传来时,如果VIew设置了OnClickListener,那么performClick方法内部会调用它的onClick方法。
希望大家能对Android View事件分发有更深刻的理解,共勉。
参考资料:
Android 开发艺术探索
以上是关于图解+源代码 全面理解Android View事件分发的主要内容,如果未能解决你的问题,请参考以下文章