Android事件分發機制

來源:互聯網
上載者:User

標籤:

        Android的事件分發機制,對於事件的分發的瞭解是非常重要的;如果你不清楚具體的原理,那麼你將會很迷茫,遇到問題時,無從下手。這裡,我將個人對Android事件分發機制的理解,描述出來,希望能對大多數夥伴的有所裨益。

       1.觸摸事件的開始

      觸摸事件,來自觸控螢幕。從觸控螢幕硬體產生事件訊號到Activity開始接收這個事件,就不做分析了,因為具體的我也不清楚。因此,這裡主要分析Activity中,事件的分發過程。

       首先由Activity進行分發,具體的分發方法如下:

  public boolean dispatchTouchEvent(MotionEvent ev) {        if (ev.getAction() == MotionEvent.ACTION_DOWN) {            onUserInteraction();        }        if (getWindow().superDispatchTouchEvent(ev)) {            return true;        }        return onTouchEvent(ev);    }
       直接從getWindow().superDispatchTouchEvent(ev)開始,如果這個方法返回true,那麼這個方法結束,不會調用return onTouchEvent(ev),這是什麼意思呢?這個意思就是如果事件被window消費了,Activity就不再對事件進行處理。如果事件並沒有被window消費掉,那麼事件能被onTouchEvent()方法處理,這個具體的處理方式,我們可以在Activity中進行重寫。很多人不明白什麼叫做事件被消費掉,所謂事件被消費掉,其實是事件得到了處理,這個事件不再進行傳遞,不會再傳遞到其他控制項。

        接下來我們分析getWindow().superDispatchTouchEvent(ev),這個方法,很多人不知道具體的實現在哪裡,這個是和Activity的啟動過程有關係,在Activity的建立過程中,會通過PhoneWindow初始化window,因此這個方法,其實是PhoneWindow的superDispatchTouchEvent        

   @Override    public boolean superDispatchTouchEvent(MotionEvent event) {        return mDecor.superDispatchTouchEvent(event);    }
       這裡,我們能看到其實最終調用的是DecorView的superDispatchTouchEvent。由於DecorView繼承LinearLayout,最後,其實還是ViewGroup的dispatchTouchEvent方法。

       總結上面所說的,Activity中,對事件的分發,主要是通過ViewGroup的dipatchTouchEvent方法來執行的。接下來我們著重分析ViewGroup中的這個方法。

       2.事件分發的核心

       2.1.ACTION_DOWN的向下傳播,找到事件觸發座標所在的最裡面的一個控制項。

       首先,我們需要知道一個事件序列,ACTION_DOWN—>ACTION_MOVE—>ACTION_UP,這隻是其中一個代表,一個觸摸事件總是從ACTION_DOWN開始的,然後才會觸發後面的事件。ACTION_DOWN這個事件,它承擔了尋找能夠處理事件的目標控制項,這個尋找的過程,具體看程式碼分析吧。

 @Override    public boolean dispatchTouchEvent(MotionEvent ev) {//擷取事件的動作        final int action = ev.getAction();//事件的x,y在當前視圖的座標        final float xf = ev.getX();        final float yf = ev.getY();//mScrollX和mScrollY是該[內容] 檢視的畫布的滑動位移,        final float scrolledXFloat = xf + mScrollX;        final float scrolledYFloat = yf + mScrollY;        final Rect frame = mTempRect;        boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;//如果是ACTION_DOWN事件        if (action == MotionEvent.ACTION_DOWN) {            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;            }            // If we're disallowing intercept or if we're allowing and we didn't            // intercept//預設是false,表示可以攔截,可以通過requestDisallowInterceptTouchEvent(boolean disallowIntercept)設定為true,表示不攔截,那麼onInterceptTouchEvent就失效了            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                for (int i = count - 1; i >= 0; i--) {                    final View child = children[i];//如果子View是可見或者是有動畫的,那麼擷取這個視圖的可點擊範圍                    if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE                            || child.getAnimation() != null) {//表示的child的原始位置,也就是scroll滑動前的位置,因此上面需要加上mScrollX                        child.getHitRect(frame);//如果該孩子位置的包含了所觸控的點                        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;//該孩子是否消費的了事件,如果該孩子消費了,則返回true,這裡是遞迴調用                            if (child.dispatchTouchEvent(ev))  {                                // Event handled, we have a target now.                                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.                        }                    }                }            }        }
        上面是ViewGroup中dispatchTouchEvent的第一部分代碼,也是ACTION_DOWN事件的主要處理過程,代碼中具體注釋了重要的過程。要注意的只有兩點:

        1.disallowIntercept,這個標誌位預設是false,表示可以攔截,主要看onInterceptTouchEvent來控制。如果表示true,表示不能攔截,onInterceptTouchEvent就算攔截了,也是無效的。

        2.最後的幾句代碼中,又調用了child.dispatchTouchEvent,說明這類似一個遞迴調用。我們可以想象,ViewGroup1的dispatchTouchEvent中調用了ViewGroup2的dispatchTouchEvent,最後調用了view.dispatchTouchEvent方法(最後一個控制項很有可能不是ViewGroup)。一直把這個事件傳遞到最後一個view,如果最後這個view的dispatchTouchEvent返回true。但這一切的前提是,事件的觸發座標落在控制項上。


       2.2.target為空白,ACTION_DOWN事件外層傳遞,父控制項獲得處理事件的機會。

       上一個過程中,ACTION_DOWN並沒有找到能夠消費它的控制項,因此,遍曆控制項完成後,進入最後一個控制項的父控制項的dispatchTouchEvent的這個過程。事件沒有消費,交給最後一個控制項的父控制項的super.dispatchTouchEvent來處理。這個處理,其實是調用的View裡面的dispatchTouchEvent,這個和ViewGroup中的是不一樣的,View中的這個方法是具體的消費過程,並不分發事件。如果當前父控制項(也就是倒數第二個控制項)也沒有消費這個事件,super.dispatchTouchEvent返回false;事件又交給了當前控制項的父控制項(倒數第三個控制項)同樣進行處理。    這裡有人不明白,為什麼是交給父控制項處理;原因是第一步裡面,我們是層層調用,父控制項調用子控制項的dispatchTouchEvent方法。這裡是target為空白,也就是說第一步的層層調用,沒有消費掉事件,還沒有返回,因此這裡面對這個情況進行處理,開始層層往上返回;父控制項得到處理事件的機會,如果父控制項沒有消費掉事件,就繼續往上返回;如果父控制項消費掉了事件,那麼它的父控制項返回true,target為當前這個控制項。

/如果target為空白,ACION_DOWN事件未消費,ACTION_DOWN事件又開始重新往上分發        final View target = mMotionTarget;        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;            }//子View沒有消費事件,交給當前的ViewGroup來處理,其實super.dispatchTouchEvent(ev);調用的View的dispatchTouchEvent            return super.dispatchTouchEvent(ev);        }
        2.3.找到了target

        存在有兩種情況:

                                   1.事件沒有被攔截,ACTION_DOWN事件順利找到了目標控制項,並且該控制項能夠對事件進行處理,消費掉。

                                   2.事件中途被攔截,事件交給了中途的一個ViewGroup處理,並且有一個ViewGroup能夠消費事件。    比如:事件中途被ViewGroupIntercept(控制項別名)被攔截,那麼它不會執行2.1的代碼,target==null,ACTION_DOWN事件,進入到代碼2.2,事件開始往上返回,但是ViewGroupIntercept的父控制項並沒有攔截,事件執行在2.1的代碼,這時候如果ViewGroupIntercept消費掉事件,返回true,那麼它的父控制項的target就指向這個控制項,這樣又重新回到了正常的分發流程。

        2.4.ACTION_MOVE,ACTION_UP等非ACTION_DOWN事件被攔截。

        ACTION_MOVE,ACTION_UP事件被攔截,肯定是事件已經找到了可消費的控制項。但是一個事件序列中,除ACTION_DOWN的事件,被動態攔截了,這裡的事件將做如下處理:

if (!disallowIntercept && onInterceptTouchEvent(ev)) {            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事件            if (!target.dispatchTouchEvent(ev)) {                // target didn't handle ACTION_CANCEL. not much we can do                // but they should have.            }            // clear the target//當前控制項的target清空,下一次的事件判斷,進入到target為空白的情況,也就是由當前控制項自己處理。            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;        }
         事件被攔截後,先給目標控制項傳遞一個cancel事件,這一個事件序列中目標控制項的事件結束。接下來,當前控制項的分發方法返回true,表示當前控制項可以消費事件,那麼他的父控制項的分發方法進入到了流程2.3,也就是target不為空白,target不為空白,事件就是正常分發(什麼是正常分發?後面會講)。

         2.5.事件的正常分發

        事件由父控制項的target傳遞到子控制項的target,這就是事件的正常分發。如果事件是ACTION_UP或者是ACTION_CANCEL,將會清空target,下一次的觸摸事件,又是如此重新開始。

 if (isUpOrCancel) {            mMotionTarget = null;        }...... return target.dispatchTouchEvent(ev);

          3.View的dispatchTouchEvent方法

          上面多次提到View.dispatchTouchEvent,現在我們來看一下:

 public boolean dispatchTouchEvent(MotionEvent event) {        if (mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED &&                mOnTouchListener.onTouch(this, event)) {            return true;        }        return onTouchEvent(event);    }
         先會判斷控制項是否設定了onTouchListener,如果設定了,並且控制項是enable,那麼事件醬油OnTouchListener的onTouch方法處理。    否則事件交給預設的處理方法onTouchEvent來處理。

         看看onTouchEvent的處理方法,onTouchEvent的處理,主要邏輯是這樣的,先判斷控制項是否是clickable,如果不可以,直接返回false,事件沒有消費掉。如果clickable為true或者longClckable為true,事件會被消費掉,具體就會回調onClick方法或者是onLongClick方法。



總結:

        1.正常不攔截事件,由action_down尋找對應能消費的target,如果存在target能消費事件,則最後事件由該target全部處理。
        2.不攔截事件,沒有target處理事件,則事件逐步往上由onTouchEvent方法處理,但是控制項非clickable,則不能處理事件。如果存在控制項是clickable的,那麼事件會被消費掉。
        3.事件分發的過程中,如果事件被攔截,則下一個事件交給攔截事件的onTouchEvent控制項處理。
        4.requestDisallowInterceptTouchEvent可以用來控制事件的分發。

       Android滑動衝突
           多個可以滑動控制項之間的嵌套很容易引起滑動衝突,解決的方法分為兩種:
     1.從外部攔截機制考慮
            外部控制項重寫onInterceptTouchEvent處理,通過計算dx和xy的進行處理,在onMove事件中動態控制
     2.內部控制項調用
            requestDisallowInterceptTouchEvent來控制,原理其實是一樣的。requestDisallowInterceptTouchEvent能夠控制事件能否往下傳遞,前面事件分發機制已經分析了。(往下的意思:View是樹形結構,最頂層是最底部的View,最下面是子View)

 

        到這裡,整個的事件分發機制就基本結束了,希望能對大家有所協助。若有什麼分析不當的地方,望大家指出。

        

   

        



Android事件分發機制

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.