標籤:
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事件分發機制