說一說android的事件分發(fā)機制吧

美女

宇宙的演化規(guī)則,讓時間有了唯一的方向,從過去到未來,從美麗到衰老,而記憶卻卻不同......

思路

1. 原理囊括: 整體上把握觸摸機制的設(shè)計目標(biāo)
2. 重要方法分析: 分析他的實現(xiàn)和細(xì)節(jié),即設(shè)計的實現(xiàn)過程
3. 局限: 雖然觸摸事件機制強大,但是并不完美,而是有一定局限的。

基礎(chǔ)原理介紹

  1. 事件先從ViewPostImeInputStage的processPointerEvent開始發(fā)送事件, 第一步是發(fā)送到DecorView的dispatchPointerEvent,然后便是 DecorView -> Activity -> PhoneWindow -> DecorView這么一個過程喲。

  2. 事件處理的一般過程:

    • 一次事件是指down, move...., up這一系列小事件組成的完整事件。

    • 事件由down開始,從控件樹自上而下找到對應(yīng)能消耗down事件的view, 然后回饋到系統(tǒng)底層, 后續(xù)的move, up 事件則會來到他的身上,讓他來處理。如果最底層的view或者中間攔截的View都沒有消耗down事件,那么后續(xù)的move, up事件是不會來到他們身上的。

    • 事件是否被認(rèn)為消耗,就看他的down事件是否被吃掉 (return true),其他的move, up有沒有被吃不關(guān)心。意思是只要down返回了true, 中間不發(fā)生攔截的話,后面的move, up就算返回了false, 依然還是會繼續(xù)來到消耗的目標(biāo)控件上來。

    • 如果發(fā)生了攔截, 事件的攔截策略:

      • 傳遞途中攔截了down, 被攔截的子view將收不到任何事件; 攔截者自己不消耗,那么攔截對象后面也不會有move,up事件,要消耗才會有后續(xù)事件呢. 還有一點值得注意,如果在父容器中攔截了down事件,子view申請父容器不要攔截,是不會生效,因為這時候子容器申請的不要攔截策略還沒有被系統(tǒng)讀取到,一般不要在down中直接攔截!

      • 攔截了move, 子view是不會接收到move事件的,當(dāng)前攔截對象即使不消耗(false)也沒關(guān)系,后面的事件也會到他身上來的。

      • 攔截了up, 子View是不會接收up事件的,這時候被攔截子view的點擊事件就沒法生效的哦!

  • 如果事件找到了目標(biāo),且沒有發(fā)生攔截,當(dāng)次down-move-up事件一般只能被該目標(biāo)view一個人使用。如果找到了目標(biāo),但是move事件被前面的容器攔截了,那么move事件是沒法再分發(fā)到該目標(biāo)控件中(當(dāng)次手勢)。所以看來,如果想先讓外面的容器view滾動一下,然后里面的view再去滾動是沒法做到了,這也就是觸摸事件的局限性之所在了,他的事件消費線路是從里到外,沒法再由外到里的哦,因此依靠事件機制, 想讓多個view一起滾動只能先里面滾動,然后外面再去滾動。

源碼分析

上面的原理介紹,均來自于源碼解讀和日志,沒有源碼支撐的一堆羅嗦說出來誰信呀. 源代碼版本有點舊,不過簡單直白~

1. ViewGroup.dispatchTouchEvent:
public boolean dispatchTouchEvent(MotionEvent ev) {
        if (!onFilterTouchEventForSecurity(ev)) {
            return false;
        }

        final int action = ev.getAction();
    //觸摸點在VeiwGroup控件中的x,y位置
        final float xf = ev.getX();
        final float yf = ev.getY();
    //計算viewGroup本身的scroll, 將scroll數(shù)值累計到觸摸點上,
    //后面計算點是否在控件本身上的時候,當(dāng)我們viewGroup滾動的時候,子控件的可點擊位置要跟隨著滾動的內(nèi)容去變化的,而比較是否在控件內(nèi)部是與布局邊界相比較的,而這邊界個又是不變的,因此我們必須要將滾動的數(shù)值給補償回來。
        final float scrolledXFloat = xf + mScrollX;
        final float scrolledYFloat = yf + mScrollY;
        final Rect frame = mTempRect;

        //檢查子view是否請求不要攔截的標(biāo)記。
        boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
        //當(dāng)次下發(fā)的是down事件, 做的邏輯處理
        if (action == MotionEvent.ACTION_DOWN) {
            //清除前面發(fā)生的事件序列的記錄消耗目標(biāo)的target,
            if (mMotionTarget != null) {
                // this is weird, we got a pen down, but we thought it was
                // already down!
                // XXX: We should probably send an ACTION_UP to the current
                // target.
                mMotionTarget = null;
            }
          //在down事件中檢查down是否當(dāng)前ViewGroup發(fā)生攔截
            if (disallowIntercept || !onInterceptTouchEvent(ev)) {
                // reset this event's action (just to protect ourselves)
                ev.setAction(MotionEvent.ACTION_DOWN);
                // We know we want to dispatch the event down, find a child
                // who can handle it, start with the front-most child.
                final int scrolledXInt = (int) scrolledXFloat;
                final int scrolledYInt = (int) scrolledYFloat;
                final View[] children = mChildren;
                final int count = mChildrenCount;
            //遍歷所有的子view, 目的是為了向下傳遞down事件,直到有人吃掉了。或者尋到view的末尾節(jié)點
                for (int i = count - 1; i >= 0; i--) {
                    final View child = children[i];
                    //只有view是visible或者正在執(zhí)行動畫。才會去檢測
                    if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE
                            || child.getAnimation() != null) {
                        //設(shè)置child的邊界位置到frame中
                        child.getHitRect(frame);
                        //這里會判斷前面計算的觸摸點是否在某個child內(nèi)部。
                        if (frame.contains(scrolledXInt, scrolledYInt)) {
                            // offset the event to the view's coordinate system
                            final float xc = scrolledXFloat - child.mLeft;
                            final float yc = scrolledYFloat - child.mTop;
                            ev.setLocation(xc, yc);
                            child.mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;
                            //如果在某個child內(nèi)部就向下分發(fā),這里很重要,view的層級一般都有很多的
                            //這里其實會發(fā)生遞歸調(diào)用。當(dāng)一級ViewGroup往下分發(fā)到二級viewgroup的時候
                            //同樣會卡在這里往下調(diào)用到第三級view,直到找到最末尾的view,判斷它是否消耗
                            //然后一層層在這里返回。
                            if (child.dispatchTouchEvent(ev))  {//遞歸
                                // 當(dāng)down事件找到了處理目標(biāo)
                                mMotionTarget = child;
                                return true;
                            }
                            // The event didn't get handled, try the next view.
                            // Don't reset the event's location, it's not
                            // necessary here.
                        }
                    }
                }
            }
        }

        //是否是up, 或者是cancel事件;
        boolean isUpOrCancel = (action == MotionEvent.ACTION_UP) ||
                (action == MotionEvent.ACTION_CANCEL);
        //如果是up, 會清除前面的禁止攔截標(biāo)記
        if (isUpOrCancel) {
            // Note, we've already copied the previous state to our local
            // variable, so this takes effect on the next event
            mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
        }

        
        final View target = mMotionTarget;
    //1. 假如前面的down事件沒有找到目標(biāo),我就自己來處理了,即調(diào)用View.dispatchTouchEvent, 這個方法本質(zhì)是就是調(diào)用View.onTouchEvent.
    //2. 或者當(dāng)前viewGroup攔截了后面的事件,那么我就自己來處理啦。
        if (target == null) {
            // We don't have a target, this means we're handling the
            // event as a regular view.
            ev.setLocation(xf, yf);
            if ((mPrivateFlags & CANCEL_NEXT_UP_EVENT) != 0) {
                ev.setAction(MotionEvent.ACTION_CANCEL);
                mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;
            }
            //當(dāng)前容器自己來消耗啦。
            return super.dispatchTouchEvent(ev);
        }

    //走到這里來,首先肯定是move,up事件。檢測是否發(fā)生了攔截
        // if have a target, see if we're allowed to and want to intercept its
        // events
        if (!disallowIntercept && onInterceptTouchEvent(ev)) {//如果move, up發(fā)生了攔截
            final float xc = scrolledXFloat - (float) target.mLeft;
            final float yc = scrolledYFloat - (float) target.mTop;
            mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;
            ev.setAction(MotionEvent.ACTION_CANCEL);
            ev.setLocation(xc, yc);
            //將cancel發(fā)給被攔截的子view, 發(fā)個告示意思一下,后面大爺來處理來了。
            if (!target.dispatchTouchEvent(ev)) {
                // target didn't handle ACTION_CANCEL. not much we can do
                // but they should have.
            }
            // clear the target
            //清除本身記住的子view.但是他作為target記錄在他的父容器中沒有被清除,下次事件就還會
            //發(fā)送到他自己身上。
            mMotionTarget = null;
            // Don't dispatch this event to our own view, because we already
            // saw it when intercepting; we just want to give the following
            // event to the normal onTouchEvent().
            return true;
        }

        if (isUpOrCancel) {
            mMotionTarget = null;
        }

        // finally offset the event to the target's coordinate system and
        // dispatch the event.
        final float xc = scrolledXFloat - (float) target.mLeft;
        final float yc = scrolledYFloat - (float) target.mTop;
        ev.setLocation(xc, yc);

        if ((target.mPrivateFlags & CANCEL_NEXT_UP_EVENT) != 0) {
            ev.setAction(MotionEvent.ACTION_CANCEL);
            target.mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;
            mMotionTarget = null;
        }
        //這里是通常的流程,即沒有發(fā)生攔截,又有target.就往他身上發(fā)事件啦。也是遞歸地往下發(fā),因為target是一層層的記錄的,找到最終的target一般都是view的子類,然后調(diào)用他的dispatchTouchEvent, onTouchEvent.
        return target.dispatchTouchEvent(ev);
    }

2. 舉個例子吧

前面的核心源碼其實有很多的遞歸調(diào)用,理解起來可能有些繞。用例子可能比較好懂些呢。

  • 布局一:LinearLayout -> FrameLayout ->TextView; 布局二:LinearLayout -> FrameLayout ->Button。

  • 布局一:

    • 當(dāng)down事件下發(fā)時候,LinearLayout會在這里找到對應(yīng)的子child-frameLayout, 然后調(diào)用他的dispatchTouchEvent。

       if (child.dispatchTouchEvent(ev))  {//遞歸處
           // 當(dāng)down事件找到了處理目標(biāo)
           mMotionTarget = child;
           return true;
       }
      

      FrameLayout.dispatchTouchEvent, 走的還是ViewGroup的dispatchTouchEvent,即遞歸執(zhí)行該方法,當(dāng)又走到這個判斷的時候,就會調(diào)用TextView.dispatchTouchEvent, 這會執(zhí)行View.dispatchTouchEvent, 這個方法主要是調(diào)用View.onTouchEvent.由于TextView的onTouchEvent默認(rèn)返回false. 所以在遞歸處返回了false, 即FrameLayout.dispatchTouchEvent在這里得到了false返回, 然后不走if, mMotionTarget=null, 繼續(xù)往下執(zhí)行:

       if (target == null) {//沒有找到消耗的目標(biāo)
                  // We don't have a target, this means we're handling the
                  // event as a regular view.
           ev.setLocation(xf, yf);
           if ((mPrivateFlags & CANCEL_NEXT_UP_EVENT) != 0) {
               ev.setAction(MotionEvent.ACTION_CANCEL);
               mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;
           }
           //當(dāng)前容器自己來消耗啦。
           return super.dispatchTouchEvent(ev);
       }
      

      在下面找不到消耗對象后,調(diào)用super.dispatchTouchEvent(ev),也就是viewGroup本身的onTouchEvent來處理了,也就是常說的如果子控件不消耗就自己來消耗這個意思。然后根據(jù)他的onTouchEvent結(jié)果來向上反饋, 這就在LinearLayout的if (child.dispatchTouchEvent(ev))遞歸處得到了返回,走Framelayout同樣的邏輯,F(xiàn)ramelayout和LinearLayout他們的onTouchEvent一般都是返回false.所以當(dāng)次down事件在這個布局層級中是沒有找到消費目標(biāo)的,后面的move, up事件是不會來到這個布局結(jié)構(gòu)中的哦,這個在View體系源碼中好像看不出來為什么不下來了,是從日志得出這個結(jié)論的哦。

      不知道我有沒有說清楚啊......

  • 布局二:

    • 當(dāng)down事件下發(fā)時候,流程和布局一 是一樣的,只是在button處的onTouchEvent返回了true, 然后在FrameLayout.dispatchTouchEvent他的內(nèi)部遞歸處返回了true, 所以進(jìn)入了if判斷,存儲他的目標(biāo)對象target=button, 然后立即返回true, FrameLayout.dispatchTouchEvent返回了true,回到LinearLayout.dispatchTouchEvent處,繼續(xù)記錄他的target(fm), 然后繼續(xù)網(wǎng)上返回,也就是層層遞歸返回啦。down就這樣結(jié)束了他短暫的一生......

       if (child.dispatchTouchEvent(ev))  {//遞歸
           // 當(dāng)down事件找到了處理目標(biāo)
           mMotionTarget = child;
           //viewGroup立即返回true.
           return true;
       }
      
    • 當(dāng)move, up事件下來的時候,會跨過前面所有的地方,直奔最后一行:

      //這里是通常的流程,即沒有發(fā)生攔截,又有target.就往他身上發(fā)事件啦。也是遞歸地往下發(fā),因為target是一層層的記錄的,找到最終的target
      return target.dispatchTouchEvent(ev);
      

      在這里先調(diào)用FrameLayout.dispatchTouchEvent,  然后FrameLayout又走一樣的流程調(diào)Button.dispatchTouchEvent。因為LinearLayout中的target是frameLayout, frameLayout的target是button, 這樣直奔Button的onTouchEvent.也就是常說的誰消耗了down事件,后續(xù)move,up事件都會來到誰身上

  • 假如前面出現(xiàn)了攔截,比如布局二中FrameLayout的在move這里搞了一次攔截,那么情況可能就和上面有不一樣了哦,我們看看code:

      if (!disallowIntercept && onInterceptTouchEvent(ev)) {//如果發(fā)生了攔截onInterceptTouchEvent返回true.
          final float xc = scrolledXFloat - (float) target.mLeft;
          final float yc = scrolledYFloat - (float) target.mTop;
          mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;
          ev.setAction(MotionEvent.ACTION_CANCEL);
          ev.setLocation(xc, yc);
          //將cancel發(fā)給被攔截的子view, 發(fā)個告示意思一下,后面大爺來處理來了。
          if (!target.dispatchTouchEvent(ev)) {
              // target didn't handle ACTION_CANCEL. not much we can do
              // but they should have.
          }
          // clear the target
          //清除本身記住的子view.但是他作為target記錄在他的父容器中沒有被清除,下次事件就還會
          //發(fā)送到他自己身上。
          mMotionTarget = null;
          // Don't dispatch this event to our own view, because we already
          // saw it when intercepting; we just want to give the following
          // event to the normal onTouchEvent().
          return true;
      }
    

    如果在Fm容器中發(fā)生了攔截,那么會發(fā)一個cancel給到Button, 其次會清除Fm中的target, 直接返回了不繼續(xù)下發(fā)了. 那么下一次呢, 下一次move事件再來的時候呢,會有啥變化呢,這時候fm中的target變成了null ! 那么就會在這里有了新的故事:

      if (target == null) {//Fm中的target為null,進(jìn)入if體
          // We don't have a target, this means we're handling the
          // event as a regular view.
          ev.setLocation(xf, yf);
          if ((mPrivateFlags & CANCEL_NEXT_UP_EVENT) != 0) {
              ev.setAction(MotionEvent.ACTION_CANCEL);
              mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;
          }
          //FM自己來消耗新的move事件啦。
          return super.dispatchTouchEvent(ev);
      }
    

    所以可以看到,fm中的target為null了,他就沒法走到最后一行去向Button去分發(fā)move事件啦。這就是為什么攔截了事件,子view收不到消息的原因啦!(值得注意的是,誰攔截了move,不管move返回的是true,還是false, 后續(xù)事件都會給他來吃了。從上面也可以看到端倪,因為沒人來針對move的返回結(jié)果來清除target呀,所以哥就不管37二十七一直發(fā)咯!)

    冬梅?啥?問你懂沒---

3. View.dispatchTouchEvent
public boolean dispatchTouchEvent(MotionEvent event) {
    if (!onFilterTouchEventForSecurity(event)) {
        return false;
    }
    //onTouch來處理啦
    if (mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED &&
        mOnTouchListener.onTouch(this, event)) {
        return true;
    }
    //前面不處理或者是false, 我就來處理啦。
    return onTouchEvent(event);
}

/ View.dispatchTouchEvent很簡單, 如果onTouch返回了true就不會給onTouchEvent, 否則將事件傳遞給onTouchEvent, dispatchTouchEvent的返回數(shù)值代表的就是onTouchEvent返回的數(shù)值。代表著有沒有消耗觸摸事件。

4. View.onTouchEvent:

該方源碼比較簡單, 這里就不看了, 簡單記錄下內(nèi)容

  • 如果控件是clickable, long_clickable那么就會返回true, 表示可以消費此事件。所以textView是不消耗, Button消耗的啦。
  • ACTION_DOWN:如果是在scrollView類似這樣的容器中,會延遲100ms來做按壓態(tài)顯示,并作長按事件檢測. 如果不是在滾動容器內(nèi), 直接顯示按壓態(tài), 然后做長按事件檢測(長按事件是500ms沒抬起就執(zhí)行l(wèi)ongClick事件)。
  • ACTION_CANCEL:設(shè)置按壓狀態(tài)為false, 取消點壓事件檢測(100ms之后要改變view狀態(tài)), 以及長按事件檢測(500ms之后響應(yīng)longClick)。
  • ACTION_MOVE:當(dāng)點擊位置不在view內(nèi)時候,move事件還是會下發(fā)到我們當(dāng)前控件上來的,這和down不一樣,android這樣設(shè)計估計是為了更好地用戶體驗吧,讓觸摸范圍更大。然而,雖然事件來到了當(dāng)前的view, 如果不在view位置內(nèi)是會移除longClick事件和presesed事件的。
  • ACTION_UP: 如果在down事件設(shè)定的是prepressed, 則立即顯示點壓狀態(tài); 當(dāng)沒有執(zhí)行l(wèi)ongClick或者longClick返回為false的情況下才會去執(zhí)行onClick事件,然后在執(zhí)行一個延遲任務(wù)來釋放前面的點壓狀態(tài),不管點壓態(tài)是在up中設(shè)定的還是在down中設(shè)定的。
  • 注意的是,如果當(dāng)次點擊事件在尋求焦點的獲取, 那么當(dāng)次點擊事件是不會生效的。下一次才會生效,這就是開發(fā)過程中為什么有時候點擊兩次才能響應(yīng)點擊事件。解決思路從焦點角度來研究。

細(xì)節(jié)

  • dispatchTouchEvent: 表示下面有沒有人消耗了觸摸事件。
  • 一個clickable或者longClickable的View會永遠(yuǎn)消費Touch事件,不管他是enabled還是disabled的。
  • move事件和up事件并不一定是共存的,可以只有down和up。
  • 在ACTION_DOWN中,如果當(dāng)前View沒有設(shè)定攔截,遍歷子view結(jié)構(gòu),尋找目標(biāo)View, 當(dāng)找到了能目標(biāo)view,也就是他的onTouchEvent能返回true. 如果后續(xù)控件體系仍然對move, up事件未添加攔截,那么后續(xù)的事件都會來到他身上。

局限所在

  • 觸摸事件傳遞機制是android的ui交互處理的一套非常重要,也很經(jīng)典的機制。看上去很漂亮優(yōu)秀,但是也有很大的先天不足,像前面所說一次觸摸事件在這樣的機制下是沒法做到先讓容器滾動然后再是子view滾動的操作了。比如在一些大型商用app上買菜的,購物的,音樂的等等,他們的很多內(nèi)容頁面是有很漂亮流暢的滾動效果的,這是單靠基本的觸摸滾動應(yīng)該是做不到的。那是什么呢,我想是應(yīng)該是靈活地運用了嵌套滾動機制吧,可以將一次觸摸事件分時分量地給到多個控件呢。對的,嵌套滾動哦~
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。