觸摸事件的分發(fā)(ViewGroup篇之一)

本篇和觸摸事件的分發(fā)(View篇)將側(cè)重于Android源碼的分析。略顯枯燥,但Read the fucking code的code不就是這樣嗎?

本文代碼基于API15分析,而不是最新的API23。因API23中多出的代碼和本文無(wú)關(guān)。
為減少篇幅完整地代碼注釋會(huì)在文末給出鏈接。

關(guān)鍵的成員變量

觸摸目標(biāo)

TouchTargetViewGroup的一個(gè)靜態(tài)內(nèi)部類。描述一個(gè)被觸摸的 childView 和他所捕獲的手指的ids。

private static final class TouchTarget {
 public View child;//被觸摸到得child
 public int pointerIdBits;//由該目標(biāo)捕獲的所有的手指的IDS 的結(jié)合位掩碼
 public TouchTarget next;//用于指向鏈表中的下一個(gè)TouchTarget
}

記錄到的全是子View的信息,在處理向子View發(fā)送事件的邏輯時(shí)使用

觸摸目標(biāo)鏈表

為了有條理得向子View分發(fā)事件,ViewGroup需要記錄所有用戶觸摸到的觸摸目標(biāo)信息。在經(jīng)過(guò)一系列判斷邏輯后,向其中的觸摸目標(biāo)分發(fā)事件。
這里是通過(guò)定義一個(gè)TouchTarget類型的成員變量mFirstTouchTarget來(lái)實(shí)現(xiàn)的,其記錄了第一個(gè)觸摸目標(biāo),是觸摸目標(biāo)鏈表的頭。通過(guò)它(的next),可以找到鏈表中所有的觸摸目標(biāo)。

    private TouchTarget mFirstTouchTarget;

ViewGroup.dispatchTouchEvent()

ViewGroup中關(guān)于事件的分發(fā)是通過(guò)重寫(xiě)dispatchTouchEvent實(shí)現(xiàn)的。這部分是事件分發(fā)過(guò)程中最為復(fù)雜和難的地方。充分了解了這部分代碼。將再也沒(méi)有難點(diǎn)來(lái)阻擋你理解觸摸事件的分發(fā)了。

ViewGroup中的dispatchTouchEvent()方法非常復(fù)雜,不了解整體設(shè)計(jì)思路,直接閱讀將會(huì)一頭霧水,云里霧里。所以,這里先把ViewGroup的的dispatchTouchEvent()用以下流程圖歸納如下:

事件的傳遞之ViewGroup.png

其中比較關(guān)鍵的有以下幾點(diǎn):

  1. 觸摸事件的安全策略
  2. 處理最初的DOWN事件
  3. 檢查事件的攔截情況
  4. 檢查是否取消
  5. 根據(jù)需要為DOWN事件更新觸摸目標(biāo)鏈表
  6. 分發(fā)觸摸事件到目標(biāo)View
  7. 根據(jù)需要,為UP、CANCEL事件更新觸摸目標(biāo)鏈表

其中,除了第一條相對(duì)分發(fā)流程比較獨(dú)立外,其余都標(biāo)有數(shù)字序號(hào),并在圖中用藍(lán)色標(biāo)注出來(lái)了。
接下來(lái),我們分別詳細(xì)講每一個(gè)點(diǎn)。

〇、觸摸事件的安全策略

根據(jù)用戶體驗(yàn)來(lái)講,用戶只會(huì)嘗試去點(diǎn)擊可以直接看到的View(或ViewGroup),所以Google據(jù)此為觸摸事件的分發(fā)制定了一個(gè)安全策略:
如果某View不處于頂部,并且View設(shè)置的屬性是該View不在頂部時(shí)不響應(yīng)觸摸事件,則不分發(fā)該事件。

不滿足安全策略需要同時(shí)滿足兩個(gè)條件:

  1. 配置設(shè)定被遮擋時(shí)需要過(guò)濾觸摸事件(mViewFlags包含F(xiàn)ILTER_TOUCHES_WHEN_OBSCURED)
  2. 觸摸事件確實(shí)被遮擋(event.getFlags()包含MotionEvent.FLAG_WINDOW_IS_OBSCURED)

若不滿足安全策略,onFilterTouchEventForSecurity(MotionEvent event)方法返回false,從上文流程圖可以看到,這種情況會(huì)放棄接下來(lái)的所有分發(fā)操作。

    /**
     * 依據(jù)安全策略,過(guò)濾觸摸事件。
     * @param event The motion event to be filtered. 需要被過(guò)濾的觸摸事件。
     * @return True if the event should be dispatched, false if the event should be dropped.
     * 該事件需要被分發(fā),則返回true。該事件需要被丟棄,則返回false。
     */
    public boolean onFilterTouchEventForSecurity(MotionEvent event) {
        //noinspection RedundantIfStatement
        if ((mViewFlags & FILTER_TOUCHES_WHEN_OBSCURED) != 0
                && (event.getFlags() & MotionEvent.FLAG_WINDOW_IS_OBSCURED) != 0) {
            // Window is obscured, drop this touch.
            return false;
        }
        return true;
    }

一、處理最初的DOWN事件,重置ViewGroup狀態(tài)

Android觸摸事件--MotionEvent一文我們可以知道:DOWN類型的事件是一系列觸摸事件的開(kāi)始。

  • 從用戶角度講:每次點(diǎn)擊屏幕時(shí),必然是首先觸發(fā)一個(gè)DOWN事件。
  • 從代碼角度講:ViewGroup最先接收到得必然是DOWN事件,之后才會(huì)陸續(xù)接收到MOVE、UP或CANCEL等類型的事件。

當(dāng)上一次觸摸結(jié)束后,ViewGroup的某些狀態(tài)可能已經(jīng)發(fā)生了變化,比如ViewGroup的成員變量mFirstTouchTarget已經(jīng)記錄了一些值。
這時(shí),做了兩個(gè)動(dòng)作:

  1. 取消并清空觸摸目標(biāo)鏈表。 cancelAndClearTouchTargets(MotionEvent event)
     /**
     * 取消并清空所有的的觸摸目標(biāo)
     * Cancels and clears all touch targets.
     */
    private void cancelAndClearTouchTargets(MotionEvent event) {
        if (mFirstTouchTarget != null) { //如果mFirstTouchTarget鏈表不為空,則清空該鏈表
            boolean syntheticEvent = false; //是否是我們?nèi)藶楹铣傻氖录?            if (event == null) {
                final long now = SystemClock.uptimeMillis();
                event = MotionEvent.obtain(now, now,
                        MotionEvent.ACTION_CANCEL, 0.0f, 0.0f, 0);//人為合成一個(gè) ACTION_CANCEL 事件
                event.setSource(InputDevice.SOURCE_TOUCHSCREEN);
                syntheticEvent = true;
            }

            for (TouchTarget target = mFirstTouchTarget; target != null; target = target.next) {
                resetCancelNextUpFlag(target.child);
                //從cancelAndClearTouchTargets的調(diào)用關(guān)系,我們可以發(fā)現(xiàn),這里發(fā)送出去的事件只可能是兩種:
                // 1、處理 DOWN 事件時(shí)的DOWN事件
                // 2、當(dāng)該ViewGroup離開(kāi)屏幕(Window)時(shí),發(fā)送上面人為合成的ACTION_CANCEL 消息。
                // 但因?yàn)榈诙€(gè)參數(shù)為true,無(wú)論是哪種事件,最終都會(huì)被轉(zhuǎn)化為取消事件。
                dispatchTransformedTouchEvent(event, true, target.child, target.pointerIdBits);
            }
            clearTouchTargets();

            if (syntheticEvent) {
                event.recycle();
            }
        }
    }
2.png

根據(jù)Android中ViewGroup源碼,該方法除了在這里之外,還會(huì)在ViewGroup的dispatchDetachedFromWindow()方法中被調(diào)用。這部分邏輯在流程圖中特別標(biāo)示出來(lái)了。在分發(fā)觸摸事件的情況下地邏輯就簡(jiǎn)單了不少。
1)遍歷觸摸目標(biāo)列表,將鏈表中的所有子View的 CANCEL_NEXT_UP_EVENT 標(biāo)志全部重置
2)將取消事件傳遞給鏈表中所有的子View。(這里事件雖未DOWN事件,但dispatchTransformedTouchEvent方法第二個(gè)參數(shù)為true,最終都會(huì)被轉(zhuǎn)化為ACTION_CANCEL事件)
3)清空觸摸鏈表 clearTouchTargets()

  1. 重置所有觸摸狀態(tài)來(lái)為一個(gè)新的循環(huán)做準(zhǔn)備。 resetTouchState();
    private void resetTouchState() {
        clearTouchTargets();//清空觸摸鏈表
        resetCancelNextUpFlag(this);//重置CANCEL_NEXT_UP_EVENT 標(biāo)志
        mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;//重置 FLAG_DISALLOW_INTERCEPT 標(biāo)志
    }

這里代碼非常簡(jiǎn)單,就是清空觸摸鏈表、重置本ViewGroup實(shí)例的 CANCEL_NEXT_UP_EVENT 標(biāo)志、重置 FLAG_DISALLOW_INTERCEPT標(biāo)志。
和cancelAndClearTouchTargets()方法主要處理觸摸鏈表中的子View不同,該方法主要是針對(duì)ViewGroup實(shí)例自身的一些處理。

二、檢查事件的攔截情況

APP開(kāi)發(fā)工程師在開(kāi)發(fā)程序時(shí),可以對(duì)ViewGroup是否攔截事件做的限定有:
1、是否允許攔截觸摸事件;由 requestDisallowInterceptTouchEvent(boolean disallowIntercept) 來(lái)設(shè)定
2、是否要攔截某個(gè)觸摸事件;由onInterceptTouchEvent(MotionEvent ev)返回值來(lái)決定

而ViewGroup內(nèi)部是如何處理這些限定呢?
我們結(jié)合代碼來(lái)看一下:

 @Override
  public boolean dispatchTouchEvent(MotionEvent ev) {
    ****代碼從略   ****
            final boolean intercepted;
            if (actionMasked == MotionEvent.ACTION_DOWN
                    || mFirstTouchTarget != null) {
                final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
                if (!disallowIntercept) {//允許攔截
                    intercepted = onInterceptTouchEvent(ev);//根據(jù)onInterceptTouchEvent(dev)決定是否攔截
                    ev.setAction(action); // restore action in case it was changed
                } else {
                    intercepted = false;//不允許攔截
                }
            } else {//不是down事件并且mFirstTouchTarget==null也沒(méi)用與之對(duì)應(yīng)的觸摸目標(biāo)
                // There are no touch targets and this action is not an initial down
                // so this view group continues to intercept touches.
                // 不存在觸摸目標(biāo),并且該事件不是down事件,那么就繼續(xù)攔截觸摸事件。
                //(比如一個(gè)空的ViewGroup,則攔截觸摸事件,通過(guò)自己的touchEvent處理)。
                // (又比如雖ViewGroup不為空,但觸摸事件并沒(méi)發(fā)生在任何子View上)。
                intercepted = true;
            }
    ****代碼從略   ****
  }

在兩種情況下,ViewGroup需要根據(jù)APP工程師的限定來(lái)決定事件是否被攔截:

  1. 當(dāng)發(fā)生down事件時(shí)
    down事件是一個(gè)完整事件序列的的起點(diǎn),當(dāng)發(fā)生down事件時(shí),這時(shí)還不知道是否有子View可消費(fèi)該事件(此時(shí)mFirstTouchTarget已經(jīng)被重置為null),必須根據(jù)APP工程師對(duì)ViewGroup的限定,方知是否需要攔截該事件。
  2. mFirstTouchTarget 不為null時(shí)
    這意味已經(jīng)(靠該序列的起點(diǎn)down事件)找到要消費(fèi)觸摸事件的目標(biāo)了,那么肯定不會(huì)是down事件了,而是move、up等類型的事件。這時(shí),我們也需要根據(jù)APP工程師對(duì)ViewGroup的限定來(lái)決定是否攔截這種類型(move、up等)的事件。

從代碼可知:只有在允許攔截并且onInterceptTouchEvent(MotionEvent ev)返回true時(shí),才會(huì)對(duì)觸摸事件攔截。

在以下情況下,觸摸事件默認(rèn)需要交給ViewGroup自己來(lái)處理,這時(shí),我們可以當(dāng)做事件被攔截了來(lái)處理:

  • 既不是down事件,此前也沒(méi)有根據(jù)事件序列的down事件找到處理目標(biāo)。(沒(méi)有能夠找到處理目標(biāo)的move、up事件,攔截并交給ViewGroup自身來(lái)處理)。

比如說(shuō)一個(gè)空的Layout布局;Layout布局不為空,但用戶從未點(diǎn)擊到任何子View上。

三、檢查事件是否被取消

            final boolean canceled = resetCancelNextUpFlag(this)
                  || actionMasked == MotionEvent.ACTION_CANCEL;

很簡(jiǎn)單兩種情況:

  1. 當(dāng)前的View或ViewGroup要被從父View中detach時(shí),PFLAG_CANCEL_NEXT_UP_EVENT就會(huì)被設(shè)為true;此時(shí),resetCancelNextUpFlag(this)返回true,canceled被賦值為true,它就不再接受觸摸事情。
  2. 觸摸事件本身就是MotionEvent.ACTION_CANCEL類型的事件。

四、根據(jù)需要,為down事件更新觸摸目標(biāo)鏈表

  1. 獲取到該觸摸事件所對(duì)應(yīng)手指ID,從觸摸目標(biāo)鏈表中清空與之對(duì)應(yīng)的所有觸摸目標(biāo)。
  2. 遍歷子View,直到找到即可接收觸摸事件,觸摸事件的坐標(biāo)又坐落在其坐標(biāo)范圍內(nèi)的childView;找到了就繼續(xù),找不到結(jié)束循環(huán)。
  3. 嘗試從已有的觸摸目標(biāo)鏈表中找到與該childView對(duì)應(yīng)的觸摸目標(biāo)實(shí)例,找到即結(jié)束。
  4. 沒(méi)有在已有鏈表中找到對(duì)應(yīng)的觸摸目標(biāo)實(shí)例,就把該事件發(fā)送給childView去處理,并生成一個(gè)觸摸目標(biāo)實(shí)例加入到觸摸目標(biāo)鏈表中。
  5. 找不到newTouchTarget,并且觸摸目標(biāo)鏈表不為空時(shí),將newTouchTarget指向觸摸目標(biāo)鏈表的最初的target去處理

?1:第一個(gè)手指按下(單點(diǎn)觸摸)時(shí)mFirstTouchTarget鏈表被清空,肯定找不到
2:多點(diǎn)觸摸的后續(xù)手指按下時(shí),從前面手指按下時(shí)產(chǎn)生的mFirstTouchTarget鏈表中尋找

五、分發(fā)觸摸事件到目標(biāo)。

如果觸摸目標(biāo)鏈表為空,直接把該ViewGroup當(dāng)成一個(gè)普通的View來(lái)處理,

如果觸摸目標(biāo)鏈表不為空,則遍歷鏈表,將事件分發(fā)給觸摸目標(biāo)對(duì)應(yīng)的childView中去。若某childView事件接收到了CANCEL事件,就從鏈表中移出該觸摸目標(biāo)。

六、處理CANCEL事件和手指抬起事件

  • 該ViewGroup接收到了ACTION_CANCEL 事件,或者是最后一個(gè)手指抬起時(shí)
    重置所有觸摸狀態(tài)來(lái)為一個(gè)新的循環(huán)做準(zhǔn)備:

      resetTouchState();
    
  • 多點(diǎn)觸摸時(shí),抬起了一根手指(非最后一個(gè))
    從觸摸鏈表中移出所有的與該手指有關(guān)的TouchTarget,不再考慮任何與該手指相關(guān)的操作(因?yàn)橐呀?jīng)抬起)。

      final int actionIndex = ev.getActionIndex();
      final int idBitsToRemove = 1 << ev.getPointerId(actionIndex);
      removePointersFromTouchTargets(idBitsToRemove);
    

http://blog.csdn.net/ns_code/article/details/49848801
http://wangkuiwu.github.io/2015/01/04/TouchEvent-ViewGroup/
http://blog.csdn.net/yanbober/article/details/45912661
http://blog.csdn.net/lfdfhl/article/details/42241253
http://www.cnblogs.com/hi0xcc/p/5583791.html

觸摸事件的安全策略

    /**
     * Filter the touch event to apply security policies.
     * 依據(jù)安全策略,過(guò)濾觸摸事件。
     * 安全策略:
     * ①:配置設(shè)定被遮擋時(shí)需要過(guò)濾觸摸事件(mViewFlags包含F(xiàn)ILTER_TOUCHES_WHEN_OBSCURED)
     * ②:觸摸事件確實(shí)被遮擋(event.getFlags()包含MotionEvent.FLAG_WINDOW_IS_OBSCURED)
     * @param event The motion event to be filtered. 需要被過(guò)濾的觸摸事件。
     * @return True if the event should be dispatched, false if the event should be dropped.
     * 該事件需要被分發(fā),則返回true。該事件需要被丟棄,則返回false。
     *
     * @see #getFilterTouchesWhenObscured
     */
    public boolean onFilterTouchEventForSecurity(MotionEvent event) {
        //noinspection RedundantIfStatement
        if ((mViewFlags & FILTER_TOUCHES_WHEN_OBSCURED) != 0
                && (event.getFlags() & MotionEvent.FLAG_WINDOW_IS_OBSCURED) != 0) {
            // Window is obscured, drop this touch.
            return false;
        }
        return true;
    }

觸發(fā)ACTION_CANCEL事件

運(yùn)行上面兩個(gè)例子,如果沒(méi)什么差錯(cuò)的話,你是不會(huì)看到ACTION_CANCEL事件的,為什么呢?

要觸發(fā)ACTION_CANCEL,就先得了解一個(gè)類ViewGroup,ViewGroup是一個(gè)放置其他views(子view)的特殊view,它是布局類(*Layout)、視圖容器(ListView、GridView、HorizontalScrollView、TabHost等等很多)的基類。

也就是說(shuō)ViewGroup一般是做為父視圖來(lái)容納、管理其他子視圖的。既然管理,在用戶手勢(shì)操作過(guò)程中,就會(huì)存在父視圖不希望子視圖響應(yīng)用戶手勢(shì)操作的情況。Android提供了一個(gè)函數(shù)public boolean onInterceptTouchEvent (MotionEvent ev),在用戶手勢(shì)操作時(shí),系統(tǒng)先調(diào)用父視圖(一個(gè)繼承自ViewGroup的類)的這個(gè)函數(shù),來(lái)決定當(dāng)前手勢(shì)操作是由父視圖還是子視圖來(lái)響應(yīng)、處理。我們仔細(xì)看看這個(gè)函數(shù)名,函數(shù)名中有一個(gè)單詞intercept,經(jīng)過(guò)查詞典,這個(gè)單詞的中文意思是攔截。在用戶的一個(gè)完整手勢(shì)操作過(guò)程中(起自ACTION_DOWN,終于ACTION_UP),對(duì)于每一次的MotionEvent``Android都會(huì)調(diào)用該函數(shù),向父視圖查詢是否攔截當(dāng)前MotionEvent,如果父視圖返回false:不攔截,則系統(tǒng)會(huì)調(diào)用子視圖的onTouchEvent函數(shù);如果父視圖返回true:攔截,則系統(tǒng)調(diào)用父視圖的onTouchEvent。等等,有人不禁要問(wèn)了,如果在這個(gè)完整手勢(shì)操作過(guò)程中,父視圖初期返回false、后期返回true會(huì)是一個(gè)什么樣的情況呢(搗亂的來(lái)了)?這個(gè)嘛,是這個(gè)樣子的,一開(kāi)始返回false,毫無(wú)疑問(wèn),子視圖會(huì)被調(diào)用onTouchEvent,但凡父視圖在函數(shù)onInterceptTouch中有一次返回了true,那這一完整手勢(shì)操作內(nèi)所有后續(xù)的MotionEvent都會(huì)調(diào)用父視圖的onTouchEvent,即使父視圖后期反悔而改成返回false也不行(沒(méi)有后悔藥)。在這種父視圖先返回false,后返回true的情況下,子視圖收不到后續(xù)的事件,而只是在父視圖由返回false改成返回true(攔截)的時(shí)候收到ACTION_CANCEL事件。

我們可以得到的結(jié)論

  • 對(duì)于一個(gè)事件序列,當(dāng)ACTION_DOWN 事件被成功攔截時(shí),那么對(duì)于剩下的一系列事件也會(huì)被攔截,并且不會(huì)再次執(zhí)行onInterceptTouchEvent方法
  • 觸摸目標(biāo)鏈表只有在 ACTION_DOWNACTION_POINTER_DOWN事件時(shí)才可能被新增,所以,如果一系列事件的后續(xù)事件(ACTION_MOVE、ACTION_UP等)要想會(huì)被處理,這一系列事件的

疑問(wèn)

  1. 在處理最初子View事件時(shí),是否會(huì)對(duì)觸摸鏈表中的子View都傳遞DOWN事件。

其他

本篇在將來(lái)的某天會(huì)有更新。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

推薦閱讀更多精彩內(nèi)容