Android事件分發機制

在我們 Android 開發中經常會遇到多個View、ViewGroup嵌套的問題,例如:當我們在一個ViewPager里面嵌套Fragment,而又在Fragment再次嵌套一個ViewPager的時候,那么兩個ViewPager就可能發生沖突,這時候就要我們對分發事件進行處理了。

在一次的完整的事件傳遞中,主要包括了三個階段:事件分發、攔截、消費。

觸摸事件的類型

首先要有事件的傳遞,那么先有事件的產生才行。那么事件的產生無疑就是通過手指對屏幕觸摸,在觸摸后,就會產生一系列的觸摸事件,觸摸事件對應的MotionEvent類,其類型主要有以下三種:

MotionEvent.ACTION_DOWN  按下View,是所有事件的開始

MotionEvent.ACTION_MOVE  滑動事件

MotionEvent.ACTION_UP    與down對應,表示抬起

正常情況下,一次手指觸摸屏幕的行為會觸發一系列點擊事件,通常有如下情況:

點擊屏幕后立即松開,事件序列為Down -> Up ,

點擊屏幕滑動滑動一會在松開,事件序列為Down -> Move => .... => Move -> up .

事件分發的三個階段

在了解了觸摸事件的三種主要類型之后,在講解Activity、View、ViewGroup事件分發的具體實現之前,先要講述事件分發分發的三個階段。

分發(Dispatch):

在Android中,所有的觸摸事件都是通過以下方法來分發事件的。

public boolean dispatchTouchEvent (MotionEvent ev)

在這個方法中,根據當前視圖的具體實現邏輯,來決定這個事件是直接消耗還是繼續分發給子視圖。

方法返回true表示事件被當前視圖消耗掉,不再繼續分發事件;方法返回值為 super.dispatchTouchEvent 表示繼續分發這個事件。

如果當前視圖是ViewGroup或者是ViewGroup的子類,則會調用 onInterceptTouchEvent 方法判斷是否攔截該事件。

攔截(Intercept):

public boolean onInterceptTouchEvent (MotionEvent ev)

這個方法只在ViewGroup或者是ViewGroup的子類中存在,View和Activity中不存在。

同理:返回true表示事件被當前視圖消耗掉,不再繼續分發事件給子視圖,同時交由自身的onTouchEvent方法進行消費;返回 false 或者返回 super.onInterceptTouchEvent 表示繼續分發這個事件。

消費(Consume):

public boolean onTouchEvent (MotionEvent ev)

事件的消費對應著 onTouchEvent 方法。

返回 true 表示當前視圖可以處理對應的事件,事件不會向上傳遞給父視圖;
返回 false 表示當前視圖不處理這個事件,事件會傳遞給父視圖的onTouchEvent方法進行處理。

用例子來深入說明一下吧:

View事件分發

新建一個View , 繼承 TextView

public class MyView extends TextView {
    private String TAG = "MyView" ;

    public MyView(Context context) {
        this(context,null);
    }

    public MyView(Context context, AttributeSet attrs) {
        this(context, attrs,0);
    }

    public MyView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }


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

        return super.dispatchTouchEvent(event);
    }

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

編寫 MyActivity ,也實現 onTouchEvent 與 dispatchTouchEvent 方法。同時也為 mTvMyView 設置點擊事件與觸碰事件。代碼如下:

public class MyActivity extends AppCompatActivity implements View.OnClickListener, View.OnTouchListener {

    private TextView mTvMyView;
    private  String TAG = "MyActivity";

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_my);
        mTvMyView = (TextView) findViewById(R.id.tv_my_view);
        mTvMyView.setOnClickListener(this);
        mTvMyView.setOnTouchListener(this);
    }


    @Override
    public void onClick(View view) {
        switch (view.getId()){
            case R.id.tv_my_view:
                Log.i(TAG, "MyView onClick ");
                break ;
        }
    }

    @Override
    public boolean onTouch(View view, MotionEvent motionEvent) {
        switch (view.getId()){
            case R.id.tv_my_view:
                switch (motionEvent.getAction()){
                    case MotionEvent.ACTION_DOWN:
                        Log.i(TAG, "MyView onTouch: ACTION_DOWN");
                        break;
                    case MotionEvent.ACTION_MOVE:
                        Log.i(TAG, "MyView onTouch: ACTION_MOVE");
                        break;
                    case MotionEvent.ACTION_UP:
                        Log.i(TAG, "MyView onTouch: ACTION_UP");
                        break;
                }
        }
        return false;
    }


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

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

布局如圖所示,只是在一個布局中添加了自己的自定義控件而已。

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/activity_my"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
        <com.zwr.androideventdispatch.view.MyView
            android:id="@+id/tv_my_view"
            android:layout_width="180dp"
            android:layout_height="180dp"
            android:layout_centerInParent="true"
            android:background="#868062"
            android:gravity="center"
            android:text="MyView"
            android:textSize="35sp" />  
</RelativeLayout>

運行 并 點擊 MyView 。

運行結果如下:

MyActivity: dispatchTouchEvent: ACTION_DOWN
MyView: dispatchTouchEvent: ACTION_DOWN
MyActivity: MyView onTouch: ACTION_DOWN
MyView: onTouchEvent: ACTION_DOWN
MyActivity: dispatchTouchEvent: ACTION_UP
MyView: dispatchTouchEvent: ACTION_UP
MyActivity: MyView onTouch: ACTION_UP
MyView: onTouchEvent: ACTION_UP
MyActivity: MyView onClick

在onTouchEvent 與dispatchTouchEvent返回的值有以下三種情況:

  1. 返回true
  2. 返回false
  3. 返回父類的同名方法

不同的返回值,最后事件的分發也會受一定的影響,所以可以畫出以下的流程圖:

  1. 由上圖可知,只要事件被攔截了,就不會再繼續分發了。
  2. 先執行onTouch方法,然后執行onClick方法。如果onTouch方法返回了true,那么onClick方法將不會被調用。

ViewGroup的事件分發機制

  1. 現在只要自定義一個ViewGroup并實現onTouchEvent、dispatchTouchEvent、onInterceptTouchEvent方法并

  2. 包裹在上述的自定義View

  3. 進行相似的操作。

     public class MyViewGroup extends RelativeLayout {
         private String TAG = "MyViewGroup";
     
         public MyViewGroup(Context context) {
             this(context,null);
         }
     
         public MyViewGroup(Context context, AttributeSet attrs) {
             this(context, attrs,0);
         }
     
         public MyViewGroup(Context context, AttributeSet attrs, int defStyleAttr) {
             super(context, attrs, defStyleAttr);
         }
     
         @Override
         public boolean dispatchTouchEvent(MotionEvent event) {
             switch (event.getAction()){
                 case MotionEvent.ACTION_DOWN:
                     Log.i(TAG, "dispatchTouchEvent: ACTION_DOWN");
                     break;
                 case MotionEvent.ACTION_MOVE:
                     Log.i(TAG, "dispatchTouchEvent: ACTION_MOVE");
                     break;
                 case MotionEvent.ACTION_UP:
                     Log.i(TAG, "dispatchTouchEvent: ACTION_UP");
                     break;
             }
     
             return super.dispatchTouchEvent(event);
         }
     
         @Override
         public boolean onTouchEvent(MotionEvent event) {
             switch (event.getAction()){
                 case MotionEvent.ACTION_DOWN:
                     Log.i(TAG, "onTouchEvent: ACTION_DOWN");
                     break;
                 case MotionEvent.ACTION_MOVE:
                     Log.i(TAG, "onTouchEvent: ACTION_MOVE");
                     break;
                 case MotionEvent.ACTION_UP:
                     Log.i(TAG, "onTouchEvent: ACTION_UP");
                     break;
             }
             return super.onTouchEvent(event);
         }
     
         @Override
         public boolean onInterceptTouchEvent(MotionEvent ev) {
             switch (ev.getAction()){
                 case MotionEvent.ACTION_DOWN:
                     Log.i(TAG, "onInterceptTouchEvent: ACTION_DOWN");
                     break;
                 case MotionEvent.ACTION_MOVE:
                     Log.i(TAG, "onInterceptTouchEvent: ACTION_MOVE");
                     break;
                 case MotionEvent.ACTION_UP:
                     Log.i(TAG, "onInterceptTouchEvent: ACTION_UP");
                     break;
             }
             return super.onInterceptTouchEvent(ev);
         }
     }
    

運行結果如下:

MyActivity: dispatchTouchEvent: ACTION_DOWN
MyViewGroup: dispatchTouchEvent: ACTION_DOWN
MyViewGroup: onInterceptTouchEvent: ACTION_DOWN
MyView: dispatchTouchEvent: ACTION_DOWN
MyActivity: MyView onTouch: ACTION_DOWN
MyView: onTouchEvent: ACTION_DOWN
MyActivity: dispatchTouchEvent: ACTION_UP
MyViewGroup: dispatchTouchEvent: ACTION_UP
MyViewGroup: onInterceptTouchEvent: ACTION_UP
MyView: dispatchTouchEvent: ACTION_UP
MyActivity: MyView onTouch: ACTION_UP
MyView: onTouchEvent: ACTION_UP
MyActivity: MyView onClick

相似的,返回不同的結果,事件的分發就有所不同。總結成流程圖如下:

事件的分發由Activity到ViewGroup,再由ViewGroup到子View。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,825評論 6 546
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,814評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,980評論 0 384
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 64,064評論 1 319
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,779評論 6 414
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 56,109評論 1 330
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 44,099評論 3 450
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,287評論 0 291
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,799評論 1 338
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,515評論 3 361
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,750評論 1 375
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,221評論 5 365
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,933評論 3 351
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,327評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,667評論 1 296
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,492評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,703評論 2 380

推薦閱讀更多精彩內容