Android觸控事件傳遞機制

Zson發表於2019-03-04

[宣告] 本文來自《顧浩鑫-Android高階進階》第一章 的學習筆記,發文目的在於傳遞分享知識。如涉及文章內容、版權和其它問題,請與本人聯絡,我將在第一時間刪除內容!

前言:在Android開發中,經常會遇到觸控事件衝突,比如ViewPager的輪播圖跟Fragment的划動事件衝突,或者輪播圖跟下拉事件衝突,自定義view的事件處理等,本文章將會詳細介紹Activity、View、ViewGroup三者的觸控事件傳遞機制,傳遞包括三個階段:分發、攔截、消費。
複製程式碼

本文章將會詳細介紹Activity、View、ViewGroup三者的觸控事件傳遞機制,傳遞包括三個階段:分發、攔截、消費。

一.觸控事件的型別

觸控事件對應的是 MotionEvent 類,事件型別主要有三種:

  1. ACTION_DOWN:使用者按下操作,表示一次觸控事件的開始。
  2. ACTION_MOVE:在按下的情況下,進行移動。輕微的移動都會傳遞到該事件。
  3. ACTION_UP:使用者手指離開螢幕,表示一次觸控事件的

注 :如果使用者僅僅的是點選而已,則只會執行到 ACTION_DOWN 和 ACTION_UP 兩個事件,不會執行到 ACTION_MOVE 事件。所以 ACTION_DOWN 和 ACTION_UP 是事件是必須的。

二.觸控事件的傳遞階段

1.分發(Dispatch)

在Android系統中所有的觸控事件都是由 dispatchTouchEvent 方法進行分發的。該方法中判斷事件是被消費(return true),還是繼續分發給子檢視處理(return super.dispatchTouchEvent),如果當前檢視是ViewGroup或者其子類,則會呼叫onInterceptTouchEvent 判斷是否截攔。

    @Override
    public boolean dispatchTouchEvent(MotionEvent event) {
        return super.dispatchTouchEvent(event);
    }
複製程式碼

2.截攔(Intercept)

事件的截攔 InterceptTouchEvent 只存在於ViewGroup及其子類,activity和View是不存在該方法。該方法判斷事件是被截攔 (return true)並交給自身的 OnToucEvent 方法進行消費,還是繼續傳遞給子檢視(return super.InterceptTouchEvent 或者 return false)。

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        return super.onInterceptTouchEvent(ev);
    }
複製程式碼

3.消費(Consume)

事件的消費通過 OnTouchEvent 方法判斷,是被消費(return true),還是不處理(return false)並將事件傳遞給父檢視的 OnTouchEvent 方法進行處理。

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        return super.onTouchEvent(event);
    }
複製程式碼

所有擁有事件傳遞能力的類:

Activity: 擁有dispatchTouchEvent 、OnTouchEvent

ViewGroup: 擁有dispatchTouchEvent 、OnInterceptTouchEvent 、OnTouchEvent

View:擁有dispatchTouchEvent 、OnTouchEvent

三、View的事件傳遞機制

3.1 dome

雖然說ViewGroup是View的子類,但是這是說的View指的是除ViewGroup之外的View控制元件子類,首先定義一個MyTextView繼承TextView,列印每次事件的觸發以變了解事件傳遞的流程。

MyTextView 類

public class MyTextView extends TextView {

    private String tag = "MyTextView";

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

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

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


    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()){
            case MotionEvent.ACTION_UP:
                Log.i(tag, "onTouchEvent ACTION_UP");
                break;
            case MotionEvent.ACTION_MOVE:
                Log.i(tag, "onTouchEvent ACTION_MOVE");
                break;
            case MotionEvent.ACTION_DOWN:
                Log.i(tag, "onTouchEvent ACTION_DOWN");
                break;
        }
        return super.onTouchEvent(event);
    }
}
複製程式碼

定義一個MainActivity來展現這個MyTextView,同時設定點選(onClick)和觸控(onTouch)監聽。 MainActivity 類

public class MainActivity extends AppCompatActivity implements View.OnClickListener,View.OnTouchListener{
    private MyTextView mMyTextView;
    private String tag = "MainActiviy";

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

        mMyTextView = findViewById(R.id.text_view);
        //  點選監聽
        mMyTextView.setOnClickListener(this);
        //  觸碰監聽
        mMyTextView.setOnTouchListener(this);
    }


    // MyTextView 點選事件
    @Override
    public void onClick(View view) {
        switch (view.getId()){
            case R.id.text_view:
                Log.i(tag, "MyTextView onClick");
                break;
        }
    }

    // MyTextView 觸碰事件
    @Override
    public boolean onTouch(View view, MotionEvent motionEvent) {
        switch (motionEvent.getAction()){
            case MotionEvent.ACTION_UP:
                Log.i(tag, "MyTextView onTouch ACTION_UP");
                break;
            case MotionEvent.ACTION_MOVE:
                Log.i(tag, "MyTextView onTouch ACTION_MOVE");
                break;
            case MotionEvent.ACTION_DOWN:
                Log.i(tag, "MyTextView onTouch ACTION_DOWN");
                break;
        }
        return false;
    }

    // Activity 的事件分發
    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        switch (ev.getAction()){
            case MotionEvent.ACTION_UP:
                Log.i(tag, "dispatchTouchEvent ACTION_UP");
                break;
            case MotionEvent.ACTION_MOVE:
                Log.i(tag, "dispatchTouchEvent ACTION_MOVE");
                break;
            case MotionEvent.ACTION_DOWN:
                Log.i(tag, "dispatchTouchEvent ACTION_DOWN");
                break;
        }
        return super.dispatchTouchEvent(ev);
    }

    // Activity 的事件消費
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()){
            case MotionEvent.ACTION_UP:
                Log.i(tag, "onTouchEvent ACTION_UP");
                break;
            case MotionEvent.ACTION_MOVE:
                Log.i(tag, "onTouchEvent ACTION_MOVE");
                break;
            case MotionEvent.ACTION_DOWN:
                Log.i(tag, "onTouchEvent ACTION_DOWN");
                break;
        }
        return super.onTouchEvent(event);
    }
}

複製程式碼

3.2 列印日誌

執行後,點選Text View反饋的列印日誌

03-28 08:05:14.824 1219-1219/com.mvp.chenzhesheng.androidadvance I/MainActiviy: dispatchTouchEvent ACTION_DOWN
03-28 08:05:14.824 1219-1219/com.mvp.chenzhesheng.androidadvance I/MyTextView: dispatchTouchEvent ACTION_DOWN
03-28 08:05:14.824 1219-1219/com.mvp.chenzhesheng.androidadvance I/MainActiviy: MyTextView onTouch ACTION_DOWN
03-28 08:05:14.824 1219-1219/com.mvp.chenzhesheng.androidadvance I/MyTextView: onTouchEvent ACTION_DOWN
03-28 08:05:15.034 1219-1219/com.mvp.chenzhesheng.androidadvance I/MainActiviy: dispatchTouchEvent ACTION_UP
03-28 08:05:15.034 1219-1219/com.mvp.chenzhesheng.androidadvance I/MyTextView: dispatchTouchEvent ACTION_UP
03-28 08:05:15.034 1219-1219/com.mvp.chenzhesheng.androidadvance I/MainActiviy: MyTextView onTouch ACTION_UP
03-28 08:05:15.034 1219-1219/com.mvp.chenzhesheng.androidadvance I/MyTextView: onTouchEvent ACTION_UP
03-28 08:05:15.044 1219-1219/com.mvp.chenzhesheng.androidadvance I/MainActiviy: MyTextView onClick
複製程式碼

dispatchTouchEvent 、 OnTouchEvent 這兩個方法的返回值存在三種情況:

  1. 直接返回true。
  2. 直接返回false。
  3. 返回父類同名方法,super.dispatchTouchEvent 或者 super.OnTouchEvent。

由於擁有不同的返回值,所以事件傳遞流程也有不同,經過不斷修改返回值測試,最終得到了點選事件的流程圖,ACTION_DOWN 和 ACTION_UP 事件的傳遞流程是相同的。

3.3 事件傳遞流程圖

Android觸控事件傳遞機制
從上面的流程圖可以得出結論:

  1. 觸控事件是從 dispatchTouchEvent 開始的,預設返回父類同名方法 super ,事件將會依照巢狀層次從外向內傳遞( MainActivityMyTextView ),到達最內層的 View 時,將由 ViewOnTouchEvent 方法處理,該方法返回 true 時進行消費不再傳遞,返回 false 時再由內向外傳遞,由外層的 OnTouchEvent 處理。
  2. 如果外層向內層傳遞過程中,人為干擾返回 true 消費,則不會繼續繼續像內部傳遞。
  3. View 的事件控制順序先執行 onTouch 再執行 onClick ,如果 onTouch 返回 true 消費,則不會繼續傳遞,也不會執行 onClick 方法。

四、ViewGroup的事件傳遞機制

4.1 dome

ViewGroupView 的控制元件容器存在,擁有 dispatchTouchEventonInterceptTouchEventonTouchEvent 三個方法,比 View 多了一個 onInterceptTouchEvent 方法。為了更好的觀察,我們需要自定義 MyRelativeLayout 繼承 RelativeLayout

MyRelativeLayout類

public class MyRelativeLayout extends RelativeLayout {

    private final static String tag = "MyRelativeLayout";

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

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

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

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

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()){
            case MotionEvent.ACTION_UP:
                Log.i(tag, "onTouchEvent ACTION_UP");
                break;
            case MotionEvent.ACTION_MOVE:
                Log.i(tag, "onTouchEvent ACTION_MOVE");
                break;
            case MotionEvent.ACTION_DOWN:
                Log.i(tag, "onTouchEvent ACTION_DOWN");
                break;
        }
        return super.onTouchEvent(event);
    }
}
複製程式碼

main_activity.xml 檔案

<?xml version="1.0" encoding="utf-8"?>
<com.mvp.chenzhesheng.androidadvance.MyRelativeLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    >

    <com.mvp.chenzhesheng.androidadvance.MyTextView
        android:id="@+id/text_view"
        android:clickable="true"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Hello World!"

</com.mvp.chenzhesheng.androidadvance.MyRelativeLayout>
複製程式碼

4.2 列印日誌

04-02 08:47:57.980 1030-1030/com.mvp.chenzhesheng.androidadvance I/MainActiviy: dispatchTouchEvent ACTION_DOWN
04-02 08:47:58.000 1030-1030/com.mvp.chenzhesheng.androidadvance I/MyRelativeLayout: dispatchTouchEvent ACTION_DOWN
04-02 08:47:58.000 1030-1030/com.mvp.chenzhesheng.androidadvance I/MyRelativeLayout: onInterceptTouchEvent ACTION_DOWN
04-02 08:47:58.000 1030-1030/com.mvp.chenzhesheng.androidadvance I/MyTextView: dispatchTouchEvent ACTION_DOWN
04-02 08:47:58.010 1030-1030/com.mvp.chenzhesheng.androidadvance I/MainActiviy: MyTextView onTouch ACTION_DOWN
04-02 08:47:58.010 1030-1030/com.mvp.chenzhesheng.androidadvance I/MyTextView: onTouchEvent ACTION_DOWN
04-02 08:47:58.200 1030-1030/com.mvp.chenzhesheng.androidadvance I/MainActiviy: dispatchTouchEvent ACTION_UP
04-02 08:47:58.200 1030-1030/com.mvp.chenzhesheng.androidadvance I/MyRelativeLayout: dispatchTouchEvent ACTION_UP
04-02 08:47:58.200 1030-1030/com.mvp.chenzhesheng.androidadvance I/MyRelativeLayout: onInterceptTouchEvent ACTION_UP
04-02 08:47:58.200 1030-1030/com.mvp.chenzhesheng.androidadvance I/MyTextView: dispatchTouchEvent ACTION_UP
04-02 08:47:58.210 1030-1030/com.mvp.chenzhesheng.androidadvance I/MainActiviy: MyTextView onTouch ACTION_UP
04-02 08:47:58.210 1030-1030/com.mvp.chenzhesheng.androidadvance I/MyTextView: onTouchEvent ACTION_UP
04-02 08:47:58.260 1030-1030/com.mvp.chenzhesheng.androidadvance I/MainActiviy: MyTextView onClick
複製程式碼

可以看到 MainActivityMyTextView 的事件傳遞處理中新增了一層 MyRelativeLayout 。通過不同返回值測試,得到一套流程圖。

4.3 流程圖

Android觸控事件傳遞機制
從上面的流程圖可以得出結論:

  1. 觸控事件傳遞是從 Activity 傳遞到 ViewGroup ,再傳遞到 View 。如果中間沒有 ViewGroup 則直接從 Activity 傳遞到 View
  2. ViewGroup 通過 onInterceptTouchEvent 方法對事件進行截攔,如果返回 false 或者 super.onInterceptTouchEvent ,則事件會繼續傳遞給子 View
  3. View 中對事件進行消費後,ViewGroup 將不會接收到任何事件。

五.總結

  1. 事件分發是由外到內,從 Activity 到具體的子 View
  2. 事件處理消費是由內到外,從子 View 到最外層 Activity
  3. 事件攔截只存在於 ViewGroup 中;
  4. 掌握事件傳遞機制可以更好的進行事件處理,無論是自定義 View 還是閱讀 Framework 層原始碼都需要對事件傳遞進行學習,才能更精緻的開發應用。

Android觸控事件傳遞機制

相關文章