android_事件分发源码解析

一、事件传递的整体过程

当用户手指触摸手机屏幕时最先将事件MotionEvent传递给activity中的dispatchTouchEvent,然后是将事件交给window去处理,window再将事件交给顶层的View,也就是DecorView处理,一级级地将事件向下传递下去。

层级关系如下:

-activity

-PhoneWindow

-DocorView

-ViewGroup

-view

在整个事件传递过程中比较关键的几个方法:

dispatchTouchEvent

1.对事件进行分发,如果事件能传递到当前 View 那么该方法一定会被调用

2.该方法的调用受当前 View#onTouchEvent 和 下级的 dispatchTouchEvent 影响

3.返回结果表示是否消费当前事件

onInterceptEvent

只有 ViewGroup 这个方法,表示是否拦截当前事件,当前 View 拦截当前事件之后,那么同一事件序列的其他事件都会交给该 View 去处理,并且该方法不会再调用

onTouchEvent

表示是否消费当前事件

首先看看Activity如何去处理事件的,Activity#dispatchTouchEvent

public boolean dispatchTouchEvent(MotionEvent ev) {

if (ev.getAction() == MotionEvent.ACTION_DOWN) {

onUserInteraction();

}

if (getWindow().superDispatchTouchEvent(ev)) {

return true;

}

return onTouchEvent(ev);

}

第五行代码中通过getWindow()将事件交给Window去处理,在Activity源码getWindow返回一个Window对象,该对象就是Window的子类PhoneWindow对象。

@Override

public boolean superDispatchTouchEvent(MotionEvent event) {

return mDecor.superDispatchTouchEvent(event);

}

在Window#superDispatchTouchEvent方法中将事件交给了mDecor去处理,mDecor是什么?

// This is the top-level view of the window, containing the window decor.

private DecorView mDecor;

看到这里就知道事件就从Window交给DecorView去处理了,从注释可以看出,该View是top-level的view也就是最顶层的View了。我们一般在Activity中setContentView中的View就是其子View。

接下来看DecorView中的superDispatchTouchEvent是怎么处理这个事件的?

public boolean superDispatchTouchEvent(MotionEvent event) {

return super.dispatchTouchEvent(event);

}

由于DecorView是继承至FrameLayout的,是ViewGroup类型的,所以在第二行代码可以看出他是调用ViewGroup中的dispatchTouchEvent方法。

以上代码片段中表达的是一个点击事件的整体传递过程: Activity -> Window -> DecorView -> ViewGroup -> View

二、下面就从 ViewGroup 开始,分析 ViewGroup 是怎么进行事件分发的:

因为事件能传递到当前 View 的话,那么该 View 的dispatchTouchEvent 就会被调用,查阅ViewGroup的dispatchTouchEvent源码,看看是怎么实现的?

if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) {

//addTouchTarget中初始化了mFirstTouchTarget对象,前提就是事件ACTION_DOWN没有被拦截,并且有子View成功处理了.

//同一个事件序列中,当前事件的上一个事件已经被处理了

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.

//mFirstTouchTarget== null就默认拦截除action_down之外的所有事件

intercepted = true;

}

在第一行代码中判断当前事件是否为ACTION_DOWN事件或者mFirstTouchTarget!=null其中一个条件成立就会将事件传递给onInterceptTouchEvent。但是这里说法不是很明确,因为当前事件能不能拦截,还需要判断mGroupFlags 标记,它是子 View 若是调用

requestDisallowInterceptTouchEvent(true) 的话,那么 disallowIntercept 的值就是 true ,表示不要当前事件,默

认情况这个标记返回值为 false ,表示子 View 没有请求父容器不要拦截当前事件。

现在看看ViewGroup#onInterceptTouchEvent方法是什么,顾名思义它是一个拦截事件的方法,发现这个方法在ViewGroup中直接返回的是false,这就说明了,ViewGroup在默认情况下是不会去拦截事件的。

public boolean onInterceptTouchEvent(MotionEvent ev) {

return false;

}

若是当前事件是DOWN事件,那么就会去调用onInterceptEvent方法,若是当前ViewGroup没有重写该方法,那么默认就返回false,也就是intercept=false;表示不拦截!代码往下走:

if (!canceled && !intercepted) {

//ViewGroup不拦截事件的情况

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 (childrenCount != 0) {

// Find a child that can receive the event.

// Scan children from front to back.

final View[] children = mChildren;

final float x = ev.getX(actionIndex);

final float y = ev.getY(actionIndex);

//找到对应接收事件的子View

for (int i = childrenCount - 1; i >= 0; i--) {

final View child = children[i];

if (!canViewReceivePointerEvents(child)//判断该view是否可见

|| !isTransformedTouchPointInView(x, y, child, null)) {//判断坐标是否在该view上

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)) {

//子View已经处理了事件,则给mFirstTouchTarget赋值

// Child wants to receive touch within its bounds.

mLastTouchDownTime = ev.getDownTime();

mLastTouchDownIndex = i;

mLastTouchDownX = ev.getX();

mLastTouchDownY = ev.getY();

//为mFirstTouchTarget赋值,并为其指向child对象

//mFirstTouchTarget是否被赋值,直接影响到ViewGroup对事件的拦截策略

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;

}

}

}

dispatchTransformedTouchEvent方法部分代码:

// Perform any necessary transformations and dispatch.

if (child == null) {

handled = super.dispatchTouchEvent(transformedEvent);

} else {

...

handled = child.dispatchTouchEvent(transformedEvent);

}

addTouchTarget方法部分代码

/**

* Adds a touch target for specified child to the beginning of the list.

* Assumes the target child is not already present.

*/

//给指定的View添加一个TouchTarget,并返回TouchTarget

//并且更新mFirstTouchTarget为target.

private TouchTarget addTouchTarget(View child, int pointerIdBits) {

TouchTarget target = TouchTarget.obtain(child, pointerIdBits);

target.next = mFirstTouchTarget;

mFirstTouchTarget = target;

return target;

}

intercepted为false,那么就会进入第一行的if语句,既然ViewGroup不拦截这个事件,那么就得找一个孩子去接收这个事件啊,所以在代码15~52行中就是找孩子的过程。24~27行遍历所有的孩子,判断孩子是否为可见的,是否正在做动画,然后判断当前的触摸坐标是否在当前的孩子上面,若都符合条件,这个child就是可以向下传递事件的孩子,然后在38行调用dispatchTransformedTouchEvent将找到的child作为参数传入, 当前child不为null,就调用child.dispatchTouchEvent方法,将事件传递给孩子的dispatchTouchEvent方法。到此事件就从父容器传递给了子容器了,完成了一轮事件的传递。想太多了,还没有完呢,先看看child.dispatchTouchEvent方法返回值是什么,接下来分为两种情况分析:

若是子容器dispatchTouchEvent返回true,表示事件已经被成功的消费了,也就是第38行返回true,那么接下来代码走到47行,为当前的child添加一个target,进入addToTarget方法瞧瞧,它为mFirstTouchTarget进行赋值,到了这里为止,回想之前进入onInterceptEvent方法的那个if条件,要么需要是DOWN事件,要么是mFirstTouchTarget!=null,至此mFirstTouchTarget!=null成立了,那么接下来的MOVE,UP事件都会进入if条件,也就是onInterceptEvent方法去询问ViewGroup是否要拦截事件。还有一步没走完,那就是若是子容器的dispatchTouchEvent返回false的情况。

若是子容器的dispatchTouchEvent返回false,这就说明事件没有被消费,也就是第38行代码返回的是false,这种情况有可能是ViewGroup没有孩子,或者说孩子的onTouchEvent方法返回了false,在这里可以看到mFirstTouchTartget就没有被赋值了,换句话说,接下的MOVE和UP事件不会再调用onInterceptEvent方法去判断是否需要拦截,因为判断条件中的mFirstTouchTarget!=null条件不成立。

现在有个问题:现在代码流程走到这里,有两种情况,第一是事件被拦截了并且找到了合适的孩子去接收该事件,并且将该事件进行消费,那么这种情况是最好的,但是如果没有找到合适的孩子去接收该事件,那么该事件该怎么处理?第二种情况是事件没有被拦截,那么当前事件该怎么处理呢?怎么处理,不能中途迷路吧,所以得找到宿主。好的,接下来继续跟进代码。

先解决第一个问题:就是事件被拦截了,但是没有找到合适的孩子去传递这个事件,刚才分析了,若是孩子没有消费调用这个事件的话那么dispatchTransformedTouchEvent(ev,false,child,idBitsToAssign)返回false,也就是mFirstTouchTarget没有被赋值,那么这种情况ViewGroup自己会去处理这个点击事件,接下来看一段代码:

// Dispatch to touch targets.

if (mFirstTouchTarget == null) {

//遍历所有的孩子之后都没有找到可以传递事件的子View

//这里注意dispatchTransformedTouchEvent的第三个参数,传递的是null,所以会去调用super.dispatchTouchEvent方法

//到了View.dispatchTouchEvent方法处理了。

// No touch targets so treat this as an ordinary view.

handled = dispatchTransformedTouchEvent(ev, canceled, null,

TouchTarget.ALL_POINTER_IDS);

}

看到没有,这里对mFirstTouchTarget做了判空处理,现在有进入之前调用的这个方法dispatchTransformedTouchEvent不过注意观察这次第三个参数传递的是null,也就child参数为null,这是因为没有找到孩子的原因。看看之前的该方法的源码,就在上面贴出来了,super.dispatchTouchEvent(transformedEvent);可以看到当child参数为null时,它调用的是super.dispatchTouchEvent方法,将ev事件向传递View,先处理第二个问题,再来看看View层是怎么处理这个事件。

解决第二个问题:这个问题跟第一问题差不多,也就是mFirstTouchTarget没有被赋值,接下来的逻辑代码跟上面的一样。

三、看完 ViewGroup 事件分发的过程,接下来分析 View 的是怎么处理事件的:

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)) {

//就算是onTouch方法返回false,只要是控件是clickable的,那么

//dispatchTouchEvent方法一定会被返回true

return true;

}

if (onTouchEvent(event)) {

return true;

}

}

if (mInputEventConsistencyVerifier != null) {

mInputEventConsistencyVerifier.onUnhandledEvent(event, 0);

}

return false;

}

在dispatchTouchEvent方法中,第9行首先判断当前View是否设置了OnTouchListener,并且判断该Viw是否为enable的,接下来就是OnTouchListener#onTouch方法了,可以看出只有这三个条件都成立了才能返回true,也才能说是事件到此被消费掉,这是条件成立的情况。若是不成立,那么就会调用onTouchEvent方法,所以默认情况下ViewGroup若是拦截了当前的事件,就会调用onTouchEvent方法就体现在这里了。接下来进入onTouchEvent方法看看源码:

public boolean onTouchEvent(MotionEvent event) {

final int viewFlags = mViewFlags;

...

if (((viewFlags & CLICKABLE) == CLICKABLE ||

(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) {

switch (event.getAction()) {

case MotionEvent.ACTION_UP:

boolean prepressed = (mPrivateFlags & PREPRESSED) != 0;

if ((mPrivateFlags & PRESSED) != 0 || prepressed) {

...

if (!mHasPerformedLongPress) {

// This is a tap, so remove the longpress check

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();

}

}

}

...

}

break;

case MotionEvent.ACTION_DOWN:

...

break;

case MotionEvent.ACTION_CANCEL:

...

break;

case MotionEvent.ACTION_MOVE:

...

break;

}

return true;

}

return false;

}

public boolean performClick() {

sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);

ListenerInfo li = mListenerInfo;

if (li != null && li.mOnClickListener != null) {

playSoundEffect(SoundEffectConstants.CLICK);

li.mOnClickListener.onClick(this);

return true;

}

return false;

在第6,7行中判断该View是否为可点击的,看方法返回值可以知道,只要是可点击的,一律都会返回true,也就是事件会被消费掉,也就是说若是当前 View 是 TextView 等不可点击的 View ,那么 onTouchEvent 方法都会返回 false,像 Button 这些可点击的 View ,它们的 onTouchEvent 方法默认返回 true,不过对于 TextView 等不可点击的 View 而言,可以为其设置 clickable 为 true 标记当前 View 是可以点击的,那么 onTouchEvent 方法就会返回 true。否则返回false,事件没被消费。虽然代码比较冗长,但是只要观察返回值即可!我们都知道点击事件的发生是具备DOWN和UP事件,如果只有DOWN没有UP事件那就不是点击事件了,所以点击事件的触发就在UP事件发生,可以看看第27行代码,调用performClick方法去执行点击事件,55~65行为代码实现。首先判断是是否通过setOnClickListener事件,如果设置了,那么就调用onclick方法。

还有一点,上面代码展示的若是当前 View 设置了 OnTouchListener 并且该 View 是 enable 的,那么事件就会传递给 OnTouchListener#onTouch 方法,如果该方法返回 true 那么就不会再去调用 该 View 的 onTouchEvent 方法了,若是返回 false 那么该 View 的 onTouchEvent 方法就会被调用,因此可以知道只要满足条件那么 onTouch() 方法会被 onTouchEvent 方法先执行。

在阅读《Android开发艺术探讨》一书后的一些结论:

同一个事件序列是从手指触摸屏幕的那一刻开始到手指离开屏幕的那一刻起结束,在这个过程所产生的一系列事件就属于同一个事件系列。

一旦一个 View 拦截了某一个事件之后,那么在接下来的同一事件序列中的其他事件都会交给该 View 去处理(前提是事件能传递到该 View),并且它的 onInterceptTouchEvent 方法将不会被调用。

onInterceptTouchEvent 方法若是返回 true 表示需要当前事件是被拦截的,因此 mFirstTouchTarget 就没有被赋值,因此在同一事件序列的其他事件到来时,就不会再去调用 onInterceptTouchEvent 方法。

某一个 View 一旦不消耗 ACTION_DOWN 事件,也就是 onTouchEvent 方法返回 false ,那么在同一事件序列的其他事件也不会传递给该 View 去处理了。因为该 View 的 onTouchEvent 方法返回 false 也就意味着 dispatchTouchEvent 方法返回 false ,表示当前事件并没有被该 View 成功处理,那么在其他事件传递到父 View 的时就会判断父 View 的 mFirstTouchTarget 为 null,就不会去将该事件分发到子 view 中去。

如果一个事件没有被处理,那么最终会回调到 Activity#onTouchEvent 方法中去处理。

View 是没有 onInterceptTouchEvent 方法的,因此一点有事件传递到该 View 的dispathTouchEvent 方法中那么它的 onTouchEvent 方法就会被调用。

View 的 clickable 或 longClickable 属性若是为 true 那么 onTouchEvent 方法默认就是返回true 也就是事件默认就会被处理调。

View 的 enable 属性不会影响 onTouchEvent 方法的执行,但是会影响 OnTouchEvent#onTouch 方法的执行。

事件的传递方向是有外往内传递的,即事件先传递给父 View 然后再传递给子 View ,可以通过 requestDisallowInterceptTouchEvent(boolean) 方法请求父容器不要拦截当前事件。

至此事件分发就简要分享到此,有需要补充的请大神们留言哦。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,189评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,577评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,857评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,703评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,705评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,620评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,995评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,656评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,898评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,639评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,720评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,395评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,982评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,953评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,195评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,907评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,472评论 2 342

推荐阅读更多精彩内容