说起Android事件分发,网上大大小小的文章不胜枚举,最近项目中遇到了些事件冲突的问题,发现自己对Android事件分发机制掌握的还不够好,于是最近整体学习了一波,虽然不是多么高端的技术,但作为Android知识图谱里的重要一环,必须得牢固掌握,下面这张图是前几天学习过程中记录整理的:
事件分发.png
Android事件分发简单的来说就是当手指触摸屏幕之后,产生的一系列ACTON_DOWN,ACTON_MOVE(多个),ACTON_UP等事件的去向,一般我们所分析的是Action_down事件的分发过程,因为move和up事件与其基本相同,对于App开发来说,最直观的页面就是Activity,事件分发是从Activity开始一直延伸到最深处的view,形如Activity-ViewGroup-View的形式。
事件分发主要设计三个重要的方法:dispatchTouchEvent,onInterceptTouchEvent,onTouchEvent,就像它们的名字一样,dispatchTouchEvent是最先调用的方法,它负责事件的分发工作,onInterceptTouchEvent方法只存在于ViewGroup中(View中不存在onInterceptTouchEvent方法),表示是否拦截Event事件,onTouchEvent方法就是具体处理事件地方了。
1.Activity事件分发
下面我们就从Activity说起,当我们点击屏幕的时候,就会产生Event事件,此时会首先调用Activity的dispatchTouchEvent方法,源码如下:
public boolean dispatchTouchEvent(MotionEvent ev) { if (ev.getAction() == MotionEvent.ACTION_DOWN) { onUserInteraction(); } if (getWindow().superDispatchTouchEvent(ev)) { return true; } return onTouchEvent(ev); }
从上面的代码我们可以看出,如果是ACTION_DOWN事件,会调用onUserInteraction()方法,onUserInteraction方法的作用是当触屏点击按home,back,menu键等都会触发此方法。下拉statubar、旋转屏幕、锁屏不会触发此方法,所以可以用在屏保应用上。
紧接着调用了getWindow().superDispatchTouchEvent(ev),即调用了Window的superDispatchTouchEvent,Window是抽象类,其子类为PhoneWindow,找到此方法源码如下:
@Overridepublic boolean superDispatchTouchEvent(MotionEvent event) { return mDecor.superDispatchTouchEvent(event); }
调用的是mDecor的superDispatchTouchEvent方法,mDecor是DecorView类型,继续查看其源码:
public boolean superDispatchTouchEvent(MotionEvent event) { return super.dispatchTouchEvent(event); }
DecorView调用的是其父类,它的父类是Framelayout,Framelayout并没有重写dispatchTouchEvent方法,所以最终调用的自然是其父类ViewGroup的dispatchTouchEvent.
在分析ViewGroup事件分发之前,先给出Activty的事件分发结论:
Activty事件分发很简单,如果重写了dispatchTouchEvent方法,无论是返回值是false还是true,此时事件分发结束,即不再向下传递,只有返回super.dispatchTouchEvent()时,才会正常往下传递;
2.ViewGroup事件分发
Activity如果没有消费事件,就会传递给ViewGroup来处理,此时首先会调用ViewGroup的dispatchTouchEvent()方法,精简源码如下:
public boolean dispatchTouchEvent(MotionEvent ev) { if (onFilterTouchEventForSecurity(ev)) { final int action = ev.getAction(); final int actionMasked = action & MotionEvent.ACTION_MASK; ---> 1 if (actionMasked == MotionEvent.ACTION_DOWN) { cancelAndClearTouchTargets(ev); //会清空mFirstTouchTarget,重置FLAG_DISALLOW_INTERCEPT resetTouchState(); } //intercepted表示是否拦截事件 final boolean intercepted; //如果是ACTION_DOWN事件或者mFirstTouchTarget!=null,进入条件,当一条事件序列来临时,mFirstTouchTarget默认为null if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) { final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0; //判断disallowIntercept的值,此值是子view通过调用getParent().requestDisallowInterceptTouchEvent(flag) //来控制的,即主动要求父控件是否拦截,true表示不要拦截,false表示拦截 //注意:requestDisallowInterceptTouchEvent控制的是下次event事件,当前的事件(一般指Action_down)是无法控制的 if (!disallowIntercept) { //不拦截则调用onInterceptTouchEvent方法,根据其返回值决定是否拦截 intercepted = onInterceptTouchEvent(ev); ev.setAction(action); } else { intercepted = false; } } else { intercepted = true; } if (!canceled && !intercepted) { if (actionMasked == MotionEvent.ACTION_DOWN || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN) || actionMasked == MotionEvent.ACTION_HOVER_MOVE) { final int childrenCount = mChildrenCount; if (newTouchTarget == null && childrenCount != 0) { ---> 2 //循环遍历子view for (int i = childrenCount - 1; i >= 0; i--) { final int childIndex = getAndVerifyPreorderedIndex( childrenCount, i, customOrder); final View child = getAndVerifyPreorderedView( preorderedList, children, childIndex); if (childWithAccessibilityFocus != null) { if (childWithAccessibilityFocus != child) { continue; } childWithAccessibilityFocus = null; i = childrenCount - 1; } if (!canViewReceivePointerEvents(child) || !isTransformedTouchPointInView(x, y, child, null)) { ev.setTargetAccessibilityFocus(false); continue; } //找到child view newTouchTarget = getTouchTarget(child); if (newTouchTarget != null) { newTouchTarget.pointerIdBits |= idBitsToAssign; break; } resetCancelNextUpFlag(child); //根据dispatchTransformedTouchEvent返回值(返回值由该子View的dispatchTouchEvent决定)判断子view是否处理该事件 --->3 if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) { mLastTouchDownX = ev.getX(); mLastTouchDownY = ev.getY(); newTouchTarget = addTouchTarget(child, idBitsToAssign); alreadyDispatchedToNewTouchTarget = true; break; } } } if (newTouchTarget == null && mFirstTouchTarget != null) { newTouchTarget = mFirstTouchTarget; while (newTouchTarget.next != null) { newTouchTarget = newTouchTarget.next; } } } } //没有找到子view --->4 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; } } //事件cancel,up等重置一些变量 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); } } return handled; }
一开始判断了onFilterTouchEventForSecurity()的返回值,关于onFilterTouchEventForSecurity的作用,点击此处查看,-->1处可以看到,当ACTION_DOWN事件来临时,会进行把一些标志位清空或者恢复初始状态,mFirstTouchTarget就是在resetTouchState()方法里进行清空的,那么mFirstTouchTarget是在哪里赋值的呢,往下寻找可以发现,当找到接收Event事件的子View时 ,mFirstTouchTarget被赋值。
紧接着一个if判断,条件是actionMasked == MotionEvent.ACTION_DOWN
或者mFirstTouchTarget != null,ACTION_DOWN事件一开始肯定为true,因为最开始产生的Event事件就是它,进入判断之后,有一个disallowIntercept变量,它的值由(mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0来决定,那么它又是干嘛的呢,通过搜索我们发现它是在requestDisallowInterceptTouchEvent方法中进行赋值的;
requestDisallowInterceptTouchEvent方法的作用就是子View可以控制父View是否拦截事件,子view通过调用getParent().requestDisallowInterceptTouchEvent(true/false)方法来控制父view是否拦截事件,true是不拦截,false是拦截。
默认情况下是false,所以此时会调用onInterceptTouchEvent方法来进行拦截操作,true表示拦截事件,交由ViewGroup自身的onTouchEvent来处理该事件,注意,一旦onInterceptTouchEvent返回true之后,在此事件序列中,之后的所有事件不再调用onInterceptTouchEvent,而是直接交由自身onTouchEvent处理,为什么呢?看这一小段代码:
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; } }
因为要想执行onInterceptTouchEvent方法,actionMasked == MotionEvent.ACTION_DOWN
或者mFirstTouchTarget != null必须满足其中之一,ACTION_DOWN肯定是false,因为后续事件不会再出现ACTION_DOWN事件,mFirstTouchTarget上面说了,是找到子view后才会赋值,此时还没开始找子view,父view就自己来处理事件了,所以mFirstTouchTarget此时为null,两个条件都不满足,不再继续调用onInterceptTouchEvent拦截后续事件,而是直接intercepted为true,交由自身onTouchEvent处理;
-->2处的代码表示开始循环遍历子View,寻找当前触摸的子view,找到之后,--->3处的代码通过判断dispatchTransformedTouchEvent方法的返回值来决定事件的流向,dispatchTransformedTouchEvent方法精简源码如下:
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel, View child, int desiredPointerIdBits) { final boolean handled; if (child == null) { handled = super.dispatchTouchEvent(event); } else { handled = child.dispatchTouchEvent(event); } return handled; }
可以看到,无论找不找的到Child View,都会调用View的dispatchTouchEvent(event)方法,此时事件分发由ViewGroup传递到View,--->4处的代码表示,如果没有子View,调用dispatchTransformedTouchEvent方法时child会传递null,接着会调用 super.dispatchTouchEvent(event),走默认的事件分发逻辑,最终还是会传递到自身来处理,因为super.dispatchTouchEvent(event)默认是不处理事件的,致此,ViewGroup的事件分发分析完毕,下面就是View的事件分发了。
3.View事件分发
对于View来说,是没有拦截一说的,即没有onInterceptTouchEvent方法,当View接收到Event事件的时候,同样会先触发dispatchTouchEvent方法,dispatchTouchEvent方法精简版源码如下:
public boolean dispatchTouchEvent(MotionEvent event) { boolean result = false; if (onFilterTouchEventForSecurity(event)) { if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) { result = true; } ListenerInfo li = mListenerInfo; //如果控件设置了mOnTouchListener,并且控件是ENABLED状态的,并且onTouch的返回值为true,则会拦截此事件,不再继续分发。 if (li != null && li.mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED && li.mOnTouchListener.onTouch(this, event)) { result = true; } //如果控件没有设置mOnTouchListener,返回值就由自身的onTouchEvent决定 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; }
通过上面的代码可以看出,onTouch方法是先于onTouchEvent执行的,如果View设置了mOnTouchListener,并且控件是ENABLED状态的,并且onTouch的返回值为true,则会拦截此事件,不再继续分发。如果View没有设置mOnTouchListener,返回值就由自身的onTouchEvent决定,onTouchEvent返回true表示自己消费事件,否则不消费,继续传递。
我们再来看下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; //如果控件的状态是DISABLED的,那么直接返回可点击的状态clickable if ((viewFlags & ENABLED_MASK) == DISABLED) { if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) { setPressed(false); } mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN; return clickable; } //控件可点击则进入判断 if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) { switch (action) { case MotionEvent.ACTION_UP: if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) { boolean focusTaken = false; if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) { removeLongPressCallback(); //触发onClick事件 if (!focusTaken) { if (mPerformClick == null) { mPerformClick = new PerformClick(); } if (!post(mPerformClick)) { performClick(); } } } removeTapCallback(); } mIgnoreNextUpEvent = false; break; case MotionEvent.ACTION_DOWN: ...... //检测长按事件 setPressed(true, x, y); checkForLongClick(0, x, y); ...... break; case MotionEvent.ACTION_CANCEL: //进行一些重置操作 break; case MotionEvent.ACTION_MOVE: if (!pointInView(x, y, mTouchSlop)) { //移除长按等回调 removeTapCallback(); removeLongPressCallback(); } break; } return true; } return false; }
首先检测View是否是clickable,以及是否禁用等逻辑,如果控件可用可点击的状态下,进入switch判断,ACTION_DOWN主要检测长按事件的触发,主要逻辑在ACTION_UP中,ACTION_UP中执行了performClick()方法,如果设置了mOnClickListener监听,则会触发onClick事件;如果控件不可点击,则直接返回false,不拦截,事件继续传递,交由;
onLongClick是在什么时候调用的呢?
其实就是在ACTIOIN_DOWN开启长按检测,执行顺序是checkForLongClick() ->performLongClick(mX, mY)->performLongClick() ->performLongClickInternal,一旦达到长按的阈值(private static final int DEFAULT_LONG_PRESS_TIMEOUT = 500ms),就会调用如下方法:
private boolean performLongClickInternal(float x, float y) { boolean handled = false; final ListenerInfo li = mListenerInfo; if (li != null && li.mOnLongClickListener != null) { //关键代码,此处触发了onLongClick回调 handled = li.mOnLongClickListener.onLongClick(View.this); } return handled; }
所以View的事件分发顺序是:dispatchTouchEvent->onTouch->onTouchEvent->on(Long)Click。
4.为什么onInterceptTouchEvent返回true可以拦截事件?
首先当onInterceptTouchEvent返回true的时候,
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; }
即intercepted的值为ture,那么此时:if (!canceled && !intercepted) {}这个判断是进不去的,也就无法传递到子view了,因为这个判断里的逻辑就是循环遍历子view,进行事件分发。
继续往下看,因为并未进入if (!canceled && !intercepted) {}判断,所以此时mFirstTouchTarget为null,会执行dispatchTransformedTouchEvent方法,由于事件并未分发到子View,所以此时参数child为null:
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel, View child, int desiredPointerIdBits) { final boolean handled; if (cancel || oldAction == MotionEvent.ACTION_CANCEL) { if (child == null) { handled = super.dispatchTouchEvent(event); } else { handled = child.dispatchTouchEvent(event); } return handled; } }
通过上面代码可以知道,当child为null的时候,会执行super.dispatchTouchEvent(event),也就是会调用自身父类的dispatchTouchEvent方法,也就是View的dispatchTouchEvent方法,而在View的dispatchTouchEvent方法中:
if (onFilterTouchEventForSecurity(event)) { if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) { result = true; } 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; } }
可以看到,如果控件没有设置onTouch相关监听的话,就会去调用 onTouchEvent(event)方法,这就是onInterceptTouchEvent返回true分发到onTouchEvent的逻辑,如果设置了mOnTouchListener,并且返回了true,那么onTouchEvent方法是不会调用的。
5.滑动冲突的一般解决方式
1.通过重写父类ViewGroup的onInterceptTouchEvent方法,根据具体业务需求来决定是否要对事件进行拦截,注意:ACTION_DOWN事件不要返回true,否则子view是无法接收到事件的。
2.子View通过调用getParent().requestDisallowInterceptTouchEvent(true/false)来干预父ViewGroup的onInterceptTouchEvent的事件分发过程。
6.一些重要结论
1.当onInterceptTouchEvent返回ture时,若onTouchEvent返回true,后续事件将不再经过该ViewGroup的onInterceptTouchEvent方法,直接交由该ViewGroup的onTouchEvent方法处理;若onTouchEvent方法返回false,后续事件都将交由父ViewGroup处理,不再经过该ViewGroup的onInterceptTouchEvent方法和onTouchEvent方法。
2.当onInterceptTouchEvent返回false时,事件继续向子View分发;
3.对于子View,当onTouchEvent返回true,父ViewGroup派发过来的touch事件已被该View消费,后续事件不会再向上传递给父ViewGroup,后续的touch事件都将继续传递给子View。
4.对于子View,onTouchEvent返回false,表明该View并不消费父ViewGroup传递来的down事件,而是向上传递给父ViewGroup来处理;后续的move、up等事件将不再传递给该View,直接由父ViewGroup处理掉。
5.onTouch先于onTouchEvent调用,onClick事件是在onTouchEvent中ACTION_UP中触发的。
作者:寒小枫
链接:https://www.jianshu.com/p/d964f5330618
共同学习,写下你的评论
评论加载中...
作者其他优质文章