1. 程式人生 > >Android觸控事件傳遞機制簡要分析

Android觸控事件傳遞機制簡要分析

Android開發中經常會遇到多個View、ViewGroup巢狀的情況,
此時就可能遇到滑動衝突的問題。
為了這種問題,就必須對View的事件傳遞機制有一定的瞭解。

本篇部落格就以一些簡單的例子,
來看看Activity、View、ViewGroup三者的觸控事件傳遞機制。

一、基本概念
Android中的觸控事件對應於MotionEvent類,事件的型別包括ACTION_DOWN、ACTION_UP、ACTION_MOVE等。
不同事件型別的意義,大家可以參看原始碼對應的註釋資訊,此處不做贅述。

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


事件的分發對應著dispatchTouchEvent方法,原型如下:

public boolean dispatchTouchEvent(MotionEvent event)

Android系統中,所有的觸控事件都是通過該方法來分發的。

自定義檢視時,可以複寫該方法實現自己的事件分發邏輯。
這個方法一般根據當前檢視的具體實現,決定是直接消費事件,
還是將事件繼續分發給子檢視處理。
若該方法返回true,則表示事件被當前檢視消費掉,不再繼續分發;
若該方法返回super.dispatchTouchEvent,則表示繼續分發事件。

攔截:
事件的攔截對應著onInterceptTouchEvent方法,原型如下:

public boolean onInterceptTouchEvent(MotionEvent ev)

該方法僅在ViewGroup及其子類中存在。

自定義檢視時,可以複寫該方法實現自己的事件攔截邏輯。
當該方法返回true時,表示當前檢視攔截事件,不再將事件分發給子檢視,
同時會將事件交由當前檢視消費;
當該方法返回false或super.onInterceptTouchEvent時,
表示當前檢視不對事件進行攔截,將事件分發給子檢視。

消費:
事件的消費對應著onTouchEvent,方法原型如下:

public boolean onTouchEvent(MotionEvent event
)

自定義檢視時,可以複寫該方法實現自己的事件消費邏輯。
當該方法返回true時,表示當前檢視可以處理對應的事件,事件不會在向上傳遞給父檢視;
當該方法返回false時,表示當前檢視不處理這個事件,事件會被遞交給父檢視的onTouchEvent處理。

二、事件傳遞機制
前文已經提到過了,在Android系統中,擁有事件傳遞、處理能力的類有以下三種:
Activity: 擁有dispatchTouchEvent和onTouchEvent兩個方法;
ViewGroup:擁有dispatchTouchEvent、onInterceptTouchEvent和onTouchEvent三個方法;
View:擁有dispatchTouchEvent和onTouchEvent兩個方法。

接下來我們通過例子,主要分析下View和ViewGroup的事件傳遞機制。
Activity傳遞事件時表現得與View比較相似,就不專門分析了。

2.1 View的事件傳遞機制
首先我們結合例子,看看View的事件傳遞機制。
考慮到ViewGroup本身就是View的子類,此處的View指的是除去ViewGroup外的控制元件。
即本身已經是最小的單位,不能再作為其它View容器的View控制元件。

為了比較直觀的看清出事件傳遞的過程,我們自定義一個繼承TextView的類,
並複寫分發和消費對應的函式,並增加一些列印日誌:

/**
 * @author zhangjian
 */

public class MyTextView extends AppCompatTextView {
    private static final String TAG = "MyTextView";

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

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

    //主要關注事件分發的流程, 就簡單以ACTION_DOWN事件為例了
    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        switch (ev.getAction()) {
            case MotionEvent.ACTION_DOWN:
                Log.v(TAG, "dispatchTouchEvent ACTION_DOWN");
                break;
            default:
                break;
        }

        return super.dispatchTouchEvent(ev);
    }

    @Override
    public boolean onTouchEvent(MotionEvent ev) {
        switch (ev.getAction()) {
            case MotionEvent.ACTION_DOWN:
                Log.v(TAG, "onTouchEvent ACTION_DOWN");
                break;
            default:
                break;
        }

        return super.onTouchEvent(ev);
    }
}

同時,我們定義一個MainActivity用來呈現MyTextView,並監聽MyTextView的點選和觸控事件:

/**
 * @author zhangjian
 */
public class MainActivity extends AppCompatActivity
        implements View.OnClickListener, View.OnTouchListener {
    private static final String TAG = "MainActivity";

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

        TextView textView = findViewById(R.id.my_text_view);
        textView.setOnClickListener(this);
        textView.setOnTouchListener(this);
    }

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

    @Override
    public boolean onTouchEvent(MotionEvent ev) {
        switch (ev.getAction()) {
            case MotionEvent.ACTION_DOWN:
                Log.v(TAG, "onTouchEvent ACTION_DOWN");
                break;
            default:
                break;
        }
        return super.onTouchEvent(ev);
    }

    @Override
    public void onClick(View view) {
        switch (view.getId()) {
            case R.id.my_text_view:
                Log.v(TAG, "MyTextView click");
                break;
            default:
                break;
        }
    }

    @Override
    public boolean onTouch(View view, MotionEvent motionEvent) {
        switch (view.getId()) {
            case R.id.my_text_view:
                switch (motionEvent.getAction()) {
                    case MotionEvent.ACTION_DOWN:
                        Log.v(TAG, "TextView onTouch ACTION_DOWN");
                        break;
                    default:
                        break;
                }

                break;
            default:
                break;
        }
        return false;
    }
}

執行上面的程式碼並點選MyTextView後,將列印如下log:

12-04 14:45:01.279 29626-29626/work.test V/MainActivity: dispatchTouchEvent ACTION_DOWN
12-04 14:45:01.279 29626-29626/work.test V/MyTextView: dispatchTouchEvent ACTION_DOWN
12-04 14:45:01.279 29626-29626/work.test V/MainActivity: TextView onTouch ACTION_DOWN
12-04 14:45:01.279 29626-29626/work.test V/MyTextView: onTouchEvent ACTION_DOWN
12-04 14:45:01.409 29626-29626/work.test V/MainActivity: MyTextView click

從上面的程式碼和日誌可以看出,dispatchTouchEvent和onTouchEvent這兩個函式的返回值可能存在以下三種情況:
返回true、返回false和返回父類的同名方法。

不同的返回值將導致事件傳遞流程相差甚遠,通過不斷修改這些方法的返回值,
我們大概可以得到類似如下的事件處理流程圖:

從上面的流程圖可以得出如下結論:
1、觸控事件的傳遞流程是從dispatchTouchEvent開始的,如果不進行人為干預(即返回父類的同名函式),
那麼事件將按照檢視的巢狀層次,從外層檢視逐步向內層傳遞,到達最內層的View時,就由它的onTouchEvent處理。
該方法如果能夠消費該事件,則返回true;如果處理不了,則返回false。
這時事件會重新向外層傳遞,並由外層View的onTouchEvent方法處理,並依此類推。
2、如果事件在向內層傳遞的過程中,若某個檢視的dispatchTouchEvent返回true,
則會導致事件提前被消費掉,內層View將不會收到這個事件;
若某個檢視的dispatchTouchEvent返回false,事件也不會繼續分發,
而被交給其父檢視的onTouchEvent處理。
3、對於View控制元件而言,消費事件的邏輯順序從前到後依次為:
onTouch、onTouchEvent、onClick。
若優先順序靠前的介面返回true,則表示事件已經被消費掉了,
不會再呼叫後續介面。

2.2 ViewGroup的事件傳遞機制
在Android中,ViewGroup主要作為View控制元件的容器。
前文已經提到過,與View控制元件相比,ViewGroup多了onInterceptTouchEvent函式。

在這一部分,我們同樣自定義一個ViewGroup並增加列印資訊,看看觸控事件傳遞的流程:

/**
 * @author zhangjian
 */

public class MyRelativeLayout extends RelativeLayout {
    private static final 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_DOWN:
                Log.v(TAG, "dispatchTouchEvent ACTION_DOWN");
                break;
            default:
                break;
        }
        return super.dispatchTouchEvent(ev);
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        switch (ev.getAction()) {
            case MotionEvent.ACTION_DOWN:
                Log.v(TAG, "onInterceptTouchEvent ACTION_DOWN");
                break;
            default:
                break;
        }
        return super.onInterceptTouchEvent(ev);
    }

    @Override
    public boolean onTouchEvent(MotionEvent ev) {
        switch (ev.getAction()) {
            case MotionEvent.ACTION_DOWN:
                Log.v(TAG, "onTouchEvent ACTION_DOWN");
                break;
            default:
                break;
        }
        return super.onTouchEvent(ev);
    }
}

同時,更新對應的layout檔案:

<?xml version="1.0" encoding="utf-8"?>
<work.test.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"
    tools:context="work.test.MainActivity">

    <work.test.MyTextView
        android:id="@+id/my_text_view"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@string/test" />

</work.test.MyRelativeLayout>

MainActivity和MyTextView的事件傳遞函式均返回super方法的情況下,
點選MyTextView的列印log如下:

12-05 10:26:17.584 12021-12021/work.test V/MainActivity: dispatchTouchEvent ACTION_DOWN
12-05 10:26:17.584 12021-12021/work.test V/MyRelativeLayout: dispatchTouchEvent ACTION_DOWN
12-05 10:26:17.584 12021-12021/work.test V/MyRelativeLayout: onInterceptTouchEvent ACTION_DOWN
12-05 10:26:17.584 12021-12021/work.test V/MyTextView: dispatchTouchEvent ACTION_DOWN
12-05 10:26:17.584 12021-12021/work.test V/MainActivity: TextView onTouch ACTION_DOWN
12-05 10:26:17.584 12021-12021/work.test V/MyTextView: onTouchEvent ACTION_DOWN
12-05 10:26:17.754 12021-12021/work.test V/MainActivity: MyTextView click

從log來看,事件分發的流程幾乎沒變,
依然從外層檢視逐步向內層傳遞,然後呼叫內層檢視的onTouchEvent處理。
不過,ViewGroup檢視呼叫dispatchTouchEvent後,
會先呼叫onInterceptTouchEvent函式。

與前文類似,我們可以修改這些函式的返回值,得到下面的流程圖:

從上面的流程圖可以得出如下結論:
1、不考慮事件攔截時,ViewGroup傳遞事件的邏輯與View完全一樣。
2、 ViewGroup通過onInterceptTouchEvent方法攔截事件時,
返回super方法或false時均表示攔截失敗,事件將繼續被傳遞給子檢視;
返回true時,表示攔截成功,此時事件交給ViewGroup的onTouchEvent處理。
3、 ViewGroup的onTouchEvent處理事件時,
如果返回false或super方法(此處是RelativeLayout),那麼表示事件未被消費掉,
需要繼續將事件遞交給父檢視消費;
如果返回true,則表示ViewGroup消費掉了事件。

三、總結
至此,我們通過demo大致瞭解了View和ViewGroup的事件傳遞流程。
後續我們再看看原始碼中到底是如何實現的。