接着看代码块3,在这段很长的代码里,首先一个if
中判断了该事件是否满足没有被拦截和被取消,之后第二个if
判断了事件类型是否为DOWN
,满足了没有被拦截和取消的DOWN
事件,接下来ViewGroup才会循环其子View找到点击事件在其内部并且能够接受该事件的子View,再通过调用dispatchTransformedTouchEvent
方法将事件分发给该子View处理,返回true说明子View成功消费事件,于是调用addTouchTarget
方法,方法中通过TouchTarget.obtain
方法获得一个包含这View的TouchTarget
节点并将其添加到链表头,并将已经分发的标记设置为true
。
接下来看代码块4:
// Dispatch to touch targets. //走到这里说明在循环遍历所有子View后没有找到接受该事件或者事件不是DOWN事件或者该事件已被拦截或取消 if (mFirstTouchTarget == null) { //mFirstTouchTarget为空说明没有子View响应消费该事件 //所有调用dispatchTransformedTouchEvent方法分发事件 //注意这里第三个参数传的是null,方法里会调用super.dispatchTouchEvent(event)即View.dispatchTouchEvent(event)方法 // No touch targets so treat this as an ordinary view. handled = dispatchTransformedTouchEvent(ev, canceled, null, TouchTarget.ALL_POINTER_IDS); } else { // mFirstTouchTarget不为空说明有子View能响应消费该事件,消费过之前的DOWN事件,就将这个事件还分发给这个View // 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; } else { final boolean cancelChild = resetCancelNextUpFlag(target.child) || intercepted; //这里传入的是target.child就是之前响应消费的View,把该事件还交给它处理 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; } }
之前在代码块3中处理分发了没被拦截和取消的DOWN
事件,那么其他MOVE
、UP
等类型事件怎么处理呢?还有如果遍历完子View却没有能接受这个事件的View又怎么处理呢?代码块4中就处理分发了这些事件。首先判断mFirstTouchTarget
是否为空,为空说明没有子View消费该事件,于是就调用dispatchTransformedTouchEvent
方法分发事件,这里注意dispatchTransformedTouchEvent
方法第三个参数View传的null
,方法里会对于这种没有子View能处理消费事件的情况,就调用该ViewGroup的super.dispatchTouchEvent
方法,即View的dispatchTouchEvent
,把ViewGroup当成View来处理,把事件交给ViewGroup处理。具体看dispatchTransformedTouchEvent
方法中的这段代码:
if (child == null) { handled = super.dispatchTouchEvent(event); } else { handled = child.dispatchTouchEvent(event); }
dispatchTransformedTouchEvent
方法中child
即传入的View为空则调用super.dispatchTouchEvent
方法分发事件,就是View类的分发方法,不为空则调用子View方法,即child.dispatchTouchEvent
分发事件,所以归根结底都是调用了View类的dispatchTouchEvent
方法处理。
至此,ViewGroup中的分发过流程结束,再来总结一下这个过程:首先过滤掉不安全的事件,接着如果事件类型是DOWN
事件认为是一个新的事件序列开始,就清空TouchTarget
链表重置相关标志位(代码块一),然后判断是否拦截该事件,这里有两步判断:一是如果是DOWN
事件或者不是DOWN
事件但是mFirstTouchTarget
不等于null
(这里mFirstTouchTarget
如果等于null
说明之前没有View消费DOWN
事件,在代码块三末尾,可以看到如果有子View消费了DOWN
事件,会调用addTouchTarget
方法,获得一个保存了该子View的TouchTarget
,并将其添加到mFirstTouchTarget
链表头),则进入第二步禁止拦截标记的判断,否则直接设置为需要拦截,进入第二步判断设置过禁止拦截标记为true
的就不拦截,否则调用ViewGroup的onInterceptTouchEvent
方法根据返回接过来决定是否拦截(代码块二)。接下来如果事件没被拦截也没被取消而且还是DOWN
事件,就循环遍历ViewGroup中的子View找到事件在其范围内并且能接受事件的子View,通过dispatchTransformedTouchEvent
方法将事件分发给该子View,然后通过addTouchTarget
方法将包含该子View的TouchTarget
插到链表头(代码块三)。最后如果没有找到能够接受该事件的子View又或者是MOVE
、UP
类型事件等再判断mFirstTouchTarget
是否为空,为空说明之前没有View能接受消费该事件,则调用dispatchTransformedTouchEvent
方法将事件交给自身处理,不为空则同样调用dispatchTransformedTouchEvent
方法,但是是将事件分发给该子View处理。
ViewGroup的onInterceptTouchEvent方法:
public boolean onInterceptTouchEvent(MotionEvent ev) { if (ev.isFromSource(InputDevice.SOURCE_MOUSE) && ev.getAction() == MotionEvent.ACTION_DOWN && ev.isButtonPressed(MotionEvent.BUTTON_PRIMARY) && isOnScrollbarThumb(ev.getX(), ev.getY())) { return true; } return false; }
在ViewGroup的dispatchTouchEvent
中没设置过禁止拦截的事件默认都会通过onInterceptTouchEvent
方法来决定是否拦截,onInterceptTouchEvent
方法里可以看到默认是返回false
,只有在事件源类型是鼠标并且是DOWN
事件是鼠标点击按钮和是滚动条的手势时才返回true
。所以默认一般ViewGroup的onInterceptTouchEvent
方法返回都为false
,也就是说默认不拦截事件。
ViewGroup的onTouchEvent方法:
ViewGroup中没有覆盖onTouchEvent
方法,所以调用ViewGroup的onTouchEvent
方法实际上调用的还是它的父类View的onTouchEvent
方法。
View的dispatchTouchEvent方法:
在ViewGroup中将事件无论是分发给子View的时候还是自己处理的,最终都会执行默认的View类的dispatchTouchEvent
方法:
public boolean dispatchTouchEvent(MotionEvent event) { ...... boolean result = false; ...... if (onFilterTouchEventForSecurity(event)) { ...... 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; }
这里同样省略一些代码只看关键的,首先同样和ViewGroup一样,做了事件安全性的过滤,接着先判断了mOnTouchListener
是否为空,不为空并且该View是ENABLED
可用的,就会调用mOnTouchListener
的onTouch
方法,如果onTouch
方法返回true
说明事件已经被消费了,就将result
标记修改为true
,这样他就不会走接下来的if
了。如果没有设置mOnTouchListener
或者onTouch
方法返回false
,则会继续调用onTouchEvent
方法。这里可以发现mOnTouchListener
的onTouch
方法的优先级是在onTouchEvent
之前的,如果在代码中设置了mOnTouchListener
监听,并且onTouch
返回true
,则这个事件就被在onTouch
里消费了,不会在调用onTouchEvent
方法。
//这个mOnTouchListener就是经常在代码里设置的View.OnTouchListenermMyView.setOnTouchListener(new View.OnTouchListener() { @Override public boolean onTouch(View v, MotionEvent event) { //这里返回true事件就消费了,不会再调用onTouchEvent方法 return true; } });
View的onTouchEvent方法:
public boolean onTouchEvent(MotionEvent event) { /---------------代码块-1------------------------------------------------------------------- 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; if ((viewFlags & ENABLED_MASK) == DISABLED) { if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) { setPressed(false); } mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN; // A disabled view that is clickable still consumes the touch // events, it just doesn't respond to them. return clickable; } /---------------代码块-1------完------------------------------------------------------------- /---------------代码块-2------------------------------------------------------------------- if (mTouchDelegate != null) { if (mTouchDelegate.onTouchEvent(event)) { return true; } } /---------------代码块-2------完------------------------------------------------------------- /---------------代码块-3------------------------------------------------------------------- if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) { switch (action) { case MotionEvent.ACTION_UP: mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN; if ((viewFlags & TOOLTIP) == TOOLTIP) { handleTooltipUp(); } if (!clickable) { removeTapCallback(); removeLongPressCallback(); mInContextButtonPress = false; mHasPerformedLongPress = false; mIgnoreNextUpEvent = false; break; } boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0; if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) { // take focus if we don't have it already and we should in // touch mode. boolean focusTaken = false; if (isFocusable() && isFocusableInTouchMode() && !isFocused()) { focusTaken = requestFocus(); } if (prepressed) { // The button is being released before we actually // showed it as pressed. Make it show the pressed // state now (before scheduling the click) to ensure // the user sees it. setPressed(true, x, y); } 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)) { //调用了OnClickListener performClick(); } } } if (mUnsetPressedState == null) { mUnsetPressedState = new UnsetPressedState(); } if (prepressed) { postDelayed(mUnsetPressedState, ViewConfiguration.getPressedStateDuration()); } else if (!post(mUnsetPressedState)) { // If the post failed, unpress right now mUnsetPressedState.run(); } removeTapCallback(); } mIgnoreNextUpEvent = false; break; case MotionEvent.ACTION_DOWN: if (event.getSource() == InputDevice.SOURCE_TOUCHSCREEN) { mPrivateFlags3 |= PFLAG3_FINGER_DOWN; } mHasPerformedLongPress = false; if (!clickable) { checkForLongClick(0, x, y); break; } if (performButtonActionOnTouchDown(event)) { break; } // Walk up the hierarchy to determine if we're inside a scrolling container. boolean isInScrollingContainer = isInScrollingContainer(); // For views inside a scrolling container, delay the pressed feedback for // a short period in case this is a scroll. if (isInScrollingContainer) { mPrivateFlags |= PFLAG_PREPRESSED; if (mPendingCheckForTap == null) { mPendingCheckForTap = new CheckForTap(); } mPendingCheckForTap.x = event.getX(); mPendingCheckForTap.y = event.getY(); postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout()); } else { // Not inside a scrolling container, so show the feedback right away setPressed(true, x, y); checkForLongClick(0, x, y); } break; case MotionEvent.ACTION_CANCEL: if (clickable) { setPressed(false); } removeTapCallback(); removeLongPressCallback(); mInContextButtonPress = false; mHasPerformedLongPress = false; mIgnoreNextUpEvent = false; mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN; break; case MotionEvent.ACTION_MOVE: if (clickable) { drawableHotspotChanged(x, y); } // Be lenient about moving outside of buttons if (!pointInView(x, y, mTouchSlop)) { // Outside button // Remove any future long press/tap checks removeTapCallback(); removeLongPressCallback(); if ((mPrivateFlags & PFLAG_PRESSED) != 0) { setPressed(false); } mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN; } break; } return true; } /---------------代码块-3------完------------------------------------------------------------- return false; }
onTouchEvent
方法里的代码也不少,不过大部分都是响应事件的一些逻辑,与事件分发流程关系不大。还是分成三块,先看第一个代码块:
final float x = event.getX(); final float y = event.getY(); final int viewFlags = mViewFlags; final int action = event.getAction(); //这里CLICKABLE、CONTEXT_CLICKABLE和CONTEXT_CLICKABLE有一个满足,clickable就为true final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE; //这里先判断当前View是否可用,如果是不可用进入if代码块 if ((viewFlags & ENABLED_MASK) == DISABLED) { //如果是UP事件并且View处于PRESSED状态,则调用setPressed设置为false if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) { setPressed(false); } mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN; // A disabled view that is clickable still consumes the touch // events, it just doesn't respond to them. //这里如果View是不可用状态,就直接返回clickable状态,不做任何处理 return clickable; }
代码块1中首先获得View是否可点击clickable
,然后判断View如果是不可用状态就直接返回clickable
,但是没做任何响应。View默认的clickable
为false
,Enabled
为ture
,不同的View的clickable
默认值也不同,Button
默认clickable
为true
,TextView
默认为false
。
if (mTouchDelegate != null) { if (mTouchDelegate.onTouchEvent(event)) { return true; } }
代码块2中会对一个mTouchDelegate
触摸代理进行判断,不为空会调用代理的onTouchEvent
响应事件并且返回true
。
if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) { switch (action) { case MotionEvent.ACTION_UP: mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN; if ((viewFlags & TOOLTIP) == TOOLTIP) { handleTooltipUp(); } if (!clickable) { removeTapCallback(); removeLongPressCallback(); mInContextButtonPress = false; mHasPerformedLongPress = false; mIgnoreNextUpEvent = false; break; } boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0; if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) { // take focus if we don't have it already and we should in // touch mode. boolean focusTaken = false; if (isFocusable() && isFocusableInTouchMode() && !isFocused()) { focusTaken = requestFocus(); } if (prepressed) { // The button is being released before we actually // showed it as pressed. Make it show the pressed // state now (before scheduling the click) to ensure // the user sees it. setPressed(true, x, y); } 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)) { //调用了OnClickListener performClick(); } } } if (mUnsetPressedState == null) { mUnsetPressedState = new UnsetPressedState(); } if (prepressed) { postDelayed(mUnsetPressedState, ViewConfiguration.getPressedStateDuration()); } else if (!post(mUnsetPressedState)) { // If the post failed, unpress right now mUnsetPressedState.run(); } removeTapCallback(); } mIgnoreNextUpEvent = false; break; case MotionEvent.ACTION_DOWN: if (event.getSource() == InputDevice.SOURCE_TOUCHSCREEN) { mPrivateFlags3 |= PFLAG3_FINGER_DOWN; } mHasPerformedLongPress = false; if (!clickable) { checkForLongClick(0, x, y); break; } if (performButtonActionOnTouchDown(event)) { break; } // Walk up the hierarchy to determine if we're inside a scrolling container. boolean isInScrollingContainer = isInScrollingContainer(); // For views inside a scrolling container, delay the pressed feedback for // a short period in case this is a scroll. if (isInScrollingContainer) { mPrivateFlags |= PFLAG_PREPRESSED; if (mPendingCheckForTap == null) { mPendingCheckForTap = new CheckForTap(); } mPendingCheckForTap.x = event.getX(); mPendingCheckForTap.y = event.getY(); postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout()); } else { // Not inside a scrolling container, so show the feedback right away setPressed(true, x, y); checkForLongClick(0, x, y); } break; case MotionEvent.ACTION_CANCEL: if (clickable) { setPressed(false); } removeTapCallback(); removeLongPressCallback(); mInContextButtonPress = false; mHasPerformedLongPress = false; mIgnoreNextUpEvent = false; mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN; break; case MotionEvent.ACTION_MOVE: if (clickable) { drawableHotspotChanged(x, y); } // Be lenient about moving outside of buttons if (!pointInView(x, y, mTouchSlop)) { // Outside button // Remove any future long press/tap checks removeTapCallback(); removeLongPressCallback(); if ((mPrivateFlags & PFLAG_PRESSED) != 0) { setPressed(false); } mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN; } break; } return true; }
代码块3中首先判断了 clickable || (viewFlags & TOOLTIP) == TOOLTIP
满足了这个条件就返回true
消费事件。接下来的switch
中主要对事件四种状态分别做了处理。这里稍微看下在UP
事件中会调用一个performClick
方法,方法中调用了OnClickListener
的onClick
方法。
public boolean performClick() { final boolean result; final ListenerInfo li = mListenerInfo; if (li != null && li.mOnClickListener != null) { playSoundEffect(SoundEffectConstants.CLICK); li.mOnClickListener.onClick(this); result = true; } else { result = false; } sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED); notifyEnterOrExitForAutoFillIfNeeded(true); return result; }
最后看到onTouchEvent
的最后一行默认返回的还是false
,就是说只有满足上述的条件之一才会返回ture
。
至此事件分发的相关源码就梳理完了,我画了几张流程图,能更清新的理解源码逻辑。
ViewGroup的dispatchTouchEvent逻辑:
View的dispathTouchEvent逻辑:
事件分发整体逻辑
4、事件分发机制相关问题
阅读了源码之后,先来解决之前提到的三个问题。
Q1:为什么日志Demo中只有ACTION_DOWN
事件有完整的从Activity到ViewGroup再到View的分发拦截和响应的运行日志,为什么ACTION_MOVE
和ACTION_UP
事件没有?
A1:日志Demo代码所有事件传递方法都是默认调用super
父类对应方法,所以根据源码逻辑可知当事件序列中的第一个DOWN
事件来临时,会按照Activity-->MyViewGroupA-->MyViewGroupB-->MyView
的顺序分发,ViewGroup中onInterceptTouchEvent
方法默认返回false
不会拦截事件,最终会找到合适的子View(这里即MyView)dispatchTransformedTouchEvent
方法,将事件交给子View的dispatchTouchEvent
处理,在dispatchTouchEvent
方法中默认会调用View的onTouchEvent
方法处理事件,这里因为MyView是继承View的,所以默认clickable
为false
,而onTouchEvent
方法中当clickable
为false
时默认返回的也是false
。最终导致ViewGroup中dispatchTransformedTouchEvent
方法返回为false
。进而导致mFirstTouchTarget
为空,所以后续MOVE
、UP
事件到来时,因为mFirstTouchTarget
为空,事件拦截标记直接设置为true
事件被拦截,就不会继续向下分发,最终事件无人消费就返回到Activity的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 { //mFirstTouchTarget为空intercepted为true且不会调用onInterceptTouchEvent方法 intercepted = true; }
Q2:为什么将设置clickable="true"
之后ACTION_MOVE
和ACTION_UP
事件就会执行了?
A2:如A1中所说,clickable
设置为true
,View的onTouchEvent
方法的返回就会为true
,消费了DOWN
事件,就会创建一个TouchTarget
插到单链表头,mFirstTouchTarget
就不会是空了,MOVE
、UP
事件到来时,就会由之前消费了DOWN
事件的View来处理消费MOVE
、UP
事件。
Q3:requestDisallowInterceptTouchEvent
方法是怎样通知父View不拦截事件,为什么连onInterceptTouchEvent
方法也不执行了?
A3:源码阅读是有看到,requestDisallowInterceptTouchEvent
方法时通过位运算设置标志位,在调用传入参数为true
后,事件在分发时disallowIntercept
会为true
,!disallowIntercept
即为false
,导致事件拦截标记intercepted
为false
,不会进行事件拦截。
Q4:View.OnClickListener
的onClick
方法与View.OnTouchListener
的onTouch
执行顺序?
A4::View.OnClickListener
的onClick
方法是在View的onTouchEvent
中performClick
方法中调用的。 而View.OnTouchListener
的onTouch
方法在View的dispatchTouchEvent
方法中看到是比onTouchEvent
方法优先级高的,并且只要OnTouchListener.Touch
返回为true
,就只会调用OnTouchListener.onTouch
方法不会再调用onTouchEvent
方法。所以View.OnClickListener
的onClick
方法顺序是在View.OnTouchListener
的onTouch
之后的。
5、滑动冲突
关于滑动冲突,在《Android开发艺术探索》中有详细说明,我这里把书上的方法结论与具体实例结合起来做一个总结。
1.滑动冲突的场景
常见的场景有三种:
外部滑动与内部滑动方向不一致
外部滑动与内部滑动方向一致
前两种情况的嵌套
2.滑动冲突的处理规则
不同的场景有不同的处理规则,例如上面的场景一,规则一般就是当左右滑动时,外部View拦截事件,当上下滑动时要让内部View拦截事件,这时候处理滑动冲突就可以根据滑动是水平滑动还是垂直滑动来判断谁来拦截事件。场景而这种同个方向上的滑动冲突一般要根据业务逻辑来处理规则,什么时候要外部View拦截,什么时候要内部View拦截。场景三就更加复杂了,但是同样是根据具体业务逻辑,来判断具体的滑动规则。
推荐阅读:终于有人把 【移动开发】 从基础到实战的全套视频弄全了
3.滑动冲突的解决方法
外部拦截法
内部拦截法
外部拦截法是从父View着手,所有事件都要经过父View的分发和拦截,什么时候父View需要事件,就将其拦截,不需要就不拦截,通过重写父View的onInterceptTouchEvent
方法来实现拦截规则。
private int mLastXIntercept; private int mLastYIntercept; 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; }
按照以上伪代码,根据不同的拦截要求进行修改就可以解决滑动冲突。
内部拦截法的思想是父View不拦截事件,由子View来决定事件拦截,如果子View需要此事件就直接消耗掉,如果不需要就交给父View处理。这种方法需要配合requestDisallowInterceptTouchEvent
方法来实现。
private int mLastX;private int mLastY;@Override public boolean dispatchTouchEvent(MotionEvent event) { 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); } //父View的onInterceptTouchEvent方法 @Override public boolean onInterceptTouchEvent(MotionEvent event) { int action = event.getAction(); if (action == MotionEvent.ACTION_DOWN) { return false; } else { return true; } }
这里父View不拦截ACTION_DOWN
方法的原因,根据之前的源码阅读可知如果ACTION_DOWN
事件被拦截,之后的所有事件就都不会再传递下去了。
4.滑动冲突实例
实例一:ScrollView与ListView嵌套
这个实例是同向滑动冲突,先看布局文件:
<?xml version="1.0" encoding="utf-8"?><cScrollView xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:id="@+id/scrollView1" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".ScrollDemo1Activity"> <LinearLayout android:layout_width="wrap_content" android:layout_height="wrap_content" android:orientation="vertical"> <com.example.sy.eventdemo.MyView android:layout_width="match_parent" android:layout_height="350dp" android:background="#27A3F3" android:clickable="true" /> <ListView android:id="@+id/lv" android:layout_width="match_parent" android:background="#E5F327" android:layout_height="300dp"></ListView> <com.example.sy.eventdemo.MyView android:layout_width="match_parent" android:layout_height="500dp" android:background="#0AEC2E" android:clickable="true" /> </LinearLayout></cScrollView>
这里MyView就是之前打印日志的View没有做任何其他处理,用于占位使ScrollView超出一屏可以滑动。
运行效果:
可以看到ScrollView与ListView发生滑动冲突,ListView的滑动事件没有触发。接着来解决这个问题,用内部拦截法。
首先自定义ScrollView,重写他的onInterceptTouchEvent
方法,拦击除了DOWN
事件以外的事件。
public class MyScrollView extends ScrollView { public MyScrollView(Context context, AttributeSet attrs) { super(context, attrs); } @Override public boolean onInterceptTouchEvent(MotionEvent ev) { if (ev.getAction() == MotionEvent.ACTION_DOWN) { onTouchEvent(ev); return false; } return true; } }
这里没有拦截DOWN
事件,所以DOWN
事件无法进入ScrollView的onTouchEvent
事件,又因为ScrollView的滚动需要在onTouchEvent
方法中做一些准备,所以这里手动调用一次。接着再自定义一个ListView,来决定事件拦截,重写dispatchTouchEvent
方法。
package com.example.sy.eventdemo;import android.content.Context;import android.os.Build;import android.support.annotation.RequiresApi;import android.util.AttributeSet;import android.view.MotionEvent;import android.widget.ListView;/** * Create by SY on 2019/4/22 */public class MyListView extends ListView { public MyListView(Context context) { super(context); } public MyListView(Context context, AttributeSet attrs) { super(context, attrs); } public MyListView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); } private float lastY; @RequiresApi(api = Build.VERSION_CODES.KITKAT) @Override public boolean dispatchTouchEvent(MotionEvent ev) { if (ev.getAction() == MotionEvent.ACTION_DOWN) { getParent().getParent().requestDisallowInterceptTouchEvent(true); } else if (ev.getAction() == MotionEvent.ACTION_MOVE) { if (lastY > ev.getY()) { // 这里判断是向上滑动,而且不能再向上滑了,说明到头了,就让父View处理 if (!canScrollList(1)) { getParent().getParent().requestDisallowInterceptTouchEvent(false); return false; } } else if (ev.getY() > lastY) { // 这里判断是向下滑动,而且不能再向下滑了,说明到头了,同样让父View处理 if (!canScrollList(-1)) { getParent().getParent().requestDisallowInterceptTouchEvent(false); return false; } } } lastY = ev.getY(); return super.dispatchTouchEvent(ev); } }
判断是向上滑动还是向下滑动,是否滑动到头了,如果滑到头了就让父View拦截事件由父View处理,否则就由自己处理。将布局文件中的空间更换。
<?xml version="1.0" encoding="utf-8"?><com.example.sy.eventdemo.MyScrollView xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:id="@+id/scrollView1" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".ScrollDemo1Activity"> <LinearLayout android:layout_width="wrap_content" android:layout_height="wrap_content" android:orientation="vertical"> <com.example.sy.eventdemo.MyView android:layout_width="match_parent" android:layout_height="350dp" android:background="#27A3F3" android:clickable="true" /> <com.example.sy.eventdemo.MyListView android:id="@+id/lv" android:layout_width="match_parent" android:background="#E5F327" android:layout_height="300dp"></com.example.sy.eventdemo.MyListView> <com.example.sy.eventdemo.MyView android:layout_width="match_parent" android:layout_height="500dp" android:background="#0AEC2E" android:clickable="true" /> </LinearLayout></com.example.sy.eventdemo.MyScrollView>
运行结果:
实例二:ViewPager与ListView嵌套
这个例子是水平和垂直滑动冲突。使用V4包中的ViewPager与ListView嵌套并不会发生冲突,是因为在ViewPager中已经实现了关于滑动冲突的处理代码,所以这里自定义一个简单的ViewPager来测试冲突。布局文件里就一个ViewPager:
<?xml version="1.0" encoding="utf-8"?><LinearLayout 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" tools:context=".ScrollDemo2Activity"> <com.example.sy.eventdemo.MyViewPager android:id="@+id/viewPager" android:layout_width="match_parent" android:layout_height="match_parent"></com.example.sy.eventdemo.MyViewPager></LinearLayout>
ViewPager的每个页面的布局也很简单就是一个ListView:
<?xml version="1.0" encoding="utf-8"?><LinearLayout 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" tools:context=".ScrollDemo2Activity"> <com.example.sy.eventdemo.MyViewPager android:id="@+id/viewPager" android:layout_width="match_parent" android:layout_height="match_parent"></com.example.sy.eventdemo.MyViewPager></LinearLayout>
开始没有处理滑动冲突的运行效果是这样的:
看到现在只能上下滑动响应ListView的滑动事件,接着我们外部拦截发解决滑动冲突,核心代码如下:
case MotionEvent.ACTION_MOVE: int gapX = x - lastX; int gapY = y - lastY; //当水平滑动距离大于垂直滑动距离,让父view拦截事件 if (Math.abs(gapX) > Math.abs(gapY)) { intercept = true; } else { //否则不拦截事件 intercept = false; } break;
onInterceptTouchEvent
中当水平滑动距离大于垂直滑动距离,让父view拦截事件,反之父View不拦截事件,让子View处理。
运行结果:
这下冲突就解决了。这两个例子分别对应了上面的场景一和场景二,关于场景三的解决方法其实也是一样,都是根据具体需求判断事件需要由谁来响应消费,然后重写对应方法将事件拦截或者取消拦截即可,这里就不再具体举例了。
6、总结
Android事件分发顺序:Activity-->ViewGroup-->View
Android事件响应顺序:View-->ViewGroup-->Activity
滑动冲突解决,关键在于找到拦截规则,根据操作习惯或者业务逻辑确定拦截规则,根据规则重新对应拦截方法即可。
Android高级架构脑图详细地址
关于FAndroid进阶知识的全部学习内容,我们这边都有系统的知识体系以及进阶视频资料,有需要的朋友可以加群免费领取安卓进阶视频教程,源码,面试资料,群内有大牛一起交流讨论技术;点击链接加入群聊【腾讯@Android高级架构】(包括自定义控件、NDK、架构设计、混合式开发工程师(React native,Weex)、性能优化、完整商业项目开发等)
Android高级进阶视频教程