Android事件分发机制浅析

业无高卑志当坚,男儿有求安得闲?这篇文章主要讲述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事件。
【Android事件分发机制浅析】

小结
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函数,不会继续递归传递,因此也就拦截了子控件的执行。

总结

  1. 事件接收先从父控件到子控件,如果父控件onInterceptTouchEvent为true,则表示拦截事件。
  2. dispatchTouchEvent的ACTION_DOWN事件中,会清除上一次的点击目标列表,且重置disallowIntercept状态为false,表示拦截,但是真正的拦截状态还是靠onInterceptTouchEvent函数的返回值决定。
  3. 如果为复杂的自定义控件,有滑动事件处理,还需要重写onInterceptTouchEvent。
  4. 如果onLongClick执行,api 23 默认时间为500毫秒,则onClick不执行。
  5. 如果onTouch事件返回为true,则会拦截onTouchEvent事件,onClick,onLongClick事件均不在执行。


网易云免费体验馆,0成本体验20+款云产品! 

更多网易研发、产品、运营经验分享请访问网易云社区


相关文章:
【推荐】  vue生态圈


    推荐阅读