在上一篇尾,DecorView并没有重写ViewGroup的dispatchTouchEvent方法, 所以事件传到Activity.dispatchTouchEvent:
public boolean dispatchTouchEvent(MotionEvent ev) { if (ev.getAction() == MotionEvent.ACTION_DOWN) { onUserInteraction(); } if (getWindow().superDispatchTouchEvent(ev)) { return true; } return onTouchEvent(ev); }
然后由getWindow().superDispatchTouchEvent(ev)传递到了DecorView.dispatchTouchEvent(ev)即ViewGroup.dispatchTouchEvent(ev), 由此Activity拦截View的事件相当的简单, 只要重写dispatchTouchEvent(ev)就可以达到,view拦截Activity的onTouchEvent也很简单, 只要Activity布局的Root消耗掉事件就行了。下面具体分析一下ViewGroup.dispatchTouchEvent(ev):
@Override public boolean dispatchTouchEvent(MotionEvent ev) { ... /** * 一个完整的事件由ACTION_DOWN ACTION_MOVE... ACTION_UP组成 * 事件的开端只有一个ACTION_DOWN, 事件的结束有ACTION_UP, ACTION_CANCEL * * 此时的ActionEvent可能是: * * ACTION_DOWN * ACTION_MOVE * ACTION_UP * ACTION_CANCEL */ /** * 单独对ACTION_DOWN对其进行动作拦截判断, ACTION_DOWN是一个事件的开始 * 如果事件的开始就被拦截了, 后面的一系列动作就不用再分发下去了 */ boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0; if (action == MotionEvent.ACTION_DOWN) { if (mMotionTarget != null) { mMotionTarget = null; } /* 如果不允许拦截 或者没有对ACTION_DOWN拦截 */ if (disallowIntercept || !onInterceptTouchEvent(ev)) { ... // 寻找焦点树把事件传递到子View中 final View[] children = mChildren; final int count = mChildrenCount; for (int i = count - 1; i >= 0; i--) { final View child = children[i]; if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null) { child.getHitRect(frame); if (frame.contains(scrolledXInt, scrolledYInt)) { ... if (child.dispatchTouchEvent(ev)) { /** * 焦点树消耗了ACTION_DOWN事件, 得到一个消耗ACTION_DOWN(事件开端)的Target * 此时的ACTION_DOWN已经消耗了, 没有必要往下面传递了 */ mMotionTarget = child; return true; } } } } } } ... final View target = mMotionTarget; /** * 此时的ActionEvent可能是: * * ACTION_DOWN * ACTION_MOVE * ACTION_UP * ACTION_CANCEL * * 对于ACTION_DOWN为何还能传递到这里, 总的原因是ACTION_DOWN事件没有被消耗, 有好几种情况: * * 1、ACTION_DOWN被拦截了, 压根就没有执行上面的if * 2、ACTION_DOWN没有被拦截, 此时可能是 * a、事件往焦点树传递到了子View, 但是焦点树没有消耗掉ACTION_DOWN * b、事件找不到有焦点的子View, 即 焦点就是本身 * */ /** * 如果事件没有被子焦点消耗, 那么就交给ViewGroup处理 * 调用View.dispatchTouchEvent(ev) * * 这里是一个常规的处理, 即事件到达时需要判断子控件树是否消耗了事件, 如果没有消耗则查看 * 自己会不会消耗掉事件 * * 到这一步ACTION_DOWN已经处理完了 */ if (target == null) { ... return super.dispatchTouchEvent(ev); } /** * 事件传递到这一步, 此时target != null, 说明 * * 1、ACTION_DOWN没有被拦截 * 2、有一个子View已经消耗了ACTION_DOWN动作, 即mMotionTarget */ /** * 此时的ActionEvent可能是: * * ACTION_MOVE * ACTION_UP * ACTION_CANCEL * * 如果此时对以上3个可能动作有拦截, 其实ACTION_CANCEL没必要拦截 * * 此时如果mMotionTarget把ACTION_DOWN消耗了, 其必将有事件的结尾信号 * 但是此时却把后续事件拦截了, 那就对mMotionTarget事件cancel了 */ if (!disallowIntercept && onInterceptTouchEvent(ev)) { ev.setAction(MotionEvent.ACTION_CANCEL); if (!target.dispatchTouchEvent(ev)) { } mMotionTarget = null; return true; } /* 完整的事件结束, 清除mMotionTarget */ boolean isUpOrCancel = (action == MotionEvent.ACTION_UP) || (action == MotionEvent.ACTION_CANCEL); if (isUpOrCancel) { mMotionTarget = null; } ... /** * 此时的ActionEvent可能是: * * ACTION_MOVE * ACTION_UP * ACTION_CANCEL * * 事件分发到此处, 说明ViewGroup没有对任何事件拦截, 并且有一个子View已经消耗了ACTION_DOWN * 既然已经消耗了ACTION_DOWN, 后续事件也应该处理 */ return target.dispatchTouchEvent(ev); }
通过上面的分析, 总结一下onInterceptTouchEvent事件拦截特性, 以及ViewGroup事件分发的特性
onInterceptTouchEvent:
1、当RootView收到事件来临通知时候, 此时如果是ACTION_DOWN, 检查是否拦截, 不拦截则发送消息给下层,试探下层是否消耗ACTION_DOWN
2、如果下层不消耗ACTION_DOWN, 则后续事件将不会发送给下层, 此时下层只能接收到一个ACTION_DOWN的探测
3、谁消耗掉ACTION_DOWN, 后续事件将会直接派送给
下面写一个Demo测试:


/** * LinearLayout对dispatchTouchEvent没有任何修改 * * @author bxwu * */ public class RootView extends LinearLayout { private static final String TAG = "RootView"; public RootView(Context context, AttributeSet attrs) { super(context, attrs); } @Override public boolean onInterceptTouchEvent(MotionEvent ev) { switch (ev.getAction()) { case MotionEvent.ACTION_DOWN: Log.d(TAG, "onInterceptTouchEvent-------->ACTION_DOWN"); return false; case MotionEvent.ACTION_MOVE: Log.d(TAG, "onInterceptTouchEvent-------->ACTION_MOVE"); return false; case MotionEvent.ACTION_UP: Log.d(TAG, "onInterceptTouchEvent-------->ACTION_UP"); return false; case MotionEvent.ACTION_CANCEL: Log.d(TAG, "onInterceptTouchEvent-------->ACTION_CANCEL"); return false; } return false; } @Override public boolean onTouchEvent(MotionEvent event) { switch (event.getAction()) { case MotionEvent.ACTION_DOWN: Log.d(TAG, "onTouchEvent-------->ACTION_DOWN"); return false; case MotionEvent.ACTION_MOVE: Log.d(TAG, "onTouchEvent-------->ACTION_MOVE"); return false; case MotionEvent.ACTION_UP: Log.d(TAG, "onTouchEvent-------->ACTION_UP"); return false; case MotionEvent.ACTION_CANCEL: Log.d(TAG, "onTouchEvent-------->ACTION_CANCEL"); return false; } // RootView 不消耗任何动作 return false; } }


public class Cell0 extends LinearLayout { private static final String TAG = "Cell0"; public Cell0(Context context, AttributeSet attrs) { super(context, attrs); } @Override public boolean onInterceptTouchEvent(MotionEvent ev) { switch (ev.getAction()) { case MotionEvent.ACTION_DOWN: Log.d(TAG, "onInterceptTouchEvent-------->ACTION_DOWN"); return false; case MotionEvent.ACTION_MOVE: Log.d(TAG, "onInterceptTouchEvent-------->ACTION_MOVE"); return false; case MotionEvent.ACTION_UP: Log.d(TAG, "onInterceptTouchEvent-------->ACTION_UP"); return false; case MotionEvent.ACTION_CANCEL: Log.d(TAG, "onInterceptTouchEvent-------->ACTION_CANCEL"); return false; } return false; } @Override public boolean onTouchEvent(MotionEvent event) { switch (event.getAction()) { case MotionEvent.ACTION_DOWN: Log.d(TAG, "onTouchEvent-------->ACTION_DOWN"); return false; case MotionEvent.ACTION_MOVE: Log.d(TAG, "onTouchEvent-------->ACTION_MOVE"); return false; case MotionEvent.ACTION_UP: Log.d(TAG, "onTouchEvent-------->ACTION_UP"); return false; case MotionEvent.ACTION_CANCEL: Log.d(TAG, "onTouchEvent-------->ACTION_CANCEL"); return false; } return true; } }




<com.bxwu.touch.RootView xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:gravity="center" android:background="#000000" > <com.bxwu.touch.Cell0 android:layout_width="200dip" android:layout_height="300dip" android:gravity="center" android:background="#ff0000"> <com.bxwu.touch.Cell1 android:layout_width="100dip" android:layout_height="200dip" android:background="#00ff00"> </com.bxwu.touch.Cell1> </com.bxwu.touch.Cell0> </com.bxwu.touch.RootView>
效果图如下:
底部黑色是RootView, 中间层是Cell0, 上层绿色是Cell1
1、RootView, Cell0, Cell1不消耗任何事件也不拦截任何事件, 即所有都return false
a、ACTION_DOWN击在了黑色区域:
b、ACTION_DOWN击在了红色区域:
c、ACTION_DOWN击在了绿色区域:
此时不论你如何滑动触摸点你只能接收到ACTION_DOWN, 而没有后续事件传递过来, 这是为什么呢? 点击区域的不同产生的效果为何不一样?
事件从DecorView(如果不清楚DecorView可以看我前一篇博客)分发时,即调用ViewGroup.dispatchTouchEvent(ev), 此时的RootView是DecorView的子View(非直接子View, 中间还包了一层, 但这不影响分析,你可以把它当做直接子View), 接着按照 以上对ViewGroup.dispatchTouchEvent(ev)分析路线走, 此时DecorView发下一个ACTION_DOWN探测动作给了RootView(因为RootView在点击区域中), 探测结果是RootView并不消耗ACTION_DOWN事件, 所以DecorView对于后续事件就不会分发下来了,当然这种情况是一种畸形的生态循环。实际上原生的View在onTouchEvent(ev)会消耗事件的。
2、如果Cell1消耗掉ACTION_DOWN, 触摸红色区域, 就会变成:
为何会成这样,ViewRoot包含了Cell0, Cell0包含了Cell1, Cell1消耗了ACTION_DOWN, 对于DecorView来说就是ViewRoot这个整体消耗了ACTION_DOWN事件, 所以会继续分发后续事件,而ViewRoot以及Cell0将不会再onTouchEvent接收到事件。
情况还有很多,本人已经晕了,虽然没有列出事件拦截的例子,但是脉络已经很清楚了,修改一下例子就能够测试各种拦截。下一篇博客会根据View.dispatchTouchEvent以及View.onTouchEvent进行整体分析。