MotionEvent事件类型
Down
:手指初次触摸到屏幕触发Move
:手指滑动触发,多次触发Up
:手指离开屏幕触发Cancel
:事件被上层拦截时触发
事件分发机制(源码)
首先,了解一下事件分发的路径:
Activity -> (PhoneWindow -> DecorView) -> ViewGroup -> View
触摸事件从Activity开始分发,几经周转,最关键的地方还是ViewGroup
,具体可以看详细源码
!!!建议在看的过程中打开源码一起看!!!
事件分发
ViewGroup#dispatchTouchEvent()
接下来,主要结合源码中关键的部分带大家走一下Down
和Move
的事件分发流程
Down
-
第一块代码(重置
Down
事件状态、拦截判断)- 初始化
Down
事件,这里resetTouchState()
重置了状态
java// Handle an initial down. if (actionMasked == MotionEvent.ACTION_DOWN) { cancelAndClearTouchTargets(ev); resetTouchState(); /* `Down`事件一定!disallowIntercept = true的原因 */ }
- 判断父容器是否拦截事件
- 相关方法:
requestDisallowInterceptTouchEvent()
和onInterceptTouchEvent()
- 相关方法:
javaif (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) { final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0; if (!disallowIntercept) { /* `Down`事件一定会到这里来,原因是resetTouchState() */ intercepted = onInterceptTouchEvent(ev); ev.setAction(action); // restore action in case it was changed } else { intercepted = false; } } else { /* `Down`后面的事件都会到这里来 */ // There are no touch targets and this action is not an initial down // so this view group continues to intercept touches. intercepted = true; }
- 初始化
-
第二块代码(遍历子View,分发事件)
- 遍历子View,分发事件。如果事件被拦截,不走这块代码
javaif(!canceled && !intercepted){ /* 具体代码被分为以下3部分 */ }
- 遍历子View,构建一个装有子View的数组
javafinal ArrayList<View> preorderedList = buildTouchDispatchChildList(); //具体代码看源码
- 倒叙访问,顶层优先,Android的xml布局中顶层的控件布局在最下方
- 将事件分发给子View,看子View是否处理
java//2.倒叙访问,顶层优先。判断View能否接收事件 -VISIBLE Animtion -View是否包含触摸点 for(){ if (!child.canReceivePointerEvents() || !isTransformedTouchPointInView(x, y, child, null)) { ev.setTargetAccessibilityFocus(false); continue; } //3.子View是否处理事件,dispatchTransformedTouchEvent()调用child.dispatchTouchEvent() // -> true当前child响应 // -> false循环下一个child if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) { // 返回true -> 修改参数记录,后面判断有用 // newTouchTarget = addTouchTarget(child, idBitsToAssign) = mFirstmFirstTouchTarget != null // target.next = null newTouchTarget = addTouchTarget(child, idBitsToAssign); alreadyDispatchedToNewTouchTarget = true; break; } }
- 当子View处理事件返回为
True
,参数的变化
javaprivate TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) { final TouchTarget target = TouchTarget.obtain(child, pointerIdBits); target.next = mFirstTouchTarget; == null mFirstTouchTarget = target; return target; }
-
第三块代码
- 如果子View没有处理事件,由ViewGroup调用父类View的
dispatchTouchEvent()
方法 - 进入
if
语句的情况:父容器拦截、子View全部不处理、第一个Move事件被取消
javaif(mFirstTouchTarget == null){ /* 被拦截/子View全部不处理 */ handled = dispatchTransformedTouchEvent(ev, canceled, null, //这里child传入null TouchTarget.ALL_POINTER_IDS); //是否处理 -> super().dispatchTouchEvnet() }
- 如果子View处理了事件
javaelse{ // Dispatch to touch targets, excluding the new touch target if we already // dispatched to it. Cancel touch targets if necessary. TouchTarget predecessor = null; TouchTarget target = mFirstTouchTarget; while (target != null) { final TouchTarget next = target.next; if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) { handled = true; /* 第二块代码中子View已经处理了`Down`事件 */ } else { // 这段代码用于被拦截的`Move`事件取消和`Move事件的分发`,故这里不展示 } predecessor = target; target = next; } }
- 如果子View没有处理事件,由ViewGroup调用父类View的
Move
Move
事件会多次触发
-
第一块代码(判断
Move
事件是否被拦截)Move
事件不会像Down
事件初始化,重置状态
java// Handle an initial down. if (actionMasked == MotionEvent.ACTION_DOWN) { /* Move事件不会进到这里初始化和重置状态 */ cancelAndClearTouchTargets(ev); resetTouchState(); } 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 { // There are no touch targets and this action is not an initial down // so this view group continues to intercept touches. intercepted = true; }
-
第二块代码
Move
不会再分发事件
javaboolean alreadyDispatchedToNewTouchTarget = false; if(!canceled && !intercepted){ if (actionMasked == MotionEvent.ACTION_DOWN /* `Move`事件不会再分发事件 */ || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN) || actionMasked == MotionEvent.ACTION_HOVER_MOVE) { } }
-
第三块代码
- else中对
Move
事件进行分发
javaTouchTarget predecessor = null; TouchTarget target = mFirstTouchTarget; while(target != null){ /* 单指只循环1次,while用来处理多指操作 */ if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) { handled = true; /* `Down`事件在第二块代码中处理过了,直接handled=true */ } else { final boolean cancelChild = resetCancelNextUpFlag(target.child) || intercepted; /* 判断是否被上层拦截,取消子View的`Move`事件 */ if (dispatchTransformedTouchEvent(ev, cancelChild, /* `Move`事件分发 */ target.child, target.pointerIdBits)) { handled = true; } predecessor = target; target = next; }
- 如果子View的事件(一般是
Move
事件,用来处理冲突)被上层拦截,或因其它原因取消,则回收当前子视图,触摸目标从下一个子视图继续 target
是在Down
事件中记录的消费事件的子View链表,用于多点触控- 多点触控:Button(A、B)先按下A,不松开A再按下B,然后松开A
javaif (cancelChild) { if (predecessor == null) { mFirstTouchTarget = next; } else { predecessor.next = next; } target.recycle(); target = next; continue; } }
- else中对
事件处理
View#dispatchTouchEvent()
- 事件处理相关方法执行顺序:
onTouch()
>onTouchEvent()
>onClick()
java
public boolean dispatchTouchEvent(MotionEvent event) {
//初始result值为false
boolean result = false;
if (onFilterTouchEventForSecurity(event)) {
ListenerInfo li = mListenerInfo;
if (li != null && li.mOnTouchListener != null
&& (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) { // 优先执行自己重写的onTouch
result = true;
}
if (!result && onTouchEvent(event)) { //由于Java的短路与,!result=false,则后面不执行
result = true;
}
}
return result;
}
public boolean onTouchEvent(MotionEvent event) {
switch(action){
case MotionEvent.ACTION_UP:
performClickInternal() -> performClick() //此处为简写,源码中是这样调用过去的
}
}
public boolean performClick(){
if (li != null && li.mOnClickListener != null) {
playSoundEffect(SoundEffectConstants.CLICK);
li.mOnClickListener.onClick(this);
result = true;
} else {
result = false;
}
return result;
}
冲突处理
对于ViewPager
和ListView
,Google工程师是对其事件冲突做过处理的,我们不妨学一下如何处理
以
MyViewPager
为MyListView
的父容器为例 -外部:MyViewPager
-内部:MyListView
内部拦截法
子控件拿到
Down
事件,决定Move
事件是否允许父容器拦截
具体实现
-
MyViewPager
中不拦截Down
事件- 首先让我们看一种感觉可以但实际上行不通的方式,为什么内部拦截法不能只在内部重写?
kotlinclass MyListView @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0 ) : ListView(context, attrs, defStyleAttr) { override fun dispatchTouchEvent(ev: MotionEvent?): Boolean { ev?.let { when (it.action) { MotionEvent.ACTION_DOWN -> { /* 这种方法并不能保证`Down`事件不会被父容器拦截,`Down`事件会重置状态 */ parent.requestDisallowInterceptTouchEvent(true) } } } return super.dispatchTouchEvent(ev) } }
- 下面才是可行的方式,因为重置状态后一定会进入
onInterceptTouchEvent()
方法判断拦截
kotlinclass MyViewPager @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null ) : ViewPager(context, attrs) { override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean { if (ev?.action == MotionEvent.ACTION_DOWN) { super.onInterceptTouchEvent(ev) return false } return true } }
-
MyListView
中拿到Down
事件,决定Move
事件的去向- 在子View的
dispatchTouchEvent()
方法中可以调用getParent().requestDisallowInterceptTouchEvent()
方法允许父容器拦截Move
事件
kotlinclass MyListView @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0 ) : ListView(context, attrs, defStyleAttr) { private var mLastX = 0f; private var mLastY = 0f; override fun dispatchTouchEvent(ev: MotionEvent?): Boolean { ev?.let { when (it.action) { MotionEvent.ACTION_DOWN -> { /* 这种方法并不能保证`Down`事件不会被父容器拦截,`Down`事件会重置状态 */ parent.requestDisallowInterceptTouchEvent(true) mLastX = ev.x mLastY = ev.y } MotionEvent.ACTION_MOVE -> { /* 作出判断,当为横向滑动时允许父容器拦截事件 */ if (abs(ev.x - mLastX) > abs(ev.y - mLastY)) { parent.requestDisallowInterceptTouchEvent(false) } } } } return super.dispatchTouchEvent(ev) } }
- 在子View的
Move
事件流程
这里介绍的是内部拦截法 中
Move
事件的分发流程,因为感觉比较特殊
-
第一个
Move
事件会被取消- 第一块代码:
Move
事件被拦截,不进入第二块代码
javaif (actionMasked == MotionEvent.ACTION_DOWN) { /* Move事件不重置状态 */ cancelAndClearTouchTargets(ev); resetTouchState(); } if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) { final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0; if (!disallowIntercept) { /* 调用了requestDisallowInterceptTouchEvent(false)进入这里 */ intercepted = onInterceptTouchEvent(ev); ev.setAction(action); // restore action in case it was changed } else { intercepted = false; } } else { /* `Down`后面的事件都会到这里来 */ intercepted = true; }
- 第三块代码:
Move
事件被取消,更新mFirstTouchTarget = null
其实也挺合乎道理,你从整体上看事件处理的对象:
MyListView
->MyViewPager
,那么记录的触摸目标是不是也要改变,要重新给它赋值,所以先把它置为空javaif (mFirstTouchTarget == null) { /* mFirstTouchEvent!=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; /* =null */ /* 第二块代码前alreadyDispatchedToNewTouchTarget=false,第二块代码中修改为true,第一个`Move`不曾进入第二块代码 */ // alreadyDispatchedToNewTouchTarget = false if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) { handled = true; } else { /* intercepted = true -> cancelChild = true -> 第一个`Move`事件被取消 */ final boolean cancelChild = resetCancelNextUpFlag(target.child) || intercepted; if (dispatchTransformedTouchEvent(ev, cancelChild, target.child, target.pointerIdBits)) { handled = true; } if (cancelChild) { if (predecessor == null) { mFirstTouchTarget = next; /* =null */ } else { predecessor.next = next; } target.recycle(); target = next; continue; } } predecessor = target; target = next; } } private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel, View child, int desiredPointerIdBits) { final int oldAction = event.getAction(); if (cancel || oldAction == MotionEvent.ACTION_CANCEL) { event.setAction(MotionEvent.ACTION_CANCEL); /* listView -> `Move`事件Cancel处理 */ if (child == null) { handled = super.dispatchTouchEvent(event); } else { handled = child.dispatchTouchEvent(event); } event.setAction(oldAction); return handled; } }
- 第一块代码:
-
第二个
Move
事件找到处理对象- 第一块代码:父容器
MyViewPager
拦截Move
事件,不走第二块代码
javaif (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) {/* `Move`事件且前一个`Move`事件置空触摸目标,走else */ 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 { /* 第一个Move事件被子View取消,第二个`Move`事件直接被父容器拦截,不走第二块代码 */ intercepted = true; }
- 第三块代码:父容器
MyViewPager
将Move
事件交给自己处理
javaif(mFirstTouchTarget == null){ /* 三种情况:被拦截/子View全部不处理/第一个Move事件取消 */ handled = dispatchTransformedTouchEvent(ev, canceled, null, TouchTarget.ALL_POINTER_IDS);
- 第一块代码:父容器
总结(内部拦截法实现原理)
子View必须拿到
Down
事件才能处理Move
事件,父容器可以在没有拿到Down
事件的情况下由子View调用requestDisallowInterceptTouchEvent()
被允许拦截Move
事件
外部拦截法
由父容器根据情况判断是否拦截事件
- 重写父容器
MyViewPager
的onInterceptTouchEvent()
方法
kotlin
class MyViewPager @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null
) : ViewPager(context, attrs) {
private var mLastX = 0f;
private var mLastY = 0f;
override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean {
super.onInterceptTouchEvent(ev)
ev?.let {
when (it.action) {
MotionEvent.ACTION_DOWN -> {
mLastX = ev.x
mLastY = ev.y
}
/* 根据情况判断是否拦截事件,如果是横向滑动则拦截事件 */
MotionEvent.ACTION_MOVE -> {
if (abs(ev.x - mLastX) > abs(ev.y - mLastY)) {
return true
}
}
}
}
return false
}
}
事件分发机制(图解)
图解的话,直接看不容易懂,还是建议先看源码,再结合图理清思路
我的分享到这里就结束啦,有问题的地方欢迎大佬指正,喜欢的小伙伴可以点个赞赞哦^_^