Android View的事件分發機制探索


概述

        Android事件傳遞機制也是Android系統中比較重要的一塊,事件類型有很多種,這里主要討論TouchEvent的事件在framework層的傳遞處理機制。因為對於App開發人員來說,理解framework層的事件傳遞機制,就差不多了。

 

 

帶着問題來思考整個事件分發過程。

 

1、為什么要有事件分發過程?

        當Android設備的屏幕,接收到觸摸的動作時,屏幕驅動把壓力信號(包括壓力大小,壓力位置等)傳遞給系統底層,然后操作系統經過一系列的處理,然后把觸摸事件一層一層的向上傳遞,最終事件會被准確的傳遞到產生事件的對象上,系統會遍歷每一個View對象,然后計算觸摸點在哪一個View中。比如A和B兩個View,是兄弟View,AView產生的觸摸事件,是不會被分發到B上面的。

 

2、怎么看待事件序列?

        在Android系統中,一個單獨的事件基本上是沒什么作用的,只有一個事件序列,才有意義。一個事件序列正常情況下,定義為 DOWN、MOVE(0或者多個)、UP/CANCEL。事件序列以DOWN事件開始,中間會有0或者多個MOVE事件,最后以UP事件或者CANCEL事件結束。

DOWN事件作為序列的開始,有一個很重要的職責,就是尋找事件序列的接受者,怎么理解呢?framework 在DOWN事件的傳遞過程中,需要根據View事件處理方法(onTouchEvent)的返回值來確定事件序列的接受者。如果一個View的onTouchEvent事件,在處理DOWN事件的時候返回true,說明它願意接受並處理該事件序列。

 

3、Android的framework層如何處理事件的分發過程?

        觸摸事件到了framework層之后,首先會被傳遞到Activity,然后Activity會把事件委托給它內部的Window對象進行分發處理,而Window對象又會委托它內部的DecorView進行事件分發處理。我們都知道,DecorView是整棵View樹的根節點,所以整個事件傳遞過程的復雜度就是事件在View樹種分發傳遞的復雜度。 Android View框架提供了3個對事件的主要操作概念。

    1、事件的分發機制,dispatchTouchEvent。主要是parent根據觸摸事件的產生位置,以及child是否願意負責處理該系列事件等狀態,向其child分發事件的機制。

    2、事件的攔截機制,onInterceptTouchEvent。主要是parent根據它內部的狀態、或者child的狀態,來把事件攔截下來,阻止其進一步傳遞到child的機制。

    3、事件的處理機制,onTouchEvent。主要是事件序列的接受者(可以是一個View或者ViewGroup),對事件作出處理,並且向其parent傳遞處理結果的機制。

 

4、上述三個機制,是怎么向其調用者傳遞處理結果的?

        在Java中,傳遞計算結果,有很多種途徑,這里采用的是一種適用於同步調用的方法,返回值的方法。每個機制都使用boolean類型作為其返回值,那么每個機制的每個返回值是什么含義呢。

    1、事件的分發機制,dispatchTouchEvent。

        true-事件被以該節點為根節點的View樹成功處理,此時該事件就算是處理完成了,事件不會再向上返還給View的父節點(把事件分發過來的那個節點)。

        false-以該節點為根節點的View樹種,沒有一個View(包括該View)成功處理了此事件,所以事件會向上返還給View的父節點(把事件分發過來的那個節點)。

    2、事件的攔截機制,onInterceptTouchEvent。主要是parent根據它內部的狀態、或者child的狀態,來把事件攔截下來,阻止其進一步傳遞到child的機制。

        true-當前ViewGroup(因為View中沒有該方法,而沒有child的VIew也不需要有攔截機制)希望該事件不再傳遞給其child,而是希望自己處理。

        false-當前ViewGroup不准備攔截該事件,事件正常向下分發給其child。

    3、事件的處理機制,onTouchEvent。主要是事件序列的接受者(可以是一個View或者ViewGroup),對事件作出處理,並且向其parent傳遞處理結果的機制。

        true-表示該View成功處理了該事件,該處理結果會向上通知給其parent。

        false-表示該View沒有成功處理該事件,那么它的parent會有機會來處理該事件(parent標記為事件序列接受者,parent 的 onTouchEvent 在 Down 事件時返回true)。

 

 

 

源代碼分析

源代碼基於SDK 23

View:

1、dispatchTouchEvent:

/** 把事件分發到目標對象,因為這里是View對象,默認不含有child,所以這里他會把事件分發給自己 */

public boolean dispatchTouchEvent(MotionEvent event);

源代碼:

不給出,有興趣的讀者執行查閱SDK

偽代碼:

public boolean dispatchTouchEvent(MotionEvent event){
    boolean result = false;
    //如果有事件監聽器,先讓監聽器處理事件。
    if (mOnTouchListener.onTouch(event)) {
        //如果監聽器成功處理了該事件,處理結果設置為true。
        result = true;
    }
    //如果沒有監聽器,就調用自身的onTouchEvent方法來處理事件。
    if (!resutlt && onTouchEvent(event)) {
        //如果自身的onTouchEvent成功處理事件,處理結果設置為true。
        result = true;
    }
    return result;
}

 


 

 

ViewGroup:

1、onInterceptTouchEvent

/** 默認實現是返回false,也就是默認不攔截任何事件 */

public boolean onInterceptTouchEvent(MotionEvent ev);

 

2、dispatchTouchEvent

/** 根據內部攔截狀態,向其child或者自己分發事件 */

public boolean dispatchTouchEvent(MotionEvent ev);

源代碼:

不給出,有興趣的讀者執行查閱SDK

偽代碼:

public boolean dispatchTouchEvent(MotionEvent ev) {
    if (ACTION_DOWN事件 || 沒有事件處理對象) {
        if (允許攔截事件,該標志位由child調用requestDisallowInterceptTouchEvent<span style="font-family:微軟雅黑;font-size:14px;">設置</span>) {
            //查詢攔截機制的結果,根據該結果來判斷是否需要攔截
            intercepted = onInterceptTouchEvent(ev);
        } else {
            //不允許攔截,那么不攔截
            intercepted = false;
        }
    } else { 
        //不是DOWN,並且有處理對象,允許攔截,中斷事件傳遞
        intercepted = true;
    }

    if (不取消 && 不攔截) {
        if (ACTION_DOWN) { //找尋接收事件序列的對象,其他事件不需要再計算事件產生對象,試想一下滑動一個ListView,當手指滑動出ListView的范圍時,依然還是ListView響應后續事件。
            for (遍歷所有childView) {
                if (觸摸點不在childView內部) {
                    continue;
                }
                if (childView.dispatchTouchEvent(event)) {
                    保存處理該事件的View,后續事件直接傳遞到該View,不要重新計算;
                }
            }
        }

        if (還沒有事件處理對象) {
            //當前View樹中沒找到合適的child處理對象,把事件給自己處理,View.dispatchTouchEvent()就是把事件分發給自己
            super.dispatchTouchEvent(event);
        } else {
            //傳遞給child
            childView.dispatchTouchEvent(event);
        }
    } else if (攔截) {
        //攔截事件,把事件給自己處理,View.dispatchTouchEvent()就是把事件分發給自己
        super.dispatchTouchEvent(event);
    }

    return 處理結果;
}

 


 

 

3、requestDisallowInterceptTouchEvent

/** 干澀parent的事件分發機制,通知parent,是否攔截后續事件,如果設置為true,parent就不會攔截該事件,不管什么狀態。設置為false,parent走正常的攔截流程 */

public void requestDisallowInterceptTouchEvent(boolean disallowIntercept);

源代碼:

不給出,有興趣的讀者執行查閱SDK

偽代碼:

public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {
    if (已經是當前要設置的狀態) {
        // 已經處於這個狀態, 假設我們的parent也是這個狀態
        return;
    }
    設置該狀態;
    // 傳遞給parent
    if (有父容器) {
        設置父容器的攔截狀態;
    }
}

 


 

 

自己動手

我們都知道,如果ScrollView內部嵌套ListView,那么ListView是不可以滑動的,效果如下圖所示:

 

 

那么其實這就是典型的事件沖突問題,就是說,原本應該被ListView用來上下滑動的事件,被ScrollView攔截了。就導致ListView不能正常滑動。

我們來看一下ScrollView的源代碼:

onInterceptTouchEvent的偽代碼:

public boolean onInterceptTouchEvent(MotionEvent ev) {
    /*
     * 這個方法決定了我們是否要攔截事件.
     * 如果返回true, onTouchEvent會被調用並且我們開始做實際的Scroll操作.
     */

    /*
    * 大部分循環的狀態: 用戶在再拖拽的狀態並且正在移動手指,
    * 我們希望攔截這個事件
    */
    final int action = ev.getAction();
    if ((action == MotionEvent.ACTION_MOVE) && (mIsBeingDragged)) {
        return true;
    }
    
    //其他操作
    ......................
}

 


所以正常的上下拖拽,ScrollView都會攔截。

 

 

那么我們下面改進一下,就是當我們滑動ScrollView中非ListView的區域時,ScrollView滑動,而我們滑動ListView的時候,ListView滑動,效果看起來如下圖所示:

 

這里解決方法如下:

既然ScrollView會攔截事件,那么當我們滑動ListView的時候,我們不希望ScrollView攔截事件,這里我們繼承ListView,在onTouchEvent中,請求ScrollView不要攔截事件。

部分代碼如下:

@Override
public boolean onTouchEvent(MotionEvent ev) {
    super.onTouchEvent(ev);
    switch (ev.getAction()) {
        case MotionEvent.ACTION_DOWN:
            getParent().requestDisallowInterceptTouchEvent(true);
            break;
        case MotionEvent.ACTION_MOVE:
            break;
        case MotionEvent.ACTION_UP:
            getParent().requestDisallowInterceptTouchEvent(false);
            break;
        default:
            break;
    }
    return  true;
}

 


這樣就可以很好的解決事件沖突的問題。

還有一種方法就是覆寫parent的onInterceptTouchEvent方法,來修改事件攔截的狀態。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM