Android事件機制深入探討(一)

簡述

關於View傳遞事件的博文很多,看的讓人眼花繚亂,最近有點時間,把自己所瞭解的做一個總結,直接進入主題了。事件的傳遞主要有三個方法:dispatchTouchEvent(事件分發)、onInterceptTouchEvent(事件攔截)、onTouchEvent(事件消費)。如下圖:

事件 Activity ViewGroup View
dispatchTouchEvent
onInterceptTouchEvent 沒有 沒有
onTouchEvent

從上面的表格我們可以看出只有攔截事件比較特殊,只存在ViewGroup中,也就是我們只能在ViewGroup中才能重寫該方法。這三個方法都有返回值,返回值爲true的話表示該事件被消費,事件傳遞終止,反之返回false,事件繼續傳遞。
事件分成好幾種類型,我們常用的就三種,從手指按下移動到擡起依次爲:ACTION_DOWN(按下)、ACTION_MOVE(移動)、ACTION_UP(擡起)。

事件的傳遞過程

事件的傳遞在我們手指按下(ACTION_DOWN)的瞬間發生了,如果手指有移動會觸發若干個移動事件(ACTION_MOVE),當你手指擡起時會觸發ACTION_UP事件,這樣爲一個事件序列。我們先來看看單個事件時怎麼傳遞的,如下一個Demo,一個Activity放有一個ViewGroup,ViewGroup放有一個View,其中ViewGroup和View都是我們自定義的,分別繼承與ViewGroup和View的子類,代碼如下:
Activity代碼如下:

public class TouchEventActivity extends AppCompatActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_touch_event);
    }

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        Log.e(getClass().getSimpleName(), "這是Activity的--->dispatchTouchEvent");
        return super.dispatchTouchEvent(ev);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        Log.e(getClass().getSimpleName(), "這是Activity的--->onTouchEvent");
        return super.onTouchEvent(event);
    }
}

佈局文件如下:
Activity所對應的佈局文件
其中自定義控件CustomLinearLayout代碼如下:

public class CustomLinearLayout extends LinearLayout {

    ....

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        Log.e(getClass().getSimpleName(), "這是ViewGroup的--->dispatchTouchEvent");
        return super.dispatchTouchEvent(ev);
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        Log.e(getClass().getSimpleName(), "這是ViewGroup的--->onInterceptTouchEvent");
        return super.onInterceptTouchEvent(ev);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        Log.e(getClass().getSimpleName(), "這是ViewGroup的--->onTouchEvent");
        return super.onTouchEvent(event);
    }
}

CustomTextView代碼如下:

public class CustomTextView extends TextView {

    ...

    @Override
    public boolean dispatchTouchEvent(MotionEvent event) {
        Log.e(getClass().getSimpleName(), "這是View的--->dispatchTouchEvent");
        return super.dispatchTouchEvent(event);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        Log.e(getClass().getSimpleName(), "這是View的--->onTouchEvent");
        return super.onTouchEvent(event);
    }
}

效果如下:
效果圖

運行,我們點擊自定義的TextView,打印出來的日誌如下:
事件日誌
什麼意思呢?不要着急,接下來我慢慢解釋。首先我們知道一次點擊,會觸發一次ACTION_DOWN、若干個ACTION_MOVE、一次ACTION_UP事件,而一次事件的傳遞是由上往下傳遞的,也就是依次通過Activity、ViewGroup、View。按下的瞬間ACTION_DOWN觸發,Activity的dispatchTouchEvent(事件分發)會先調用,這個跟我們的第一行的日誌不謀而合,其實Activity的dispatchTouchEvent方法可以用下面的僞代碼表示:

@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
    if (viewGroup或者view.dispatchTouchEvent(ev)) {
        return true;
    }
    return onTouchEvent(ev);
}

這裏我不貼源代碼了,僞代碼看比較好理解。
Activity中的dispatchTouchEvent會調用ViewGroup或者View的dispatchTouchEvent的方法,而當ViewGroup或者View返回false時纔會調用本身的onTouchEvent方法。第一行日就容易理解了,是執行Activity的dispatchTouchEvent打印出來的,而後會調用CustomLinearLayout的dispatchTouchEvent的事件分發方法,這就有了第二行的日誌。

這裏有個知識點,就是ViewGroup的dispatchTouchEvent方法會調用自身的onInterceptTouchEvent(事件攔截)方法,這一點跟Activity中的有點不一樣,因爲Activity中並沒有事件攔截方法,如果ViewGroup的onInterceptTouchEvent事件攔截方法返回true,那麼View中的dispatchTouchEvent方法不會被調用,反而會執行ViewGroup的onTouchEvent方法,那麼該事件(ACTION_DOWN事件)傳遞結束。

如果返回的是false(默認就是返回false),那麼View 中的dispatchTouchEvent方法會被調用,因爲View是最底層的控件,事件無法繼續再往下傳遞,只能自身消費,所以dispatchTouchEvent又會調用onTouchEvent方法,在我們這個例子中,onTouchEvent返回的是默認值false,也就是沒有消費該事件。

我們知道事件的傳遞是從上往下傳遞的,那麼當事件傳遞到最底層的View並且該事件沒有被消費,又該如何呢?其實上面的日誌已經告訴我們了,當最底層的View並沒有消費該事件時,該事件會一層層往上拋,接下來會執行ViewGroup的onTouchEvent方法,如果返回true的話,事件傳遞停止,如果還是一樣返回默認值false的話,Activity的onTouchEvent方法會被調用,到此ACTION_DOWN事件的傳遞結束,這就是一次完整的事件傳遞過程,下圖爲事件傳遞的流程圖:

事件傳遞流程圖

細心同學又會問了,既然事件的傳遞結束了,爲什麼Activity的dispatchTouchEvent、onTouchEvent又被執行了兩次呢(日誌打印出來的)?

沒錯,確實是執行了,剛纔我們說過了:手指按下移動到擡起,會執行一次ACTION_DOWN(按下)、若干次ACTION_MOVE(移動)和一次ACTION_UP(擡起),被執行了兩次是因爲執行了一次ACTION_MOVE(一次是偶然的,如果你手指多滑動,會執行多次的)和一次ACTION_UP事件,也就是還有兩次完整的事件傳遞過程,但是我們發現後面這兩次跟ACTION_DOWN不一樣,只調用兩次Activity的dispatchTouchEvent、onTouchEvent方法,這是爲什麼呢?

因爲Android本身的事件傳遞機制就是這樣的,我們把手指按下擡起所發生的事件傳遞稱爲一個事件序列,看似3個或3個以上獨立的事件組成,其實不然,它們還是有聯繫的,因爲當dispatchTouchEvent在進行事件分發的時候,只有前一個action返回true,纔會觸發下一個action,什麼意思呢?剛纔的例子Activity的dispatchTouchEvent的方法中viewGroup或者view.dispatchTouchEvent(ev)返回的是默認值false,接下來ACTION_MOVE、ACTION_UP兩個事件並不會觸發ViewGroup的dispatchTouchEvent方法(因爲你前一個action【ACTION_UDOWN】返回的false),反而是直接執行自身的onTouchEvent的方法。所以打印出來的日誌就是這樣的。這告訴我們如果一個事件序列的ACTION_DOWN事件你沒消費掉,那麼該事件序列的ACTION_MOVE、ACTION_UP並不會在被執行了。

接下來我們稍微改一下代碼,把CustomTextView的onTouchEvent改成返回true,如下:

@Override
public boolean onTouchEvent(MotionEvent event) {
    Log.e(getClass().getSimpleName(), "這是View的--->onTouchEvent");
    return true;
}

運行點擊,我們來看一下日誌,如下圖:
日誌
CustomTextView的onTouchEvent消費了事件,所以該序列的後續事件都會完整的傳遞到CustomTextView中,並且都會在該方法中終止事件的傳遞。

我們再來看看把CustomTextView的dispatchTouchEvent也改成直接返回true,是個什麼情況,完整的代碼如下:

public class CustomTextView extends TextView {

    ...

    @Override
    public boolean dispatchTouchEvent(MotionEvent event) {
        Log.e(getClass().getSimpleName(), "這是View的--->dispatchTouchEvent");
        return true;
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        Log.e(getClass().getSimpleName(), "這是View的--->onTouchEvent");
        return true;
    }
}

執行點擊,日誌如下:
日誌
我們看到每次事件都會傳遞到View的dispatchTouchEvent,但是onTouchEvent並不會被執行,關鍵代碼就在View中的dispatchTouchEvent返回值不一樣:

    return super.dispatchTouchEvent(ev);
    return true;

因爲onTouchEvent是在super.dispatchTouchEvent方法中執行的,所以我們雖然返回了true,每次事件都會傳遞過來但是並不會執行onTouchEvent方法。

哈哈,是不是有點複雜啊,好好品味哈,不然後面越看你會越亂。

接下來我們也把ViewGroup(CustomLinearLayout)代碼也改一下,把方法onInterceptTouchEvent的返回值改爲true,代碼如下:

@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
    Log.e(getClass().getSimpleName(), "這是ViewGroup的--->onInterceptTouchEvent");
    return true;
}

點擊執行,日誌如下:

日誌
我們可以看到事件傳遞到ViewGroup的onInterceptTouchEvent後會直接調用本身的onTouchEvent方法,並沒有把事件傳遞給View的dispatchTouchEvent方法,因爲返回true,就表明我們攔截了事件並把事件交給自己處理,也阻止了事件繼續往下傳遞,但是我們雖然攔截了事件,但並沒有消費該事件,所以後續的事件ViewGroup並沒有接收到,現在我們再把ViewGroup的onTouchEvent改爲返回 true,代碼如下:

@Override
public boolean onTouchEvent(MotionEvent ev) {
    Log.e(getClass().getSimpleName(), "這是ViewGroup的--->onTouchEvent");
    return true;
}

運行、點擊,日誌如下:
日誌
如我們所說,onTouchEvent返回了true消費了該事件,每次事件都會傳遞到ViewGroup,並且在onTouchEvent結束事件的傳遞,但是你們發現沒有onInterceptTouchEvent只會被執行一次,沒錯就是這麼奇葩,只要你攔截了該事件,就是這樣的,onInterceptTouchEvent並不會執行第二次,讀者記住,不要問我爲什麼,機制就是這樣的,爲什麼要這樣,我也不懂,哈哈。

接下來我們把ViewGroup的dispatchTouchEvent的返回值也改成true,完整的代碼如下:

public class CustomLinearLayout extends LinearLayout {

    ...

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        Log.e(getClass().getSimpleName(), "這是ViewGroup的--->dispatchTouchEvent");
        return true;
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        Log.e(getClass().getSimpleName(), "這是ViewGroup的--->onInterceptTouchEvent");
        return true;
}

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        Log.e(getClass().getSimpleName(), "這是ViewGroup的--->onTouchEvent");
        return true;
    }
}

一樣,點擊,運行,日誌如下:
日誌
是不是有點懵逼的感覺,沒錯,就是這麼任性,哈哈,事件傳遞到ViewGroup的dispatchTouchEvent中,這時,我們返回true,表明我們消費了該事件,所以後續事件都會繼續傳遞過來,但是我們直接把

return super.dispatchTouchEvent(ev);

改成:

return true;

ViewGroup的onInterceptTouchEvent和View的dispatchTouchEvent並不會被執行了,因爲這些都放在super.dispatchTouchEvent裏面執行的,所以我們打印出來的日誌就這樣了,有同學會問Activity的onTouchEvent爲什麼沒被執行,那你就沒認真看之前的僞代碼了,我們返回了true,Activity的onTouchEvent是不會被執行的,再好好想想哈。

最後我們再改個地方的代碼,就是把Activity的dispatchTouchEvent的返回值改爲返回true,完整的如下:

public class TouchEventActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_touch_event);
    }

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        Log.e(getClass().getSimpleName(), "這是Activity的--->dispatchTouchEvent");
        return true;
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        Log.e(getClass().getSimpleName(), "這是Activity的--->onTouchEvent");
        return false;
    }
}

運行,點擊,日誌如下:
日誌
我們看到只有Activity的dispatchTouchEvent被執行了3次,知道爲什麼了吧,這個就不仔細闡述了。

這次的事件機制的講解就先到這裏,下次我們再深入瞭解,因爲事件機制很複雜,所以讀者先好好理解這篇所講的內容,不然後面會越亂。
(小編想說爲了讓你們看得簡約一點,點擊一次剛好要執行一次ACTION_MOVE方法好累,得試好多次,哈哈,看在這樣,請請請關注、收藏,謝謝了)

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章