從Log中看View的事件分發機制

怕什麼真理無窮 進一寸有進一寸的歡喜

在實際項目的開發中,View的事件分發機制起到了很重要的作用,尤其是遇到不按常理出牌的產品經理的時候,沒事給你一個嵌套,沒事再給你加個嵌套,那麼事件分發就顯得尤爲重要。對於經常打交道的滑動衝突,瞭解了View事件分發的原理,也就可以很容易的解決。實踐出真知,我們從一個Demo出發,通過Log的形式分析View是如何將事件分發的,又是如何攔截並作出相應處理的。

一、創建Demo

首先,創建一個簡單的Demo,自定義最外層的ViewGroup,命名爲CustomViewGroup1,重寫onInterceptTouchEvent,onTouchEvent,dispatchTouchEvent三個方法,並將背景設爲紅色。

public class CustomViewGroup1 extends FrameLayout {
    private static final String TAG = "CustomeViewGroup1";

    public CustomViewGroup1(Context context) {
        super(context);
    }

    public CustomViewGroup1(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        Log.d(TAG, "onInterceptTouchEvent:" );
        return super.onInterceptTouchEvent(ev);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        Log.d(TAG, "onTouchEvent:");
        return super.onTouchEvent(event);
    }

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        Log.d(TAG, "dispatchTouchEvent:");
        return super.dispatchTouchEvent(ev);
    }
}

和上述代碼一樣,自定義中間層的CustomViewGroup2,重寫攔截和處理的方法,將背景設爲藍色。
接着自定義View,重寫onTouchEvent和dispatchTouchEvent方法,將背景設置爲黃色。

public class CustomView extends Button {
    private static final String TAG = "CustomView";

    public CustomView(Context context) {
        super(context);
    }

    public CustomView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        Log.d(TAG, "onTouchEvent:");
        return super.onTouchEvent(event);
    }

    @Override
    public boolean dispatchTouchEvent(MotionEvent event) {
        Log.d(TAG, "dispatchTouchEvent:");
        return super.dispatchTouchEvent(event);
    }
}

最後運行的效果爲

紅色爲最外層CustomViewGroup1,藍色爲中間層CustomViewGroup2,黃色爲CustomView。

二、分析

2.1

從上面自定義ViewGroup的代碼可以看出,View的事件分發涉及到了幾個重要的方法,磨刀不誤砍柴工,下面就先來分析一下這幾個方法。

  • boolean dispatchTouchEvent(MotionEvent event):事件分發
  • boolean onInterceptTouchEvent(MotionEvent ev):事件攔截
  • boolean onTouchEvent(MotionEvent event):事件處理

dispatchTouchEvent爲事件分發的第一步,將事件向下分發;onInterceptTouchEvent表示是否攔截當前事件,當返回true時,表示攔截,也就是下面的事件就不會觸發;onTouchEvent表示處理事件,返回結果表示是否消耗當前事件。這裏不清楚沒關係,接着看下面的分析。

上述三種方法默認返回false,不攔截也不消耗事件

2.2

接下來就通過Log一步一步的看事件分發的過程。當點擊黃色界面,也就是最底層的View時,Log如下:


從上面的Log可以看出,在默認的情況下,最先觸發CustomeViewGroup1(紅色界面)的dispatchTouchEvent和onInterceptTouchEvent方法,接着觸發CustomViewGroup2(藍色界面)的dispatchTouchEvent和onInterceptTouchEvent方法,最後才觸發CustomView(黃色界面)的dispatchTouchEvent和onTouchEvent,一級一級向下傳遞。
上述爲默認情況,不攔截也不消耗任何事件,事件就會一層一層的向下傳遞,那麼當我們在第一層CustomeViewGroup1的onInterceptTouchEvent中返回true,將點擊事件攔截下來,會出現什麼情況呢?

public class CustomViewGroup1 extends FrameLayout {
...
 @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        Log.d(TAG, "onInterceptTouchEvent:" );
        return true;
    }
}

同樣點擊黃色區域,再看Log:



這時我們發現,Log上只走了CustomeViewGroup1的方法,並且走了onTouchEvent,這就說明CustomeViewGroup1將事件攔截了下來,並沒有分發下去,自己就將事件進行了處理。那麼我們再試着將CustomeViewGroup2的onInterceptTouchEvent返回true,依據上面的分析我們猜測到CustomeViewGroup2就停止傳遞,接着會觸發onTouchEvent。同樣我們修改代碼並點擊VIew黃色區域。

public class CustomViewGroup2 extends FrameLayout {
...
    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        Log.d(TAG, "onInterceptTouchEvent: ");
        return true;
    }
}

從上述Log可以看出我們的猜測是正確的,在CustomeViewGroup2進行攔截,事件在分發到CustomeViewGroup2就沒有繼續向下分發了。不過我們又發現Log的最後兩行,先觸發CustomViewGroup2的onTouchEvent接着觸發CustomViewGroup1的 onTouchEvent,這是屬於onTouchEvent返回false,CustomViewGroup2沒有消耗掉當前事件,導致將事件返回給了他的上一層處理。

2.3、小結

從上面的Log分析我們可以得出三種不同情況下的事件分發規則:
1、在正常情況下,三個方法的返回值都爲false,不攔截也不消耗事件,那麼事件就會從ViewGroup1傳遞到ViewGroup2再傳遞到最後的View。
2、當ViewGroup1的onInterceptTouchEvent方法返回true,那麼ViewGroup1就攔截下了這個事件,後面的ViewGroup2和View就接受不到任何事件的信號,並調用ViewGroup1的onTouchEvent方法對事件進行處理。
3、當ViewGroup2的onInterceptTouchEvent方法返回true,那麼ViewGroup2攔截下當前事件,並調用自己的onTouchEvent方法,若ViewGroup2的onTouchEvent返回false,表示不消耗這個事件,那麼就會返回他的上一層,將這個事件交給上一層處理,也就是ViewGroup1的onTouchEvent方法會被調用。相反,ViewGroup2的onTouchEvent返回true,就不會返回給上一層。

簡單的打個比方,ViewGroup1代表校長,ViewGroup2代表老師,View則代表學生
1、正常情況下是校長將一個任務首先下發給老師,接着老師下發給學生,學生收到任務,並執行該任務。
2、第二種情況,校長髮現該任務自己可以處理就不用下發給老師,及校長的onInterceptTouchEvent返回true,將這個任務攔截下來了。後面的老師和學生也就不知道該任務是什麼。
3、第三種情況,校長將任務下發給老師,老師卻將任務攔截了下來,並沒有告訴學生,即ViewGroup2的onInterceptTouchEvent方法返回true。當老師準備自己處理的時候發現這個任務太難,自己解決不了,就onTouchEvent方法返回false,將事情又返回給更高一級的校長處理。

三、onTouchEvent

接下來分析onTouchEvent中是如何處理動作的,我們在onTouchEvent的方法中加上三種最常見的動作的Case,ACTION_DOWN手指點擊,ACTION_MOVE手指滑動,ACTION_UP手指擡起。

   @Override
    public boolean onTouchEvent(MotionEvent event) {
        Log.d(TAG, "onTouchEvent:");
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                Log.d(TAG, "onTouchEvent: ACTION_DOWN");
                break;
            case MotionEvent.ACTION_MOVE:
                Log.d(TAG, "onTouchEvent: ACTION_MOVE");
                break;
            case MotionEvent.ACTION_UP:
                Log.d(TAG, "onTouchEvent: ACTION_UP");
                break;
        }
        return super.onTouchEvent(event);
    }

點擊畫面並滑動,



我們可以看到日誌Log上只有ACTION_DOWN事件發生,並沒有觸發滑動MOVE和手指擡起的UP事件,這是因爲ACTION_DOWN事件默認是返回false,表示不消耗DOWN事件,也就是說DOWN動作發生後是不會向下傳遞,也不會觸發MOVE和UP的動作。所以我們需要觸發後面的動作時,需要將返回值設爲 true,表示消耗當前事件。

   @Override
    public boolean onTouchEvent(MotionEvent event) {
        Log.d(TAG, "onTouchEvent:");
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                Log.d(TAG, "onTouchEvent: ACTION_DOWN");
                return true;
            case MotionEvent.ACTION_MOVE:
                Log.d(TAG, "onTouchEvent: ACTION_MOVE");
                break;
            case MotionEvent.ACTION_UP:
                Log.d(TAG, "onTouchEvent: ACTION_UP");
                break;
        }
        return super.onTouchEvent(event);
    }

這時我們同樣去點擊和滑動畫面,


可以從上圖看到,MOVE和UP動作都相應觸發。

四、最後

至此,View的事件分發基本上從頭捋了一遍,更多的源碼的細節準備好好研究一下,在後面的文章中展示出來。其實將事件分發的文章網絡上比比皆是,本沒有想法去記錄這篇文章,只是因爲前幾天在做Android測試題的時候,有涉及到事件分發,當時將onInterceptTouchEvent和onTouchEvent的返回值弄反。原本以爲自己記得很清楚,到了用的時候卻弄混,才發現有些東西需要自己去好好的寫上一遍,纔會真正明白,也記得更牢。古人云:好記性不如爛筆頭,不是沒有道理的。


文章同步個人博客:https://fuusy.github.io
公衆號:小猿說,歡迎關注,分享Android知識。

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