Android中物理输入设备的接入与使用

Android中物理输入设备的接入与使用

Android可以使用蓝牙接入手柄,蓝牙接键盘、OTG接键盘鼠标。本文整理了关于如何处理这些外部设备的输入信息的方法。

设备接入后,Android系统会做一次中转。,把具体的事件按照传统的Android事件做分发。
作为开发者,我们要处理以上全部的硬件事件,只需要关注View中的三个函数即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class PhysicalView extends View {

    ...

    @Override
    public boolean dispatchGenericMotionEvent(MotionEvent event) {
        return handle;
    }

    @Override
    public boolean dispatchKeyEvent(KeyEvent event) {
        return handle;
    }

    @Override
    public boolean dispatchCapturedPointerEvent(MotionEvent event) {
        return handle;
    }
}

使用时的注意点是,PhysicalView 这个View必须有子View是可点击的/可以获得输入焦点的。
上述的几个方法是以PhysicalView为根布局的设定下,对硬件事件做拦截处理。
返回true,即消费了事件,阻止了默认分发行为。
以下内容是我阅读文档+尝试+请教已有经验的同事整理出来的。

手柄事件

回调dispatchGenericMotionEvent、dispatchKeyEvent

如何判断是手柄设备?

1
2
3
4
5
6
7
// isGamepad(event.getDevice())
public final boolean isGamepad(@Nullable InputDevice dev) {
    if (dev == null) return false;
    int sources = dev.getSources();
    return ((sources & InputDevice.SOURCE_GAMEPAD) == InputDevice.SOURCE_GAMEPAD)
            && ((sources & InputDevice.SOURCE_JOYSTICK) == InputDevice.SOURCE_JOYSTICK);
}

手柄输入有哪些裸数据?

手柄有这些按键

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
KeyEvent.KEYCODE_BUTTON_A:
KeyEvent.KEYCODE_BUTTON_B:
KeyEvent.KEYCODE_BUTTON_X:
KeyEvent.KEYCODE_BUTTON_Y:
KeyEvent.KEYCODE_BUTTON_L1:
KeyEvent.KEYCODE_BUTTON_R1:
KeyEvent.KEYCODE_BUTTON_L2:
KeyEvent.KEYCODE_BUTTON_R2:
KeyEvent.KEYCODE_BACK:
KeyEvent.KEYCODE_BUTTON_SELECT:
KeyEvent.KEYCODE_BUTTON_START:
KeyEvent.KEYCODE_BUTTON_THUMBL:
KeyEvent.KEYCODE_BUTTON_THUMBR:
KeyEvent.KEYCODE_DPAD_UP:
KeyEvent.KEYCODE_DPAD_DOWN:
KeyEvent.KEYCODE_DPAD_LEFT:
KeyEvent.KEYCODE_DPAD_RIGHT:

event.getAction() == KeyEvent.ACTION_DOWN 即键盘按下。在手柄这里也是一个意思。

手柄一般有左右手柄球、上下左右方向键。具体可以是这样处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
//left control ball
float axisX = event.getAxisValue(MotionEvent.AXIS_X);
float axisY = event.getAxisValue(MotionEvent.AXIS_Y);
if (Float.compare(axisX, oldAxisX) != 0 || Float.compare(axisY, oldAxisY) != 0) {
    //
}
oldAxisX = axisX;
oldAxisY = axisY;

// right control ball
float axisZ = event.getAxisValue(MotionEvent.AXIS_Z);
float axisRZ = event.getAxisValue(MotionEvent.AXIS_RZ);
if (Float.compare(axisZ, oldAxisZ) != 0 || Float.compare(axisRZ, oldAxisRZ) != 0) {
    //
}
oldAxisZ = axisZ;
oldAxisRZ = axisRZ;

// direction left right
float axisHatX = event.getAxisValue(MotionEvent.AXIS_HAT_X);
if (Float.compare(axisHatX, -1.0f) == 0) {// left
    //
} else if (Float.compare(axisHatX, 1.0f) == 0) {// right
    //
} else if (Float.compare(axisHatX, 0f) == 0) {
    if (Float.compare(oldAxisHatX, -1.0f) == 0) {// release left
        //
    } else if (Float.compare(oldAxisHatX, 1.0f) == 0) {// release right
        //
    }
}
oldAxisHatX = axisHatX;

// direction up down
float axisHatY = event.getAxisValue(MotionEvent.AXIS_HAT_Y);
if (Float.compare(axisHatY, -1.0f) == 0) {// up
    //
} else if (Float.compare(axisHatY, 1.0f) == 0) {// down
    //
} else if (Float.compare(axisHatY, 0f) == 0) {
    if (Float.compare(oldAxisHatY, -1.0f) == 0) {// release  up
        //
    } else if (Float.compare(oldAxisHatY, 1.0f) == 0) {// release down
        //
    }
}
oldAxisHatY = axisHatY;

手柄模式?

值得注意的是手柄一般可以切模式。一些资料可能会这样介绍,在Android中也有概念会简化化成,能不能拿到某个按键的的范围。

1
2
3
4
5
6
7
public final boolean isSupportMode(InputDevice dev){
    if(dev == null)return false;
    if(dev.getMotionRange(MotionEvent.AXIS_LTRIGGER) != null && dev.getMotionRange(MotionEvent.AXIS_RTRIGGER) != null){
        return true;
    }
    return dev.getMotionRange(MotionEvent.AXIS_GAS) != null && dev.getMotionRange(MotionEvent.AXIS_BRAKE) != null;
}

我弄了几个手柄,觉得这种方式不可靠。更为健壮的做法是,直观接受系统发出来的事件,按照标准的规格处理就好。
在硬件差异的屏蔽上,系统做了很多努力,这里没必要在手动做一层拦截(拦截存在误判)。

键盘事件

回调dispatchKeyEvent

如何判断是键盘设备?

1
2
3
4
5
6
// isKeyboard(event.getDevice())
public final boolean isKeyboard(@Nullable InputDevice dev) {
    if (dev == null) return false;
    return (dev.getSources() & InputDevice.SOURCE_KEYBOARD) != 0
            && (dev.getKeyboardType() == InputDevice.KEYBOARD_TYPE_ALPHABETIC);
}

Android的键盘码是在KeyEvent中定义的。它与window/mac上的scancode是两套独立的系统,和H5中的key也是不兼容的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<!DOCTYPE html>
<html>

<head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <title>YESHEN</title>
    <style type="text/css">
    body {
        margin: 16px;
    }
    </style>
</head>

<body>
    <input id="yeshen-log" placeholder="Click here, then press down a key." size="40">
</body>
<script type="text/javascript">
const input = document.getElementById('yeshen-log');
input.addEventListener('keydown', function logKey(e) {
    console.log(e.key, e.keyCode,e.keyCode.toString(16));
});
</script>
</html>

H5上的键盘码可以如上这样看。我在Android的API中未能找到 e.key e.keyCode 的直接映射关系。

鼠标事件

回调dispatchGenericMotionEvent;dispatchCapturedPointerEvent(Android8.0+支持鼠标捕获)

如何判断是鼠标设备?

1
2
3
4
5
6
7
8
// isMouse(event.getDevice())
public final boolean isMouse(@Nullable InputDevice dev) {
    if (dev == null) return false;
    return (dev.getSources() & InputDevice.SOURCE_MOUSE) == InputDevice.SOURCE_MOUSE
            && (dev.getSources() & (InputDevice.SOURCE_KEYBOARD | InputDevice.SOURCE_JOYSTICK)) == 0;
}

// all message from dispatchCapturedPointerEvent

如何做鼠标捕获

1
2
3
# 参考
https://developer.android.com/training/gestures/movement#pointer-capture
https://stackoverflow.com/a/12743948
1
2
3
4
5
6
7
8
9
private boolean pointerCapture(View v) {
    if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return false;
    if (!v.hasPointerCapture()) {
        v.requestPointerCapture();
        return true;
    }
    return false;
}
// after PointerCapture,mouse event will dispatch by dispatchCapturedPointerEvent

需要注意的是,插拔鼠标的时候,会有大量的鼠标事件产生,处理鼠标事件的代码耗时越短越好。

什么是鼠标捕获呢?

要从什么是鼠标开始说起。Android中有个累是专门处理指针的,它会获取鼠标的物理输入,然后转化成相对坐标,在屏幕上绘制出来。
在Android8+之后开放了View获取鼠标原始数据的方法。也就是说,我们可以自己处理鼠标的绘制与显示。
鼠标捕获的使用场景主要是在FPS游戏中,鼠标的方向就是枪头的方向。

鼠标有哪些原始数据?

  1. 左/右/中间 鼠标是否按下的事件
1
2
3
4
5
6
7
8
if(event.getActionMasked()==MotionEvent.ACTION_BUTTON_PRESS || event.getActionMasked()==MotionEvent.ACTION_DOWN){
    if ((event.getButtonState() & MotionEvent.BUTTON_PRIMARY) != 0) {// left mouse button
    }
    if ((event.getButtonState() & MotionEvent.BUTTON_TERTIARY) != 0) {//middle mouse button
    }
    if ((event.getButtonState() & MotionEvent.BUTTON_SECONDARY) != 0) {//right mouse button
    }
}
  1. 中间滚轮滚动的事件
1
2
3
if(event.getActionMasked()==MotionEvent.ACTION_SCROLL){
    float vscroll = event.getAxisValue(MotionEvent.AXIS_VSCROLL)
}
  1. 鼠标移动的事件
1
2
float axisX = event.getAxisValue(MotionEvent.AXIS_X);
float axisY = event.getAxisValue(MotionEvent.AXIS_Y);