事件分发和滑动冲突知识点总结
来源:互联网 发布:单片机时序图 编辑:程序博客网 时间:2024/05/19 02:43
View的事件分发和滑动冲突学习总结
前言
本文分两个部分,第一部分会先过一遍事件分发机制的流程并做一些结论性的总结,然后从源码层面分析这些流程。第二部分会介绍滑动冲突问题的一些解决方案。查了比较多的资料,也有一些自己的看法,由于知识有限,差错之处希望各位不吝指出。
View 的事件分发机制
简介
当一个MotionEvent产生了以后,系统需要把这个事件传递给一个具体的View,这个传递过程就是分发过程。这个过程由三个方法共同完成:
public boolean dispatchTouchEvent(MotionEvent ev)
这个方法用来进行事件的分发,如果事件能够传递给当前View,那么这个方法就一定会被调用。他的返回结果受到当前View的onTouchEvent和下级的dispatchTouchEvent的影响,表示的是当前View(包括其子View)是否消耗这个事件。
public boolean onInterceptTouchEvent(MotionEvent event)
这个方法在该ViewGroup的dispatchTouchEvent方法中调用,用来判断该View是否拦截某个事件,若拦截,那么该View将直接拦截与该事件同一事件序列的剩余事件,对这一事件序列不再调用onInterceptTouchEvent判断是否拦截。返回值表示是否拦截当前事件。这个方法在View的子类ViewGroup中而不在View中。
public boolean onTouchEvent(MotionEvent event)
在dispatchTouchEvent中调用,用来处理点击事件,返回结果表示是否消耗当前事件,如果不消耗,则不能接受同一事件序列内的剩余事件。
图解–来自Kelin
模拟流程的伪代码
public boolean dispatchTouchEvent(MontionEvent ev){ boolean consume = false; if(onInterceptTouchEvent(ev)){ consume = onTouchEvent(ev); }else{ consume = child.dispatchTouchEvent(ev); } return consume;}
结论
- 事件序列是指从手指接触屏幕开始,到手指离开屏幕结束这个过程中产生的一系列事件,这个事件序列以down开始,中间有数量不定的move,最终以up结束。
- 某个ViewGroup被判定到拦截某一事件M,那么M所在序列中M之后的事件,都会被这个ViewGroup处理(如果事件能传递给它),而且这个ViewGroup不会再调用其onInterceptTouchEvent方法去判断是否拦截,而是默认拦截。也就是说,onInterceptTouchEvent这个方法并不会总是被调用。
- 如果某个View不消耗ACTION_DOWN事件,那么同一事件序列中的其他事件都不会再给它处理,而是给它的父元素处理。
- 如果View不消耗ACTION_DOWN以外的其他事件,那么这个点击事件会消失,此时父元素的onTouchEvent不会被调用,并且当前View可以持续接收到后续事件,消失的事件会传递给Activity处理。
- ViewGroup默认不拦截任何事件。
- View没有onInterceptTouchEvent方法,一旦有事件传递给它,它的onTouchEvent方法就会被调用。
- View的onTouchEvent默认会消耗事件,除非它是不可点击的(clickable和longClickable同时为false)。View的longClickable属性默认为false。
- 一个disable的View依然可能会消耗事件,只要clickable或longClickablec中有一个为true。但并不运行onClickListener的onClick方法和onLongClickListener中的onLongClick方法甚至是onTouchListener中的onTouch方法。
- 事件的传递过程是由内向外的,即事件总是先传递给父元素,然后再由父元素分发给子View,通过requestDisallowInterceptTouchEvent方法可以在子元素中干预父元素的分发过程,当ACTION_DOWN事件除外。
分析
接下来就该上源码了,首先是ViewGroup的dispatchTouch方法(分析在注释里)
public boolean dispatchTouchEvent(MotionEvent ev) { if (mInputEventConsistencyVerifier != null) { mInputEventConsistencyVerifier.onTouchEvent(ev, 1); } // If the event targets the accessibility focused view and this is it, start // normal event dispatch. Maybe a descendant is what will handle the click. if (ev.isTargetAccessibilityFocus() && isAccessibilityFocusedViewOrHost()) { ev.setTargetAccessibilityFocus(false); } boolean handled = false; if (onFilterTouchEventForSecurity(ev)) { final int action = ev.getAction(); final int actionMasked = action & MotionEvent.ACTION_MASK; //第一步,进行初始化操作 /*判断是否为ACTION_DOWN,如果是, 就将一些标志位进行重置等操作,包括 disallowIntercept,所以不能被不允许拦截 注意,在cancelAndClearTouchTargets(ev)中 会将mFirstTouchTarget设置为null*/ 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(); } // 第二步,检查是否拦截 final boolean intercepted; /* 判断是否为ACTION_DOWN或mFirstTouchTarget是否为null 当满足其中一个的时候,进入并调用 onInterceptTouchEvent(ev) 来判断是否拦截。这里需要注意的是,在ViewGroup的子View处理 事件成功的时候,mFirstTouchTarget会被赋值并指向子元素。 也就是说,当事件不为ACTION_DOWN时,如果想调用 onInterceptTouchEvent(ev)判断是否拦截, 就必须让mFirstTouchTarget != null, 而这个条件必须是前一个事件没有被拦截且 ACTION_DWON能被子View消耗(如果ACTION_Down不能被消耗, 则mFirstTouchTarget是不会被赋值的。而如果是其他事件不 被消耗,由于子View消耗ACTION_DOWN时对 mFirstTouchTarget 赋了值,所以还是会进入调用onInterceptTouchEvent)。*/ if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) { /* 如果子View调用了requestDisallowInterceptTouchEvent方法, 则disallowIntercept为true,那么除了ACTION_DOWN,其他事件都不允许被拦截*/ 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; } // If intercepted, start normal event dispatch. Also if there is already // a view that is handling the gesture, do normal event dispatch. if (intercepted || mFirstTouchTarget != null) { ev.setTargetAccessibilityFocus(false); } //第三步:检查cancel // Check for cancelation. final boolean canceled = resetCancelNextUpFlag(this) || actionMasked == MotionEvent.ACTION_CANCEL; //第四步:事件分发 // Update list of touch targets for pointer down, if needed. final boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0; TouchTarget newTouchTarget = null; boolean alreadyDispatchedToNewTouchTarget = false; if (!canceled && !intercepted) { // If the event is targeting accessiiblity focus we give it to the // view that has accessibility focus and if it does not handle it // we clear the flag and dispatch the event to all children as usual. // We are looking up the accessibility focused host to avoid keeping // state since these events are very rare. View childWithAccessibilityFocus = ev.isTargetAccessibilityFocus() ? findChildWithAccessibilityFocus() : null; //处理ACTION_DOWN事件,如果子View处理成功, //那么mFirstTouchTarget会被赋值 if (actionMasked == MotionEvent.ACTION_DOWN || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN) || actionMasked == MotionEvent.ACTION_HOVER_MOVE) { final int actionIndex = ev.getActionIndex(); // always 0 for down final int idBitsToAssign = split ? 1 << ev.getPointerId(actionIndex) : TouchTarget.ALL_POINTER_IDS; // Clean up earlier touch targets for this pointer id in case they // have become out of sync. removePointersFromTouchTargets(idBitsToAssign); final int childrenCount = mChildrenCount; if (newTouchTarget == null && childrenCount != 0) { // 依据Touch坐标寻找子View来接收Touch事件 final float x = ev.getX(actionIndex); final float y = ev.getY(actionIndex); // Find a child that can receive the event. // Scan children from front to back. final ArrayList<View> preorderedList = buildTouchDispatchChildList(); final boolean customOrder = preorderedList == null && isChildrenDrawingOrderEnabled(); final View[] children = mChildren; // 遍历子View判断哪个子View接受Touch事件 for (int i = childrenCount - 1; i >= 0; i--) { final int childIndex = getAndVerifyPreorderedIndex( childrenCount, i, customOrder); final View child = getAndVerifyPreorderedView( preorderedList, children, childIndex); // If there is a view that has accessibility focus we want it // to get the event first and if not handled we will perform a // normal dispatch. We may do a double iteration but this is // safer given the timeframe. 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); 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); } if (preorderedList != null) preorderedList.clear(); } if (newTouchTarget == null && mFirstTouchTarget != null) { // Did not find a child to receive the event. // Assign the pointer to the least recently added target. newTouchTarget = mFirstTouchTarget; while (newTouchTarget.next != null) { newTouchTarget = newTouchTarget.next; } newTouchTarget.pointerIdBits |= idBitsToAssign; } } } // Dispatch to touch targets. /*mFirstTouchTarget == null有两种可能, 一是ACTION_DOWN被拦截或没有被处理,二是前一个事件被拦截。 不管是一还是二,当前事件和同一序列后续事件都不会被子View处理*/ if (mFirstTouchTarget == null) { // No touch targets so treat this as an ordinary view. // 向子View传递一个cancel事件, //dispatchTransformedTouchEvent()可以将事件分发给子View处理 handled = dispatchTransformedTouchEvent(ev, canceled, null, TouchTarget.ALL_POINTER_IDS); } else { //如果之前的事件没有被拦截 // 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 { //若当前事件被拦截,cancelChild则为true final boolean cancelChild = resetCancelNextUpFlag(target.child) || intercepted; //如果cancelChild为true,那么向子View分发一个cancel事件 //从这里可以看出,如果拦截一个子View的事件,则会向 //它分发一个cancel事件使得它状态重置 if (dispatchTransformedTouchEvent(ev, cancelChild, target.child, target.pointerIdBits)) { handled = true; } if (cancelChild) { if (predecessor == null) { //如果intercepted为true //mFirstTouchTarget最后会被赋值为null mFirstTouchTarget = next; } else { predecessor.next = next; } target.recycle(); target = next; continue; } } predecessor = target; target = next; } } //处理ACTION_UP和ACTION_CANCEL,主要是还原操作 // Update list of touch targets for pointer up or cancel, if needed. if (canceled || actionMasked == MotionEvent.ACTION_UP || actionMasked == MotionEvent.ACTION_HOVER_MOVE) { resetTouchState(); } else if (split && actionMasked == MotionEvent.ACTION_POINTER_UP) { final int actionIndex = ev.getActionIndex(); final int idBitsToRemove = 1 << ev.getPointerId(actionIndex); removePointersFromTouchTargets(idBitsToRemove); } } if (!handled && mInputEventConsistencyVerifier != null) { mInputEventConsistencyVerifier.onUnhandledEvent(ev, 1); } return handled; }
然后是View的onTouchEvent:
public boolean onTouchEvent(MotionEvent event) { final float x = event.getX(); final float y = event.getY(); final int viewFlags = mViewFlags; final int action = event.getAction(); //判断其属性是否为DISABLED if ((viewFlags & ENABLED_MASK) == DISABLED) { //如果当前事件为ACTION_UP且该View的状态为Pressed if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) { //清除掉Pressed状态 setPressed(false); } // A disabled view that is clickable still consumes the touch // events, it just doesn't respond to them. //如果这个View是clickable或longClickable, //则返回true,即消费该事件 return (((viewFlags & CLICKABLE) == CLICKABLE || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE); } //如果设置有代理,那就执行代理的onTouchEvent //一般是由于View太小不好按,才会设置代理。 if (mTouchDelegate != null) { if (mTouchDelegate.onTouchEvent(event)) { return true; } } //按照逻辑的连贯性,接下来我们先看ACTION_DOWN,最后再看ACTION_UP if (((viewFlags & CLICKABLE) == CLICKABLE || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE) { switch (action) { case MotionEvent.ACTION_UP: /*检测到ACTION_UP的时候, 不管是Pressed还是PrePressed状态,只要期间没有 ACTION_MOVE,即Pressed和PrePressed状态没有被取消, 就可以执行onClick方法,不同的是,由于PrePerssed 状态还没有被转换为Pressed状态的(mPendingCheckForTap 进程未被执行),所以在这里要setPressed(true, x, y);*/ 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. // 当前并不是Pressed状态,所以在这里setPressed setPressed(true, x, y); } //如果不是长按事件且下个ACTION_UP事件不被忽略 if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) { // This is a tap, so remove the longpress check /*移除等待长按的线程,这个线程做的事情其实就是 等待一段时间后调用longClick方法,如果你按下时间 足够,那就会执行这个方法。如果你中途移动或抬起, 那这个线程就会被停止*/ 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方法里面会判断 //onClickListener是否为null,并执行onClick方法 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: mHasPerformedLongPress = false;//初始化长按标志为false 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. /* 如果是在可滑动的容器中,接到ACTION_DOWN事件时, 不能直接将View设置为Pressed状态,得先等一下 (让手指保持当前状态115ms,即ViewConfiguration.getTapTimeout()), 这是为了避免将外部的滑动当作点击。如果不设置这个状态, 那么即使用户想滑动,当一碰到就会显示Pressed的状态,这是 不合理的。在对ACTION_MOVE的处理我们也可以看到, 如果滑出了View的范围,那这个PrePressed 状态会被去除,如果不是在可滑动的容器中,则直接设置为 Pressed状态。*/ 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: setPressed(false); removeTapCallback(); removeLongPressCallback(); mInContextButtonPress = false; mHasPerformedLongPress = false; mIgnoreNextUpEvent = false; break; /*如果手指移出了View的范围,则取消 “延迟115ms并设置为Pressed” 这一操作,也就是说如果在115ms 你的手指移动出这一范围,就不算是Pressed。 如果已经是Pressed状态,则进一步把 "等待500ms,并设置为longPressed” 这一操作也取消了,并setPressed(false)。*/ case MotionEvent.ACTION_MOVE: drawableHotspotChanged(x, y); // Be lenient about moving outside of buttons if (!pointInView(x, y, mTouchSlop)) { // Outside button removeTapCallback(); if ((mPrivateFlags & PFLAG_PRESSED) != 0) { // Remove any future long press/tap checks removeLongPressCallback(); setPressed(false); } } break; } return true; } return false; } private final class CheckForTap implements Runnable { public void run() { /*取消mPrivateFlags的PREPRESSED, 然后设置PRESSED标识,刷新背景, 如果View支持长按事件,则再发一个延时消息,检测长按;*/ mPrivateFlags &= ~PREPRESSED; mPrivateFlags |= PRESSED; refreshDrawableState(); if ((mViewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) { postCheckForLongClick(ViewConfiguration.getTapTimeout()); } } } class CheckForLongPress implements Runnable { private int mOriginalWindowAttachCount; public void run() { // 1、如果此时设置了长按的回调,则执行长按时的回调,且如果长按的回调返回true;才把mHasPerformedLongPress置为ture; // 2、否则,如果没有设置长按回调或者长按回调返回的是false;则mHasPerformedLongPress依然是false; if (isPressed() && (mParent != null) && mOriginalWindowAttachCount == mWindowAttachCount) { if (performLongClick()) { mHasPerformedLongPress = true; } } } }
滑动冲突的处理
外部拦截法
让在点击事件都先经过父容器的拦截处理,若父容器需要此事件就拦截,若不需要就不拦截。这个方法需要重写父容器的onInterceptTouchEvent方法,在内部做相应的拦截即可,
伪代码如下:
public boolean onInterceptTouchEvent(MotionEvent event){ boolean intercepted = false; switch(event.getAction()){ case MotionEvent.ACTION_DOWN: intercepted = false; break; case MotionEvent.ACTION_MOVE: if(父容器需要当前事件){ intercepted = false; }else{ intercepted = false; } break; case MotionEvent.ACTION_UP: intercepted = false; break; default: break; } return intercepted;}
这里的ACTION_DOWN必须返回false,否则事件就没法在传递给子元素了,而ACTION_UP在这里意义不大,但考虑到ACTION_UP如果被拦截,那子元素的onClick事件就无法触发,所以也让它返回false。
内部拦截法
内部拦截罚是指父容器不拦截任何事件,所以的事件都传递给子元素,如果子元素选哟此事件就直接消耗掉,否则则由父容器来处理,这种方法需要配合requestDisallowInterceptTouchEvent方法才能正常工作。伪代码如下:
public boolean dispatchTouchEvent(MotionEvent event){ switch(event.getAction()){ case MotionEvent.ACTION_DOWN: parent.requestDisallowInterceptTouchEvent(true); break; case MotionEvent.ACTION_MOVE: if(父容器需要此类点击事件){ parent.requestDisallowIntercepTouchEvent(false); } break; case MotionEvent.ACTION_UP: break; case default: break; } return super.dispatchTouchEvent(event);}
参考资料
《Android开发艺术探索》
onepiece2的博客
林子木的博客
大空ts翼的简书
Rancune的简书
phantomVK的博客
Quinn的github
Kelin的简书
- 事件分发和滑动冲突知识点总结
- Android事件分发和滑动冲突
- Android View的事件分发机制和滑动冲突解决方案
- Android View的事件分发机制和滑动冲突解决方案
- Android 事件分发和 View 的滑动冲突
- Android View的事件分发机制和滑动冲突解决方案
- Android View的事件分发机制和滑动冲突解决
- 【Android API】Android事件分发机制和滑动冲突
- 事件分发:弹性滑动、滑动冲突
- android 事件分发与滑动冲突
- Android事件分发机制与滑动冲突
- android事件分发与滑动冲突
- 事件分发机制与滑动冲突
- 【Android View事件分发机制】滑动冲突
- Android事件分发机制、滑动冲突解决
- 事件分发机制、滑动冲突详细讲解
- android-事件分发:弹性滑动、滑动冲突等
- View的事件体系(下)(事件分发,滑动冲突)
- 装饰器模式
- c++里的线程相关创建
- 单机Redis的安装以及基本操作简介
- Impala 4、Impala JDBC
- poj 1065 Wooden Sticks
- 事件分发和滑动冲突知识点总结
- POJ 1185 炮兵阵地(状压DP)
- Python —— 深拷贝 VS 浅拷贝
- Impala 5、Impala 性能优化
- JAVA环境的配置与安装
- HDU 6070 Dirt Ratio
- HDU-6078 Wavel Sequence(dp+树状数组)
- HDU3342:Legal or Not(拓扑排序)
- I