如果你在游戏中支持游戏控制器,则你有责任确保游戏在不同 Android 版本的设备上对控制器的响应是一致的。这样可以让你的游戏触及更广泛的受众,并且你的玩家即使更换或升级 Android 设备,也可以使用他们的控制器享受流畅的游戏体验。
本课程演示了如何在 Android 4.1 及更高版本中使用 API,并以向后兼容的方式,让你的游戏能够在运行 Android 3.1 及更高版本的设备上支持以下功能:
- 游戏可以检测到新的游戏控制器是否已添加、更改或移除。
- 游戏可以查询游戏控制器的功能。
- 游戏可以识别来自游戏控制器的传入运动事件。
本课程中的示例基于上方可供下载的示例 ControllerSample.zip
提供的参考实现。此示例展示了如何实现 InputManagerCompat
接口以支持不同的 Android 版本。要编译此示例,你必须使用 Android 4.1 (API level 16) 或更高版本。编译后,此示例应用将作为构建目标在运行 Android 3.1 (API level 12) 或更高版本的任何设备上运行。
准备抽象化游戏控制器支持的 API
假设你希望能够确定游戏控制器的连接状态是否在运行 Android 3.1 (API level 12) 的设备上发生变化。但是,这些 API 仅在 Android 4.1 (API level 16) 及更高版本中可用,因此你需要提供一个支持 Android 4.1 及更高版本的实现,同时提供一个支持 Android 3.1 到 Android 4.0 的回退机制。
为了帮助你确定哪些功能需要此类旧版本的回退机制,表 1 列出了 Android 3.1 (API level 12) 和 4.1 (API level 16) 之间游戏控制器支持的差异。
表 1. 跨不同 Android 版本的游戏控制器支持 API。
控制器信息 | 控制器 API | API level 12 | API level 16 |
---|---|---|---|
设备识别 | getInputDeviceIds() |
• | |
getInputDevice() |
• | ||
getVibrator() |
• | ||
SOURCE_JOYSTICK |
• | • | |
SOURCE_GAMEPAD |
• | • | |
连接状态 | onInputDeviceAdded() |
• | |
onInputDeviceChanged() |
• | ||
onInputDeviceRemoved() |
• | ||
输入事件识别 | 方向键按下( KEYCODE_DPAD_UP 、 KEYCODE_DPAD_DOWN 、 KEYCODE_DPAD_LEFT 、 KEYCODE_DPAD_RIGHT 、 KEYCODE_DPAD_CENTER ) |
• | • |
游戏手柄按钮按下( BUTTON_A 、 BUTTON_B 、 BUTTON_THUMBL 、 BUTTON_THUMBR 、 BUTTON_SELECT 、 BUTTON_START 、 BUTTON_R1 、 BUTTON_L1 、 BUTTON_R2 、 BUTTON_L2 ) |
• | • | |
操纵杆和 Hat Switch 移动( AXIS_X 、 AXIS_Y 、 AXIS_Z 、 AXIS_RZ 、 AXIS_HAT_X 、 AXIS_HAT_Y ) |
• | • | |
模拟触发器按下( AXIS_LTRIGGER 、 AXIS_RTRIGGER ) |
• | • |
你可以使用抽象化来构建跨平台的版本感知游戏控制器支持。这种方法包括以下步骤:
- 定义一个中间 Java 接口,用于抽象化游戏所需的游戏控制器功能的实现。
- 创建该接口的代理实现,该实现使用 Android 4.1 及更高版本中的 API。
- 创建该接口的自定义实现,该实现使用 Android 3.1 到 Android 4.0 之间的 API。
- 创建在运行时切换这些实现并开始在游戏中使用该接口的逻辑。
有关如何使用抽象化来确保应用可以在不同 Android 版本中向后兼容的概览,请参阅创建向后兼容的界面。
为向后兼容添加接口
为了提供向后兼容性,你可以创建自定义接口,然后添加特定版本的实现。这种方法的一个优点是可以让你镜像 Android 4.1 (API level 16) 中支持游戏控制器的公共接口。
Kotlin
// The InputManagerCompat interface is a reference example. // The full code is provided in the ControllerSample.zip sample. interface InputManagerCompat { val inputDeviceIds: IntArray fun getInputDevice(id: Int): InputDevice fun registerInputDeviceListener( listener: InputManager.InputDeviceListener, handler: Handler? ) fun unregisterInputDeviceListener(listener:InputManager.InputDeviceListener) fun onGenericMotionEvent(event: MotionEvent) fun onPause() fun onResume() interface InputDeviceListener { fun onInputDeviceAdded(deviceId: Int) fun onInputDeviceChanged(deviceId: Int) fun onInputDeviceRemoved(deviceId: Int) } }
Java
// The InputManagerCompat interface is a reference example. // The full code is provided in the ControllerSample.zip sample. public interface InputManagerCompat { ... public InputDevice getInputDevice(int id); public int[] getInputDeviceIds(); public void registerInputDeviceListener( InputManagerCompat.InputDeviceListener listener, Handler handler); public void unregisterInputDeviceListener( InputManagerCompat.InputDeviceListener listener); public void onGenericMotionEvent(MotionEvent event); public void onPause(); public void onResume(); public interface InputDeviceListener { void onInputDeviceAdded(int deviceId); void onInputDeviceChanged(int deviceId); void onInputDeviceRemoved(int deviceId); } ... }
InputManagerCompat
接口提供以下方法:
getInputDevice()
- 镜像
getInputDevice()
。获取表示游戏控制器功能的InputDevice
对象。 getInputDeviceIds()
- 镜像
getInputDeviceIds()
。返回一个整数数组,其中每个整数都是不同输入设备的 ID。如果你正在构建一个支持多玩家的游戏并希望检测连接了多少个控制器,这将非常有用。 registerInputDeviceListener()
- 镜像
registerInputDeviceListener()
。允许你注册以便在新设备添加、更改或移除时获得通知。 unregisterInputDeviceListener()
- 镜像
unregisterInputDeviceListener()
。注销输入设备监听器。 onGenericMotionEvent()
- 镜像
onGenericMotionEvent()
。允许你的游戏拦截和处理MotionEvent
对象和轴值,这些对象和轴值表示操纵杆移动和模拟触发器按下等事件。 onPause()
- 当主 Activity 暂停或游戏失去焦点时,停止轮询游戏控制器事件。
onResume()
- 当主 Activity 恢复或游戏启动并在前台运行时,开始轮询游戏控制器事件。
InputDeviceListener
- 镜像
InputManager.InputDeviceListener
接口。通知游戏何时添加、更改或移除游戏控制器。
接下来,为 InputManagerCompat
创建跨不同平台版本的实现。如果你的游戏在 Android 4.1 或更高版本上运行并调用 InputManagerCompat
方法,则代理实现会调用 InputManager
中等效的方法。但是,如果你的游戏在 Android 3.1 到 Android 4.0 上运行,则自定义实现仅使用 Android 3.1 及更早版本中引入的 API 来处理对 InputManagerCompat
方法的调用。无论运行时使用哪个版本特定实现,该实现都会将调用结果透明地传回给游戏。

图 1. 接口和版本特定实现的类图。
在 Android 4.1 及更高版本上实现接口
InputManagerCompatV16
是 InputManagerCompat
接口的一个实现,它将方法调用代理到实际的 InputManager
和 InputManager.InputDeviceListener
。 InputManager
是从系统 Context
获取的。
Kotlin
// The InputManagerCompatV16 class is a reference implementation. // The full code is provided in the ControllerSample.zip sample. public class InputManagerV16( context: Context, private val inputManager: InputManager = context.getSystemService(Context.INPUT_SERVICE) as InputManager, private val listeners: MutableMap<InputManager.InputDeviceListener, V16InputDeviceListener> = mutableMapOf() ) : InputManagerCompat { override val inputDeviceIds: IntArray = inputManager.inputDeviceIds override fun getInputDevice(id: Int): InputDevice = inputManager.getInputDevice(id) override fun registerInputDeviceListener( listener: InputManager.InputDeviceListener, handler: Handler? ) { V16InputDeviceListener(listener).also { v16listener -> inputManager.registerInputDeviceListener(v16listener, handler) listeners += listener to v16listener } } // Do the same for unregistering an input device listener ... override fun onGenericMotionEvent(event: MotionEvent) { // unused in V16 } override fun onPause() { // unused in V16 } override fun onResume() { // unused in V16 } } class V16InputDeviceListener( private val idl: InputManager.InputDeviceListener ) : InputManager.InputDeviceListener { override fun onInputDeviceAdded(deviceId: Int) { idl.onInputDeviceAdded(deviceId) } // Do the same for device change and removal ... }
Java
// The InputManagerCompatV16 class is a reference implementation. // The full code is provided in the ControllerSample.zip sample. public class InputManagerV16 implements InputManagerCompat { private final InputManager inputManager; private final Map<InputManagerCompat.InputDeviceListener, V16InputDeviceListener> listeners; public InputManagerV16(Context context) { inputManager = (InputManager) context.getSystemService(Context.INPUT_SERVICE); listeners = new HashMap<InputManagerCompat.InputDeviceListener, V16InputDeviceListener>(); } @Override public InputDevice getInputDevice(int id) { return inputManager.getInputDevice(id); } @Override public int[] getInputDeviceIds() { return inputManager.getInputDeviceIds(); } static class V16InputDeviceListener implements InputManager.InputDeviceListener { final InputManagerCompat.InputDeviceListener mIDL; public V16InputDeviceListener(InputDeviceListener idl) { mIDL = idl; } @Override public void onInputDeviceAdded(int deviceId) { mIDL.onInputDeviceAdded(deviceId); } // Do the same for device change and removal ... } @Override public void registerInputDeviceListener(InputDeviceListener listener, Handler handler) { V16InputDeviceListener v16Listener = new V16InputDeviceListener(listener); inputManager.registerInputDeviceListener(v16Listener, handler); listeners.put(listener, v16Listener); } // Do the same for unregistering an input device listener ... @Override public void onGenericMotionEvent(MotionEvent event) { // unused in V16 } @Override public void onPause() { // unused in V16 } @Override public void onResume() { // unused in V16 } }
在 Android 3.1 到 Android 4.0 上实现接口
要创建支持 Android 3.1 到 Android 4.0 的 InputManagerCompat
实现,可以使用以下对象:
- 一个用于跟踪连接到设备的游戏控制器的设备 ID 的
SparseArray
。 - 一个用于处理设备事件的
Handler
。当应用启动或恢复时,Handler
会收到一条消息,开始轮询游戏控制器断开连接。Handler
将启动一个循环来检查每个已知的连接游戏控制器,并查看是否返回设备 ID。返回null
值表示游戏控制器已断开连接。当应用暂停时,Handler
将停止轮询。 - 一个
InputManagerCompat.InputDeviceListener
对象的Map
。你将使用这些监听器来更新跟踪的游戏控制器的连接状态。
Kotlin
// The InputManagerCompatV9 class is a reference implementation. // The full code is provided in the ControllerSample.zip sample. class InputManagerV9( val devices: SparseArray<Array<Long>> = SparseArray(), private val listeners: MutableMap<InputManager.InputDeviceListener, Handler> = mutableMapOf() ) : InputManagerCompat { private val defaultHandler: Handler = PollingMessageHandler(this) … }
Java
// The InputManagerCompatV9 class is a reference implementation. // The full code is provided in the ControllerSample.zip sample. public class InputManagerV9 implements InputManagerCompat { private final SparseArray<long[]> devices; private final Map<InputDeviceListener, Handler> listeners; private final Handler defaultHandler; … public InputManagerV9() { devices = new SparseArray<long[]>(); listeners = new HashMap<InputDeviceListener, Handler>(); defaultHandler = new PollingMessageHandler(this); } }
实现一个扩展 Handler
的 PollingMessageHandler
对象,并重写 handleMessage()
方法。此方法检查附加的游戏控制器是否已断开连接,并通知注册的监听器。
Kotlin
private class PollingMessageHandler( inputManager: InputManagerV9, private val mInputManager: WeakReference<InputManagerV9> = WeakReference(inputManager) ) : Handler() { override fun handleMessage(msg: Message) { super.handleMessage(msg) when (msg.what) { MESSAGE_TEST_FOR_DISCONNECT -> { mInputManager.get()?.also { imv -> val time = SystemClock.elapsedRealtime() val size = imv.devices.size() for (i in 0 until size) { imv.devices.valueAt(i)?.also { lastContact -> if (time - lastContact[0] > CHECK_ELAPSED_TIME) { // check to see if the device has been // disconnected val id = imv.devices.keyAt(i) if (null == InputDevice.getDevice(id)) { // Notify the registered listeners // that the game controller is disconnected imv.devices.remove(id) } else { lastContact[0] = time } } } } sendEmptyMessageDelayed(MESSAGE_TEST_FOR_DISCONNECT, CHECK_ELAPSED_TIME) } } } } }
Java
private static class PollingMessageHandler extends Handler { private final WeakReference<InputManagerV9> inputManager; PollingMessageHandler(InputManagerV9 im) { inputManager = new WeakReference<InputManagerV9>(im); } @Override public void handleMessage(Message msg) { super.handleMessage(msg); switch (msg.what) { case MESSAGE_TEST_FOR_DISCONNECT: InputManagerV9 imv = inputManager.get(); if (null != imv) { long time = SystemClock.elapsedRealtime(); int size = imv.devices.size(); for (int i = 0; i < size; i++) { long[] lastContact = imv.devices.valueAt(i); if (null != lastContact) { if (time - lastContact[0] > CHECK_ELAPSED_TIME) { // check to see if the device has been // disconnected int id = imv.devices.keyAt(i); if (null == InputDevice.getDevice(id)) { // Notify the registered listeners // that the game controller is disconnected imv.devices.remove(id); } else { lastContact[0] = time; } } } } sendEmptyMessageDelayed(MESSAGE_TEST_FOR_DISCONNECT, CHECK_ELAPSED_TIME); } break; } } }
要开始和停止轮询游戏控制器断开连接,请重写以下方法:
Kotlin
private const val MESSAGE_TEST_FOR_DISCONNECT = 101 private const val CHECK_ELAPSED_TIME = 3000L class InputManagerV9( val devices: SparseArray<Array<Long>> = SparseArray(), private val listeners: MutableMap<InputManager.InputDeviceListener, Handler> = mutableMapOf() ) : InputManagerCompat { ... override fun onPause() { defaultHandler.removeMessages(MESSAGE_TEST_FOR_DISCONNECT) } override fun onResume() { defaultHandler.sendEmptyMessageDelayed(MESSAGE_TEST_FOR_DISCONNECT, CHECK_ELAPSED_TIME) } ... }
Java
private static final int MESSAGE_TEST_FOR_DISCONNECT = 101; private static final long CHECK_ELAPSED_TIME = 3000L; @Override public void onPause() { defaultHandler.removeMessages(MESSAGE_TEST_FOR_DISCONNECT); } @Override public void onResume() { defaultHandler.sendEmptyMessageDelayed(MESSAGE_TEST_FOR_DISCONNECT, CHECK_ELAPSED_TIME); }
要检测输入设备是否已添加,请重写 onGenericMotionEvent()
方法。当系统报告运动事件时,检查此事件是否来自已跟踪的设备 ID 或新的设备 ID。如果设备 ID 是新的,则通知注册的监听器。
Kotlin
override fun onGenericMotionEvent(event: MotionEvent) { // detect new devices val id = event.deviceId val timeArray: Array<Long> = mDevices.get(id) ?: run { // Notify the registered listeners that a game controller is added ... arrayOf<Long>().also { mDevices.put(id, it) } } timeArray[0] = SystemClock.elapsedRealtime() }
Java
@Override public void onGenericMotionEvent(MotionEvent event) { // detect new devices int id = event.getDeviceId(); long[] timeArray = mDevices.get(id); if (null == timeArray) { // Notify the registered listeners that a game controller is added ... timeArray = new long[1]; mDevices.put(id, timeArray); } long time = SystemClock.elapsedRealtime(); timeArray[0] = time; }
通过使用 Handler
对象将 DeviceEvent
Runnable
对象发送到消息队列来实现监听器的通知。 DeviceEvent
包含对 InputManagerCompat.InputDeviceListener
的引用。当 DeviceEvent
运行时,会调用监听器的相应回调方法来指示游戏控制器是已添加、已更改还是已移除。
Kotlin
class InputManagerV9( val devices: SparseArray<Array<Long>> = SparseArray(), private val listeners: MutableMap<InputManager.InputDeviceListener, Handler> = mutableMapOf() ) : InputManagerCompat { ... override fun registerInputDeviceListener( listener: InputManager.InputDeviceListener, handler: Handler? ) { listeners[listener] = handler ?: defaultHandler } override fun unregisterInputDeviceListener(listener: InputManager.InputDeviceListener) { listeners.remove(listener) } private fun notifyListeners(why: Int, deviceId: Int) { // the state of some device has changed listeners.forEach { listener, handler -> DeviceEvent.getDeviceEvent(why, deviceId, listener).also { handler?.post(it) } } } ... } private val sObjectQueue: Queue<DeviceEvent> = ArrayDeque<DeviceEvent>() private class DeviceEvent( private var mMessageType: Int, private var mId: Int, private var mListener: InputManager.InputDeviceListener ) : Runnable { companion object { fun getDeviceEvent(messageType: Int, id: Int, listener: InputManager.InputDeviceListener) = sObjectQueue.poll()?.apply { mMessageType = messageType mId = id mListener = listener } ?: DeviceEvent(messageType, id, listener) } override fun run() { when(mMessageType) { ON_DEVICE_ADDED -> mListener.onInputDeviceAdded(mId) ON_DEVICE_CHANGED -> mListener.onInputDeviceChanged(mId) ON_DEVICE_REMOVED -> mListener.onInputDeviceChanged(mId) else -> { // Handle unknown message type } } } }
Java
@Override public void registerInputDeviceListener(InputDeviceListener listener, Handler handler) { listeners.remove(listener); if (handler == null) { handler = defaultHandler; } listeners.put(listener, handler); } @Override public void unregisterInputDeviceListener(InputDeviceListener listener) { listeners.remove(listener); } private void notifyListeners(int why, int deviceId) { // the state of some device has changed if (!listeners.isEmpty()) { for (InputDeviceListener listener : listeners.keySet()) { Handler handler = listeners.get(listener); DeviceEvent odc = DeviceEvent.getDeviceEvent(why, deviceId, listener); handler.post(odc); } } } private static class DeviceEvent implements Runnable { private int mMessageType; private int mId; private InputDeviceListener mListener; private static Queue<DeviceEvent> sObjectQueue = new ArrayDeque<DeviceEvent>(); ... static DeviceEvent getDeviceEvent(int messageType, int id, InputDeviceListener listener) { DeviceEvent curChanged = sObjectQueue.poll(); if (null == curChanged) { curChanged = new DeviceEvent(); } curChanged.mMessageType = messageType; curChanged.mId = id; curChanged.mListener = listener; return curChanged; } @Override public void run() { switch (mMessageType) { case ON_DEVICE_ADDED: mListener.onInputDeviceAdded(mId); break; case ON_DEVICE_CHANGED: mListener.onInputDeviceChanged(mId); break; case ON_DEVICE_REMOVED: mListener.onInputDeviceRemoved(mId); break; default: // Handle unknown message type ... break; } // Put this runnable back in the queue sObjectQueue.offer(this); } }
现在你有两个 InputManagerCompat
的实现:一个适用于运行 Android 4.1 及更高版本的设备,另一个适用于运行 Android 3.1 到 Android 4.0 的设备。
使用版本特定实现
版本特定切换逻辑在一个充当工厂的类中实现。
Kotlin
object Factory { fun getInputManager(context: Context): InputManagerCompat = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { InputManagerV16(context) } else { InputManagerV9() } }
Java
public static class Factory { public static InputManagerCompat getInputManager(Context context) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { return new InputManagerV16(context); } else { return new InputManagerV9(); } } }
现在你只需实例化一个 InputManagerCompat
对象,并在主 View
中注册一个 InputManagerCompat.InputDeviceListener
。由于你设置了版本切换逻辑,你的游戏会自动使用适合设备运行的 Android 版本的实现。
Kotlin
class GameView(context: Context) : View(context), InputManager.InputDeviceListener { private val inputManager: InputManagerCompat = Factory.getInputManager(context).apply { registerInputDeviceListener(this@GameView, null) ... } ... }
Java
public class GameView extends View implements InputDeviceListener { private InputManagerCompat inputManager; ... public GameView(Context context, AttributeSet attrs) { inputManager = InputManagerCompat.Factory.getInputManager(this.getContext()); inputManager.registerInputDeviceListener(this, null); ... } }
接下来,如处理游戏控制器中的 MotionEvent 中所述,在主视图中重写 onGenericMotionEvent()
方法。你的游戏现在应该能够在运行 Android 3.1 (API level 12) 及更高版本的设备上一致地处理游戏控制器事件。
Kotlin
override fun onGenericMotionEvent(event: MotionEvent): Boolean { inputManager.onGenericMotionEvent(event) // Handle analog input from the controller as normal ... return super.onGenericMotionEvent(event) }
Java
@Override public boolean onGenericMotionEvent(MotionEvent event) { inputManager.onGenericMotionEvent(event); // Handle analog input from the controller as normal ... return super.onGenericMotionEvent(event); }
你可以在上方可供下载的示例 ControllerSample.zip
中提供的 GameView
类中找到此兼容性代码的完整实现。