View事件傳遞之父View和子View之間的那點事

Android事件傳遞流程在網上能夠找到不少資料,FrameWork層輸入事件和消費事件,能夠參考:android

  1. [Touch事件派發過程詳解] 1

這篇blog闡述了底層是如何處理屏幕輸,並往上傳遞的。Touch事件傳遞到ActivityDecorView時,往下走就是ViewGroup和子View之間的事件傳遞,能夠參考郭神的這兩篇博客緩存

  1. [Android事件分發機制徹底解析,帶你從源碼的角度完全理解(上)] 3
  2. [Android事件分發機制徹底解析,帶你從源碼的角度完全理解(下)] 4

郭神的兩篇博客清楚明白地說明了View之間事件傳遞的大方向,可是具體的一些晦暗的細節闡述較少,本文主要是總結這兩篇博客的同時,側重於兩點:ide

  1. 事件分發過程當中一些細節到底如何實現的?
  2. view到底如何和父View搶事件,父View又是如何攔截事件不發送給子View,以及若是咱們須要處理這種混亂的關係才能讓二者和諧相處?。

MotionEvent抽象

要明白View的事件傳遞,頗有必要先說一下Touch事件是如何在Android系統中抽象的,這主要使用的就是MotionEvent。這個類經歷了幾回重大的修改,一次是在2.x版本支持多點觸摸,一次是4.x將大部分代碼甩給native層處理。函數

一次簡單的事件

咱們先舉個栗子來講明一次完整的事件,用戶觸屏 滑動 到手機離開屏幕,這認爲是一次完整動做序列(movement traces)。一個動做序列中包含不少動做Action,好比在用戶按下時,會封裝一個MotionEvent,分發給視圖樹,咱們能夠經過motionevent.getAction拿到這個動做是ACTION_DOWN。一樣,在手指擡起時,咱們能夠接收到Action類型是Action_UPMotionEvent。對於滑動(MOVE)這個操做,Android爲了從效率出發,會將多個MOVE動做打包到一個MotionEvent中。經過getX getY能夠獲取當前的座標,若是要訪問打包的緩存數據,能夠經過getHistorical**()函數來獲取。佈局

加入多點觸摸

對於單點的操做來看,MotionEvent顯得比較簡單,可是考慮引入多點觸摸呢?咱們定義一個接觸點爲(Pointer)。咱們從onTouch接受到一個MotionEvent,怎麼拿到多個觸碰點的信息?爲了解開筆者剛開始學習這部分知識時的困惑,咱們首先樹立起一種概念:一個MotionEvent只容許有一個Action(動做),並且這個Action會包含觸發此次Action的觸碰點信息,對於MOVE操做來講,必定是當前全部觸碰點都在動。只有ACTION_POINTER_DOWN這類事件事件會在Action裏面指定是哪個POINTER按下。學習

MotionEvent的底層實現中,是經過一個16位來存儲ActionPointer信息(PointerIndex)。低8位表示Action,理論上能夠表示255種動做類型;高8位表示觸發這個ActionPointerIndex,理論上Android最多能夠支持255點同時觸摸,可是在上層代碼使用的時候,默認多點最多存在32個,否則事件在分發的時候會有問題。ui

MotionEvent中多個手指的操做API大部分都是經過pointerindex來進行的,如:獲取不一樣Pointer的觸碰位置,getX(int pointerIndex);獲取PointerId等等。大部分狀況下,pointerid == pointeridexthis

ACTION_DOWN OR ACTION_POINTER_DOWN:spa

這兩個按下操做的區別是ACTION_DOWN是一個系列動做的開始,而ACTION_POINTER_DOWN是在一個系列動做中間有另一個觸碰點觸碰到屏幕。.net

這部分詳細的描述,請參考:
android觸控,先了解MotionEvent

到這裏,鋪墊終於結束了,咱們開始直奔主題。

View的事件傳遞

AndroidTouch事件傳遞到Activity頂層的DecorView(一個FrameLayout)以後,會經過ViewGroup一層層往視圖樹的上面傳遞,最終將事件傳遞給實際接收的View。下面給出一些重要的方法。若是你對這個流程比較熟悉的話,能夠跳過這裏,直接進入第二部分。

dispatchTouchEvent

事件傳遞到一個ViewGroup上面時,會調用dispatchTouchEvent。代碼有刪減

public boolean dispatchTouchEvent(MotionEvent ev) {

    boolean handled = false;
    if (onFilterTouchEventForSecurity(ev)) {
        final int action = ev.getAction();
        final int actionMasked = action & MotionEvent.ACTION_MASK;

        // Attention 1 :在按下時候清除一些狀態
        if (actionMasked == MotionEvent.ACTION_DOWN) {
            cancelAndClearTouchTargets(ev);
            //注意這個方法
            resetTouchState();
        }

        // Attention 2:檢查是否須要攔截
        final boolean intercepted;
        //若是剛剛按下 或者 已經有子View來處理
        if (actionMasked == MotionEvent.ACTION_DOWN
                || mFirstTouchTarget != null) {
            final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
            if (!disallowIntercept) {
                intercepted = onInterceptTouchEvent(ev);
                ev.setAction(action); // restore action in case it was changed
            } else {
                intercepted = false;
            }
        } else {
            //  不是一個動做序列的開始 同時也沒有子View來處理,直接攔截
            intercepted = true;
        }

          //事件沒有取消 同時沒有被當前ViewGroup攔截,去找是否有子View接盤
        if (!canceled && !intercepted) {
                //若是這是一系列動做的開始  或者有一個新的Pointer按下 咱們須要去找可以處理這個Pointer的子View
            if (actionMasked == MotionEvent.ACTION_DOWN
                    || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
                    || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
                final int actionIndex = ev.getActionIndex(); // always 0 for down
                
                //上面說的觸碰點32的限制就是這裏致使
                final int idBitsToAssign = split ? 1 << ev.getPointerId(actionIndex)
                        : TouchTarget.ALL_POINTER_IDS;

                final int childrenCount = mChildrenCount;
                if (newTouchTarget == null && childrenCount != 0) {
                    final float x = ev.getX(actionIndex);
                    final float y = ev.getY(actionIndex);
                    
                    //對當前ViewGroup的全部子View進行排序,在上層的放在開始
                    final ArrayList<View> preorderedList = buildOrderedChildList();
                    final boolean customOrder = preorderedList == null
                            && isChildrenDrawingOrderEnabled();
                    final View[] children = mChildren;
                    for (int i = childrenCount - 1; i >= 0; i--) {
                        final int childIndex = customOrder
                                ? getChildDrawingOrder(childrenCount, i) : i;
                        final View child = (preorderedList == null)
                                ? children[childIndex] : preorderedList.get(childIndex);
                            
                              // canViewReceivePointerEvents visible的View均可以接受事件
                              // isTransformedTouchPointInView 計算是否落在點擊區域上
                        if (!canViewReceivePointerEvents(child)
                                || !isTransformedTouchPointInView(x, y, child, null)) {
                            ev.setTargetAccessibilityFocus(false);
                            continue;
                        }
                            
                              //可以處理這個Pointer的View是否已經處理以前的Pointer,那麼把
                        newTouchTarget = getTouchTarget(child);
                        if (newTouchTarget != null) {
                            // Child is already receiving touch within its bounds.
                            // Give it the new pointer in addition to the ones it is handling.
                            newTouchTarget.pointerIdBits |= idBitsToAssign;
                            break;
                        }                           }
                        //Attention 3 : 直接發給子View
                        if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
                            // Child wants to receive touch within its bounds.
                            mLastTouchDownTime = ev.getDownTime();
                            if (preorderedList != null) {
                                // childIndex points into presorted list, find original index
                                for (int j = 0; j < childrenCount; j++) {
                                    if (children[childIndex] == mChildren[j]) {
                                        mLastTouchDownIndex = j;
                                        break;
                                    }
                                }
                            } else {
                                mLastTouchDownIndex = childIndex;
                            }
                            mLastTouchDownX = ev.getX();
                            mLastTouchDownY = ev.getY();
                            newTouchTarget = addTouchTarget(child, idBitsToAssign);
                            alreadyDispatchedToNewTouchTarget = true;
                            break;
                        }

                    }
                }

            }
        }

        // 前面已經找到了接收事件的子View,若是爲NULL,表示沒有子View來接手,當前ViewGroup須要來處理
        if (mFirstTouchTarget == null) {
            // ViewGroup處理
            handled = dispatchTransformedTouchEvent(ev, canceled, null,
                    TouchTarget.ALL_POINTER_IDS);
        } else {
           
            if(alreadyDispatchedToNewTouchTarget) {
                                //ignore some code
                    if (dispatchTransformedTouchEvent(ev, cancelChild,
                            target.child, target.pointerIdBits)) {
                        handled = true;
                }
            }

        }
    return handled;
}

上面代碼中的Attention在後面部分將會涉及,重點注意。

這裏須要指出一點的是,一系列動做中的不一樣Pointer能夠分配給不一樣的View去響應。ViewGroup會維護一個PointerId和處理View的列表TouchTarget,一個TouchTarget表明一個能夠處理Pointer的子View,固然一個View能夠處理多個Pointer,好比兩根手指都在某一個子View區域。TouchTarget內部使用一個int來存儲它能處理的PointerId,一個int32位,這也就是上層爲啥最多隻能容許同時最多32點觸碰。

看一下Attention 3 處的代碼,咱們常常說viewdispatchTouchEvent若是返回false,那麼它就不能系列動做後面的動做,這是爲啥呢?由於Attention 3處若是返回false,那麼它不會被記錄到TouchTarget中,ViewGroup認爲你沒有能力處理這個事件。

這裏能夠看到,ViewGroup真正處理事件是在dispatchTransformedTouchEvent裏面,跟進去看看:

dispatchTransformedTouchEvent

private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
        View child, int desiredPointerIdBits) {

      //沒有子類處理,那麼交給viewgroup處理
    if (child == null) {
        handled = super.dispatchTouchEvent(transformedEvent);
    } else {
        final float offsetX = mScrollX - child.mLeft;
        final float offsetY = mScrollY - child.mTop;
        transformedEvent.offsetLocation(offsetX, offsetY);
        if (! child.hasIdentityMatrix()) {
            transformedEvent.transform(child.getInverseMatrix());
        }

        handled = child.dispatchTouchEvent(transformedEvent);
    }
    return handled;
}

能夠看到這裏無論怎麼樣,都會調用ViewdispatchTouchEvent,這是真正處理這一次點擊事件的地方。

dispatchTouchEvent

public boolean dispatchTouchEvent(MotionEvent event) {
        if (onFilterTouchEventForSecurity(event)) {
        //先走View的onTouch事件,若是onTouch返回True
        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;
        }
    }
      return result;
    }

咱們給View設置的onTouch事件處在一個較高的優先級,若是onTouch執行返回true,那麼就不會去走viewonTouchEvent,而咱們一些點擊事件都是在onTouchEvent中處理的,這也是爲何onTouch中返回true,view的點擊相關事件不會被處理。

小小總結一下這個流程

ViewGroup在接受到上級傳下來的事件時,若是是一系列Touch事件的開始(ACTION_DOWN),ViewGroup會先看看本身需不須要攔截這個事件(onInterceptTouchEventViewGroup的默認實現直接返回false表示不攔截),接着ViewGroup遍歷本身全部的View。找到當前點擊的那個View,立刻調用目標ViewdispatchTouchEvent。若是目標ViewdispatchTouchEvent返回false,那麼認爲目標View只是在那個位置而已,它並不想接受這個事件,只想安安靜靜的作一個View(我靜靜地看着大家裝*)。此時,ViewGroup還會去走一下本身dispatchTouchEvent,Done!

子View和父View的撕*大戰

終於來到本文的重要環節,子View和父佈局(ViewGroup)是如何撕逼的。咱們常常遇到這樣的問題:在ListView中放一個ViewPager不能滑動的問題,其實這裏就會涉及到子View和佈局之間的協商,事件處理到底你上仍是我上。

首先須要明確一點的是,一個事件確定是由ViewGroup傳遞給本身的子View的,因此ViewGroup具備絕對的權威來禁止事件往下傳,這就是onInterceptTouchEvent方法。能夠看上面ViewGroup中的dispatchTouchEventAttention 1Attention 2

先看Attetion2
進行判斷有有兩個條件:1,若是是一次新的事件 or 在一次事件中可是已經有子View來處理這個事件,那麼父類須要去看看是否攔截此次事件。不然,直接攔截(此時處於一系列動做的中間,並且沒有子view來接盤,那麼ViewGroup就直接攔下來)。

決定是否攔截有兩個步驟,

  1. disallowIntercept 是否駁回攔截,默認false。注意這個值是子View和撕*的關鍵,由於ViewGroup開放了給這個標記賦值的接口requestDisallowInterceptTouchEvent(),並且這個方法直接往上遞歸,這個ViewGroup的各級父容器都會設置駁回攔截。
  2. onInterceptTouchEvent 雖然ViewGroup中默認返回false,可是在不少有滑動功能的ViewGroup裏面(如scrollview ListView等)會處理各類狀況,決定是否攔截這個事件,因此就會出現以前說的ListView中的Viewpager不能滑動的問題,緣由是事件被父View攔截了。

Attetion1的位置若是是一次新的ACTION_DOWN,那麼會把以前事件傳遞設置的各類狀態清除。

對ViewGroup來講須要作什麼

對於一個須要攔截事件的ViewGroup,它一般都有一些特殊的操做,好比ScrollView,好比ViewPager,它重寫onInterceptTouchEvent是很是關鍵的,這也是能和子View和諧相處的關鍵。舉個例子,我本身定義了一個ViewGroup

@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
    if(ev.getActionMasked() == MotionEvent.ACTION_DOWN) {
        return true;
    }
    return super.onInterceptTouchEvent(ev);
}

這樣會發生什麼?

全部位於MyViewGroup中的子View收不到任何的事件,緣由能夠看一下Attention2的代碼,判斷是否攔截是在系列動做按下時會進行判斷,若是此時攔截,那麼直接不會去查找相應處理的子View,因此touchtarget爲空,那麼接下來的動做都直接被ViewGroup笑納。

因此哪怕再強勢的ViewGroup,通常都是在Down的時候給子類機會去掉用requestDisallowInterceptTouchEvent,如設置駁回攔截,那麼在ViewGroup分發事件的時候,會跳過onInterceptTouchEvent的執行。

子View須要作什麼

對於子View來講,在合適的時機調用requestDisallowInterceptTouchEvent便可。固然啥時候合適?對於一個View來講,那就是在dispatchTouchEvent或者onTouchEvent來調用。

對於ViewGroup來講,一般咱們會在onInterceptTouchEvent進行判斷。好比咱們常常會遇到在ListView裏面套了ViewPager致使ViewPager不能滑動的問題,一般的處理方式:

@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
    if (absListView != null) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                mDownX = event.getX();
                mDownY = event.getY();
                    //ACTION_DOWN的時候,趕忙把事件hold住
                getParent().requestDisallowInterceptTouchEvent(true);
                break;
            case MotionEvent.ACTION_MOVE:

                if(Math.abs(event.getX() - mDownX)>Math.abs(event.getY()-mDownY)) {
                    getParent().requestDisallowInterceptTouchEvent(true);
                }else {
                    //發現不是本身處理,還給父類
                    getParent().requestDisallowInterceptTouchEvent(false);
                }
                break;
            case MotionEvent.ACTION_CANCEL:
            case MotionEvent.ACTION_UP:
                    //其實這裏是多餘的
                getParent().requestDisallowInterceptTouchEvent(false);
        }

    }
    return super.onInterceptTouchEvent(event);
}

總結

原本打算寫一個短篇的,結果一個不當心,弄成了長篇大論。

最後須要注意一點的是,全部咱們上述討論的內容都是在一層層遞歸中進行,並且requestDisallowInterceptTouchEvent這個函數也是遞歸調用的。

咱們能夠認爲ViewGroup是一個具備絕對話語權可是從不專政的霸道總裁,它本身能夠攔截處理某些事件,好比Viewpager的橫滑,可是它也能夠給子View足夠的空間去要求這個事件給本身處理。做爲一名開發者,一方面在本身定義ViewGroup時須要考慮可以給子View足夠空間中斷本身的攔截;一方面本身定義View時,咱們須要在合適的時候跟父View索要事件。ViewPager(新版)做爲容器來講,它須要攔截橫滑事件,同時,本身具有了和父View爭搶事件的能力,因此無論把ViewPager放到什麼佈局中,它都能正確處理。看看它的onInterceptTouchEvent怎麼寫的吧,完美的體現了這一思想。

相關文章
相關標籤/搜索