这篇文章已经写得非常经典了:
图解 Android 事件分发机制
点击事件传递规则
MotionEvent
在手指触摸屏幕后产生的一系列事件中,典型的事件类型有如下几种:
- ACTION_DOWN 在屏幕按下时
- ACTION_MOVE 在屏幕上滑动时
- ACTION_UP 手指在屏幕抬起时
而我们常会遇到的点击事件一般为以下两种情况:
1.点击屏幕后松手,事件序列为ACTION_DOWN->ACTION_UP
2.点击屏幕后滑动再松开,事件序列为ACTION_DOWN->ACTION_MOVE->ACTION_MOVE....->ACTION_UP
事件分发的三个重要方法
- dispatchTouchEvent(MotionEvent event)
用来进行事件的分发,如果事件能传递给当前View,那么此方法一定会被调用,返回结果受当前View的onTouchEvent和下级View的dispatchTouchEvent方法影响,表示是否消费当前事件。
- onInterceptTouchEvent(MotionEvent ev)
在上述方法内部被调用,用来判断是否拦截某个事件,如果当前View拦截了某个事件,那么在同一个事件序列当中,此方法不会被再次调用,返回结果表示是否拦截当前事件。
- onTouchEvent(MotionEvent ev)
用来处理点击事件,在dispatchTouchEvent()方法中进行调用。返回结果表示是否消耗当前事件,如果不消耗,则在同一个事件序列中,当前View无法再次接收到事件。
上述三个方法的区别可以用伪代码表示:
/**
* 点击事件产生后
*/
// 步骤1:调用dispatchTouchEvent()
public boolean dispatchTouchEvent(MotionEvent ev) {
boolean consume = false; //代表 是否会消费事件
// 步骤2:判断是否拦截事件
if (onInterceptTouchEvent(ev)) {
// a. 若拦截,则将该事件交给当前View进行处理
// 即调用onTouchEvent ()方法去处理点击事件
consume = onTouchEvent (ev) ;
} else {
// b. 若不拦截,则将该事件传递到下层
// 即 下层元素的dispatchTouchEvent()就会被调用,重复上述过程
// 直到点击事件被最终处理为止
consume = child.dispatchTouchEvent (ev) ;
}
// 步骤3:最终返回通知 该事件是否被消费(接收 & 处理)
return consume;
}
通过上述伪代码,可以大致的了解点击事件的传递规则:
点击事件产生后,首先会传递给根
ViewGroup
,这个时候它的dispatchTouchEvent
就会被调用,若此时这个ViewGroup
的onIterceptTouchEvent
方法返回true,则表示当前ViewGroup要拦截这个事件,接着这个事件就会交给此ViewGroup
进行处理,即它的onTouchEvent方法就会被调用。
若onInterceptTouchEvent
方法返回false,则表示当前ViewGroup
不拦截这个事件,这时当前事件就会继续传递给它的子元素,接着子元素dispatchTouchEvent
方法就会被调用,如此直到事件被最终处理。
当一个View
需要处理事件时,如果它设置了onTouchListener
,那么onTouchListener
中的onTouch
方法会被回调。这时事件如何处理还要看onTouch
的返回值,如果返回false,则当前View的onTouchEvent
方法会被调用,如果返回true,那么onTouchEvent
方法将不会被调用。由此可见,
给View设置的onTouchListener
,其优先级比onTouchEvent
要高。
在onTouchEvent
方法中,如果当前设置的有onClickListener
,那么它的onClick方法会被调用。可以看出,平时我们常用的onClickListener
,其优先级更低,即处于事件传递的尾端。
事件分发的流程
当一个点击事件产生后,它的传递过程遵循如下顺序:Activity->Window->View,即事件总是先传递给Activity,Activity再传递给Window,最后Window再传递给顶级View。顶级View接收到事件后,就会按照事件分发机制去分发事件。
考虑一种情况,如果一个view的onTouchEvent
返回false,那么它的父容器的onTouchEvent
将会被调用,依此类推,如果所有的元素都不处理这个事件,那么这个事件将会最终传递给Activity处理,即Activity的onTouchEvent
方法会被调用。(程序员世界里的能力强弱问题,难题由上而下的分配,解决不了,交给上级解决)
小结
1.同一个事件序列是指从手指接触屏幕的那一刻起,到手指离开屏幕的那一刻结束,在这个过程中所产生的一系列事件,这个事件序列以down事件开始,中间含有数量不定的move事件,最后以up结束;
2.正常情况下,一个事件序列只能被一个View拦截且消耗。这一条的原因可以参考(3),因为一旦一个元素拦截了某此事件,那么同一个事件序列内的所有事件都会直接交给它处理,因此同一个事件序列中的事件不能分别由两个View同时处理,但是通过特殊手段可以做到,比如一个Vew将本该自己处理的事件通过onTouchEvent强行传递给其他View处理;
3.某个View一旦决定拦截,那么这一个事件序列都只能由它来处理(如果事件序列能够传递给它的话),并且它的onInterceprTouchEvent不会再被调用。这条也很好理解,就是说当一个View决定拦截一个事件后,那么系统会把同一个事件序列内的其他方法都直接交给它来处理,因此就不用再调用这个View的onInterceptTouchEvent去询问它是否要拦截了。
4.某个View一旦开始处理事件,如果它不消耗ACTON_DOWN事件(onTouchEvent返回了false),那么同一事件序列中的其他事件都不会再交给它来处理,并且事件将重新交由它的父元素去处理,即父元素的onTouchEvent会被调用。意思就是事件一旦交给一个View处理,那么它就必须消耗掉,否则同一事件序列中剩下的事件就不再交给它来处理了,这就好比上级交给程序员一件事,如果这件事没有处理好,短期内上级就不敢再把事情交给这个程序员做了。
5.如果View不消耗除
ACTION_DOWN
以外的其他事件,那么这个点击事件会消失,此时父元素的onTouchEvent
并不会被调用,并且当前View可以持续收到后续的事件,最终这些消失的点击事件会传递给Activity处理。
6.ViewGroup默认不拦截任何事件。Android源码中ViewGroup的
onInterceptTouchEvent
方法默认返回false。
7.View没有
onInterceptTouchEvent
方法,一旦有点击事件传递给它,那么它的onTouchEvent
方法就会被调用。
8.view的
onTouchEvent
默认都会消耗事件(返回true),除非它是不可点击的(clickable和longClickable同时为false),View的longClickable
属性默认都为false,clickable属性要分情况,比如Button的clickable属性默认为true,而TextView 的clickable属性默认为false
9.View 的enable.属性不影响onTouchEvent的默认返回值。哪怕一个View是disable状态的,只要它的clickable或者longclickable有一个为true,那么它的onTouchEvent就返会true。
10.onclick会发生的前提是当前的View是可点击的,并且他收到了down和up的事件。
11.事件传递过程是由外到内的,理解就是事件总是先传递给父元素,然后再由父元素分发给子View,通过
requestDisallowInterptTouchEvent
方法可以再子元素中干预元素的事件分发过程,但是ACTION_DOWN除外。
事件传递的源码解析
Activity对点击事件的分发过程
点击事件用MotionEvent
来表示,当一个点击操作发生的时候,事件最先传递给Activity
,由Activity
的dispatchTouchEvent
来进行事件的派发,具体的工作是由Activity
内部的window
来完成的,window
会将事件传递给decor view,decor view一般都是当前界面的底层容器(setContentView所设置的父容器),通过Activity.getWindow.getDecorView()获得。我们先从Activity的dispatchTouchEvent
开始分析:
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
//事件交给Activity所依附的window,如果true那就结束了
//superdispatchTouchEvent(ev)方法也是抽象的,必须找到window的实现类,window的实现类是phonewindow,phoneWindow将事件传递给了DecorView。
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}
//phoneWindow将事件传递给了DecorView.
public boolean superDispatchTouchEvent(MotionEvent ev){
return mDecor.superDispatchTouchEvent(ev);
}
//从这里开始,事件已经传递到顶级View了,就是在Activity中通过setContentview所设置的View,另外顶级View也叫根View,顶级View一般来说都是VewGroup。
public class DecorView extends FrameLayout implements RootViewSurfaceTaker {
private DecorView mDecor;
@Override
public final View getDecorView(){
if(mDecor == null){
installDesor():
}
return mDecor;
}
}
顶级View对事件的分发过程
点击事件达到顶级View(一般是一个ViewGroup)以后,
(1)会调用ViewGroup的dispatchTouchEvent
方法
(2)如果顶级ViewGroup拦截事件即 onIntercepTouchEvent返回true,则事件由ViewGroup处理
(3)如果ViewGroup的mOnTouchListener被设置,则onTouch会被调用,否则onTouchEvent会被调用。也就是说如果都提供的话,onTouch会屏蔽掉onTouchEvent。在onTouchEvent中,如果设置了 mOnClickListener,则onClick会被调用。
(4)如果顶级ViewGroup不拦截事件,则事件会传递给它所在的点击事件链上的子View,这时子View的dispatchTouchEvent会被调用。
到此为止,事件已经从顶级View传递给了下一层View,接下来的传递过程和顶级View是一致的, 如此循环,完成整个事件的分发。
ViewGroup对点击事件的分发过程
点击事件达到顶级view(一般是一个viewGroup)以后,会调用viewgroup
的diapatchtouchevent
方法,如果viewGroup
拦截事件即onInterceptTouchEvent
返回true,则事件由viewGroup
处理,这是如果viewGroup
的ontouchlistener
被设置了,则onTouch
会被调用,如果onTouch返回true,就会屏蔽掉
onTouchEvent,如果返回false,会接着执行
OnTouchEvent方法,好了 下面我们看一下
dispatchtouchevent`方法的源码:
// Check for interception.
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 {
// There are no touch targets and this action is not an initial down
// so this view group continues to intercept touches.
intercepted = true;
}
ViewGroup
在两种情况下都会判断是否要拦截当前事件:
事件类型为ACTION_DOWN:此前由我们触发的点击事件,也就是说ACTION_MOVE 和ACTION_UP事件来时,则不触发拦截事件
mFirstTouchTarget != null:当ViewGroup不拦截事件并将事件交给子View的时候该不等式成立。反过来,事件被ViewGroup拦截时,该不等式不成立
那么当ACTION_DOWN和ACTION_UP事件到来时,由于(actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null)
这个条件为false,将导致ViewGroup
的onInterceptTouchEvent不会被调用,并且同一序列中的其他事件都会默认交给它处理。
当然,这里有一种特殊情况,那就是 FLAG_DISALLOW_INTERCEPT
标记位,这个标记位是通过 requestDisallowInterceptTouchEvent
方法来设置的,一般用于子 View 中。
FLAG_DISALLOW_INTERCEPT
一旦设置后, ViewGroup
将只能拦截ACTION_DOWN事件。为什么说是除了 ACTION_DOWN
以外的其他事件呢?
这是因为 ViewGroup
在分发事件时,如果是ACTION_DOWN
就会重置 FLAG_DISALLOW_INTERCEPT
这个标记位,将导致子 View 中设置的这个标记位无效。 因此,当面对 ACT1ON_DOWN
事件时,ViewGroup
总是会调用自己的 onlnterceptTouchEvent
方法来询问自己是否要拦截事件,这一点从源码中也可以看出来。
在下面的代码中,ViewGroup
会在 ACTION_DOWN
事件到来时做重置状态的操作,而在resetTouchState
方法中会对 FLAG_DISALLOW_INTERCEPT
进行重置,因此子 View 调用 request- DisallowInterceptTouchEvent
方法并不能影响 ViewGroup
对ACTION DOWN
事件的处理。
// Handle an initial down.
if (actionMasked == MotionEvent.ACTION_DOWN) {
// Throw away all previous state when starting a new touch gesture.
// The framework may have dropped the up or cancel event for the previous gesture
// due to an app switch, ANR, or some other state change.
cancelAndClearTouchTargets(ev);
resetTouchState();
}
小结
当ViewGroup
决定拦截事件后,那么后续的点击事件将会默认交给它处理并且不再调用它的onlnterceptTouchEvent
方法,这证实了 前面提到的第3条结论。FLAG_DISALLOW_INTERCEPT
这个标志的作用是让 ViewGroup
不再拦截事件,当然前提是ViewGroup
不拦截 ACTION_DOWN
事件,这证实 了 前面提到的第11条结论。
3.某个View一旦决定拦截,那么这一个事件序列都只能由它来处理(如果事件序列能够传递给它的话),并且它的onInterceprTouchEvent不会再被调用。这条也很好理解,就是说当一个View决定拦截一个事件后,那么系统会把同一个事件序列内的其他方法都直接交给它来处理,因此就不用再调用这个View的onInterceptTouchEvent去询问它是否要拦截了。
11.事件传递过程是由外到内的,理解就是事件总是先传递给父元素,然后再由父元素分发给子View,通过requestDisallowInterptTouchEvent方法可以在子元素中干预元素的事件分发过程,但是ACTION_DOWN除外。
那么这段分析对我们有什么价值呢?总结起来有两点:
第一点,
onlnterceptTouchEvent
不是每次事件都会被调用的,如果我们想提前处理所有的点击事件,要选择dispatchTouchEvent
方法,只有这个方法能确保每次都会调用,当然前提是事件能够传递到当前的ViewGroup
;
另外一点,
FLAG_DISALLOW_INTERCEPT
标记位的作用给我们提供了一个思路,当面对滑动冲突时,我们可以是不是考虑用这种方法去解决问题?
接着再看当 ViewGroup 不拦截事件的时候,事件会向下分发交由它的子 View 进行处理,这段源码如下所示。
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
......
final View[] children = mChildren;
//遍历所有子View
for (int i = childrenCount - 1; i >= 0; i--) {
final int childIndex = getAndVerifyPreorderedIndex(
childrenCount, i, customOrder);
final View child = getAndVerifyPreorderedView(
preorderedList, children, childIndex);
//判断子View是否能接收点击事件
if (childWithAccessibilityFocus != null) {
if (childWithAccessibilityFocus != child) {
continue;
}
childWithAccessibilityFocus = null;
i = childrenCount - 1;
}
//判断子元素在播放动画时落在子元素的区域内
if (!canViewReceivePointerEvents(child)
|| !isTransformedTouchPointInView(x, y, child, null)) {
ev.setTargetAccessibilityFocus(false);
continue;
}
//判断子元素点击事件是否落在子元素的区域内
newTouchTarget = getTouchTarget(child);
if (newTouchTarget != null) {
// Child is already receiving touch within its bounds.
// Give it the new pointer in addition to the ones it is handling.
newTouchTarget.pointerIdBits |= idBitsToAssign;
break;
}
resetCancelNextUpFlag(child);
//事件传递到子View,下面追踪该方法
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
// Child wants to receive touch within its bounds.
mLastTouchDownTime = ev.getDownTime();
if (preorderedList != null) {
// childIndex points into presorted list, find original index
for (int j = 0; j < childrenCount; j++) {
if (children[childIndex] == mChildren[j]) {
mLastTouchDownIndex = j;
break;
}
}
} else {
mLastTouchDownIndex = childIndex;
}
mLastTouchDownX = ev.getX();
mLastTouchDownY = ev.getY();
newTouchTarget = addTouchTarget(child, idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true;
break;
}
// The accessibility focus didn't handle the event, so clear
// the flag and do a normal dispatch to all children.
ev.setTargetAccessibilityFocus(false);
}
......
}
ViewGroup直接使用for遍历所有子View,对子View的各种状态进行判断,最后调用dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)
将事件传递给子View,下面是dispatchTransformedTouchEvent()
方法的部分源码:
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
View child, int desiredPointerIdBits) {
final boolean handled;
// Canceling motions is a special case. We don't need to perform any transformations
// or filtering. The important part is the action, not the contents.
final int oldAction = event.getAction();
if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
event.setAction(MotionEvent.ACTION_CANCEL);
// focus-1
if (child == null) {
handled = super.dispatchTouchEvent(event);
} else {
handled = child.dispatchTouchEvent(event);
}
event.setAction(oldAction);
return handled;
}
其最后就是分发给子View的dispatchTouchEvent()方法。
查看focus-1处的代码:
if (child == null) {
handled = super.dispatchTouchEvent(event);
} else {
handled = child.dispatchTouchEvent(event);
}
child 传递的不是null,因此它会直接调用子元素的 dispatchTouchEvent 方法,这样事件就交由子元素处理了,从而完成了一轮事件分发。
回到ViewGroup的dispatchTouchEvent()
方法:
// 事件传递到子View
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
// Child wants to receive touch within its bounds.
mLastTouchDownTime = ev.getDownTime();
if (preorderedList != null) {
// childIndex points into presorted list, find original index
for (int j = 0; j < childrenCount; j++) {
if (children[childIndex] == mChildren[j]) {
mLastTouchDownIndex = j;
break;
}
}
} else {
mLastTouchDownIndex = childIndex;
}
mLastTouchDownX = ev.getX();
mLastTouchDownY = ev.getY();
// focus-1
newTouchTarget = addTouchTarget(child, idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true;
break;
}
如果子元素的 dispatchTouchEvent 返回 true,这时我们暂时不用考虑事件在子元素内部是怎么分发的,那么 mFirstTouchTarget 就会被赋值同时跳出 for 循环,如下所示。
newTouchTarget = addTouchTarget(child, idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true;
break;
这几行代码完成了 mFirstTouchTarget
的赋值并终止对子元素的遍历。如果子元素的 dispatchTouchEvent
返回 false,ViewGroup
就会把事件分发给下一个子元素(如果还有下一个子元素的话)。
其实 mFirstTouchTarget
真正的赋值过程是在 addTouchTarget
内部完成的,从下面的 addTouchTarget
方法的内部结构可以看出,mFirstTouchTarget
其实是一种单链表结构。 mFirstTouchTarget
是否被赋值,将直接影响到 ViewGroup 对事件的拦截策略,如果 mFirstTouchTarget 为 null,那么 ViewGroup 就默认拦截接下来同一序列中所有的点击事件,这一点在前面已经做了分析。
private TouchTarget addTouchTarget(View child, int pointerIdBits) {
TouchTarget target = TouchTarget.obtain(child, pointerldBits);
target.next = mFirstTouchTarget;
mFirstTouchTarget = target;
return target;
}
如果遍历所有的子元素后事件都没有被合适地处理,这包含两种情况:
第一种是 ViewGroup 没有子元素;
第二种是子元素处理了点击事件,但是在 dispatchTouchEvent 中返回了 false,这一般是因为子元素在 onTouchEvent 中返回了 false,在这两种情况下,ViewGroup 会自己处理点击事件,这里就证实了 前面提到的第4条结论,代码如下所示。
4.某个View一旦开始处理事件,如果它不消耗ACTON_DOWN事件(onTouchEvent返回了false),那么同一事件序列中的其他事件都不会再交给它来处理,并且事件将重新交由它的父元素去处理,即父元素的onTouchEvent会被调用。意思就是事件一旦交给一个View处理,那么它就必须消耗掉,否则同一事件序列中剩下的事件就不再交给它来处理了,这就好比上级交给程序员一件事,如果这件事没有处理好,短期内上级就不敢再把事情交给这个程序员做了。
// Dispatch to touch targets.
if (mFirstTouchTarget == null) {
// No touch targets so treat this as an ordinary view.
handled = dispatchTransformedTouchEvent(ev, canceled, null, TouchTarget.ALL_POINTER_IDS);
}
注意上面这段代码,这里第三个参数 child 为 null,从前面的分析可以知道,它会调用 super.dispatchTouchEvent(event), 很显然,这里就转到了 View 的 dispatchTouchEvent 方法,即点击事件开始交由 View 来处理,请看下面的分析。
View对点击事件的处理
View对点击事件的处理要简单一点,注意这里的View不包含ViewGroup。先看它的dispatchTouchEvent
方法,如下所示:
public boolean dispatchTouchEvent(MotionEvent event) {
boolean result = false;
......
if (onFilterTouchEventForSecurity(event)) {
if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
result = true;
}
//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;
}
}
......
return result;
}
首先判断有没有设置OnTouchListener,如果OnTouchListener中的onTouch方法返回true,那么onTouchEvent就不会被调用,可见OnTouchListener的优先级高于onTouchEvent,这样的好处是方便在外界处理点击事件。
接着再分析onTouchEvent的实现
public boolean onTouchEvent(MotionEvent event) {
......
//当View处于不可用状态下,也会消耗点击事件
if ((viewFlags & ENABLED_MASK) == DISABLED) {
if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
setPressed(false);
}
// A disabled view that is clickable still consumes the touch
// events, it just doesn't respond to them.
return (((viewFlags & CLICKABLE) == CLICKABLE
|| (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
|| (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE);
}
......
//对点击事件的具体处理
if (((viewFlags & CLICKABLE) == CLICKABLE ||
(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) ||
(viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE) {
switch (action) {
case MotionEvent.ACTION_UP:
boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
......
if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
// This is a tap, so remove the longpress check
removeLongPressCallback();
// Only perform take click actions if we were in the pressed state
if (!focusTaken) {
// Use a Runnable and post this rather than calling
// performClick directly. This lets other visual state
// of the view update before click actions start.
if (mPerformClick == null) {
mPerformClick = new PerformClick();
}
if (!post(mPerformClick)) {
performClick();
}
}
}
......
}
}
return true;
}
......
}
只要View的CLICKABLE和LONG_CLICKABLE有一个为true,那么它就会消耗这个事件,即onTouchEvent方法返回true,不管它是不是DISABLE状态,这就证实了前面提到的第8、9、10条结论:
8.view的onTouchEvent默认都会消耗事件(返回true),除非它是不可点击的(clickable和longClickable同时为false),View的longClickable属性默认都为false,clickable属性要分情况,比如Button的clickable属性默认为true,而TextView 的clickable属性默认为false
9.View 的enable.属性不影响onTouchEvent的默认返回值。哪怕一个View是disable状态的,只要它的clickable或者longclickable有一个为true,那么它的onTouchEvent就返会true。
10.onclick会发生的前提是当前的View是可点击的,并且他收到了down和up的事件。
在ACTION_UP事件中,会触发PerformClick()方法,如果View设置了OnClickListener,那么PerformClick()方法内部会调用它的onClick()方法。
通过setClickable和setLongClickable会分别改变View的CLICKABLE和LONG_CLICKABLE属性。setOnClickListener会自动将View的CLICKABLE设为true,setOnLongClickListener会自动将View的LONG_CLICKABLE设为true。
View的滑动冲突
常见的滑动冲突的场景
- 场景1--外部滑动方向和内部滑动方向不一致
- 场景2--外部滑动方向和内部滑动方向一致
-
场景3--上面两种情况的嵌套
滑动冲突的处理规则
- 对于场景1,根据滑动是水平滑动还是竖直滑动来判断到底由谁来拦截事件。
如何判断滑动方向?可以通过水平和竖直方向的距离差来判断,比如竖直方向的滑动距离大就判断为竖直滑动,否则判断为水平滑动。
- 对于场景2,根据业务规则来决定由谁拦截事件。
- 对于场景3,根据业务规则来决定由谁拦截事件。
滑动冲突的解决方式
针对场景1:
- 外部拦截法
点击事件都先经过父容器的拦截处理,如果父容器需要此事件就拦截,如果不需要此事件就不拦截,这样就可以解决滑动冲突的问题,这种方法比较符合点击事件的分发机制。外部拦截法需要重写父容器的onInterceptTouchEvent方法,在内部做相应的拦截即可。伪代码如下:
public boolean onInterceptTouchEvent(MotionEvent event){
boolean intercepted = false;
int x = (int)event.getX();
int y = (int)event.getY();
switch(event.getAction()){
case MotionEvent.ACTION_DOWN:{
intercepted = false;
break;
}
case MotionEvent.ACTION_MOVE:{
if(父容器需要当前的点击事件){
intercepted = true;
}
else{
intercepted = false;
}
break;
}
case MotionEvent.ACTION_UP:{
intercepted = false;
break;
}
default:
break;
}
mLastXIntercept = x;
mLastYIntercept = y;
return intercepted;
}
父容器拦截ACTION_DOWN事件时必须返回false,因为如果拦截ACTION_DOWN的话,接下来的事件都会交给父容器处理,子View则没有机会收到事件,更谈不上处理事件。在拦截ACTION_UP事件也必须返回false,因为父容器拦截ACTION_UP事件没有意义,一旦返回true(拦截),子View就不能响应ACTION_UP事件,进一步导致子View的Click事件不能响应。其实这种方法就是在ACTION_MOVE事件做判断,是否拦截ACTION_MOVE的事件,毕竟是滑动事件,最有用也就是ACTION_MOVE事件了。
-
内部拦截法
内部拦截稍微复杂点,就是父容器不做拦截,直接传递给子View处理事件。如果符合子View的滑动方式,就消耗这个事件,否则交回给父容器处理。
主要利用了子View设置父容器的一个标志位FLAG_DISALLOW_INTERCEPT
,是否让父容器拦截事件。子View拦截ACTION_DOWN
事件时,设置让父容器不能拦截事件。在ACTION_MOVE
判断是否符合自己的滑动规则,如果不符合,允许父容器拦截事件。它的伪代码如下,我们需要重写子元素的dispatchTouchEvent方法:
public boolean dispatchTouchEvent(MotionEvent event){
boolean intercepted = false;
int x = (int)event.getX();
int y = (int)event.getY();
switch(event.getAction()){
case MotionEvent.ACTION_DOWN:{
parent.requestDisallowInterceptTouchEvent(true);
break;
}
case MotionEvent.ACTION_MOVE:{
int deltaX = x - mLastX;
int deltaY = y - mLastY;
if(父容器需要当前的点击事件){
parent.requestDisallowInterceptTouchEvent(false);
}
break;
}
case MotionEvent.ACTION_UP:{
break;
}
default:
break;
}
mLastX = x;
mLastY = y;
return super.dispatchTouchEvent(event);
}
除了子元素需要做处理以外,父元素也要默认拦截除了ACTION_DOWN以外的其它事件,这样当子元素调用parent.requestDisallowInterceptTouchEvent(false)方法时,父元素才能继续拦截所需的事件。父元素修改如下:
public boolean onInterceptTouchEvent(MotionEvent event){
int action = event.getAction();
if(action==MotionEvent.ACTION_DOWN){
return false;
}
else{
return true;
}
}
为什么父容器不能拦截ACTION_DOWN事件呢?那是因为ACTION_DOWN事件并不受FLAG_DISALLOW_INTERCEPT这个标记位的控制,所以一旦父容器拦截ACTION_DOWN事件,那么所有的事件都无法传递到子元素中去,这样内部拦截法就无法起作用了。