View的事件分发机制

1.View点击事件的传递规则

首先,用户触摸屏幕的时候系统必须对事件做出相应反应.而这个事件就是产生一个MotionEvent然后按照一定的规则传递给每一个View去进行相应的处理.这就是我们所谓的事件分发了.点击事件的分发主要设计一下几个主要的方法:

用来进行事件的分发.如果有事件传递给当前的View,那么该View一定会去调用这个方法

返回值受当前View的onTouchEvent和下级View的dispatchTouchEvent的影响

返回值表示当前的事件时候已经被处理完成

public boolean dispatchTouchEvent(MotionEvent e)

这个方法在上面那个方法中内部调用,用来判断是否拦截这个事件

如果当前View拦截了某个事件 那么在同一个事件序列中就不会再次被调用

返回值表示是否拦截当前事件

public boolean onInterceptTouchEvent(MotionEvent e)

也是在第一个方法中去调用 用来处理拦截下来的事件

返回值为真表示该事件已经被处理 否则 没有处理

在同一事件序列中View无法再次接收到事件

public boolean onTouchEvent(MotionEvent e)

可以用一段伪代码来表示一下三者的关系:

public boolean dispatchTouchEvent(MotionEvent e) {

 boolean consume = false;

 if (onInterceptTouchEvent(e)) {

 consume = onTouchEvent(e);

 } else {

 cnsume = childView.dispathcTouchEvent(e);

}

return cosume;

} 

从上面的代码中我们基本可以总结出这样的结论:
对于一个根ViewGroup来说,当接收到一个MotionEvent的时候:
调用dispatchTouchEvent方法调用onInterceptTouchEvent方法返回值为true,则表示拦截当前事件,调用onTouchEvent来处理这个事件
返回值为false,则当前事件将会被传递给childView,childView继续调用dispatchTouchEvent方法

如此往复,直至事件被处理.
当一个View需要处理一个事件的时候
如果该View设置了onTouchListener,则会调用onTouch方法如果onTouch方法返回false,则去调用onTouchEvent如果设置了onClickListener,那么在onTouchEvent方法中将会调用onClick方法

反之,onTouchEvent则不会被调用

当一个事件产生的时候,它的传递过程遵循这样的过程:Activity->Windows->View;事件总是先传递给Activity,Activity在传递给Windows,Windows在传递给View;如果View将事件处理了,则该事件相应就结束了.否则,事件将一级一级的继续返回,最终会传递给Activity的onTouchEvent处理.

@Override

public boolean onTouchEvent(MotionEvent event) {

 Toast.makeText(MainActivity.this, event.getAction() + "我是Activity", Toast.LENGTH_SHORT).show();

 return super.onTouchEvent(event);

}

当你没有给任何控件设置相应事件的时候(也就是都会返回false),那么你就会看到Activity的onTouchEvent被调用了.
在开发艺术这本书中提到了几个结论:
1.同一个事件序列是指手指接触屏幕那一刻起,到手指离开屏幕那一刻结束,在这个过程中所有产生的事件都属于这一个事件序列.包括一个ACTION_DOWN,一个ACTION_UP和n个ACTION_MOVE;

2.某一个View一旦决定拦截事件,那么这一事件序列都只能由它来处理.

这个结论认真想了一下,似乎有点问题;假如这个View我设置了onTouchListener,但是我依然返回false,这个事件序列仍然会传递给父View,当然了,这个View也只能接收到一个ACTION_DOWN事件,ACTION_UP和ACTION_MOVE不会接收到.假如这个View同时还设置了onClickListener,onTouchEvent返回false的时候事件会交给onTouchEvent处理这个事件,不会在交给父View处理了.这个问题还是需要结合源码来看一下;
3.某个View一旦开始处理一个事件,如果它不消耗ACTION_DOWN事件,那么同一时间序列也不会交给他来处理了.

4.如果View不消耗ACTION_DWON以外的事件,那么这个点击事件就会消失,不会在交还给父View处理.最后会交回activity处理.

5.ViewGroup默认不拦截任何事件,他的onInterceptTouch方法默认返回false;View没有onInterceptTouch方法,收到事件他的onTouchEvent事件就会被调用.

6.View的onTouchEvent默认都会消耗事件(返回true),除非他是不可点击的(clickable和龙Clickable同时为false).View的longClickable默认都是false,clickable分情况.

2.Activity对事件的分发

点击事件用MotionEvent来表示,当点击事件发生的时候,事件最先传递给当前Activity,由Activity的dispatchTouchEvent来进行事件派发,具体工作是由Activity内部的Windows来处理的.Windows会将事件传递给decor view,即当前View的root view.先看一下Activity的dispatchTouchEvent源码:

public boolean diapatchTouchEvent(MotionEvent e) {

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

 onUserInteraction();

 }

 if (getWindows.superDispatchTouchEvent()) {

 return true;

 }

 return onTouchEvent(e);

}

这里有个onUserInteraction
方法,点进去发现这个方法是一个空方法,文档是这样写的:

Called whenever a key, touch, or trackball event is dispatched to the activity. Implement this method if you wish to know that the user has interacted with the device in some way while your activity is running. This callback and onUserLeaveHint() are intended to help activities manage status bar notifications intelligently; specifically, for helping activities determine the proper time to cancel a notfication. All calls to your activity’s onUserLeaveHint() callback will be accompanied by calls to onUserInteraction(). This ensures that your activity will be told of relevant user activity such as pulling down the notification pane and touching an item there. Note that this callback will be invoked for the touch down action that begins a touch gesture, but may not be invoked for the touch-moved and touch-up actions that follow.

大体意思就是说onUserInteraction是帮助我们知道用户开始和屏幕进行交互的回调函数.另外,还会和onUserLeaveHint
一起更加智能的管理状态栏通知.

Called as part of the activity lifecycle when an activity is about to go into the background as the result of user choice. For example, when the user presses the Home key, onUserLeaveHint() will be called, but when an incoming phone call causes the in-call Activity to be automatically brought to the foreground, onUserLeaveHint() will not be called on the activity being interrupted. In cases when it is invoked, this method is called right before the activity’s onPause() callback. This callback and onUserInteraction() are intended to help activities manage status bar notifications intelligently; specifically, for helping activities determine the proper time to cancel a notfication.

这里这两个方法对我们不是很重要了,根据分析可以知道,activity通过windows来分发事件,当所有的view都没有接收处理事件的时候,activity就会自己调用自己的onTouchEvent()来处理这个事件了.
继续看getWindows.superDispatchTouchEvent()
,window
是个抽象类,

Abstract base class for a top-level window look and behavior policy. An instance of this class should be used as the top-level view added to the window manager. It provides standard UI policies such as a background, title area, default key processing, etc. The only existing implementation of this abstract class is android.view.PhoneWindow, which you should instantiate when needing a Window.

根据文档的描述,我们可以知道window
的唯一实现类是android.view.PhoneWindow
,那么他的dispatchTouchEvent
是怎么实现的呢?

//PhoneWindow#superDispatchTouchEvent

public boolean superDispatchTouchEvent(MotionEvent e) {

 return mDecor.superDispatchTouchEvent(e);

}

这里的思路很清晰了,直接分发给mDecor处理,那么DecorView是什么东西呢?关于activity的层次点击这里

//this is the top-level view of the window,containing the window decor(装饰)

private final class DecorView extends FrameLayout implements RootViewSurfaceTaker

private DecorView mDecor;

@Override

public final View getDecorView() {

 if (null == mDecor) {

 installDecor();

 }

 return mDecor;

}

到了这事件会继续分发,到我们通过setContentView设置的ViewGroup那里继续处理.

3.ViewGroup对事件的分发

// 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;

 }

这段代码是来判断是否要来拦截当前的点击事件的.可以看出当这个事件是一个事件序列的开端,也就是一个ACTION_DOWN,就用去调用onInterceptTouch方法去判断是否要去拦截这个事件;或者当mFirstTouchTarget不为空的时候,也会去判断.相反,就不会拦截了.这也说明了一个事件如果View不去处理他的ACTION_DWON事件为什么就能在去处理其他的事件了.
当ViewGroup不处理事件要继续分发的时候代码是这样的:

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 ArrayList<View> preorderedList = buildOrderedChildList();

final boolean customOrder = preorderedList == null

&& isChildrenDrawingOrderEnabled();

final View[] children = mChildren;

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

 final int childIndex = customOrder

 ? getChildDrawingOrder(childrenCount, i) : i;

 final View child = (preorderedList == null)

 ? children[childIndex] : preorderedList.get(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();

}

逻辑也比清晰,遍历ViewGroup的所有子View,找出能接受事件的所有元素;要满足两个条件:1.坐标是否落在子View中2.是否正在播放动画.满足这两个条件,就会分发给他来处理,要是返回了true就表示事件已经被处理,mFirstTouchTarget就会被赋值并终止此次分发,否则继续分发过程.

4.View对点击事件的处理过程

//View#dispatchTouchEvent

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,如果设置了就去调用onTouch方法,如果返回了false,则去调用onTouchEvent方法; 在view设置了onClickListener或者onLongClickListener后,会自动将CLICKABLE或者LONG_CLICKABLE变成ture;

最后三张图十分清晰

效果图如下:
View不处理事件流程图(View没有消费事件)


View处理事件
Touch interest
Touch interest

事件拦截
Touch Intercept
Touch Intercept

附录

以后每个知识点的实践学习代码会上传到我的GitHub,欢迎大家一起学习-.-~

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

推荐阅读更多精彩内容