1. 程式人生 > >完全理解Android TouchEvent事件分發機制(一)

完全理解Android TouchEvent事件分發機制(一)

本文能給你帶來和解決一些你模糊的Touch事件概念及用法

  • 1.掌握View及ViewGroup的TouchEvent事件分發機制
  • 2.為解決View滑動衝突及點選事件消費提供支援
  • 3.為你解決面試中的一些問題。

Touch事件分發中只有兩個主角:ViewGroup和View。

Activity的Touch事件事實上是呼叫它內部的ViewGroup的Touch事件,可以直接當成ViewGroup處理。

Activity、ViewGroup、View都關心Touch事件,其中ViewGroup的關心的事件有三個:onInterceptTouchEvent、dispatchTouchEvent、onTouchEvent。

Activity和View關心的事件只有兩個:dispatchTouchEvent、onTouchEvent。

只有ViewGroup可以對事件進行攔截。

在Android中Touch**觸控事件**主要包括點選(onClick)、長按(onLongClick)、拖拽(onDrag)、滑動(onScroll)等,

其中Touch的第一個狀態是 ACTION_DOWN,表示按下了屏幕後,touch將會有後續事件,比如移動、擡起等。

一個Action_DOWN,一個ACTION_UP,許多個ACTION_MOVE,構成了Android中眾多的Touch互動事件。

安卓裡經常會有多個佈局巢狀,View重疊,View的Visibility設定等等,還有ViewGroup包含View的情況。
這個時候點選到子View時,其實也是同時點到ViewGroup這個父控制元件的,那是把這個點選事件應該是怎麼分發的呢(有沒有遇到過listview或recyclerview的item事件或者是item中的控制元件是不是沒反應撒

)?

觸控事件分發機制涉及的三個重要方法:

 public boolean dispatchTouchEvent(MotionEvent event)

dispatchTouchEvent用來進行事件的分發。如果事件能夠傳遞給當前的View,那麼此方法一定會被呼叫,
返回結果受當前View或者是ViewGroup的onTouchEvent和下級View的dispatchTouchEvent方法的影響,表示是否消耗當前事件。

public boolean onInterceptTouchEvent(MotionEvent event)

onInterceptTouchEvent是ViewGroup提供的方法,用來判斷是否攔截某個事件,如果當前View攔截了某個事件,
那麼在同一個事件序列當中,此方法不會被再次呼叫,返回結果表示是否攔截當前事件。預設返回false,返回true表示攔截。

  public boolean onTouchEvent(MotionEvent event)

onTouchEvent在dispatchTouchEvent方法中呼叫,用來處理點選事件,返回結果表示是否消耗當前的事件,如果不消耗,
則在同一個事件序列中,當前View無法再次接受到事件。view中預設返回true,表示消費了這個事件。

今天所使用的Demo目錄結構及Activity如圖所示:

首先我們來看一下dispatchTouchEvent(MotionEvent event)

佈局activity_touch_test.xml

<?xml version="1.0" encoding="utf-8"?>
<com.shanlovana.rcimageview.touchviews.GrandPaViewGroup
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/activity_touch_test"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    tools:context="com.shanlovana.rcimageview.TouchTestActivity">

    <com.shanlovana.rcimageview.touchviews.FatherViewGroup
        android:layout_width="match_parent"
        android:gravity="center"
        android:layout_height="match_parent">

        <com.shanlovana.rcimageview.touchviews.LogImageView
            android:layout_width="300dp"
            android:layout_height="200dp"
            android:src="@drawable/damimi"/>


    </com.shanlovana.rcimageview.touchviews.FatherViewGroup>


</com.shanlovana.rcimageview.touchviews.GrandPaViewGroup>

下面是三層佈局及預覽情況:

點選一下圖片:看一下列印:

03-31 09:02:40.554 10898-10898/com.shanlovana.rcimageview E/ShanCanCan: GrandPaViewGroup  dispatchTouchEvent  Event 0
03-31 09:02:40.555 10898-10898/com.shanlovana.rcimageview E/ShanCanCan: GrandPaViewGroup  onInterceptTouchEvent  Event 0
03-31 09:02:40.555 10898-10898/com.shanlovana.rcimageview E/ShanCanCan: FatherViewGroup  dispatchTouchEvent  Event 0
03-31 09:02:40.555 10898-10898/com.shanlovana.rcimageview E/ShanCanCan: FatherViewGroup  onInterceptTouchEvent  Event 0
03-31 09:02:40.555 10898-10898/com.shanlovana.rcimageview E/ShanCanCan: LogImageView  dispatchTouchEvent  Event 0
03-31 09:02:40.556 10898-10898/com.shanlovana.rcimageview E/ShanCanCan: LogImageView  onTouchEvent  Event 0
03-31 09:02:40.558 10898-10898/com.shanlovana.rcimageview E/ShanCanCan: FatherViewGroup  onTouchEvent  Event 0
03-31 09:02:40.559 10898-10898/com.shanlovana.rcimageview E/ShanCanCan: GrandPaViewGroup  onTouchEvent  Event 0

原始碼中0,1,2,3,4所代表的Action

   public static final int ACTION_DOWN = 0;


    public static final int ACTION_UP   = 1;


    public static final int ACTION_MOVE  = 2;


    public static final int ACTION_CANCEL = 3;


    public static final int ACTION_OUTSIDE = 4;

為什麼是這樣一個從父級到子級再到父級的順序呢?

來,follow me進入原始碼檢視,所有的核心在於ViewGroup的dispatchTouchEvent方法:

boolean dispatchTouchEvent() {
    // 是否攔截
     final boolean intercepted;
    intercepted = onInterceptTouchEvent(ev);

    // final boolean canceled = resetCancelNextUpFlag(this)
                 //   || actionMasked == MotionEvent.ACTION_CANCEL;
    if( !intercepted) {
        // 如果不攔截遍歷所有child,判斷是否有分發
        boolean handled;
        if (child == null) {
            // 等同於handled = onTouchEvent()
            handled = super.dispatchTouchEvent();
        } else {
            // 如果有child,再呼叫child的分發方法
            handled = child.dispatchTouchEvent();
        }

        if(handled) {
            touchTarget = child;
            break;
        }   
    }

    if(touchTarget == null) {
        // 如果所有child中都沒有消費掉事件
        // 那麼就把自己作為沒child的普通View
        handled = super.dispatchTouchEvent();
    }

    return handled;
}

  public boolean onInterceptTouchEvent(MotionEvent ev) {
        if (ev.isFromSource(InputDevice.SOURCE_MOUSE)
                && ev.getAction() == MotionEvent.ACTION_DOWN
                && ev.isButtonPressed(MotionEvent.BUTTON_PRIMARY)
                && isOnScrollbarThumb(ev.getX(), ev.getY())) {
            return true;
        }
        return false;
    }

dispatchTouchEvent方法的作用是將螢幕點選事件進行向下分發(子一級)傳遞到目標控制元件上,或者傳遞給自己。

如果事件被(自己或者下面某一層的子控制元件)處理掉了的話,就返回true,否則返回false

那問題來了,如果我沒有child了,或者我就是一個View,那我的dispatchTouchEvent返回值要如何獲取呢?

這種情況下就會使用父類的dispatchTouchEvent方法,
也就是呼叫View類中的實現,簡化程式碼如下:

boolean dispatchTouchEvent() {
    // 實質上就是呼叫onTouchEvent用其返回值
    ListenerInfo li = mListenerInfo;
    if (li != null && li.mOnTouchListener != null
            && (mViewFlags & ENABLED_MASK) == ENABLED
            && li.mOnTouchListener.onTouch(this, event)) {
        result = true;
    }

    if (!result && onTouchEvent(event)) {
        result = true;
    }
    return result;
}

由此可見,只要是enable=false或者沒有設定過touchListener, 那麼他一定會呼叫onTouchEvent,且dispatchTouchEvent的返回值就是onTouchEvent的返回值。

ViewGroup進行事件的分發,一直到自己或者是最底層的View,邏輯圖如下。

現在我們基本知道了事件的分發dispatchTouchEvent,最終呼叫了onTouchEvent方法

接著我們來理解和講解onInterceptTouchEvent攔截方法

該方法用於攔截事件向下分發

當返回值為true時,就會攔截TouchEvent不再向下傳遞,直接交給自己的onTouchEvent方法處理。返回false則不攔截。

把**Demo中的Parent層的onInterceptTouchEvent返回值改為tru**e。

執行一下,點View,看下輸出結果:

03-31 11:45:39.953 23170-23170/com.shanlovana.rcimageview E/ShanCanCan: GrandPaViewGroup  dispatchTouchEvent  Event 0
03-31 11:45:39.953 23170-23170/com.shanlovana.rcimageview E/ShanCanCan: GrandPaViewGroup  onInterceptTouchEvent  Event 0
03-31 11:45:39.953 23170-23170/com.shanlovana.rcimageview E/ShanCanCan: FatherViewGroup  dispatchTouchEvent  Event 0
03-31 11:45:39.953 23170-23170/com.shanlovana.rcimageview E/ShanCanCan: FatherViewGroup  onInterceptTouchEvent  Event 0
03-31 11:45:39.954 23170-23170/com.shanlovana.rcimageview E/ShanCanCan: FatherViewGroup  onTouchEvent  Event 0
03-31 11:45:39.955 23170-23170/com.shanlovana.rcimageview E/ShanCanCan: GrandPaViewGroup  onTouchEvent  Event 0

即當事件一層層向下傳遞到parent時,被他就攔截了下來然後自己消費使用。

intercepted為true,沒有進入FatherViewGroup的條件,就跳過了child.dispatchTouchEvent的向下事件分發(結合我的demo看比較直觀)。

最後我們來講解 onTouchEvent方法

方法的主體內容其實是處理具體操作邏輯的,是產生一次點選還是一次橫縱向的滑動等

而他的返回值才會影響整個事件分發機制,
意義在於通知父級的ViewGroup們是否已經消費找到目標Target了。

把示例中的Parent的TouchEvent返回值改為true。攔截方法不變
點一下View(小夥子,如果你不是點一下,會出現不同的結果哦),則輸出日誌為:

03-31 12:01:21.661 2596-2596/com.shanlovana.rcimageview E/ShanCanCan: GrandPaViewGroup  dispatchTouchEvent  Event 0
03-31 12:01:21.674 2596-2596/com.shanlovana.rcimageview E/ShanCanCan: GrandPaViewGroup  onInterceptTouchEvent  Event 0
03-31 12:01:21.676 2596-2596/com.shanlovana.rcimageview E/ShanCanCan: FatherViewGroup  dispatchTouchEvent  Event 0
03-31 12:01:21.677 2596-2596/com.shanlovana.rcimageview E/ShanCanCan: FatherViewGroup  onInterceptTouchEvent  Event 0
03-31 12:01:21.677 2596-2596/com.shanlovana.rcimageview E/ShanCanCan: LogImageView  dispatchTouchEvent  Event 0
03-31 12:01:21.678 2596-2596/com.shanlovana.rcimageview E/ShanCanCan: LogImageView  onTouchEvent  Event 0
03-31 12:01:21.681 2596-2596/com.shanlovana.rcimageview E/ShanCanCan: FatherViewGroup  onTouchEvent  Event 0
03-31 12:01:21.723 2596-2596/com.shanlovana.rcimageview E/ShanCanCan: GrandPaViewGroup  dispatchTouchEvent  Event 1
03-31 12:01:21.723 2596-2596/com.shanlovana.rcimageview E/ShanCanCan: GrandPaViewGroup  onInterceptTouchEvent  Event 1
03-31 12:01:21.723 2596-2596/com.shanlovana.rcimageview E/ShanCanCan: FatherViewGroup  dispatchTouchEvent  Event 1
03-31 12:01:21.723 2596-2596/com.shanlovana.rcimageview E/ShanCanCan: FatherViewGroup  onTouchEvent  Event 1

先看Down的邏輯,對應的原始碼執行順序如下

Father呼叫super.dispatchTouchEvent實際上是呼叫了onTouchEvent方法,

這裡因為我們修改成了true,所以dispatchTouchEvent最終也返回true。

所以返回到GrandPa中,touchTarget 就非空了,

因此GrandPa的onTouchEvent也沒有執行~

可以看出來,事件一旦被某一層消費掉,其它層就不會再消費了

到這裡其實對事件分發的機制就有個大概瞭解了看了原始碼也知道里面的原理是怎麼回事。

突然有事,明天接著更新。