Android事件分发机制
CVTE一面答的很不好的题目之一,特别写一篇博客反思自己。我记得我当时答的糊里糊涂的,只说了连事件从什么时候开始响应的也没说,只说了从子控件开始接受到,如果不消费,或者没有子控件能够消费时,就向上传递,一直传到根布局后自动消费。
所谓的安卓事件是什么?具体来说的就是<stron>两个操作;抽象着来说就是下面的表格。</stron>
MotionEvent/事件类型 | 具体操作 |
---|---|
ACTION_DOWN | 点下View |
ACTION_UP | 抬起View |
ACTION_MOVE | 滑动View |
ACTION_CANCEL | 非人为因素取消 |
事件序列一般组成:
点击的事件组成就是:Down --> Up
滑动的事件组成就是:Down --> Move --> Move .... --> Up
事件分发
- 使用到的函数
- dispatchTouchEvent():用于事件分发
- onTouchEvent():消费事件
- onInterceptTouchEvent():判断是否拦截事件,仅存在于ViewGroup
- 分发对象
- Activity
- ViewGroup
- View
Activity的事件分发
public boolean dispatchTouchEvent(MotionEvent ev) { // 从判断语句中可以得出所有事件的起点就是Down if (ev.getAction() == MotionEvent.ACTION_DOWN) { // 实现屏保功能 onUserInteraction(); } // 向上传递至ViewGroup,调用其dispatchTouchEvent if (getWindow().superDispatchTouchEvent(ev)) { return true; } return onTouchEvent(ev); }
文章中我已经添加了注释内容,其中getWindow()
获得就是一个Window
抽象类,根据其子类PhoneWindow
我们可以很容易得知最后调用的其实就是ViewGroup
的dispatchTouchEvent()
方法/** * 实际上就是判断事件是否是DOWN事件,event的坐标是否在边界内等 */ public boolean onTouchEvent(MotionEvent event) { if (mWindow.shouldCloseOnTouch(this, event)) { finish(); return true; } return false; }
最后就是Activity
中的onTouchEvent()
方法了,这个模块干的事情在注释中也就很清晰明了了。ViewGroup的事件分发
public boolean dispatchTouchEvent(MotionEvent ev) { ········ // 初始化Down事件 if (actionMasked == MotionEvent.ACTION_DOWN) { // 丢弃之前手头上干的事情,重新开始响应Down事件 cancelAndClearTouchTargets(ev); resetTouchState(); } // 检查是否需要拦截 final boolean intercepted; if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) { // 这个与运算是用于影响除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 { // 当前按压的位置没有控件,或者当前控件并不可被点击,直接被ViewGroup拦截 intercepted = true; } ········ /** *这个判断里面同样的还是判断响应的事件,然后就是通过一个for循环判断位置来判断当前的子控件是否在对应的位置内 * 还有非常重要的一点就是这个循环的判断还是倒叙的 */ 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--) { final int childIndex = getAndVerifyPreorderedIndex(childrenCount, i, customOrder); final View child = getAndVerifyPreorderedView(preorderedList, children, childIndex); // 后面的篇幅主要用于判断当前的控件的各种属性是否是满足需要的。比如说位置、是否可以点击、是否隐藏等一系列信息 ········ } ········ }
public boolean onInterceptTouchEvent(MotionEvent ev) { if (ev.isFromSource(InputDevice.SOURCE_MOUSE) && ev.getAction() == MotionEvent.ACTION_DOWN && ev.isButtonPressed(MotionEvent.BUTTON_PRIMARY) && isOnScrollbarThumb(ev.getX(), ev.getY())) { return true; } return false; }
由onInterceptTouchEvent(MotionEvent ev)
函数可知,默认其实并不会去拦截。所以就一般情况而言,dispatchTouchEvent()
方法是需要去循环遍历子控件集合去寻找对应的控件的。
使用一个伪代码解释以上的逻辑
public boolean dispatchTouchEvent(MotionEvent ev) { boolean consume = false; if(onInterceptTouchEvent(ev)){ // 自己拦截,自己消费 onTouchEvent(ev); }else{ // 不拦截,分发给子View进行消费 consume = child.dispatchTouchEvent(ev); } return consume; }
View事件分发
public boolean dispatchTouchEvent(MotionEvent event) { ····· boolean result = false; 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)) { result = true; } if (!result && onTouchEvent(event)) { result = true; } } if (!result && mInputEventConsistencyVerifier != null) { mInputEventConsistencyVerifier.onUnhandledEvent(event, 0); } return result; }
通过以往的实践,我们知道只有通过设置了的View才能够去监听事件,那么在dispatchTouchEvent()
方法中也是一样的,如果View并没有被设置,变量result
也不会被赋值成为true。
从代码中很容易看出
onTouch()
方法的优先级大于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(); final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE; ····· if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) { switch (action) { case MotionEvent.ACTION_UP: ····· boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0; if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) { boolean focusTaken = false; if (isFocusable() && isFocusableInTouchMode() && !isFocused()) { focusTaken = requestFocus(); } if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) { removeLongPressCallback(); if (!focusTaken) { if (mPerformClick == null) { mPerformClick = new PerformClick(); } if (!post(mPerformClick)) { performClickInternal(); } } } ····· } ····· mIgnoreNextUpEvent = false; break; ····· } return true; } return false; }
在onTouchEvent()
方法中其实具体干了一件事情,那就是区别到底是长按事件还是点击事件。
那么先行判断的是长按事件还是点击事件呢?答案很明显,在代码行中removeLongPressCallback();
有一个这样的函数,这就是去除长按事件回调的函数,所以答案就是长按事件是第一个被判断的事件,然后才是点击事件。
判断这个方法的事件的方法就是通过做出Up动作时的时间和做出Down动作时的时间间隔。如果Down和Up两个动作之间的时间间隔小于500ms,就是点击事件。