說明
鼠標的適配,此處介紹兩種適配方式(可能不全)。具體的適配需要請教相關人員,或者是拿到硬件設備一個個的調試。但是不管怎么,基本上還是走的事件的分發流程。或者dispatchKeyEvent
,或者dispatchTouchEvent
,前者代表按鍵事件KeyEvent
的分發流程,后者代表MotionEvent
的分發流程。兩個事件都是InputEvent
的子類,所以流程和方法等,都存在着極大的相似性。
通常,鼠標的使用是搭配界面來的,在 Android 中,界面就是 Activity。而監聽鼠標的按鍵操作,是配合着事件分發來的。Activity 一路下發到 View。
通常,鼠標會有左、中、右三個鍵,但是因為左右鍵可以互相切換,所以鼠標的三個鍵通稱為主鍵(Primary Button)、二鍵(Secondary Button)、三鍵(Tertiary Button)。某些鼠標可能還有更多的鍵,如控制鍵、錄音鍵等,這些鍵的判斷方式可能需要自定義,視硬件的不同而改變,通常需要和硬件、系統開發等協商確定。
通用適配方案
判斷事件來源
每個事件都有個事件源,標准的鼠標事件源是 InputDevice.SOURCE_MOUSE
,可以通過 InputEvent.isFromSource
方法判斷:
// 方法:InputEvent.isFromSource。MotionEvent 繼承自 InputEvent。
// 事件源是鼠標時,返回 true,否則返回 false。
motionEvent.isFromSource(InputDevice.SOURCE_MOUSE);
判斷鼠標鍵按下抬起
上面講過,鼠標通常會有左、中、右三個鍵,但是因為左右鍵可以互相切換,所以鼠標的三個鍵通稱為主鍵(Primary Button)、二鍵(Secondary Button)、三鍵(Tertiary Button)。一般情況下,其鍵位對應情況是:主鍵 <---> 左鍵,二鍵 <---> 右鍵,三鍵 <---> 中鍵。在 Android 中,三個鍵對應的常量是:
- 主鍵:MotionEvent.BUTTON_PRIMARY
- 二鍵:MotionEvent.BUTTON_SECONDARY
- 三鍵:MotionEvent.BUTTON_TERTIARY
某些鼠標還有更多的鍵,如控制鍵、錄音鍵等,這些鍵的判斷方式可以使用系統的判斷方式,也可能需要自定義,視硬件的不同而改變,通常需要和硬件、系統開發等協商確定。以系統的錄音鍵為例,其按鍵常量為:
- 錄音鍵:KeyEvent.KEYCODE_MEDIA_RECORD
要判斷鼠標鍵被按下了,需要經過兩步:
- 判斷是否有按鍵被按下或者抬起了
- 判斷被按下的鍵是鼠標的按鍵
針對第 1 點:我們知道在觸摸事件中,ACTION_DOWN 標識了按下動作。而對於按鈕,也對應者一個動作 ACTION_BUTTON_PRESS。而對於抬起動作,則對應了 ACTION_UP 和 ACTION_BUTTON_RELEASE,所以要判斷是否有鼠標按鈕被按下和抬起,我們可以使用以下的代碼:
// 事件 MotionEvent 從 Activity 或者 View 的 onTouchEvent 方法獲取
public boolean onMouseEvent(MotionEvent motionEvent) {
if(!motionEvent.isFromSource(InputDevice.SOURCE_MOUSE)) {
// 事件不來自鼠標,不處理
Log.d(TAG, "事件源不是鼠標");
return false;
}
switch (motionEvent.getActionMasked()) {
// 判斷兩個動作,基本上能保證准確
case MotionEvent.ACTION_BUTTON_PRESS:
case MotionEvent.ACTION_DOWN:
// 鼠標按鍵按下
judgeButtonPress(motionEvent);
Log.d(TAG, "消費來自鼠標的事件");
return true;
case MotionEvent.ACTION_BUTTON_RELEASE:
case MotionEvent.ACTION_UP:
// 鼠標按鍵抬起
judgeButtonRelease(motionEvent);
Log.d(TAG, "消費來自鼠標的事件");
return true;
}
return false;
}
針對第 2 點:MotionEvent 傳遞下來時,會有對應的所有 Button 的狀態。通過 MotionEvent.getButtonState()
方法,可以得到所有的狀態,特定的狀態可以通過位運算得到。
但是注意,雖然我們能夠通過判斷 ACTION_BUTTON_RELEASE 來確定有抬起事件,但是我們並不知道是鼠標的哪個鍵被抬起了(`MotionEvent.getButtonState() 方法無法確定),我們只能知道按鍵的按下狀態。所以我們需要在按鍵被按下時,記錄按鍵被按下了,以便后續判斷抬起狀態,抬起時重置變量。在一個鼠標的事件序列中,抬起動作應該總在按下動作之后。所以我們在按下時記錄,在抬起時重置,是可行的。
下面是判斷按鍵按下的方法:
public void judgeButtonPress(MotionEvent motionEvent) {
// 幾個鍵可能同時被按下(概率較低)
if (isButtonPress(motionEvent, MotionEvent.BUTTON_PRIMARY)) {
// 主鍵被按下了,此處變量緩存,用於后面判斷抬起
isPrimaryPressed = true;
}
if (isButtonPress(motionEvent, MotionEvent.BUTTON_SECONDARY)) {
// 二鍵被按下了
isSecondPressed = true;
}
if (isButtonPress(motionEvent, MotionEvent.BUTTON_TERTIARY)) {
// 三鍵被按下了
isTertiaryPressed = true;
}
}
/**
* 判斷是否按鍵被按下
* 兩種場景:
* 1. 大於目標 SDK 版本,如果按下,則不管(1);如果沒按下,則需要用低版本的檢測方法再檢測一
* 次,雙重保險,不會出錯。對於國內廠商的魔改系統,這是很重要的一點經驗(2)
* 2. 小於目標 SDK 版本,則用低版本的檢測方法(3)
* 綜上,可以把 (2)、(3) 合為一種判斷
* */
public boolean isButtonPress(MotionEvent motionEvent, int button) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP
&& motionEvent.isButtonPressed(button)) {
return true;
}
// 位運算,下面兩種寫法等效
// 寫法 1:(motionEvent.getButtonState() & MotionEvent.BUTTON_PRIMARY) != 0
// 下面是寫法 2,是 Google 官方采用的寫法,詳見 MotionEvent.isButtonPressed() 方法
if (button == 0) {
return false;
}
return (motionEvent.getButtonState() & button) == button;
}
上面的代碼定義了用方法isButtonPress
來判斷鼠標按鍵的狀態,true 表示按下,false 表示未按下。以主鍵為例,判斷鼠標抬起的邏輯如下:
private void judgeButtonRelease(MotionEvent motionEvent) {
// 通過當前事件,判斷鼠標鍵是否被按下
boolean nowPrimaryPressState =
isButtonPress(motionEvent, MotionEvent.BUTTON_PRIMARY);
// 鼠標可能同時被按下,也有可能同時被松開
// 之前主鍵被按下了,這次事件,主鍵沒按下。則表明主鍵松開了
if(isPrimaryPressed && !nowPrimaryPressState) {
// 重置變量
isPrimaryPressed = false;
}
}
判斷抬起狀態
在 Android 中,可以通過判斷 MotionEvent.ACTION_BUTTON_RELEASE,來確定是不是鼠標的抬起動作,代碼使用如下:
public boolean onMouseEvent(MotionEvent motionEvent) {
if(!motionEvent.isFromSource(InputDevice.SOURCE_MOUSE)) {
// 事件不來自鼠標
Log.d(TAG, "事件源不是鼠標");
return false;
}
switch (motionEvent.getActionMasked()) {
case MotionEvent.ACTION_BUTTON_RELEASE:
// 按鍵抬起
judgeButtonRelease(motionEvent);
Log.d(TAG, "消費來自鼠標的事件");
return true;
}
return false;
}
然后通過按下時記錄的變量,進行對比,以判斷是否有按鍵抬起:
private void judgeButtonRelease(MotionEvent motionEvent) {
// ACTION_BUTTON_RELEASE 事件,不會攜帶按鍵信息,只能通過按下的狀態來判斷。
// 通過當前事件,判斷鼠標鍵是否被按下
boolean nowPrimaryPressState = isButtonPress(motionEvent, MotionEvent.BUTTON_PRIMARY);
boolean nowSecondaryPressState = isButtonPress(motionEvent, MotionEvent.BUTTON_SECONDARY);
// 鼠標可能同時被按下,也有可能同時被松開
if(isPrimaryPressed && isSecondPressed && !nowPrimaryPressState && !nowSecondaryPressState) {
// 之前兩個鍵按下,現在兩個鍵沒按下(兩個鍵的 Release)
isPrimaryPressed = false;
isSecondPressed = false;
} else if(isPrimaryPressed && !nowPrimaryPressState) {
// 之前主鍵按下,現在按鍵沒按下(單個鍵的 Release)
isPrimaryPressed = false;
// Release 后,可以觸發相關動作,此處省略
} else if(isSecondPressed && !nowSecondaryPressState) {
// 之前二鍵按下,現在二鍵沒按下(單個鍵的 Release)
isSecondPressed = false;
// Release 后,可以觸發相關動作,此處省略
}
// 三鍵的判斷邏輯類似,此處省略
}
鼠標的移動
鼠標的移動,主要是通過鼠標的指針位置來判斷的,下面的方法用來獲取鼠標指針的位置:
public boolean onMouseEvent(MotionEvent motionEvent) {
if(!motionEvent.isFromSource(InputDevice.SOURCE_MOUSE)) {
// 事件不來自鼠標
Log.d(TAG, "事件源不是鼠標");
return false;
}
switch (motionEvent.getActionMasked()) {
case MotionEvent.ACTION_MOVE:
case MotionEvent.ACTION_HOVER_MOVE:
// 獲取指針在屏幕上 X 軸的位置,值的單位是 dp
float axisX = motionEvent.getAxisValue(MotionEvent.AXIS_X);
// 獲取指針在屏幕上 Y 軸的位置,值的單位是 dp
float axisY = motionEvent.getAxisValue(MotionEvent.AXIS_Y);
// 另一種寫法:int x = (int)motionEvent.getRawX(); int y = (int)motionEvent.getRawY();
Log.d(TAG, "消費來自鼠標的事件");
return true;
}
return false;
}
鼠標的滾動
鼠標的滾動,采用以下方法判斷
public boolean onMouseEvent(MotionEvent motionEvent) {
if(!motionEvent.isFromSource(InputDevice.SOURCE_MOUSE)) {
// 事件不來自鼠標
Log.d(TAG, "事件源不是鼠標");
return false;
}
switch (motionEvent.getActionMasked()) {
case MotionEvent.ACTION_SCROLL:
// 一般的鼠標,這個用不上。獲取水平方向上的滾動距離,值從 -1(向左滾動) 到 1(向右滾動)
float hScroll = motionEvent.getAxisValue(MotionEvent.AXIS_HSCROLL);
// 獲取垂直方向上的滾動距離,值從 -1(向下滾動) 到 1(向上滾動)
float vScroll = motionEvent.getAxisValue(MotionEvent.AXIS_VSCROLL);
Log.d(TAG, "消費來自鼠標的事件");
return true;
}
return false;
}
外設關注的方法
在事件分發流程中,要判斷外設的事件,還有幾個方法值得關注:
public class TestView extends View {
/**
* 分發通用觸摸事件
*/
@Override
public boolean dispatchGenericMotionEvent(MotionEvent event) {
return handle;
}
/**
* 分發按鍵事件
*/
@Override
public boolean dispatchKeyEvent(KeyEvent event) {
return handle;
}
/**
* Android 8.0+ 支持鼠標捕獲(鼠標的指針捕獲)
*/
@Override
public boolean dispatchCapturedPointerEvent(MotionEvent event) {
return handle;
}
}
指針捕獲
指針捕獲就不講了,可以參考以下兩篇文章:
// 參考鏈接:https://developer.android.com/training/gestures/movement#pointer-capture
// 參考鏈接:https://blog.csdn.net/yeshennet/article/details/105802579
注意點
- 按鍵事件(KeyEvent)和觸摸事件(MotionEvent),可以同時存在,並不互斥。所以,項目中,針對鼠標的標准鍵和額外鍵。需要同時處理兩個事件分發流程(dispatchKeyEvent,dispatchTouchEvent),二者都是 Activity ---> ViewGroup ---> View 這樣的順序分發。為了防止異步帶來的影響,處理時,二者最好處於同一線程,並定義好各種情況的處理邏輯。
- KeyEvent 並沒有 Cancel 事件,所以走 KeyEvent 的事件流是以 Up 動作作為事件流的結尾(我從我們公司的系統工程師那里了解到的)。這樣會帶來一個問題:可能某些異常場景兼容不了。舉個例子,有個按住說話,松開發送的功能(類似於微信的語音消息錄制)。如果你用一根手指按住屏幕說話,然后說話時按下第二根、第三根手指。那么在按到第三根手指時,系統會下發一個 Cancel 事件,結束事件流,此時,當前被錄制的語音就會被取消掉,不會發送出去。但是,如果你使用了像話筒這樣的外設,話筒有個錄音鍵,錄音鍵按下錄音,松開發送。這個錄音鍵走了 KeyEvent 的流程,那么無論什么場景,話筒錄制的語音都會被發送出去。因為 KeyEvent 沒有 Cancel 事件,只有 Up 事件,每個 KeyEvent 的事件流都是以 Up 事件結束的。而 Up 動作對應的是 松開錄音鍵發送語音消息 這條規則。
實際適配邏輯
好了,以上是理論上適配鼠標的主要知識點。下面來講講實際的適配過程。
理想中的鼠標適配邏輯是很清晰的,但是實際上,鼠標的不同鍵,可能對應不同的功能。假如鼠標二鍵需要實現 Android 系統 back 鍵的功能,該怎么辦呢?這種功能當然是由系統工程師和專門的硬件工程師去做啦。我們這些應用小開發,只能根據他們定的規則去適配。
但是如果鼠標二鍵真的實現了 Android 系統 back 鍵的功能。該怎么適配呢?我們需要根據以下的知識點來適配:
- back 鍵下發,走的是 KeyEvent 的流程
- back 鍵的鍵值是
KeyEvent.KEYCODE_BACK
- 鼠標鍵的按下、抬起狀態,在 keyEvent 中,對應的是
KeyEvent.ACTION_DOWN
/KeyEvent.ACTION_UP
KeyEvent.ACTION_DOWN
/KeyEvent.ACTION_UP
對應 Activity 的onKeyDown
/onKeyUp
方法
下面是部分代碼:
// 自定義的 Activity 類中
// 此方法的參數,向下傳遞時,可剔除 keyCode,少傳一個參數
// keyCode 可以使用 event.getKeyCode() == keyCode 判斷
public boolean onKeyDown(int keyCode, KeyEvent event) {
// DOWN 和 UP 聚合起來,統一處理
if(handleKeyEvent(event)) {
// 被處理了
return true;
}
return super.onKeyDown(keyCode, event);
}
@Override
public boolean onKeyUp(int keyCode, KeyEvent event) {
if(handleKeyEvent(event)) {
// 被處理了
return true;
}
return super.onKeyUp(keyCode, event);
}
/**
* true 表示處理事件,false 表示不處理事件
* */
boolean handleKeyEvent(KeyEvent keyEvent) {
if(keyEvent.getKeyCode() != KeyEvent.KEYCODE_BACK) {
return false;
}
if(keyEvent.getAction() == KeyEvent.ACTION_DOWN) {
// 按鍵按下
return onButtonPress(keyEvent);
} else if (keyEvent.getAction() == KeyEvent.ACTION_UP) {
// 按鍵抬起
return onButtonRelease(keyEvent);
}
return false;
}
注意點
- 對於 home 鍵的事件,我們無法做到攔截,只能通過生命周期或者 home 鍵的廣播處理一些需要在 home 鍵觸發時處理的業務流程
- 對於 back 事件,我們可以攔截,只需要在 KeyEvent 下發時消費掉事件即可
上面的適配邏輯只是一種思路,具體的場景,具體的問題,具體處理。