Android事件分发机制浅析
Posted
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Android事件分发机制浅析相关的知识,希望对你有一定的参考价值。
本文来自网易云社区
作者:孙有军
我们只看最重要的部分
1: 事件为ACTION_DOWN时,执行了cancelAndClearTouchTargets函数,该函数主要清除上一次点击传递的路径,之后执行了resetTouchState,重置了touch状态,其中执行了 mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;就是拦截状态为false,这个与requestDisallowInterceptTouchEvent函数相关。
2: 获取intercepted的值,首先判断了disallowIntercept状态,是否拦截子控件的事件执行。从代码可以看到当disallowIntercept为false时,该状态主要取决于onInterceptTouchEvent函数的返回值,这就是前面我们拦截的函数,如果为true,这时intercepted为true标识拦截。
3: 接着判断了!canceled && !intercepted的值,canceled这里为false,如果intercepted为false,则会进入判断条件,这里假设不拦截,进入后继续判断如果是ACTION_DOWN事件,则会继续进入判断,遍历所有子控件,isTransformedTouchPointInView会判断当前点击区域是否在控件内,如果不在则遍历下一个,之后调用dispatchTransformedTouchEvent函数。最后在调用addTouchTarget函数,将当前选中的控件,挂载到当前点击目标链表。alreadyDispatchedToNewTouchTarget赋值为true。
接着判断mFirstTouchTarget是否为空,经过上一步的addTouchTarget的执行,这里mFirstTouchTarget不为空。第一个事件alreadyDispatchedToNewTouchTarget为true,且target == newTouchTarget,因此handled值为true,如果是后续的事件,则会进入dispatchTransformedTouchEvent中。
我们接着看看第三部中的dispatchTransformedTouchEvent函数:
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel, View child, int desiredPointerIdBits) { final boolean handled; // Calculate the number of pointers to deliver. final int oldPointerIdBits = event.getPointerIdBits(); final int newPointerIdBits = oldPointerIdBits & desiredPointerIdBits; ....... 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); } ....... // Done. transformedEvent.recycle(); return handled; }
这里会进入到第10行,且传递过来的child不为空,因此会继续执行child.dispatchTouchEvent,这里继续执行ViewGroup的dispatchTouchEvent,一直递归执行,直到真正接受点击的控件,到最后child会为空,这里要么是一个View控件,要么是未包含任何子控件的ViewGroup,这时这里会执行View的dispatchTouchEvent。
从上述执行逻辑可以直到,先从DecorView一直递归到Layout,最后再到TextView,这里我们去看看View的dispatchTouchEvent:
public boolean dispatchTouchEvent(MotionEvent event) { final int actionMasked = event.getActionMasked(); if (actionMasked == MotionEvent.ACTION_DOWN) { // Defensive cleanup for new gesture stopNestedScroll(); } if (onFilterTouchEventForSecurity(event)) { //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; } } 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; }
首先会停止掉嵌套滑动,之后先判断了ListenerInfo不为空,这里只要是设置了onTouch,onKey,onHover,onDrag等等中的任何一个这里就不为空,具体可以去看看ListenerInfo包含的listenerInfo类型。其次判断了mOnTouchListener不为空,只要设置了onTouchListener这里就不为空,再之后判断了该控件是否是enabled,一般都会enabled,可以代码设置为false,再之后调用了mOnTouchListener的onTouch事件,这里就是外面传进来的onTouchListener,从这里可以看到无论onTouch返回任何值,onTouch事件都会执行,但是如果返回为true,则会导致result为true,!result && onTouchEvent(event)因为短路,不会执行到onTouchEvent事件。
小结
1:onTouch返回为true导致onTouchEvent不能执行 2:如果enable为false,因为短路onTouch不会执行
到此还没有看到任何onClick事件的执行,我们继续去看看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(); 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) { // 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)) { 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; 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); } break; case MotionEvent.ACTION_CANCEL: setPressed(false); removeTapCallback(); removeLongPressCallback(); mInContextButtonPress = false; mHasPerformedLongPress = false; mIgnoreNextUpEvent = false; break; 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; }
我们首先看ACTION_DOWN事件,这里主要看checkForLongClick,CheckForTap中也调用了该函数,这里就是添加一个长按事件,如果达到长按标准且长按listener不为空,则执行长按事件,接着我们看ACTION_UP,这里看到如果不是长按事件,则调用了performClick,performClick里面执行了onClick事件。
小结
1:onClick事件与onLongClick事件是在onTouchEvent中执行的 2:如果执行了长按事件则onClick不执行 3:就api 23代码,长按的时间间隔为500毫秒
上面解析了intercepted为false的情况,那intercepted为true,它到底是怎么拦截的?
如果intercepted为true,则!canceled && !intercepted为false,不能进入该判断,mFirstTouchTarget为空,会继续执行如下分支:
if (mFirstTouchTarget == null) { // No touch targets so treat this as an ordinary view. handled = dispatchTransformedTouchEvent(ev, canceled, null, TouchTarget.ALL_POINTER_IDS); }
这里第三参数传递的child为null,因此就会执行该控件onTouch与onTouchEvent函数,不会继续递归传递,因此也就拦截了子控件的执行。
总结
事件接收先从父控件到子控件,如果父控件onInterceptTouchEvent为true,则表示拦截事件。
dispatchTouchEvent的ACTION_DOWN事件中,会清除上一次的点击目标列表,且重置disallowIntercept状态为false,表示拦截,但是真正的拦截状态还是靠onInterceptTouchEvent函数的返回值决定。
如果为复杂的自定义控件,有滑动事件处理,还需要重写onInterceptTouchEvent。
如果onLongClick执行,api 23 默认时间为500毫秒,则onClick不执行。
如果onTouch事件返回为true,则会拦截onTouchEvent事件,onClick,onLongClick事件均不在执行。
网易云免费体验馆,0成本体验20+款云产品!
更多网易研发、产品、运营经验分享请访问网易云社区
相关文章:
【推荐】 vue生态圈
以上是关于Android事件分发机制浅析的主要内容,如果未能解决你的问题,请参考以下文章
Android 事件分发事件分发源码分析 ( Activity 中各层级的事件传递 | Activity -> PhoneWindow -> DecorView -> ViewGroup )(代码片段