Android 手勢檢測---GestureDetector

在開發 Android 手機應用過程中,可能需要對一些手勢作出響應,如:單擊、雙擊、長按、滑動、縮放等。這些都是很常用的手勢。就拿最簡單的雙擊來說吧,假如我們需要判斷一個控件是否被雙擊(即在較短的時間內快速的點擊兩次),似乎是一個很容易的任務,但仔細考慮起來,要處理的細節問題也有不少,例如:

  1. 記錄點擊次數,爲了判斷是否被點擊超過 1 次,所以必須記錄點擊次數。
  2. 記錄點擊時間,由於雙擊事件是較快速的點擊兩次,像點擊一次後,過來幾分鐘再點擊一次肯定不能算是雙擊事件,所以在記錄點擊次數的同時也要記錄上一次的點擊時間,我們可以設置本次點擊距離上一次時間超過一定時間(例如:超過100ms)就不識別爲雙擊事件。
  3. 點擊狀態重置,在響應雙擊事件,或者判斷不是雙擊事件的時候要重置計數器和上一次點擊時間。重置既可以在點擊的時候判斷並進行重新設置,也可以使用定時器等超過一定時間後重置狀態。

這樣看起來,判斷一個雙擊事件就有這麼多麻煩事情,更別其他的手勢了,雖然這些看起來都很簡單,但設計起來需要考慮的細節情況實在是太多了。

那麼有沒有一種更好的方法來方便的檢測手勢呢?當然有啦,因爲這些手勢很常用,系統早就封裝了一些方法給我們用,接下來我們就看看它們是如何使用的。

GestureDetector

GestureDetector 可以使用 MotionEvents 檢測各種手勢和事件。GestureDetector.OnGestureListener 是一個回調方法,在發生特定的事件時會調用 Listener 中對應的方法回調。這個類只能用於檢測觸摸事件的 MotionEvent,不能用於軌跡球事件。
(話說軌跡球已經消失多長時間了,估計很多人都沒見過軌跡球這種東西)。

如何使用:

  • 創建一個 GestureDetector 實例。
  • 在onTouchEvent(MotionEvent)方法中,確保調用 GestureDetector 實例的 onTouchEvent(MotionEvent)。回調中定義的方法將在事件發生時執行。
  • 如果偵聽 onContextClick(MotionEvent),則必須在 View 的 onGenericMotionEvent(MotionEvent)中調用 GestureDetector OnGenericMotionEvent(MotionEvent)。

GestureDetector 本身的方法比較少,使用起來也非常簡單,下面讓我們先看一下它的簡單使用示例,分解開來大概需要三個步驟。

// 1.創建一個監聽回調
SimpleOnGestureListener listener = new SimpleOnGestureListener() {
    @Override public boolean onDoubleTap(MotionEvent e) {
        Toast.makeText(MainActivity.this, "雙擊666", Toast.LENGTH_SHORT).show();
        return super.onDoubleTap(e);
    }
};

// 2.創建一個檢測器
final GestureDetector detector = new GestureDetector(this, listener);

// 3.給監聽器設置數據源
view.setOnTouchListener(new View.OnTouchListener() {
    @Override public boolean onTouch(View v, MotionEvent event) {
        return detector.onTouchEvent(event);
    }
});

接下來我們先了解一下 GestureDetector 裏面都有哪些內容。

1. 構造函數

GestureDetector 一共有 5 種構造函數,但有 2 種被廢棄了,1 種是重複的,所以我們只需要關注其中的 2 種構造函數即可,如下:

構造函數
GestureDetector(Context context, GestureDetector.OnGestureListener listener)
GestureDetector(Context context, GestureDetector.OnGestureListener listener, Handler handler)

第 1 種構造函數裏面需要傳遞兩個參數,上下文(Context) 和 手勢監聽器(OnGestureListener),這個很容易理解,就不再過多敘述,上面的例子中使用的就是這一種。

第 2 種構造函數則需要多傳遞一個 Handler 作爲參數,這個有什麼作用呢?其實作用也非常簡單,這個 Handler 主要是爲了給 GestureDetector 提供一個 Looper。

在通常情況下是不需這個 Handler 的,因爲它會在內部自動創建一個 Handler 用於處理數據,如果你在主線程中創建 GestureDetector,那麼它內部創建的 Handler 會自動獲得主線程的 Looper,然而如果你在一個沒有創建 Looper 的子線程中創建 GestureDetector 則需要傳遞一個帶有 Looper 的 Handler 給它,否則就會因爲無法獲取到 Looper 導致創建失敗。

第 2 種構造函數使用方式如下(下面是兩種在子線程中創建 GestureDetector 的方法):

// 方式一、在主線程創建 Handler
final Handler handler = new Handler();
new Thread(new Runnable() {
    @Override public void run() {
        final GestureDetector detector = new GestureDetector(MainActivity.this, new
                GestureDetector.SimpleOnGestureListener() , handler);
        // ... 省略其它代碼 ...
    }
}).start();

// 方式二、在子線程創建 Handler,並且指定 Looper
new Thread(new Runnable() {
    @Override public void run() {
        final Handler handler = new Handler(Looper.getMainLooper());
        final GestureDetector detector = new GestureDetector(MainActivity.this, new
                GestureDetector.SimpleOnGestureListener() , handler);
        // ... 省略其它代碼 ...
    }
}).start();

當然了,使用其它創建 Handler 的方式也是可以的,重點傳遞的 Handler 一定要有 Looper,敲黑板,重點是 Handler 中的 Looper。假如子線程準備了 Looper 那麼可以直接使用第 1 種構造函數進行創建,如下:

new Thread(new Runnable() {
    @Override public void run() {
        Looper.prepare(); // <- 重點在這裏
        final GestureDetector detector = new GestureDetector(MainActivity.this, new
                GestureDetector.SimpleOnGestureListener());
        // ... 省略其它代碼 ...
    }
}).start();

2.手勢監聽器

既然是手勢檢測,自然要在對應的手勢出現的時候通知調用者,最合適的自然是事件監聽器模式。目前 GestureDetecotr 有四種監聽器。

監聽器 簡介
OnContextClickListener 這個很容易讓人聯想到ContextMenu,然而它和ContextMenu並沒有什麼關係,它是在Android6.0(API 23)才添加的一個選項,是用於檢測外部設備上的按鈕是否按下的,例如藍牙觸控筆上的按鈕,一般情況下,忽略即可。
OnDoubleTapListener 雙擊事件,有三個回調類型:雙擊(DoubleTap)、單擊確認(SingleTapConfirmed) 和 雙擊事件回調(DoubleTapEvent)
OnGestureListener 手勢檢測,主要有以下類型事件:按下(Down)、 一扔(Fling)、長按(LongPress)、滾動(Scroll)、觸摸反饋(ShowPress) 和 單擊擡起(SingleTapUp)
SimpleOnGestureListener 這個是上述三個接口的空實現,一般情況下使用這個比較多,也比較方便。

2.1 OnContextClickListener

由於 OnContextClickListener 主要是用於檢測外部設備按鈕的,關於它需要注意一點,如果偵聽 onContextClick(MotionEvent),則必須在 View 的 onGenericMotionEvent(MotionEvent)中調用 GestureDetector 的 OnGenericMotionEvent(MotionEvent)。

由於目前我們用到這個監聽器的場景並不多,所以也就不展開介紹了,重點關注後面幾個監聽器。

2.2 OnDoubleTapListener

這個很明顯就是用於檢測雙擊事件的,它有三個回調接口,分別是 onDoubleTap、onDoubleTapEvent 和 onSingleTapConfirmed。

2.2.1 onDoubleTap 與 onSingleTapConfirmed

如果你只想監聽雙擊事件,那麼只用關注 onDoubleTap 就行了,如果你同時要監聽單擊事件則需要關注 onSingleTapConfirmed 這個回調函數

有人可能會有疑問,監聽單擊事件爲什麼要使用 onSingleTapConfirmed,使用 OnClickListener 不行嗎?從理論上是可行的,但是我並不推薦這樣使用,主要有兩個原因:
1.它們兩個是存在一定衝突的,如果你看過 事件分發機制詳解 就會知道,如果想要兩者同時被觸發,則 setOnTouchListener 不能消費事件,如果 onTouchListener 消費了事件,就可能導致 OnClick 無法正常觸發。
2.需要同時監聽單擊和雙擊,則說明單擊和雙擊後響應邏輯不同,然而使用 OnClickListener 會在雙擊事件發生時觸發兩次,這顯然不是我們想要的結果。而使用 onSingleTapConfirmed 就不用考慮那麼多了,你完全可以把它當成單擊事件來看待,而且在雙擊事件發生時,onSingleTapConfirmed 不會被調用,這樣就不會引發衝突。

如果你需要同時監聽兩種點擊事件可以這樣寫:

GestureDetector detector = new GestureDetector(this, new GestureDetector
        .SimpleOnGestureListener() {
    @Override public boolean onSingleTapConfirmed(MotionEvent e) {
        Toast.makeText(MainActivity.this, "單擊", Toast.LENGTH_SHORT).show();
        return false;
    }
    @Override public boolean onDoubleTap(MotionEvent e) {
        Toast.makeText(MainActivity.this, "雙擊", Toast.LENGTH_SHORT).show();
        return false;
    }
});

關於 onSingleTapConfirmed 原理也非常簡單,這一個回調函數在單擊事件發生後300ms後觸發(注意,不是立即觸發的),只有在確定不會有後續的事件後,既當前事件肯定是單擊事件才觸發 onSingleTapConfirmed,所以在進行點擊操作時,onDoubleTap 和 onSingleTapConfirmed 只會有一個被觸發,也就不存在衝突了。

當然,如果你對事件分發機制非常瞭解的話,隨便怎麼用都行,條條大路通羅馬,我這裏只是推薦一種最簡單而且不容易出錯的實現方案。

2.2.2 onDoubleTapEvent

有些細心的小夥伴可能注意到還有一個 onDoubleTapEvent 回調函數,它是幹什麼的呢?它在雙擊事件確定發生時會對第二次按下產生的 MotionEvent 信息進行回調。

至於爲什麼要存在這樣的回調,就要涉及到另一個比較細緻的問題了,那就是 onDoubleTap 的觸發時間,如果你在這些函數被調用時打印一條日誌,那麼你會看到這樣的信息:

GCS-LOG: onDoubleTap
GCS-LOG: onDoubleTapEvent - down
GCS-LOG: onDoubleTapEvent - move
GCS-LOG: onDoubleTapEvent - move
GCS-LOG: onDoubleTapEvent - up

通過觀察這些信息你會發現它們的調用順序非常有趣,首先是 onDoubleTap 被觸發,之後依次觸發 onDoubleTapEvent 的 down、move、up 等信息,爲什麼說它們有趣呢?是因爲這樣的調用順序會引發兩種猜想,第一種猜想是 onDoubleTap 是在第二次手指擡起(up)後觸發的,而 onDoubleTapEvent 是一種延時回調。第二種猜想則是 onDoubleTap 在第二次手指按下(dowm)時觸發,onDoubleTapEvent 是一種實時回調。

通過測試和觀察源碼發現第二種猜想是正確的,因爲第二次按下手指時,即便不擡起也會觸發 onDoubleTap 和 onDoubleTapEvent 的 down,而且源碼中邏輯也表明 onDoubleTapEvent 是一種實時回調。

這就引發了另一個問題,雙擊的觸發時間,雖然這是一個細微到很難讓人注意到的問題,假如說我們想要在第二次按下擡起後才判定這是一個雙擊操作,觸發後續的內容,則不能使用 onDoubleTap 了,需要使用 onDoubleTapEvent 來進行更細微的控制,如下:

final GestureDetector detector = new GestureDetector(MainActivity.this, new GestureDetector.SimpleOnGestureListener() {
    @Override public boolean onDoubleTap(MotionEvent e) {
        Logger.e("第二次按下時觸發");
        return super.onDoubleTap(e);
    }

    @Override public boolean onDoubleTapEvent(MotionEvent e) {
        switch (e.getActionMasked()) {
            case MotionEvent.ACTION_UP:
                Logger.e("第二次擡起時觸發");
                break;
        }
        return super.onDoubleTapEvent(e);
    }
});

如果你不需要控制這麼細微的話,忽略即可(Logger 是我自己封裝的日誌庫,忽略即可)。

2.3 OnGestureListener

這個是手勢檢測中較爲核心的一個部分了,主要檢測以下類型事件:按下(Down)、 一扔(Fling)、長按(LongPress)、滾動(Scroll)、觸摸反饋(ShowPress) 和 單擊擡起(SingleTapUp)。

2.3.1 onDown
@Override public boolean onDown(MotionEvent e) {
    return true;
}

看過前面的文章應該知道,down 在事件分發體系中是一個較爲特殊的事件,爲了保證事件被唯一的 View 消費,哪個 View 消費了 down 事件,後續的內容就會傳遞給該 View。如果我們想讓一個 View 能夠接收到事件,有兩種做法:

1、讓該 View 可以點擊,因爲可點擊狀態會默認消費 down 事件。

2、手動消費掉 down 事件。

由於圖片、文本等一些控件默認是不可點擊的,所以我們要麼聲明它們的 clickable 爲 true,要麼在發生 down 事件是返回 true。所以 onDown 在這裏的作用就很明顯了,就是爲了保證讓該控件能擁有消費事件的能力,以接受後續的事件。

2.3.2 onFling

Fling 中文直接翻譯過來就是一扔、拋、甩,最常見的場景就是在 ListView 或者 RecyclerView 上快速滑動時手指擡起後它還會滾動一段時間纔會停止。onFling 就是檢測這種手勢的。

@Override
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float
        velocityY) {
    return super.onFling(e1, e2, velocityX, velocityY);
}

在 onFling 的回調中共有四個參數,分別是:

參數 簡介
e1 手指按下時的 Event。
e2 手指擡起時的 Event。
velocityX 在 X 軸上的運動速度(像素/秒)。
velocityY 在 Y 軸上的運動速度(像素/秒)。

我們可以通過 e1 和 e2 獲取到手指按下和擡起時的座標、時間等相關信息,通過 velocityX 和 velocityY 獲取到在這段時間內的運動速度,單位是像素/秒(即 1 秒內滑動的像素距離)。

這個我們自己用到的地方比較少,但是也可以幫助我們簡單的做出一些有趣的效果,例如下面的這種彈球效果,會根據滑動的力度和方向產生不同的彈跳效果。

其實這種原理非常簡單,簡化之後如下:

  1. 記錄 velocityX 和 velocityY 作爲初始速度,之後不斷讓速度衰減,直至爲零。
  2. 根據速度和當前小球的位置計算一段時間後的位置,並在該位置重新繪製小球。
  3. 判斷小球邊緣是否碰觸控件邊界,如果碰觸了邊界則讓速度反向。

根據這三條基本的邏輯就可以做出比較像的彈球效果,具體的Demo可以看這裏

2.3.3 onLongPress

這個是檢測長按事件的,即手指按下後不擡起,在一段時間後會觸發該事件。

@Override 
public void onLongPress(MotionEvent e) {
}
2.3.4 onScroll

onScroll 就是監聽滾動事件的,它看起來和 onFling 比較像,不同的是,onSrcoll 後兩個參數不是速度,而是滾動的距離。

@Override
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float 
        distanceY) {
    return super.onScroll(e1, e2, distanceX, distanceY);
}
參數  
e1 手指按下時的Event
e2 手指擡起時的Event
distanceX 在 X 軸上劃過的距離
distanceY 在 Y 軸上劃過的距離
2.3.5 onShowPress

它是用戶按下時的一種回調,主要作用是給用戶提供一種視覺反饋,可以在監聽到這種事件時可以讓控件換一種顏色,或者產生一些變化,告訴用戶他的動作已經被識別。

不過這個消息和 onSingleTapConfirmed 類似,也是一種延時回調,延遲時間是 180 ms,假如用戶手指按下後立即擡起或者事件立即被攔截,時間沒有超過 180 ms的話,這條消息會被 remove 掉,也就不會觸發這個回調。

@Override 
public void onShowPress(MotionEvent e) {
}
2.3.6 onSingleTapUp
@Override 
public boolean onSingleTapUp(MotionEvent e) {
    return super.onSingleTapUp(e);
}

這個也很容易理解,就是用戶單擊擡起時的回調,但是它和上面的 onSingleTapConfirmed 之間有何不同呢?和 onClick 又有何不同呢?

單擊事件觸發:

GCS: onSingleTapUp
GCS: onClick
GCS: onSingleTapConfirmed
類型 觸發次數 摘要
onSingleTapUp 1 單擊擡起
onSingleTapConfirmed 1 單擊確認
onClick 1 單擊事件

雙擊事件觸發:

GCS: onSingleTapUp
GCS: onClick
GCS: onDoubleTap // <- 雙擊
GCS: onClick
類型 觸發次數 摘要
onSingleTapUp 1 在雙擊的第一次擡起時觸發
onSingleTapConfirmed 0 雙擊發生時不會觸發。
onClick 2 在雙擊事件時觸發兩次。

可以看出來這三個事件還是有所不同的,根據自己實際需要進行使用即可

2.4 SimpleOnGestureListener

這個裏面並沒有什麼內容,只是對上面三種 Listener 的空實現,在上面的例子中使用的基本都是這監聽器。因爲它用起來更方便一點。

這主要是 GestureDetector 構造函數的設計問題,以只監聽 OnDoubleTapListener 爲例,如果想要使用 OnDoubleTapListener 接口則需要這樣進行設置:

GestureDetector detector = new GestureDetector(this, new GestureDetector
        .SimpleOnGestureListener());
detector.setOnDoubleTapListener(new GestureDetector.OnDoubleTapListener() {
    @Override public boolean onSingleTapConfirmed(MotionEvent e) {
        Toast.makeText(MainActivity.this, "單擊確認", Toast.LENGTH_SHORT).show();
        return false;
    }

    @Override public boolean onDoubleTap(MotionEvent e) {
        Toast.makeText(MainActivity.this, "雙擊", Toast.LENGTH_SHORT).show();
        return false;
    }

    @Override public boolean onDoubleTapEvent(MotionEvent e) {
        // Toast.makeText(MainActivity.this,"",Toast.LENGTH_SHORT).show();
        return false;
    }
});

既然都已經創建 SimpleOnGestureListener 了,再創建一個 OnDoubleTapListener 顯然十分浪費,如果構造函數不使用 SimpleOnGestureListener,而是使用 OnGestureListener 的話,會多出幾個無用的空實現,顯然很浪費,所以在一般情況下,老老實實的使用 SimpleOnGestureListener 就好了。

3. 相關方法

除了各類監聽器之外,與 GestureDetector 相關的方法其實並不多,只有幾個,下面來簡單介紹一下。

方法 摘要
setIsLongpressEnabled 通過布爾值設置是否允許觸發長按事件,true 表示允許,false 表示不允許。
isLongpressEnabled 判斷當前是否允許觸發長按事件,true 表示允許,false 表示不允許。
onTouchEvent 這個是其中一個重要的方法,在最開始已經演示過使用方式了。
onGenericMotionEvent 這個是在 API 23 之後才添加的內容,主要是爲 OnContextClickListener 服務的,暫時不用關注。
setContextClickListener 設置 ContextClickListener 。
setOnDoubleTapListener 設置 OnDoubleTapListener 。

關於手勢檢測部分的 GestureDetector 相關內容基本就這麼多了,其實手勢檢測還有一個 ScaleGestureDetector 也是爲手勢檢測服務

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章