1. 程式人生 > >Android 觸控事件分發和攔截機制

Android 觸控事件分發和攔截機制

Android 開發中,很多情況下,我們需要對觸控事件進行處理,但是當面對錯綜複雜的 Android 佈局時,我們如何準確的將一個使用者的觸控事件傳遞到對應的控制元件中並讓它進行處理呢?

首先,我們先假設這裡有這樣一個佈局:

這裡寫圖片描述

我們可以很清楚的看到,一個很明顯的巢狀佈局,外面兩個紅色的和黃色的都是佈局,中間一個紫色的控制元件。如果此時,我們單擊一下 myView 這個控制元件,觸控事件(單擊也是觸控事件)是怎麼傳遞的呢?

Android 中,觸控事件的傳遞是由外向內的,也就是說,這個觸控事件從 myLinearLayout 開始(由更上面一層的元件將觸控事件傳遞給 myLinearLayout),依次通過 myFrameLayout,最後傳遞到 MyView 這個控制元件中,因為 myView 沒有子控制元件。所以這個事件就由 myView 控制元件進行處理,然後將處理的結果返回到它的父控制元件:myFrameLayout ,之後繼續返回給 myLinearLayout 控制元件。。。

當然,我們上面看到的情況是最一般的情況,觸控事件由外向裡傳遞,處理結果由裡向外傳遞。我們也可以通過重寫控制元件或者佈局裡面的一些方法來攔截觸控事件。
首先,對於 ViewGroup 來說,我們可以選擇性的重寫下面三個方法:

public boolean dispatchTouchEvent(MotionEvent ev);
public boolean onInterceptTouchEvent(MotionEvent ev);
public boolean onTouchEvent(MotionEvent ev);

我們分別來看一下這三個方法:

這裡寫圖片描述

這個方法的作用是把觸控事件的分發方法,其返回值代表觸控事件是否被當前 View 處理完成(true/false)。

這裡寫圖片描述

這個方法是觸控事件攔截機制的核心方法,官方文件解釋的很詳細,簡單來說就是如果這個方法的返回值是 true,那麼當前觸控事件就不會傳遞給子 View 控制元件(即被當前 ViewGroup 控制元件攔截並由當前 ViewGroup控制元件的 onTouchEvent(MotionEvent event) 方法進行處理),如果返回值是 false ,那麼這個觸控事件就會傳遞給子 View 控制元件,由子 View 控制元件去處理。

這裡寫圖片描述

這個是 ViewGroup 控制元件處理觸控事件的方法,一般來說,ViewGroup 控制元件的觸控事件在這個方法中處理。如果這個方法返回 true,證明當前觸控事件被當前 ViewGroup 控制元件處理完成並消耗了,如果返回 false,證明當前觸控事件沒有被當前 ViewGroup 控制元件處理完成。

結合我們上面所講的,筆者用一張圖來表示這三個方法的影響關係(觸控事件由外向裡的傳遞過程。這裡是筆者個人的理解):

這裡寫圖片描述

用偽程式碼表示 ViewGroup 中三個方法的呼叫關係:

public  boolean dispatchTouchEvent(MotionEvent e) {
    bool result = false;
    if(interceptTouchEvent(e)) {
        result = onTouchEvent(e);
    } else {
        result = child.dispatchTouchEvent(e);
    }
    return result;
}

上面的三個方法是 ViewGroup 物件中擁有的,而對於 View 物件來說,只有下面兩個方法:

public boolean dispatchTouchEvent(MotionEvent ev);
public boolean onTouchEvent(MotionEvent ev);

View 物件沒有 onInterceptTouchEvent 方法,即沒有攔截事件的方法(因為 View 物件已經是最內層 View 控制元件,它沒有子 View 了),所以不存在攔截事件這個說法,如果觸控事件傳遞到最內層 View 控制元件,那麼這個 View 控制元件的 onTouchEvent 方法一定會被呼叫用於處理觸控事件。其虛擬碼如下:

/**
 * dispatchTouchEvent(MotionEvent ev) 方法的返回值代表這個 View 是否成功處理觸控事件
 */
public boolean dispatchTouchEvent(MotionEvent ev) {
    return onTouchEvent(ev);
}

接下來要明白:

1、無論是對於 View 還是  ViewGroup來說,一個 觸控事件(MotionEvent 物件) 只要能傳遞給這個 View/ViewGroup ,
那麼這個 View/ViewGroup 的 dispatchTouchEvent(MotionEvent event) 就一定會被呼叫

2、如果一個 View/ViewGroup 的 onTouchEvent(MotionEvent event) 方法被呼叫,
那麼其返回值代表這個 View/ViewGroup  有沒有成功的處理這個事件,
如果返回的 true,那麼這個觸控事件接下來的一系列(直到手指鬆開之前) 都會傳遞給這個 View/ViewGroup 處理,
但是這個過程中其父 ViewGroup 仍然可以通過 interceptTouchEvent(MotionEvent e) 方法攔截這個觸控事件,
如果在傳遞的過程中被攔截了,那麼久不會傳遞到這個 View/ViewGroup 上。

3、無論何時,只要一個 View/ViewGroup 的 onTouchEvent(MotionEvent event) 方法返回了 false,
證明這個 View/ViewGroup 沒有處理完成這個觸控事件,
那麼接下來的一系列的觸控事件都不會傳遞給當前 View/ViewGroup 處理。

暫時不明白也沒關係,下面的例子會有介紹

這是事件由外至內的傳遞過程,那麼如果觸控事件傳遞到最低層的 View 控制元件之後怎麼傳遞呢?上文提到過,事件處理之後一般的過程是由裡向外傳遞,也就是說最裡層的 View 控制元件的 onTouchEvent 處理完了之後,然後逐漸向外傳遞觸控事件(即將觸控事件傳遞給外層的 ViewGroup ,並由 ViewGroup 的 onTouchEvent 方法繼續處理),直到傳遞到最外層的 ViewGroup 。當然,這裡我們也可以通過改變 View 控制元件的 onTouchEvent 方法的返回值來該表觸控事件的傳遞:返回 false:這個觸控事件需要外層 ViewGroup 處理,傳遞這個觸控事件給外層 ViewGroup,返回 true:這個觸控事件已經被當前View/ViewGroup 處理完成了,不會傳遞給外層 ViewGroup。

我們還是用一張圖來表示觸控事件的由裡向外傳遞過程:

這裡寫圖片描述

好了,下面通過例子來看一下:
建立一個Android 工程:
為了實現兩個 ViewGroup 並且重寫裡面的事件攔截的三個方法,我們需要繼承 ViewGroup,這裡為了簡單起見,筆者直接繼承了一個 LinearLayout 和一個 FrameLayout 。同樣的 myView 也是繼承了 Button, 看看程式碼:

MyLinearLayout.java:


import android.content.Context;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.widget.LinearLayout;

/**
 * Created by 指點 on 2017/3/21.
 */

public class MyLinearLayout extends LinearLayout {

    private static final String str = "LinearLayout";

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

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

    /*
     * 分發觸控事件的方法,當這個 ViewGroup 能夠接收到觸控事件的時候,
     * 這個方法首先被呼叫,用於分發接收到的觸控事件,父類方法預設返回 false
     */
    @Override
    public boolean dispatchTouchEvent(MotionEvent event) {
        Log.i(str, "dispatchTouchEvent");
        return super.dispatchTouchEvent(event);
    }

    /*
     * 重寫父類的攔截觸控事件方法,,ViewGroup 獨有的方法,
     * 如果返回值為 true,那麼這個觸控事件由這個 ViewGroup 處理,
     * 會呼叫 onTouchEvent 方法,並且攔截觸控事件,不讓子 View 接收到。
     */
    @Override
    public boolean onInterceptTouchEvent(MotionEvent event) {
        super.onInterceptTouchEvent(event);
        Log.i(str, "onInterceptTouchEvent");
        return false;
    }

    /*
     * 如果接收到的觸控事件由這個 View/ViewGroup 處理,那麼呼叫這個方法用於處理這個觸控事件
     */
    @Override
    public boolean onTouchEvent(MotionEvent event) { 
        super.onTouchEvent(event);
        Log.i(str, "onTouchEvent");
        return false;
    }
}

MyFrameLayout.java:


import android.content.Context;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.widget.FrameLayout;

/**
 * Created by 指點 on 2017/3/21.
 */

public class MyFrameLayout extends FrameLayout {

    private static final String str = "FrameLayout";

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

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



    /*
     * 分發觸控事件的方法,當這個 ViewGroup 接收到觸控事件的時候,
     * 這個方法首先被呼叫,用於分發接收到的觸控事件,父類方法預設返回false
     */
    @Override
    public boolean dispatchTouchEvent(MotionEvent event) {
        Log.i(str , "dispatchTouchEvent");
        return super.dispatchTouchEvent(event);
    }

    /*
     * 重寫父類的攔截觸控事件方法,ViewGroup 獨有的方法,
     * 如果返回值為 true,那麼這個觸控事件由這個 ViewGroup 處理,
     * 會呼叫 onTouchEvent 方法,
     */
    @Override
    public boolean onInterceptTouchEvent(MotionEvent event) {
        super.onInterceptTouchEvent(event);
        Log.i(str , "onInterceptTouchEvent");
        return false;
    }

    /*
     * 如果接收到的觸控事件由這個 ViewGroup 處理,那麼呼叫這個方法用於處理這個觸控事件
     */
    @Override
    public boolean onTouchEvent(MotionEvent event) { // 重寫父類的處理觸控事件的方法
        super.onTouchEvent(event);
        Log.i(str, "onTouchEvent");
        return false;
    }
}

MyView.java:


import android.content.Context;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.widget.Button;

/**
 * Created by 指點 on 2017/3/21.
 */

public class MyView extends Button {

    private static final String str = "MyView";

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

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

    /*
     * 分發觸控事件的方法,當這個 View 接收到觸控事件的時候,
     * 這個方法首先被呼叫,用於分發接收到的觸控事件,父類的方法預設返回 false
     */
    @Override
    public boolean dispatchTouchEvent(MotionEvent event) {
        Log.i(str, "dispatchTouchEvent");
        return super.dispatchTouchEvent(event);
    }

    /*
     * 因為這已經是一個 View,因此它是觸控事件處理的最底層,如果觸控事件能夠傳遞給它,
     * 那麼它的 onTouchEvent 方法一定會被呼叫
     */
    @Override
    public boolean onTouchEvent(MotionEvent event) { // 重寫父類的處理觸控事件的方法
        super.onTouchEvent(event);
        Log.i(str, "onTouchEvent");
        return false;
    }
}

這裡分別重寫了 LinearLayout 、FrameLayout 、Button 的對應事件處理方法,返回值均為 false,並且在方法中打上了 LogCat。

接下來是 佈局檔案 activity_main.xml:


<?xml version="1.0" encoding="utf-8"?>
<com.company.zhidian.motioneventandscroll.MyLinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/myLinearLayout"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <com.company.zhidian.motioneventandscroll.MyFrameLayout
        android:id="@+id/myFrameLayout"
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <com.company.zhidian.motioneventandscroll.MyView
            android:id="@+id/myView"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="myView">
        </com.company.zhidian.motioneventandscroll.MyView>
    </com.company.zhidian.motioneventandscroll.MyFrameLayout>

</com.company.zhidian.motioneventandscroll.MyLinearLayout>

最後是 MainActivity.java:


import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.Button;

public class MainActivity extends AppCompatActivity {

    private MyFrameLayout myFrameLayout = null;
    private Button button = null;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

    }
}

下面來看看結果:

這裡寫圖片描述

單擊按鈕後看看LogCat:

這裡寫圖片描述

這裡的觸控事件確實是從外到內傳遞,經 MyView 的 onTouchEvent 方法處理之後,又向外傳遞到 LinearLayout 。因為我們這裡的程式碼中的 onInterceptTouchEvent 方法和 onTouchEvent 方法均是返回 false,所以這裡並沒有任何事件攔截現象,現在我們把 LinearLayout 中的 onInterceptTouchEvent 方法的返回值改為 true 試試:

這裡寫圖片描述

可以看到,這裡只調用了 LinearLayout 中的 onTouchEvent 方法就結束了,證明觸控事件確實被 LinearLayout 控制元件攔截並處理了。如果只把 FrameLayout 中的 onInterceptTouchEvent 方法的返回值改為 true 呢?

這裡寫圖片描述

同樣的,這裡觸控事件經 LinearLayout 傳遞到 FrameLayout 後,被 FrameLayout 攔截處理,所以這裡 MyView 仍然沒有接收到觸控事件,而是直接由 FrameLayout 向外傳遞給 LinearLayout 。
上面是對觸控事件由外向內傳遞的實驗,那麼由內向外呢?

我們把 MyView 的 onTouchEvent 方法的返回值改為 true,LinearLayout 、FrameLayout 方法的 onInterceptTouchEvent 方法和 onTouchEvent 方法返回值全改為 false:

這裡寫圖片描述

我們會發現,會出現上圖中兩遍同樣的LogCat資訊,因為單擊是兩個動作(ACTION_DOWN、ACTION_UP)。
可能小夥伴要問了,為什麼就這裡有兩遍一樣LogCat 資訊,上次的程式碼就沒有呢。這裡其實就是我們上問文講的 :

如果一個 View/ViewGroup 的 onTouchEvent(MotionEvent event) 方法被呼叫,
那麼其返回值代表這個 View/ViewGroup  有沒有成功的處理這個事件,如果返回的 true,
那麼這個觸控事件接下來的一系列(直到手指鬆開之前) 都會傳遞給這個 View/ViewGroup 處理

即為上文中的第 2 點,很明顯,我們上次的程式碼中所有 onTouchEvent(MotionEvent e) 方法返回的全是 false,對應於上文中第 3 點所講的:

無論何時,只要一個 View/ViewGroup 的 onTouchEvent(MotionEvent event) 方法返回了 false,
證明這個 View/ViewGroup 沒有處理完成這個觸控事件,
那麼接下來的一系列的觸控事件都不會傳遞給當前 View/ViewGroup 處理。

所以之前只有一遍 LogCat ,即只有 ACTION_DOWN 型別的 MotionEvent 物件被傳遞了,ACTION_UP 型別的 MotionEvent 物件並沒有傳遞給這個 View 處理。

接下來我們會發現 FrameLayout 、 LinearLayout 的 onTouchEvent 方法都不會被呼叫,因為觸控事件在 MyView 的 onTouchEvent 事件中就被處理消耗掉了(也可以理解為被攔截了),所以自然不能傳給 外層的 ViewGroup 。

如果你把 FrameLayout 的 onTouchEvent 方法的返回值設為 true ,其餘的設定為 false,你會得到下面的結果:

這裡寫圖片描述

同樣是兩個一樣的LogCat,類似的,觸控事件在 FrameLayout 的 onTouchEvent 方法中被攔截了。因而 LinearLayout 不能接收到觸控事件,它的 onTouchEvent 方法不會被呼叫。

好了,對於Android 事件分發攔截,總結起來就是:
先由外向裡,再由裡向外。
由外向裡的過程中:onInterceptTouchEvent 方法(ViewGroup才有)的返回值決定是否攔截觸控事件(true:攔截,false:不攔截)。如果 ViewGroup 攔截了觸控事件,那麼其 onTouchEvent 就會被呼叫用來處理觸控事件。
由裡向外的過程中:onTouchEvent 方法的返回值決定是否處理完成觸控事件(true:已經處理完成,不需要給父 ViewGroup 處理,false:還沒處理完成 ,需要傳遞給父 ViewGroup 處理)。

如果部落格中有什麼不正確的地方,還請多多指點,如果覺得我寫的不錯,那麼請點個贊支援我吧。

謝謝觀看。。。