文章目录
情境(Situation)冲突(Complication)疑问(Question)答案(Answer)剖析论点约法三章点论据人机交互View树类图注释DecorViewWindowCallbackWrapperActivityPhoneWindowViewGroupView事件流论证一张图标准常见错误最佳实践渔方法论利器利进阶参考长歌☞阅读原文
情境(Situation)
1. 专注于移动互联网数年,作为高P的我【鼓掌】竟然对事件分发机制见招拆招,似懂非懂。不专业,没法忍。
2. View树的递归嵌套逻辑让广大一线同行云里雾里,手足无措。
冲突(Complication)
1. 网上好多相关主题的博客,描述信息点非常多(但是ACTION_CANCEL描述很少),看完后不明觉厉。
2. 事件分发主要用于解决自定义炫酷控件以及滑动嵌套引发的冲突问题(程序傻傻分不清是横滑还是竖滑),发现同行各种写法都有,雷无处不在【人在家中坐,锅从天上来】。
我的机会来了
疑问(Question)
1. 有没有体系化剖析套路?
2. 指出常见错误,给出最佳实践?
3. 清晰明了的给出一张图,便于查阅?
4. “鱼”和“渔”可以兼得?
答案(Answer)
剖析
论点
约法三章
1. 限于个人水平,本文只包含单点触控事件(ACTION_DOWN,ACTION_MOVE,ACTION_UP,ACTION_CANCEL)。
2. Window类相关的我不会,肤浅的认为和事件分发关系不大(求大牛点拨),直接跳过。
3. 一家之言,姑妄言之,姑妄听之。
点
1. 事件流一致性保证(Consistency Guarantees):按下开始,中间可能伴随着移动,松开或者取消结束。ACTION_DOWN -> ACTION_MOVE(*) -> ACTION_UP/ACTION_CANCEL。
2. View类的dispatchTouchEvent方法完成事件的消费处理,ViewGroup的dispatchTouchEvent方法完成事件的分发处理。正常情况下不建议重写该方法改变系统事件分发机制。
3. ViewGroup类的onInterceptTouchEvent方法完成事件的拦截处理。事件分发路径上的ViewGroup,在ACTION_DOWN或者不是自己直接消费事件时一定会调用onInterceptTouchEvent方法。
4. View类的onTouchEvent方法完成具体处理事件消费,即触发点击监听(OnClickListener)和长时间点击监听(OnLongClickListener)以及按键状态、焦点相关处理。
1. 如果设置了OnTouchListener,会先调用OnTouchListener,如果该监听onTouch返回true,则不会调用onTouchEvent,直接返回已消费;
2. 如果设置了TouchDelegate ,onTouchEvent中会先调用TouchDelegate,如果该类onTouchEvent返回true,则直接返回已消费;
3. 如果View 可点击,执行onTouchEvent中事件处理,并返回true;
1. ACTION_DOWN:置按键标志位为按下状态,并触发延时(500ms)执行长按点击事件。
2. ACTION_MOVE:如果按键坐标超出该控件区域,则置按键标志位为非按下状态,并且移除ACTION_DOWN触发的延时执行长按点击事件。
3. ACTION_UP:如果按键标志位为按下状态,并且ACTION_DOWN触发的长按点击事件还未执行,则移除长按点击事件,执行点击事件。
4. ACTION_CANCEL:置按键标志位为非按下状态,移除ACTION_DOWN触发的延时执行长按点击事件。
4. 否则不可点击,返回false;
论据
基于Android 8.0 (API Level 28)源码解析
人机交互
赏析
用户的按键行为->手机传感器->ViewRootImpl->DecorView->WindowCallbackWrapper->Activity->PhoneWindow->DecorView->ViewGroup*->View->程序员的代码逻辑->硬件(显示器、扬声器等)响应输出->用户感知
View树
赏析
1. View是由树形结构组织,节点为ViewGroup或者View。ViewGroup可以包含多个子节点,View没有子节点。
2. Android中View树的根节点为DecorView(父View为FrameLayout,属于ViewGroup)。
3. Android中用户可自定义的View子树根节点id为“android:id/content”。
{:.info}
类图
赏析
1. ViewRootImpl是Android层逻辑起始点,用于接收来自系统底层的事件消息。相当于View管理类,本身不是View。(BTW:View绘制流程的三部曲(measure、layout、draw)也由该类触发的。)
2. DecorView是Android View树的根节点,持有window对象。本身能够直接进行真正事件分发能力(继承了父类ViewGroup和View的事件分发处理功能),但是事件分发会直接调用window,间接传递到Activity的事件分发,后续会由Activity回调DecorView的真正事件分发能力。对应图中的环形依赖。
3. Activity是Android中的页面,真正的事件分发由该类的dispatchTouchEvent触发。(Easter Eggs:如果你想让用户操作不了你的界面,蒙一层透明的View是不是有点low,直接重写该方法就可以控制。)
4. ViewGroup负责事件分发和拦截处理。按下事件和后续事件(移动、释放或者取消)处理不相同。
1. 按下事件,先判断是否拦截。
1. 如果不拦截的话,分发事件寻找目标消费子View(逆序遍历子View,递归调用子View的事件分发,判断是否有子View消费。mFirstTouchTarget存储目标消费子View对象)。
1. 如果有子View消费,则目标子View消费事件。
2. 否则自己尝试消费事件。
2. 否则直接自己尝试消费事件。
2. 后续事件
1. 如果按下事件找到了目标消费子View,则判断是否拦截,否则不拦截。
2. 如果有目标消费子View,则根据是否拦截。
1. 如果没有拦截,正常传送后续事件;
2. 如果有拦截,则当前事件转换为取消事件发送给目标消费子View,并且重置目标消费子View为空,接下来的后续事件直接自己尝试消费事件(不管是否消费,后续事件都会接收到&尝试处理事件分发);
3. 否则自己尝试消费事件。(不会调用是否拦截,其实拦截或者不拦截,都是自己消费事件。)
5. View负责事件消费事件处理。
1. 调用mOnTouchListener的onTouch。
1. 如果消费,直接返回true;
2. 否则,继续调用onTouchEvent方法;
1. 如果为启用的(enable),返回可点击(clickable)。
2. 否则,调用mTouchDelegate的onTouchEvent。
1. 如果消费,直接返回true;
2. 否则,
1. 如果可点击(clickable)
1. 进行事件流(ACTION_DOWN,ACTION_MOVE,ACTION_UP,ACTION_CANCEL)处理(包含焦点、按键状态、按键和长时间按键);
2. 返回true。
2. 否则返回false;
注释
DecorView
/*** Decor的意思是:装饰,布置。* View树的根节点。* 事件分发的启点,ViewRootImpl最先调用dispatchPointerEvent(实现在父类View里面)。* 事件调用在DecorView里面形成了一个环。(先通过Window交由Activity分发,Activity再调用DecorView中的真正事件分发方法)*/public class DecorView extends FrameLayout {private PhoneWindow mWindow;@Overridepublic boolean dispatchTouchEvent(MotionEvent ev) {// DecorView直接覆盖ViewGroup的事件分发实现,其实这只是饶了个圈,// 正真的事件分发会由Activity回调到superDispatchTouchEvent(ViewGroup的事件分发处理)。// 调用Window的WindowCallbackWrapper对象继续分发。final Window.Callback cb = mWindow.getCallback();return cb != null && !mWindow.isDestroyed() && mFeatureId < 0? cb.dispatchTouchEvent(ev) : super.dispatchTouchEvent(ev);}public boolean superDispatchTouchEvent(MotionEvent event) {// 调用父类ViewGroup进行事件分发处理。return super.dispatchTouchEvent(event);}}
WindowCallbackWrapper
/*** Wrapper的意思是包装材料。* 实实在在的一个壳,包裹着Activity。*/public class WindowCallbackWrapper implements Window.Callback {final Window.Callback mWrapped;@Overridepublic boolean dispatchTouchEvent(MotionEvent event) {// 交给Callback(具体对象为Activity)接力事件分发。return mWrapped.dispatchTouchEvent(event);}}
Activity
/*** Activity和View不一样,Activity就是一个壳,没有事件分发机制,View树如果没有消费,Activity捡个漏。*/public class Activity implements Window.Callback {private Window mWindow;public boolean dispatchTouchEvent(MotionEvent ev) {if (ev.getAction() == MotionEvent.ACTION_DOWN) {onUserInteraction();}// 交给Window(具体对象为PhoneWindow)接力事件分发。if (getWindow().superDispatchTouchEvent(ev)) {// View树消费掉事件return true;}// 如果View树没有消费事件,Activity消费事件的机会来了。// 启示:如果View树消费事件,在按下事件的后续事件中,如果父ViewGroup进行拦截,// 虽然后续返回的消费状态对整个事件流没有影响,但是会对Activity有影响(View数不消费,Activity有机会消费)。return onTouchEvent(ev);}public boolean onTouchEvent(MotionEvent event) {// 事件消费处理,系统默认基本不干啥if (mWindow.shouldCloseOnTouch(this, event)) {finish();return true;}return false;}}
PhoneWindow
/*** PhoneWindow也是一个壳,将事件转回给DecorView分发处理。*/public class PhoneWindow extends Window {private DecorView mDecor;@Overridepublic boolean superDispatchTouchEvent(MotionEvent event) {// 交给DecorView接力事件分发(自此,环形结束,开始ViewGroup和View中事件分发和消费闪亮登场)。return mDecor.superDispatchTouchEvent(event);}}
ViewGroup
/*** ViewGroup,View容器的意思。* dispatchTouchEvent完成时间分发逻辑。* onInterceptTouchEvent:为事件拦截接口,父控件可以主动截留事件自己消费,否则只能等子Viwe树都不消费才能捡漏。【有控制权就是爸爸】*/public abstract class ViewGroup extends View implements ViewParent {@Overridepublic 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;// 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();}// Check for interception.// 是否拦截判断final boolean intercepted;// 拦截条件1,要么是按下事件,要么自己不直接消费事件。if (actionMasked == MotionEvent.ACTION_DOWN|| mFirstTouchTarget != null) {// 拦截条件2,允许拦截开关打开。//(默认状态是打开的,其他View可以调用requestDisallowInterceptTouchEvent进行控制,// 多为子View掉父View,滑动冲突外部拦截法就是靠调用这个接口控制父View拦截)。【爸爸的权利也不是绝对的】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);}// 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;// 递归查找目标消费子View条件1:事件没有被取消,也没有被拦截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;// 递归查找目标消费子View条件2:事件必须是按下事件。【多点触控的不讨论,关键是我也不会】if (actionMasked == MotionEvent.ACTION_DOWN|| (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {final int actionIndex = ev.getActionIndex(); // always 0 for downfinal 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) {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后绘制,盖在上面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;}// 消费事件View资格1:事件的坐标在View区域内。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资格2:自己或者子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 indexfor (int j = 0; j < childrenCount; j++) {if (children[childIndex] == mChildren[j]) {mLastTouchDownIndex = j;break;}}} else {mLastTouchDownIndex = childIndex;}mLastTouchDownX = ev.getX();mLastTouchDownY = ev.getY();// 标记当前View为目标消费子View,消费路径上都是父View标记直接子View(下发分发不用再找了)。不存在跨级。// 我也没有搞明白为啥整一个链式结构存目标消费子View。我没有遇到多余1个目标消费子View的情况。【看逻辑,如果有子View消费,则跳出循环,不会继续分发】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.// 没有目标子View消费,自己消费。(要么自己拦截了,要么子View树没有消费)if (mFirstTouchTarget == null) {// No touch targets so treat this as an ordinary 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;// 如果是按下事件,则已消费,直接置消费状态为trueif (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {handled = true;} else {final boolean cancelChild = resetCancelNextUpFlag(target.child)|| intercepted;// 非按下事件,要么持续正常处理消费,要么被拦截(事件转成取消事件,还是继续分发给目标View)if (dispatchTransformedTouchEvent(ev, cancelChild,target.child, target.pointerIdBits)) {handled = true;}if (cancelChild) {// 如果是取消事件(要么被拦截,要么传过来的就是取消事件),则清空目标消费子View。if (predecessor == null) {mFirstTouchTarget = next;} else {predecessor.next = next;}target.recycle();target = next;continue;}}predecessor = target;target = next;}}// 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;}// 拦截处理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;}// 事件分发处理封装部分逻辑的子方法,实现取消事件转换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);if (child == null) {handled = super.dispatchTouchEvent(event);} else {handled = child.dispatchTouchEvent(event);}event.setAction(oldAction);return handled;}// Calculate the number of pointers to deliver.final int oldPointerIdBits = event.getPointerIdBits();final int newPointerIdBits = oldPointerIdBits & desiredPointerIdBits;// If for some reason we ended up in an inconsistent state where it looks like we// might produce a motion event with no pointers in it, then drop the event.if (newPointerIdBits == 0) {return false;}// If the number of pointers is the same and we don't need to perform any fancy// irreversible transformations, then we can reuse the motion event for this// dispatch as long as we are careful to revert any changes we make.// Otherwise we need to make a copy.final MotionEvent transformedEvent;if (newPointerIdBits == oldPointerIdBits) {if (child == null || child.hasIdentityMatrix()) {if (child == null) {handled = super.dispatchTouchEvent(event);} else {final float offsetX = mScrollX - child.mLeft;final float offsetY = mScrollY - child.mTop;event.offsetLocation(offsetX, offsetY);handled = child.dispatchTouchEvent(event);event.offsetLocation(-offsetX, -offsetY);}return handled;}transformedEvent = MotionEvent.obtain(event);} else {transformedEvent = event.split(newPointerIdBits);}// Perform any necessary transformations and dispatch.if (child == null) {handled = super.dispatchTouchEvent(transformedEvent);} else {final float offsetX = mScrollX - child.mLeft;final float offsetY = mScrollY - child.mTop;transformedEvent.offsetLocation(offsetX, offsetY);if (! child.hasIdentityMatrix()) {transformedEvent.transform(child.getInverseMatrix());}handled = child.dispatchTouchEvent(transformedEvent);}// Done.transformedEvent.recycle();return handled;}}
View
public class View {public final boolean dispatchPointerEvent(MotionEvent event) {// View树接收事件的起点,由ViewRootImpl调用DecorView的该方法开始,// 接下来会调用到DecorView的dispatchTouchEvent方法。if (event.isTouchEvent()) {return dispatchTouchEvent(event);} else {return dispatchGenericMotionEvent(event);}}// 事件消费处理public boolean dispatchTouchEvent(MotionEvent event) {// If the event should be handled by accessibility focus first.if (event.isTargetAccessibilityFocus()) {// We don't have focus or no virtual descendant has it, do not handle the event.if (!isAccessibilityFocusedViewOrHost()) {return false;}// We have focus and got the event, then use normal event dispatch.event.setTargetAccessibilityFocus(false);}boolean result = false;if (mInputEventConsistencyVerifier != null) {mInputEventConsistencyVerifier.onTouchEvent(event, 0);}final int actionMasked = event.getActionMasked();if (actionMasked == MotionEvent.ACTION_DOWN) {// Defensive cleanup for new gesturestopNestedScroll();}if (onFilterTouchEventForSecurity(event)) {if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {result = true;}//noinspection SimplifiableIfStatementListenerInfo li = mListenerInfo;// 优先mOnTouchListener消费处理,如果消费,直接返回已消费if (li != null && li.mOnTouchListener != null&& (mViewFlags & ENABLED_MASK) == ENABLED&& li.mOnTouchListener.onTouch(this, event)) {result = true;}// 自己处理消费,封装在onTouchEvent内if (!result && onTouchEvent(event)) {result = true;}}if (!result && mInputEventConsistencyVerifier != null) {mInputEventConsistencyVerifier.onUnhandledEvent(event, 0);}// Clean up after nested scrolls if this is the end of a gesture;// also cancel it if we tried an ACTION_DOWN but we didn't want the rest// of the gesture.if (actionMasked == MotionEvent.ACTION_UP ||actionMasked == MotionEvent.ACTION_CANCEL ||(actionMasked == MotionEvent.ACTION_DOWN && !result)) {stopNestedScroll();}return result;}// 针对完整事件流(ACTION\_DOWN -> ACTION\_MOVE(*) -> ACTION\_UP/ACTION\_CANCEL)完成按键监听、长时间按键监听、焦点以及按键状态处理。public boolean onTouchEvent(MotionEvent event) {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;}// 有效触摸代理消费事件,可用于扩大点击热点控制。如果消费,直接返回已消费。if (mTouchDelegate != null) {if (mTouchDelegate.onTouchEvent(event)) {return 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// ACTION_DOWN触发的长按点击事件还未执行,则移除长按点击事件,removeLongPressCallback();// Only perform take click actions if we were in the pressed stateif (!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();}}}if (mUnsetPressedState == null) {mUnsetPressedState = new UnsetPressedState();}if (prepressed) {postDelayed(mUnsetPressedState,ViewConfiguration.getPressedStateDuration());} else if (!post(mUnsetPressedState)) {// If the post failed, unpress right nowmUnsetPressedState.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.// 置按键标志位为按下状态,并触发延时(500ms)执行长按点击事件。// 以下为滚动和非滚动下的处理。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 awaysetPressed(true, x, y);checkForLongClick(0, x, y);}break;case MotionEvent.ACTION_CANCEL:if (clickable) {setPressed(false);}// 置按键标志位为非按下状态,移除ACTION_DOWN触发的延时执行长按点击事件。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// 检查按键坐标是否超出该View区域。if (!pointInView(x, y, mTouchSlop)) {// Outside button// Remove any future long press/tap checks// 置按键标志位为非按下状态,并且移除ACTION\_DOWN触发的延时执行长按点击事件。removeTapCallback();removeLongPressCallback();if ((mPrivateFlags & PFLAG_PRESSED) != 0) {setPressed(false);}mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;}break;}return true;}return false;}}
事件流
DemoParentInterceptTouchEventActivity页面git仓库
使用MECE(Mutually Exclusive Collectively Exhaustive,相互独立,完全穷尽)法则
启示
1. ACTION_DOWN执行事件分发查找(遍历子View,递归分发查找,如果子View未消费,则回退到自己消费,依次向上回溯,找到目标消费View为止)找到目标消费子View。后续事件不再需要查找,直接发送给目标消费子View,如果没有,则自己消费。
2. 事件已消费路径上(终点为目标消费View),如果有父控件拦截事件,则第一次拦截后,会将当前事件转为ACTION_CANCEL传递给目标消费子View,后续事件则直接自己处理消费,不论是否消费,均能收到后续事件流
论证
1. 从事件流可证明事件一致性保证(Consistency Guarantees):
1. ViewGroup在ACTION_DOWN的事件分发返回false(不消费事件),则不再会收到后续事件(ACTION_MOVE、ACTION_UP/ACTION_CANCEL)。
2. ViewGroup在ACTION_DOWN的事件分发返回true(消费事件),则会收到后续事件(ACTION_MOVE、ACTION_UP/ACTION_CANCEL),如果ViewGroup拦截后续事件,则第一次拦截会将事件转为ACTION_CANCEL传递给目标消费子View(终止子View接收后续事件),接下来的后续事件自己消费。
3. ViewGroup在非ACTION_DOWN的事件分发返回消费状态对整体事件流没有影响。
2. 从注释可证明:
View.dispatchTouchEvent方法完成事件的消费处理;
ViewGroup.dispatchTouchEvent方法完成事件的分发处理;
ViewGroup.onInterceptTouchEvent方法完成事件的拦截处理;
事件分发路径上的ViewGroup,在ACTION_DOWN或者不是自己直接消费事件时一定会调用onInterceptTouchEvent方法。
以及View类的onTouchEvent方法完成具体处理事件消费。
一张图
赏析
1. ACTION_DOWN会触发查找目标消费View,优先子View尝试消费,如果子View仍然没有消费,则依次回溯到父控件尝试消费(直至DecorView,然后Activity尝试消费),如果找到了,则回溯返回true。
2. ACTION_DOWN后续事件执行的前提是事件分发路径的终点就是目标消费View,目标消费View的父控件均会调用到事件拦截(让父控件有机会拦截下来,改变事件流),如果目标消费View的父控件拦截,拦截时的事件会转换为ACTION_CANCEL继续按原路径分发,后续的事件则不再分发给目标消费View,而是拦截的父控件自己消费。
3. 非ACTION_DOWN返回的消费状态对事件流没有影响,如果未消费,会回调给Activity处理。
标准
常见错误
1. 不知道onInterceptTouchEvent和onTouchEvent什么时候会调用,但是知道dispatchTouchEvent每次都会调用,就把逻辑直接写在dispatchTouchEvent的重写方法里面。
问题:不满足事件流一致性,存在目标消费View没有接收到ACTION_UP/ACTION_CANCEL就结束了,导致焦点、按键状态或者按键事件不符合预期。
2. 发现onInterceptTouchEvent经常调用到,逻辑写在onInterceptTouchEvent里面。
问题:onInterceptTouchEvent在View自己消费情况下或者拦截之后的事件流不再会调用到,会把坑隐藏得更深【不好复现的Bug才是最难解决的Bug】。
3. 鸟枪法,dispatchTouchEvent、onInterceptTouchEvent、onTouchEvent均会调用到逻辑。
问题:路子太野。。。
4. 觉得自己很牛X,逻辑分散在dispatchTouchEvent、onInterceptTouchEvent、onTouchEvent里面。
问题:可读性差,逻辑混乱。
5. 事件消息只处理了ACTION_DOWN、ACTION_MOVE、ACTION_UP,没有对ACTION_CANCEL或者其他多点触控事件容错处理。
问题:总会出现不常见的问题。
最佳实践
1. 明确事件流调用顺序以及拦截后的事件流。
2. dispatchTouchEvent:正常情况下不建议重写dispatchTouchEvent方法改变系统事件分发机制,可以看到,Google就没有几个类重新该方法。最多记下坐标点,但千万调用super. dispatchTouchEvent保证系统事件分发正常调用。
3. onInterceptTouchEvent:只处理拦截逻辑,在合适事件将事件流导到onTouchEvent。
4. onTouchEvent:真正处理逻辑。
5. 除常见事件处理外,一定要上剩余事件容错处理。
渔
方法论
1. MECE法则和金字塔原理
2. SCQA 架构如何理解?
利器
1. AS源码英文翻译,参考AS翻译插件Translation
2. Android源码调试
1. Android模拟器GenyMotion
2. GenyMotion创建和App的build.gradle中targetSdkVersion相同API Level模拟器即可Debug对应上源码。进阶参考如何调试Android Framework?
3. Android Studio你不知道的调试技巧
3. 关键日志输出,使用静态代理,进阶参考Android插件化原理解析——Hook机制之动态代理
4. 绘图工具
1. ProcessOn
2. Edraw
5. 个人主页
1. 将纯文本转化为静态网站和博客
2. TeXt主题模板
3. 怎样引导新手使用 Markdown?
利
1. 随心所欲控制事件流【大权在手,天下我有】
2. 事件分发不再是个事,怕个球
3. 各种酷炫动画和自定义控件燥起来
4. 再也不用担心面试中尬聊事件分发
5. 借鉴上述不成熟的“渔”去爱干嘛干嘛
进阶
1. 滚动控件和按键冲突处理,界面布局滚动
2. 滑动冲突
1. NestedScrolling机制
2. Android NestedScrolling机制完全解析 带你玩转嵌套滑动
3. 外部拦截法&内部拦截法
3. 手势(GestureDecetor)
参考
1. 图解 Android 事件分发机制
2. Android 响应用户屏幕手势操作
3. Android MotionEvent详解
4. android触控,先了解MotionEvent(一)
5. Android多点触控之——MotionEvent(触控事件)
6. 图解Android事件传递之View篇
7. 图解Android事件传递之ViewGroup篇
长歌
念奴娇·天丁震怒
完颜亮(金代)
天丁震怒,掀翻银海,散乱珠箔(bó)。
六出奇花飞滚滚,平填了山中丘壑。(六出:雪花六角,因用为雪花的别名。)
皓虎颠狂,素麟猖獗(chāng jué),掣(chè, 拉)断珍珠索。(皓虎:白色的老虎。素麟:白色的麒麟。)
玉龙酣战,鳞甲满天飘落。
谁念万里关山,征夫僵立,缟(gǎo)带沾旗脚。(僵立:因寒冷而冻得僵硬直立。缟带:白色的衣带。)
色映戈矛,光摇剑戟(jǐ ),杀气横戎幕。(戎幕:行军作战时的营帐。)
貔(pí)虎豪雄,偏裨(pí)英勇,共与谈兵略。(裨:副,偏,小。)
须拼一醉,看取碧空寥廓(liáo kuò)。