Android 鼠標外設適配方法摘要


說明

鼠標的適配,此處介紹兩種適配方式(可能不全)。具體的適配需要請教相關人員,或者是拿到硬件設備一個個的調試。但是不管怎么,基本上還是走的事件的分發流程。或者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. 判斷是否有按鍵被按下或者抬起了
  2. 判斷被按下的鍵是鼠標的按鍵

針對第 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

注意點

  1. 按鍵事件(KeyEvent)和觸摸事件(MotionEvent),可以同時存在,並不互斥。所以,項目中,針對鼠標的標准鍵和額外鍵。需要同時處理兩個事件分發流程(dispatchKeyEvent,dispatchTouchEvent),二者都是 Activity ---> ViewGroup ---> View 這樣的順序分發。為了防止異步帶來的影響,處理時,二者最好處於同一線程,並定義好各種情況的處理邏輯。
  2. KeyEvent 並沒有 Cancel 事件,所以走 KeyEvent 的事件流是以 Up 動作作為事件流的結尾(我從我們公司的系統工程師那里了解到的)。這樣會帶來一個問題:可能某些異常場景兼容不了。舉個例子,有個按住說話,松開發送的功能(類似於微信的語音消息錄制)。如果你用一根手指按住屏幕說話,然后說話時按下第二根、第三根手指。那么在按到第三根手指時,系統會下發一個 Cancel 事件,結束事件流,此時,當前被錄制的語音就會被取消掉,不會發送出去。但是,如果你使用了像話筒這樣的外設,話筒有個錄音鍵,錄音鍵按下錄音,松開發送。這個錄音鍵走了 KeyEvent 的流程,那么無論什么場景,話筒錄制的語音都會被發送出去。因為 KeyEvent 沒有 Cancel 事件,只有 Up 事件,每個 KeyEvent 的事件流都是以 Up 事件結束的。而 Up 動作對應的是 松開錄音鍵發送語音消息 這條規則。





實際適配邏輯

好了,以上是理論上適配鼠標的主要知識點。下面來講講實際的適配過程。

理想中的鼠標適配邏輯是很清晰的,但是實際上,鼠標的不同鍵,可能對應不同的功能。假如鼠標二鍵需要實現 Android 系統 back 鍵的功能,該怎么辦呢?這種功能當然是由系統工程師和專門的硬件工程師去做啦。我們這些應用小開發,只能根據他們定的規則去適配。

但是如果鼠標二鍵真的實現了 Android 系統 back 鍵的功能。該怎么適配呢?我們需要根據以下的知識點來適配:

  1. back 鍵下發,走的是 KeyEvent 的流程
  2. back 鍵的鍵值是KeyEvent.KEYCODE_BACK
  3. 鼠標鍵的按下、抬起狀態,在 keyEvent 中,對應的是KeyEvent.ACTION_DOWN/KeyEvent.ACTION_UP
  4. 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 下發時消費掉事件即可

上面的適配邏輯只是一種思路,具體的場景,具體的問題,具體處理。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM