在開發 Android 手機應用過程中,可能需要對一些手勢作出響應,如:單擊、雙擊、長按、滑動、縮放等。這些都是很常用的手勢。就拿最簡單的雙擊來說吧,假如我們需要判斷一個控件是否被雙擊(即在較短的時間內快速的點擊兩次),似乎是一個很容易的任務,但仔細考慮起來,要處理的細節問題也有不少,例如:
- 記錄點擊次數,爲了判斷是否被點擊超過 1 次,所以必須記錄點擊次數。
- 記錄點擊時間,由於雙擊事件是較快速的點擊兩次,像點擊一次後,過來幾分鐘再點擊一次肯定不能算是雙擊事件,所以在記錄點擊次數的同時也要記錄上一次的點擊時間,我們可以設置本次點擊距離上一次時間超過一定時間(例如:超過100ms)就不識別爲雙擊事件。
- 點擊狀態重置,在響應雙擊事件,或者判斷不是雙擊事件的時候要重置計數器和上一次點擊時間。重置既可以在點擊的時候判斷並進行重新設置,也可以使用定時器等超過一定時間後重置狀態。
這樣看起來,判斷一個雙擊事件就有這麼多麻煩事情,更別其他的手勢了,雖然這些看起來都很簡單,但設計起來需要考慮的細節情況實在是太多了。
那麼有沒有一種更好的方法來方便的檢測手勢呢?當然有啦,因爲這些手勢很常用,系統早就封裝了一些方法給我們用,接下來我們就看看它們是如何使用的。
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 秒內滑動的像素距離)。
這個我們自己用到的地方比較少,但是也可以幫助我們簡單的做出一些有趣的效果,例如下面的這種彈球效果,會根據滑動的力度和方向產生不同的彈跳效果。
其實這種原理非常簡單,簡化之後如下:
- 記錄 velocityX 和 velocityY 作爲初始速度,之後不斷讓速度衰減,直至爲零。
- 根據速度和當前小球的位置計算一段時間後的位置,並在該位置重新繪製小球。
- 判斷小球邊緣是否碰觸控件邊界,如果碰觸了邊界則讓速度反向。
根據這三條基本的邏輯就可以做出比較像的彈球效果,具體的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 也是爲手勢檢測服務