如果您在游戏中支持游戏控制器,您有责任确保您的游戏能够在运行不同 Android 版本的设备上始终如一地响应控制器。这可以让您的游戏覆盖更广泛的受众,即使玩家更换或升级 Android 设备,他们也可以通过控制器享受无缝的游戏体验。
本课程演示如何以向后兼容的方式使用 Android 4.1 及更高版本中提供的 API,使您的游戏能够在运行 Android 3.1 及更高版本的设备上支持以下功能:
- 游戏可以检测到新的游戏控制器是否已添加、更改或移除。
- 游戏可以查询游戏控制器的功能。
- 游戏可以识别来自游戏控制器的传入运动事件。
本课程中的示例基于上面可下载的示例代码ControllerSample.zip
提供的参考实现。此示例演示如何实现InputManagerCompat
接口以支持不同版本的Android。要编译该示例,您必须使用Android 4.1(API级别16)或更高版本。编译后,示例应用可在运行Android 3.1(API级别12)或更高版本的任何设备上运行作为构建目标。
准备为游戏控制器支持抽象 API
假设您希望能够确定在运行 Android 3.1(API 级别 12)的设备上游戏控制器的连接状态是否已更改。但是,这些 API 仅在 Android 4.1(API 级别 16)及更高版本中可用,因此您需要提供一个支持 Android 4.1 及更高版本的实现,同时提供一个支持 Android 3.1 到 Android 4.0 的回退机制。
为了帮助您确定哪些功能需要针对旧版本的这种回退机制,表 1 列出了 Android 3.1(API 级别 12)和 4.1(API 级别 16)之间游戏控制器支持的差异。
控制器信息 | 控制器API | API级别12 | API级别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 ) |
• | • | |
摇杆和帽开关移动(AXIS_X ,AXIS_Y ,AXIS_Z ,AXIS_RZ ,AXIS_HAT_X ,AXIS_HAT_Y ) |
• | • | |
模拟触发器按下(AXIS_LTRIGGER ,AXIS_RTRIGGER ) |
• | • |
您可以使用抽象来构建跨平台工作的版本感知游戏控制器支持。此方法涉及以下步骤:
- 定义一个中间 Java 接口,该接口抽象化游戏所需的 game 控制器功能的实现。
- 创建接口的代理实现,该实现使用 Android 4.1 及更高版本的 API。
- 创建接口的自定义实现,该实现使用 Android 3.1 到 Android 4.0 之间可用的 API。
- 创建在运行时在这些实现之间切换的逻辑,然后开始在游戏中使用该接口。
有关如何使用抽象确保应用程序能够以向后兼容的方式跨不同版本的 Android 工作的概述,请参阅创建向后兼容的 UI。
添加用于向后兼容性的接口
为了提供向后兼容性,您可以创建一个自定义接口,然后添加特定于版本的实现。这种方法的一个优点是它允许您镜像 Android 4.1(API 级别 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()
- 当主活动暂停或游戏不再具有焦点时,停止轮询游戏控制器事件。
onResume()
- 当主活动恢复或游戏启动并在前台运行时,开始轮询游戏控制器事件。
InputDeviceListener
- 镜像
InputManager.InputDeviceListener
接口。让您的游戏知道何时添加、更改或删除了游戏控制器。
接下来,为InputManagerCompat
创建可在不同平台版本上运行的实现。如果您的游戏在 Android 4.1 或更高版本上运行并调用InputManagerCompat
方法,则代理实现将调用InputManager
和InputManager.InputDeviceListener
中的等效方法。但是,如果您的游戏在 Android 3.1 到 Android 4.0 上运行,则自定义实现将通过仅使用在 Android 3.1 之后引入的 API 来处理对InputManagerCompat
方法的调用。无论在运行时使用哪个特定于版本的实现,实现都会将调用结果透明地传递回游戏。
在 Android 4.1 及更高版本上实现接口
InputManagerCompatV16
是InputManagerCompat
接口的一个实现,它将方法调用代理到实际的InputManager
和InputManager.InputDeviceListener
。从系统Context
获取InputManager
。
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
实现,您可以使用以下对象:
- 一个
SparseArray
设备 ID 用于跟踪连接到设备的游戏控制器。 - 一个用于处理设备事件的
Handler
。当应用程序启动或恢复时,Handler
会接收一条消息,以开始轮询游戏控制器断开连接的情况。Handler
将启动一个循环,检查每个已知的已连接游戏控制器,并查看是否返回了设备 ID。null
返回值表示游戏控制器已断开连接。Handler
在应用程序暂停时停止轮询。 - 一个
Map
对象,其中包含InputManagerCompat.InputDeviceListener
对象。您将使用这些侦听器来更新已跟踪游戏控制器的连接状态。
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); ... } }
接下来,重写主视图中的 onGenericMotionEvent()
方法,如 处理来自游戏控制器的 MotionEvent 中所述。您的游戏现在应该能够在运行 Android 3.1(API 级别 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
类中找到此兼容性代码的完整实现。