Core
事件分发中有一个重要的规则:一个触控点的一个事件序列只能给一个view处理
分析:以DOWN事件为序列分发判定,ViewGroup为消费DOWN事件的View生成一个TouchTarget(这个TouchTarget就包含了该view的实例与触控id,id可以是多个以应对多指触控),后续MOVE、UP都会交给这个TouchTarget。如果TouchTarget为空则ViewGroup自己处理。如果viewGroup消费了down事件,那么子view将无法收到任何事件。
除非异常情况:
1、比如ScrollView嵌套ScrollView情况下DOWN事件虽然MOVE事件被外ScrollView拦截而生成CANCEL事件分发给内ScrollView)。
2、界面跳转等情况。
此时View需要对CANCEL事件做一些状态的恢复工作,如终止动画,恢复view大小等等。
viewGroup是如何分发(dispatch)事件的?
viewGroup分发事件信息分为三个步骤:拦截、寻找子控件、派发事件。
第一步会判断当前ViewGroup是否disallowIntercept,如果不是(也就是允许拦截)则调用onInterceptTouchEvent方法判断是否要进行拦截。(如果拦截则直接走自身的onTouchEvent())
第二步是如果这个事件是down事件并且没有拦截或取消,那么需要为他寻找一个消费此事件的子控件,如果找到则为他创建一个TouchTarget。
第三步是派发事件,如果存在TouchTarget,说明找到了消费事件序列的子view,直接分发给他。如果没有则交给自己处理。
View
的enable
属性不影响onTouchEvent
的默认返回值, 哪怕一个View是disable
状态. 只要它的clickable
或者longClickable
有一个为true. 那么它的onTouchEvent()
就返回true.
简述:当手指触碰到屏幕时,屏幕硬件底层产生中断上报并通过native层传到Activity开始事件分发。Down事件会使Activity先调用onUserInteration
,之后由PhoneWindow(ViewGroup)调用superDispatchTouchEvent
将事件分发给DecorView,如果DecorView的View树没有处理则再调用到Activity的onTouchEvent
消费。
DecorView事实上是一个最顶层存在的一个FrameLayout,以DecorView开始从顶向下的开始ViewGroup的分发过程:首先判断ViewGroup是否被禁止拦截(ViewGroup.requestDisallowInterceptTouchEvent
可使当前ViewGroup及其父至顶都不可拦截事件),没有禁止则先由ViewGroup.onInterceptTouchEvent
返回true/false判断是否拦截事件,if true,则事件不再往下传递而是调用拦截者的ViewGroup.onTouchEvent
,else 继续往下传递,ViewGroup往其child ViewGroup/View传递。ViewGroup则重复此过程。直到最底的View。
View在View.dispathTouchEvent
中分发:先判断本身是否有OnTouchListener且本身状态为enable,由该OnTouchListener.onTouch
先处理,由其返回值为true/false,if true 则OnTouchListener已经消费了事件,else 调用View.onTouchEvent
进行处理,同样返回true则为View消费了事件,返回false则由其父ViewGroup的onTouchEvent
ps:如果View没有消费ACTION_DOWN事件,则之后的ACTION_MOVE等事件都不会再接收。
经典伪代码
viewGroup是如何分发(dispatch)事件的?
viewGroup分发事件信息分为三个步骤:拦截、寻找子控件、派发事件。
第一步会判断当前ViewGroup是否disallowIntercept,如果不是(也就是允许拦截)则调用onInterceptTouchEvent方法判断是否要进行拦截。(如果拦截则直接走自身的onTouchEvent())
第二步是如果这个事件是down事件并且没有拦截或取消,那么需要为他寻找一个消费此事件的子控件,如果找到则为他创建一个TouchTarget。
第三步是派发事件,如果存在TouchTarget,说明找到了消费事件序列的子view,直接分发给他。如果没有则交给自己处理。
具体:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84
| public class MViewGroup extends MView implements ViewParent { private MView child; private boolean isChildNeedEvent = false; private boolean isSelfNeedEvent = false; private boolean isDisallowIntercept = false;
public MViewGroup(MView child) { this.child = child; child.parent = this; }
@Override public boolean dispatch(MotionEvent ev) { boolean handled = false;
if (ev.getActionMasked() == MotionEvent.ACTION_DOWN) { clearStatus();
if (!isDisallowIntercept && onIntercept(ev)) { isSelfNeedEvent = true; handled = onTouch(ev); }else{ handled = child.dispatch(ev); if (handled) isChildNeedEvent = true;
if (!handled) { handled = onTouch(ev); if (handled) isSelfNeedEvent = true; } } } else {
if (isSelfNeedEvent) { handled = onTouch(ev); } else if (isChildNeedEvent) { if (!isDisallowIntercept && onIntercept(ev)) { isSelfNeedEvent = true;
MotionEvent cancel = MotionEvent.obtain(ev); cancel.setAction(MotionEvent.ACTION_CANCEL); handled = child.dispatch(cancel); cancel.recycle(); } else { handled = child.dispatch(ev); } } }
if (ev.getActionMasked() == MotionEvent.ACTION_UP || ev.getActionMasked() == MotionEvent.ACTION_CANCEL) { this.clearStatus(); }
return handled; }
private void clearStatus() { isChildNeedEvent = false; isSelfNeedEvent = false; isDisallowIntercept = false; }
public boolean onIntercept(MotionEvent ev) { return false; }
@Override public boolean onTouch(MotionEvent ev) { return false; }
@Override public void requestDisallowInterceptTouchEvent(boolean isDisallowIntercept) { this.isDisallowIntercept = isDisallowIntercept; if (parent != null) { parent.requestDisallowInterceptTouchEvent(isDisallowIntercept); } } }
|
简单:
1 2 3 4 5 6 7 8 9 10 11 12 13
| public boolean dispatchTouchEvent(MotionEvent event) {
boolean consume = false; if (onInterceptTouchEvent(event)){ consume = onTouchEvent(event); }else{ consume = child.dispatchTouchEvent(event); } return consume; }
|
流程图
onInterceptTouchEvent
返回值true表示事件拦截, onTouch/onTouchEvent
返回值true表示事件消费。
- 触摸事件先交由
Activity.dispatchTouchEvent
。再一层层往下分发,当中间的ViewGroup都不拦截时,进入最底层的View后,开始由最底层的OnTouchEvent
来处理,如果一直不消费,则最后返回到Activity.OnTouchEvent
。
- ViewGroup才有
onInterceptTouchEvent
拦截方法。在分发过程中,中间任何一层ViewGroup都可以直接拦截,则不再往下分发,而是交由发生拦截操作的ViewGroup的OnTouchEvent
来处理。
- 子View可调用
requestDisallowInterceptTouchEvent
方法,来设置disallowIntercept=true
,从而阻止父ViewGroup的onInterceptTouchEvent
拦截操作。
- OnTouchEvent由下往上冒泡时,当中间任何一层的OnTouchEvent消费该事件,则不再往上传递,表示事件已处理。
- 如果View没有消费ACTION_DOWN事件,则之后的ACTION_MOVE等事件都不会再接收。
- 只要
View.onTouchEvent
是可点击或可长按,则消费该事件.
onTouch
优先于onTouchEvent
执行,上面流程图中省略,onTouch
的位置在onTouchEvent
前面。当onTouch
返回true,则不执行onTouchEvent
,否则会执行onTouchEvent。onTouch
只有View设置了OnTouchListener
,且是enable的才执行该方法。
那主要的分发流程是什么:
在程序的主界面情况下,布局的顶层view是DecorView,他会先把事件交给Activity,Activity调用PhoneWindow的方法进行分发,PhoneWindow会调用DecorView的父类ViewGroup的dispatchTouchEvent方法进行分发。也就是Activity->Window->ViewGroup的流程。ViewGroup则会向下去寻找合适的控件并把事件分发给他。
事件一定会经过Activity吗?
不是的。我们的程序界面的顶层viewGroup,也就是decorView中注册了Activity这个callBack,所以当程序的主界面接收到事件之后会先交给Activity。
但是,如果是另外的控件树,如dialog、popupWindow等事件流是不会经过Activity的。只有自己界面的事件才会经Activity。
Activity的分发方法中调用了onUserInteraction()方法,你能说说这个方法有什么作用吗?
好的。这个方法在Activity接收到down的时候会被调用,本身是个空方法,需要开发者自己去重写。
通过官方的注释可以知道,这个方法会在我们以任意的方式开始与Activity进行交互的时候被调用。比较常见的场景就是屏保:当我们一段时间没有操作会显示一张图片,当我们开始与Activity交互的时候可在这个方法中取消屏保;另外还有没有操作自动隐藏工具栏,可以在这个方法中让工具栏重新显示。
前面你讲到最后会分发到viewGroup,那么viewGroup是如何分发事件的?
viewGroup处理事件信息分为三个步骤:拦截、寻找子控件、派发事件。
事件分发中有一个重要的规则:一个触控点的一个事件序列只能给一个view处理,除非异常情况。所以如果viewGroup消费了down事件,那么子view将无法收到任何事件。
viewGroup第一步会判读这个事件是否需要分发给子view,如果是则调用onInterceptTouchEvent方法判断是否要进行拦截。
第二步是如果这个事件是down事件,那么需要为他寻找一个消费此事件的子控件,如果找到则为他创建一个TouchTarget。
第三步是派发事件,如果存在TouchTarget,说明找到了消费事件序列的子view,直接分发给他。如果没有则交给自己处理。
你前面讲到“一个触控点的一个事件序列只能给一个view处理,除非异常情况”,这里有什么异常情况呢?如果发生异常情况该如何处理?
这里的异常情况主要有两点:1.被viewGroup拦截,2.出现界面跳转等其他情况。
当事件流中断时,viewGroup会发送一个ACTION_CANCEL事件给到view,此时需要做一些状态的恢复工作,如终止动画,恢复view大小等等。
哦?说到多指,那你知道ViewGroup是如何将多个手指产生的事件准确分发给不同的子view吗
这个问题的关键在于MotionEvent以及ViewGroup内部的TouchTarget。
每个MotionEvent中都包含了当前屏幕所有触控点的信息,他的内部用了一个数组来存储不同的触控id所对应的坐标数值。
当一个子view消费了down事件之后,ViewGroup会为该view创建一个TouchTarget,这个TouchTarget就包含了该view的实例与触控id。这里的触控id可以是多个,也就是一个view可接受多个触控点的事件序列。
当一个MotionEvent到来之时,ViewGroup会将其中的触控点信息拆开,再分别发送给感兴趣的子view。从而达到精准发送触控点信息的目的。
那view支持处理多指信息吗?
View默认是不支持的。他在获取触控点信息的时候并没有传入触控点索引,也就是获取的是MotionEvent内部数组中的第一个触控点的信息。多指需要我们自己去重写方法支持他。
嗯嗯…那View是如何处理触摸事件的?
首先,他会判断是否存在onTouchListener,存在则会调用他的onTouch方法来处理事件。如果该方法返回true那么就分发结束直接返回。而如果该监听器为null或者onTouch方法返回了false,则会调用onTouchEvent方法来处理事件。
onTouchEvent方法中支持了两种监听器:onClickListener和onLongClickListener。View会根据不同的触摸情况来调用这两个监听器。同时进入到onTouchEvent方法中,无论该view是否是enable,只要是clickable,他的分发方法都是返回true。
总结一下就是:先调用onTouchListener,再调用onClickListener和onLongClickListener。
View 是最后一个处理事件分发的节点了为什么了还会有dispatchTouchEvent呢?
是因为,在该方法中,要对view的长按事件,触摸事件,点击事件进行调度处理
整理自:Android事件分发机制 - Gityuan博客 | 袁辉辉的技术博客
(32条消息) Android事件分发机制五:面试官你坐啊_一只修仙的猿-CSDN博客_android事件分发机制面试
相关源码
Activity和PhoneWindow相关
Activity是Android四大基本组件之一,当手指触摸到屏幕时,屏幕硬件一行行不断地扫描每个像素点,获取到触摸事件后,从底层产生中断上报。再通过native层调用Java层InputEventReceiver中的dispatchInputEvent方法。经过层层调用,交由Activity的dispatchTouchEvent方法来处理。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| #Activity.java class Activity extends...implement ... { public boolean dispatchTouchEvent(MotionEvent ev) { if (ev.getAction() == MotionEvent.ACTION_DOWN) { onUserInteraction(); }
if (getWindow().superDispatchTouchEvent(ev)) { return true; } return onTouchEvent(ev); }
public boolean onTouchEvent(MotionEvent event) { if (mWindow.shouldCloseOnTouch(this, event)) { finish(); return true; }
return false; } }
|
PhoneWindow的最顶View是DecorView,再交由DecorView处理。而DecorView的父类的父类是ViewGroup,接着调用 ViewGroup.dispatchTouchEvent()方法。
1 2 3 4 5 6
| #PhoneWindow.java PhoneWindow implement Window { public boolean superDispatchTouchEvent(KeyEvent event) { return mDecor.superDispatcTouchEvent(event); } }
|
ViewGroup
首先判断ViewGroup是否被禁止拦截(ViewGroup.requestDisallowInterceptTouchEvent
可使当前ViewGroup及其父至顶都不可拦截事件),没有禁止则先由ViewGroup.onInterceptTouchEvent
返回true/false判断是否拦截事件,if true,则事件不再往下传递而是调用拦截者的ViewGroup.onTouchEvent
,else 继续往下传递。
dispatchTransformedTouchEvent
是真正处理ViewGroup事件的方法,其中会判断是否当前ViewGroup如果已经没有child ViewGroup/View了,则由自身的onTouchEvent
处理;有的话ViewGroup 遍历child传入该方法中判断点击坐标是否在child区域中,是则调用对应child ViewGroup/View的dispatchTouchEvent分发。
ViewGroup则重复此过程。直到最底的View。
只有当Down事件被dispatchTouchEvent
接受时才会继续处理其系列MOVE、UP等事件。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154
| public boolean dispatchTouchEvent(MotionEvent ev) { ... boolean handled = false; if (onFilterTouchEventForSecurity(ev)) { final int action = ev.getAction(); final int actionMasked = action & MotionEvent.ACTION_MASK;
if (actionMasked == MotionEvent.ACTION_DOWN) { cancelAndClearTouchTargets(ev); resetTouchState(); }
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); } else { intercepted = false; } } else { intercepted = true; } ...
if (!canceled && !intercepted) { View childWithAccessibilityFocus = ev.isTargetAccessibilityFocus() ? findChildWithAccessibilityFocus() : null;
if (actionMasked == MotionEvent.ACTION_DOWN || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN) || actionMasked == MotionEvent.ACTION_HOVER_MOVE) { if (newTouchTarget == null && childrenCount != 0) { for (int i = childrenCount - 1; i >= 0; i--) { if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) { mLastTouchDownTime = ev.getDownTime(); if (preorderedList != null) { 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; } ev.setTargetAccessibilityFocus(false); } if (preorderedList != null) preorderedList.clear(); }
if (newTouchTarget == null && mFirstTouchTarget != null) { newTouchTarget = mFirstTouchTarget; while (newTouchTarget.next != null) { newTouchTarget = newTouchTarget.next; } newTouchTarget.pointerIdBits |= idBitsToAssign; } } } if (mFirstTouchTarget == null) { handled = dispatchTransformedTouchEvent(ev, canceled, null, TouchTarget.ALL_POINTER_IDS); } else { 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; } }
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; }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71
| private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel, View child, int desiredPointerIdBits) { final boolean handled;
final int oldAction = event.getAction(); if (cancel || oldAction == MotionEvent.ACTION_CANCEL) { event.setAction(MotionEvent.ACTION_CANCEL); if (child == null) { handled = super.dispatchTouchEvent(event); } else { handled = child.dispatchTouchEvent(event); } event.setAction(oldAction); return handled; }
final int oldPointerIdBits = event.getPointerIdBits(); final int newPointerIdBits = oldPointerIdBits & desiredPointerIdBits;
if (newPointerIdBits == 0) { return false; }
final MotionEvent transformedEvent; if (newPointerIdBits == oldPointerIdBits) { if (child == null || child.hasIdentityMatrix()) { if (child == null) { handled = super.dispatchTouchEvent(event); } else { final float offsetX = mScrollX - child.mLeft; final float offsetY = mScrollY - child.mTop; event.offsetLocation(offsetX, offsetY); handled = child.dispatchTouchEvent(event);
event.offsetLocation(-offsetX, -offsetY); } return handled; } transformedEvent = MotionEvent.obtain(event); } else { transformedEvent = event.split(newPointerIdBits); }
if (child == null) { handled = super.dispatchTouchEvent(transformedEvent); } else { final float offsetX = mScrollX - child.mLeft; final float offsetY = mScrollY - child.mTop; transformedEvent.offsetLocation(offsetX, offsetY); if (! child.hasIdentityMatrix()) { transformedEvent.transform(child.getInverseMatrix()); } handled = child.dispatchTouchEvent(transformedEvent); }
transformedEvent.recycle(); return handled; }
|
1 2 3 4 5 6
| private TouchTarget addTouchTarget(View child, int pointerIdBits) { TouchTarget target = TouchTarget.obtain(child, pointerIdBits); target.next = mFirstTouchTarget; mFirstTouchTarget = target; return target; }
|
View
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
| public boolean dispatchTouchEvent(MotionEvent event) { ...
final int actionMasked = event.getActionMasked(); if (actionMasked == MotionEvent.ACTION_DOWN) { stopNestedScroll(); }
if (onFilterTouchEventForSecurity(event)) { 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; } }
if (!result && mInputEventConsistencyVerifier != null) { mInputEventConsistencyVerifier.onUnhandledEvent(event, 0); }
if (actionMasked == MotionEvent.ACTION_UP || actionMasked == MotionEvent.ACTION_CANCEL || (actionMasked == MotionEvent.ACTION_DOWN && !result)) { stopNestedScroll(); }
return result; }
|