上一篇讲解了View的事件分发机制,查看点击链接View事件分发机制查看。本文基于Android9.0的源码进行分析ViewGroup的事件分发机制和事件冲突解决方案,源码点击https://github.com/Oaman/Forward查看。
本文分如下几个步骤分析
ViewGroup的Down事件的分发源码分析ViewGroup的Move事件的分发源码分析ViewGroup的滑动事件冲突处理实战 + 源码分析ViewGroup的dispatchTouchEvent是由Activity的dispatchTouchEvent,到PhoneWindow的superDispatchTouchEvent,再到DecorView的superDispatchTouchEvent,最后才到ViewGroup的dispatchTouchEvent的, 流程图如下:
ViewGroup的dispatchTouchEvent是用来进行事件分发的,我们将其分为三个部分进行分析,后面分析中主要是对这三个大的步骤进行分析(后面分析中提到的步骤1|2|3指的就是这里)。
@Override public boolean dispatchTouchEvent(MotionEvent ev) { boolean handled = false; if (onFilterTouchEventForSecurity(ev)) { // 此处会清除requestDisallowInterceptTouchEvent设置的FLAG if (actionMasked == MotionEvent.ACTION_DOWN) { cancelAndClearTouchTargets(ev); resetTouchState(); } // 1 判断是否拦截事件 intercepted代表是否拦截, // intercepted的值是根据disallowIntercept和onInterceptTouchEvent(ev)共同决定的 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 { intercepted = true; } // 2 如果不拦截的话就走这里分发的逻辑,不过这里只能在DOWN事件的时候分发 if (!canceled && !intercepted) { if (actionMasked == MotionEvent.ACTION_DOWN || ...) { final View[] children = mChildren; for (int i = childrenCount - 1; i >= 0; i--) { final View child = getAndVerifyPreorderedView( preorderedList, children, childIndex); if (!canViewReceivePointerEvents(child) || !isTransformedTouchPointInView(x, y, child, null)) { ev.setTargetAccessibilityFocus(false); continue; } if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) { newTouchTarget = addTouchTarget(child, idBitsToAssign); alreadyDispatchedToNewTouchTarget = true; break; } } } } // 3 根据上面事件处理情况,继续下一步的事件分发和处理,最终返回结果handled 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; } } } } return handled; }三个步骤分别是:
注释1处判断是否拦截事件 intercepted代表是否拦截。注释2处如果不拦截的话就走这里分发的逻辑,不过这里只能在DOWN事件的时候分发。注释3根据上面事件处理情况,继续下一步的事件分发和处理,最终返回结果handled。根据父容器是否拦截,这里分为两种情况来分析源码
这一种情况比较简单,因为onInterceptTouchEvent返回true, 所以在上面的三步分析中,第一步intercepted为true, 那么第二步不会进入,直接走到第三步,源码如下:
if (mFirstTouchTarget == null) { // No touch targets so treat this as an ordinary view. handled = dispatchTransformedTouchEvent(ev, canceled, null, TouchTarget.ALL_POINTER_IDS); } 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; } if (child == null) { // 1 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); } return handled; }上面进入到dispatchTransformedTouchEvent方法中,因为参数child为null, 所以直接走到了注释1处的super.dispatchTouchEvent中,就是进入到了View的事件分发处理中,Down事件结束。点击链接查看View事件分发机制。
如果父容器不拦截的话,那么intercepted就为false, 就会走第二步的事件Down的分发逻辑,源码如下:
// 1 if (!canceled && !intercepted) { // 2 if (actionMasked == MotionEvent.ACTION_DOWN || ...) { final View[] children = mChildren; //3 for (int i = childrenCount - 1; i >= 0; i--) { final View child = getAndVerifyPreorderedView( preorderedList, children, childIndex); // 4 if (!canViewReceivePointerEvents(child) || !isTransformedTouchPointInView(x, y, child, null)) { ev.setTargetAccessibilityFocus(false); continue; } // 5 if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) { // 6 newTouchTarget = addTouchTarget(child, idBitsToAssign); alreadyDispatchedToNewTouchTarget = true; break; } } } }上面因为注释1处的intercepted为false,所以进入if条件。在注释2处,当前是Down事件,所以进入注释2处的if判断,在注释3处for循环获取子View,在注释4处判断获取的View是否满足事件分发的条件,比如点击的坐标是否位于View内部等;在注释5处真正的用来判断是否view能处理这个Down事件,如果所有View都不能处理的话,还是走到类似于父类拦截的那种情况,最终是由父容器处理此次Down事件。
如果有子View处理Down事件的话,注释5这里返回true,就会走注释6处的addTouchTarget方法,并且将alreadyDispatchedToNewTouchTarget赋值true, 然后break跳出for循环, addTouchTarget源码如下:
private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) { final TouchTarget target = TouchTarget.obtain(child, pointerIdBits); target.next = mFirstTouchTarget; mFirstTouchTarget = target; return target; }上面主要实现的目的是将mFirstTouchTarget赋值不为null, 它是一个TouchTarget类型,里面包含了处理此次Down事件的View,并且内部维护了一个next指针指向下一个TouchTarget。这里mFirstTouchTarget和alreadyDispatchedToNewTouchTarget的值在后面会用到。接下来我们分析三大步骤中的第三步,源码如下:
// 1 if (mFirstTouchTarget == null) { handled = dispatchTransformedTouchEvent(ev, canceled, null, TouchTarget.ALL_POINTER_IDS); } else { TouchTarget predecessor = null; // 2 TouchTarget target = mFirstTouchTarget; while (target != null) { final TouchTarget next = target.next; // 3 if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) { handled = true; } else { final boolean cancelChild = resetCancelNextUpFlag(target.child) || intercepted; if (dispatchTransformedTouchEvent(ev, cancelChild, target.child, target.pointerIdBits)) { handled = true; } } } }因为mFirstTouchTarget这时候已经不为null了,所以这里不会进入到注释1处,在注释2处将mFirstTouchTarget赋值给target,在注释3处很明显两个值都满足条件,所以进入注释3的if判断,然后handled为true, 返回true,Down事件结束。
我们接着上面Down事件的情况(父容器不拦截)分析,Move事件在第一个步骤中,intercepted为false,在步骤2中是对Down事件进行分发的,Move事件是进入不到步骤2中的,接着进入到步骤3中的源码,源码如下:
// 1 boolean alreadyDispatchedToNewTouchTarget = false; if (mFirstTouchTarget == null) { handled = dispatchTransformedTouchEvent(ev, canceled, null, TouchTarget.ALL_POINTER_IDS); } else { TouchTarget predecessor = null; // 2 TouchTarget target = mFirstTouchTarget; while (target != null) { final TouchTarget next = target.next; // 3 if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) { handled = true; } else { final boolean cancelChild = resetCancelNextUpFlag(target.child) || intercepted; // 3 if (dispatchTransformedTouchEvent(ev, cancelChild, target.child, target.pointerIdBits)) { handled = true; } } } }Move事件在上面注释1处会将alreadyDispatchedToNewTouchTarget赋值为false,在注释2处将mFirstTouchTarget赋值给target, 在注释3处执行事件的分发,如何找到处理事件的View的呢?我们看到就是通过target.child找到的,也就是我们在Down事件中保存的view,到此正常的Move事件分发分析完毕。
Move事件因为不做事件分发处理,所以直接找到在Down事件中保存的view来处理Move事件即可。
(demo源码见https://github.com/Oaman/Forward)
首先要明确一点,就是滑动事件冲突的解决都是在Move事件中解决的。我们首先通过源码来分析事件冲突应该怎么做?
@Override public boolean dispatchTouchEvent(MotionEvent ev) { boolean handled = false; if (onFilterTouchEventForSecurity(ev)) { // 此处会清除requestDisallowInterceptTouchEvent设置的FLAG if (actionMasked == MotionEvent.ACTION_DOWN) { cancelAndClearTouchTargets(ev); // 0 resetTouchState(); } // 1 判断是否拦截事件 intercepted代表是否拦截, // intercepted的值是根据disallowIntercept和onInterceptTouchEvent(ev)共同决定的 final boolean intercepted; if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) { final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0; // 2 if (!disallowIntercept) { intercepted = onInterceptTouchEvent(ev); ev.setAction(action); // restore action in case it was changed } else { // 3 intercepted = false; } } else { intercepted = true; } // 如果不拦截的话就走这里分发的逻辑,不过这里只能在DOWN事件的时候分发 if (!canceled && !intercepted) { if (actionMasked == MotionEvent.ACTION_DOWN || ...) { final View[] children = mChildren; for (int i = childrenCount - 1; i >= 0; i--) { final View child = getAndVerifyPreorderedView( preorderedList, children, childIndex); if (!canViewReceivePointerEvents(child) || !isTransformedTouchPointInView(x, y, child, null)) { ev.setTargetAccessibilityFocus(false); continue; } if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) { newTouchTarget = addTouchTarget(child, idBitsToAssign); alreadyDispatchedToNewTouchTarget = true; break; } } } } // 根据上面事件处理情况,继续下一步的事件分发和处理,最终返回结果handled 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; } } } } return handled; }我们前面说过,第一步中的intercepted的最终取值也受disallowIntercept的影响,在Down事件分发的时候,从代码上看如果disallowIntercept为true的话,那么直接就会走到注释3的地方,将intercepted赋值为false。而disallowIntercept取值是由ViewGroup.requestDisallowInterceptTouchEvent决定的,源码如下:
/** * @param disallowIntercept if true means son will get this event. */ @Override public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) { if (disallowIntercept == ((mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0)) { // We're already in this state, assume our ancestors are too return; } if (disallowIntercept) { //0x10000 = 524288 mGroupFlags |= FLAG_DISALLOW_INTERCEPT; } else { //~ 1000 0000 0000 0000 0000 //~ 0 0111 1111 1111 1111 1111 mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT; } // Pass it up to our parent if (mParent != null) { mParent.requestDisallowInterceptTouchEvent(disallowIntercept); } }从上面方法注释处知道,如果这里将disallowIntercept赋值为true的话,理论上是可以禁止父类拦截的,则会由子类得到事件。但是实际上真的是这样吗?经过测试就会发现,Down事件如果父容器不主动分发给子View的话,子View是拿不到的,源码逻辑见resetTouchState(),这个方法在Down事件的时候会触发,会重置requestDisallowInterceptTouchEvent设置的FLAG值。所以如果想要在Down事件的时候将Down事件分发给子View的话,需要父容器协助,下面分析滑动冲突的解决思路。
一般滑动事件冲突解决方案有两种,内部拦截法和外部拦截法,
我们这里首先看内部拦截法,假如ViewPage嵌套了ListView(子ListView上下滑动,父ViewPager左右滑动),那么这种冲突的处理通过内部拦截法的代码如下:
public class MyListView extends ListView { ... private int mLastX, mLastY; /** * 处理之间冲突 - 内部拦截法 */ @Override public boolean dispatchTouchEvent(MotionEvent ev) { int x = (int) ev.getX(); int y = (int) ev.getY(); switch (ev.getAction()) { case MotionEvent.ACTION_DOWN: // 1 getParent().requestDisallowInterceptTouchEvent(true); break; case MotionEvent.ACTION_MOVE: int deltaX = x - mLastX; int deltaY = y - mLastY; if (Math.abs(deltaX) > Math.abs(deltaY)) { // 2 getParent().requestDisallowInterceptTouchEvent(false); } break; default: break; } mLastX = x; mLastY = y; return super.dispatchTouchEvent(ev); } }我们自定义了ListView, 重写了它的dispatchTouchEvent,在Down事件的时候,注释1处调用了getParent().requestDisallowInterceptTouchEvent(true),在注释2的Move事件的时候调用了getParent().requestDisallowInterceptTouchEvent(false), 上面分析过了,需要父容器协助来帮助子View得到Down事件,所以父容器的代码如下:
public class MyViewPager extends ViewPager { ... @Override public boolean onInterceptTouchEvent(MotionEvent ev) { super.onInterceptTouchEvent(ev); boolean intercept = ev.getAction() != MotionEvent.ACTION_DOWN; return intercept; } }就是说在Down事件的时候,父容器不拦截事件,并且子ListView设置了getParent().requestDisallowInterceptTouchEvent(true),那么Down事件就由子View处理了,当左右滑动的时候,子ListView设置getParent().requestDisallowInterceptTouchEvent(false)将事件重新交给父ViewPager处理,就实现了滑动事件冲突的解决。
外部拦截法的实现思想和内部拦截法一样的,下面我们从源码层分析子ListView是如何将Move事件还给父ViewPager的。
因为现在分析的是Move事件,假如子ListView左右滑动就会将Move事件交给父ViewPager,下面从源码层分析如何实现的。
因为左右滑动的时候执行了requestDisallowInterceptTouchEvent(false),所以此时intercepted=true,接着就走到了下面的代码中:
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 { // 1 final boolean cancelChild = resetCancelNextUpFlag(target.child) || intercepted; // 2 if (dispatchTransformedTouchEvent(ev, cancelChild, target.child, target.pointerIdBits)) { handled = true; } if (cancelChild) { if (predecessor == null) { // 3 mFirstTouchTarget = next; } else { predecessor.next = next; } target.recycle(); target = next; continue; } } } }因为mFirstTouchTarget不等与null, 所以直接走到了注释1处,因为intercepted为true,所以cancelChild就为true,就会执行注释2处的dispatchTransformedTouchEvent方法,在注释3处将mFirstTouchTarget置为null方法源码如下:
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel, View child, int desiredPointerIdBits) { final boolean handled; final int oldAction = event.getAction(); // 1 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; } 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()); } // 2 handled = child.dispatchTouchEvent(transformedEvent); } return handled; }上面注释1处,因为cancel为true,所以会执行child, 也就是ListView的CANCEL事件,所以CANCEL事件什么时候执行?CANCEL事件在事件被上层拦截的时候触发。
接着执行到注释2处的代码,此Move事件结束,此次的Move事件其实对于ViewPager没有任何影响,不过因为Move事件有多个,我们继续看下一个Move事件。
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 { intercepted = true; }这里因为是Move事件并且mFirstTouchTarget=null, 所以intercepted = true,所以直接走到了第三步骤的if,源码如下:
if (mFirstTouchTarget == null) { handled = dispatchTransformedTouchEvent(ev, canceled, null,TouchTarget.ALL_POINTER_IDS); }到这里就将Move又交给了父容器,实现了滑动冲突的处理,另外在此实战中,左右滑动时候上下滑动是不可能的,因为父类处理了事件,是不会再给子类处理的;但是上下滑动时候可以左右滑动,因为事件是可以再次给父类的。就是父容器可以抢子view的事件。