ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Android 官方培训课程中文版:跨 Android 版本兼容地支持游戏控制器(InputManagerCompat 抽象层实战)

Android 官方培训课程中文版:跨 Android 版本兼容地支持游戏控制器(InputManagerCompat 抽象层实战) 文档教程移动开发【免费下载链接】android-training-course-in-chineseAndroid官方培训课程中文版项目地址https://gitcode.com/gh_mirrors/an/android-training-course-in-chinese点击查看免费下载本指南对应 android-training-course-in-chinese 仓库中 input/game-controller/compatibility.md 一课的内容。它解决的是游戏开发者最常见的痛点之一当你的游戏要为玩家提供手柄支持时如何让同一套游戏代码在从 Android 2.3API level 9到 Android 4.1API level 16及以上的所有设备上对控制器的检测、查询与事件处理保持一致。读完本文你将掌握通过「接口抽象 双版本实现 运行时工厂切换」的兼容层方案写出无需改动游戏逻辑即可自适应系统版本的控制器支持代码。背景为什么游戏控制器支持需要跨版本兼容Android framework 为游戏控制器提供了检测输入设备、查询设备能力以及处理按键/摇杆事件的 API但这些 API 并非在最早的系统版本中就全部存在。当我们为游戏接入控制器支持时如果直接调用高版本 API游戏在旧设备上就会崩溃或功能缺失如果只使用旧 API又会损失新系统上更完善的控制器能力。在本课程对应的原始参考实现ControllerSample.zip中官方给出的目标非常明确用 Android 4.1API level 16或更高版本编译示例代码但生成的 App 可以运行在 Android 2.3API level 9或更高版本的任何设备上。这要求我们在三件事上做到跨版本一致游戏能够检测是否有新的游戏控制器接入、变更或移除游戏能够查询游戏控制器的兼容性设备是否支持手柄按键、摇杆等游戏能够识别从游戏控制器传入的动作事件。版本差异图谱API level 9 / 12 / 16 之间的能力鸿沟为了确定哪些功能需要提供回退机制原始文档给出了下表对比了 Android 2.3API level 9、Android 3.1API level 12与 Android 4.1API level 16之间游戏控制器支持的关键差异*表示该 API 在对应版本上可用Controller InformationController APIAPI level 9API level 12API level 16Device IdentificationgetInputDeviceIds()*Device IdentificationgetInputDevice()*Device IdentificationgetVibrator()*Device IdentificationSOURCE_JOYSTICK**Device IdentificationSOURCE_GAMEPAD**Connection StatusonInputDeviceAdded()*Connection StatusonInputDeviceChanged()*Connection StatusonInputDeviceRemoved()*Input Event IdentificationD-pad pressKEYCODE_DPAD_UP、KEYCODE_DPAD_DOWN、KEYCODE_DPAD_LEFT、KEYCODE_DPAD_RIGHT、KEYCODE_DPAD_CENTER***Input Event IdentificationGamepad button pressBUTTON_A、BUTTON_B、BUTTON_THUMBL、BUTTON_THUMBR、BUTTON_SELECT、BUTTON_START、BUTTON_R1、BUTTON_L1、BUTTON_R2、BUTTON_L2**Input Event IdentificationJoystick and hat switch movementAXIS_X、AXIS_Y、AXIS_Z、AXIS_RZ、AXIS_HAT_X、AXIS_HAT_Y**Input Event IdentificationAnalog trigger pressAXIS_LTRIGGER、AXIS_RTRIGGER**从表中可以读出三条关键结论D-pad 按键码是最基础的输入从 API level 9 起就一直可用手柄按键Gamepad button、摇杆/方向帽坐标轴、模拟扳机从 API level 12Android 3.1才开始出现设备识别与连接状态监听InputManager系列 API、InputDeviceListener回调则是 API level 16Android 4.1的专属能力——这正是兼容层需要重点解决的部分。抽象化四步法兼容层的总体思路原始文档给出的解决方案是使用**抽象化abstraction**概念建立一套能够跨平台版本工作且可识别版本的游戏控制器支持共四个步骤定义一个中间 Java 接口抽象化游戏需要的游戏控制器功能实现创建一个使用 Android 4.1 及更高版本 API 的接口代理实现创建一个使用 Android 2.3 到 Android 4.0 之间可用 API 的接口自定义实现创建运行时在上述实现之间切换的逻辑并在游戏代码中开始使用该接口。这套模式与仓库中 ui/backward-compatible-ui/index.md 一课所讲的「创建向后兼容 UI」思想同源先抽象接口再分别提供新版本代理实现与旧版本自实现最后用版本感知的工厂在运行时选择具体实现。本节的主角InputManagerCompat接口正是这套思想的游戏控制器落地版本。定义向后兼容接口 InputManagerCompat第一步是定义一个与官方InputManagerAPI 形态相近的自定义接口。这样做的好处是可以借鉴 Android 4.1API level 16上支持游戏控制器的公共接口设计让高版本实现成为「直接代理」低版本实现成为「模拟代理」游戏代码看到的则是完全一致的方法签名。// 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); } ... }接口方法逐一解析getInputDevice(int id)借鉴官方InputManager.getInputDevice(int)。返回包含一个游戏控制器兼容性信息的InputDevice对象供游戏查询设备支持的能力例如是否具备手柄按键、摇杆。getInputDeviceIds()借鉴InputManager.getInputDeviceIds()。返回整型数组每个成员代表一个不同输入设备的 ID。对想要支持多玩家、检测当前连接了多少个控制器的游戏非常有用。registerInputDeviceListener(listener, handler)借鉴InputManager.registerInputDeviceListener(...)。注册监听器当有新设备添加、设备改变或设备移除时收到通知。unregisterInputDeviceListener(listener)借鉴InputManager.unregisterInputDeviceListener(...)。注销输入设备监听器。onGenericMotionEvent(MotionEvent)借鉴View.onGenericMotionEvent(MotionEvent)。让游戏截取并处理MotionEvent对象以及代表类似移动摇杆、按下模拟扳机等事件的坐标值。onPause()当主 Activity 暂停或游戏不再聚焦时停止轮询游戏控制器事件。onResume()当主 Activity 恢复或游戏重新在前台运行时启动轮询游戏控制器事件。InputDeviceListener借鉴InputManager.InputDeviceListener接口。当控制器被添加、改变或移除时通知游戏。说明onPause()/onResume()在高版本实现中是空操作因为高版本有系统级监听器但在低版本实现中承担着启动/停止轮询的关键职责——这正体现了接口设计对不同版本能力差异的优雅收纳。代理实现 InputManagerV16Android 4.1 及更高版本当游戏运行在 Android 4.1 或更高版本时InputManagerCompat的代理实现会直接把方法调用转发给系统级InputManager通过系统Context获取以及InputManager.InputDeviceListener实现透明转发、零成本复用系统能力// 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 mInputManager; private final Map mListeners; public InputManagerV16(Context context) { mInputManager (InputManager) context.getSystemService(Context.INPUT_SERVICE); mListeners new HashMap(); } Override public InputDevice getInputDevice(int id) { return mInputManager.getInputDevice(id); } Override public int[] getInputDeviceIds() { return mInputManager.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); mInputManager.registerInputDeviceListener(v16Listener, handler); mListeners.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 } }实现要点通过context.getSystemService(Context.INPUT_SERVICE)获得系统InputManager单例监听器适配模式由于游戏侧注册的是InputManagerCompat.InputDeviceListener而系统要求注册的是InputManager.InputDeviceListener这里通过内部静态类V16InputDeviceListener做一次适配包装并在mListenersMap中保存「游戏 listener → 系统 listener」的映射以便注销时能精确地反查并移除对应的系统监听器onGenericMotionEvent()、onPause()、onResume()在 V16 中均为空实现——系统监听机制已经覆盖了设备增删改的检测无需轮询。自实现 InputManagerV9Android 2.3 到 Android 4.0在缺少InputManager与InputDeviceListener的旧系统上InputManagerV9使用不晚于 Android 2.3 引入的 API 模拟出同样的能力。它依赖三个核心对象SparseArray设备表跟踪已连接设备的游戏控制器Handler轮询机制App 启动或恢复时收到消息开始轮询控制器是否断开暂停时停止Maplistener, handler监听器表用于向游戏侧更新控制器的连接状态。// 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 mDevices; private final Map mListeners; private final Handler mDefaultHandler; … public InputManagerV9() { mDevices new SparseArray(); mListeners new HashMap(); mDefaultHandler new PollingMessageHandler(this); } }轮询断开检测PollingMessageHandlerPollingMessageHandler继承Handler并重写handleMessage()定期检查已连接的游戏控制器是否已经断开并通知已注册的 listener。为了不阻止旧设备上内存回收InputManagerV9实例被以WeakReference方式持有private static class PollingMessageHandler extends Handler { private final WeakReference mInputManager; PollingMessageHandler(InputManagerV9 im) { mInputManager new WeakReference(im); } Override public void handleMessage(Message msg) { super.handleMessage(msg); switch (msg.what) { case MESSAGE_TEST_FOR_DISCONNECT: InputManagerV9 imv mInputManager.get(); if (null ! imv) { long time SystemClock.elapsedRealtime(); int size imv.mDevices.size(); for (int i 0; i size; i) { long[] lastContact imv.mDevices.valueAt(i); if (null ! lastContact) { if (time - lastContact[0] CHECK_ELAPSED_TIME) { // check to see if the device has been // disconnected int id imv.mDevices.keyAt(i); if (null InputDevice.getDevice(id)) { // Notify the registered listeners // that the game controller is disconnected ... imv.mDevices.remove(id); } else { lastContact[0] time; } } } } sendEmptyMessageDelayed(MESSAGE_TEST_FOR_DISCONNECT, CHECK_ELAPSED_TIME); } break; } } }核心逻辑解读每个设备在mDevices中对应一个long[1]时间戳数组记录「最后一次联系时间」每隔CHECK_ELAPSED_TIME示例中为3000L毫秒检查一次若距上次联系已超时则调用InputDevice.getDevice(id)验证设备是否仍存在返回null表示控制器已断开此时通知 listener 并移除记录否则刷新时间戳通过sendEmptyMessageDelayed()自我调度实现周期性轮询形成轻量的消息循环。启动与停止轮询onPause / onResume当主 Activity 暂停或恢复时通过移除/发送轮询消息来启停检测private static final int MESSAGE_TEST_FOR_DISCONNECT 101; private static final long CHECK_ELAPSED_TIME 3000L; Override public void onPause() { mDefaultHandler.removeMessages(MESSAGE_TEST_FOR_DISCONNECT); } Override public void onResume() { mDefaultHandler.sendEmptyMessageDelayed(MESSAGE_TEST_FOR_DISCONNECT, CHECK_ELAPSED_TIME); }借助动作事件发现新设备onGenericMotionEvent旧系统没有设备接入的回调因此 V9 采用「被动发现」策略当系统上报任意MotionEvent时检查事件来源设备 ID 是否已在mDevices中若是陌生设备 ID则视为新控制器接入通知 listener 并将该设备加入跟踪表同时刷新其时间戳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; }通知分发DeviceEvent 与消息队列listener 的通知通过Handler向消息队列投递一个DeviceEvent实现Runnable完成。DeviceEvent携带一个InputManagerCompat.InputDeviceListener引用运行时根据消息类型回调对应的「添加 / 改变 / 移除」方法Override public void registerInputDeviceListener(InputDeviceListener listener, Handler handler) { mListeners.remove(listener); if (handler null) { handler mDefaultHandler; } mListeners.put(listener, handler); } Override public void unregisterInputDeviceListener(InputDeviceListener listener) { mListeners.remove(listener); } private void notifyListeners(int why, int deviceId) { // the state of some device has changed if (!mListeners.isEmpty()) { for (InputDeviceListener listener : mListeners.keySet()) { Handler handler mListeners.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 sObjectQueue new ArrayDeque(); ... 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); } }值得注意的优化细节DeviceEvent对象并非每次新建而是从静态ArrayDeque对象池中取用用毕归还offer避免高频事件通知造成对象频繁分配与 GC 压力——这是一个适合游戏场景的小型对象复用范例。至此我们已经拥有两个InputManagerCompat实现一个服务于 Android 4.1 及以上InputManagerV16一个服务于 Android 2.3 到 Android 4.0InputManagerV9。运行时版本切换Factory 工厂与 GameView 集成版本切换逻辑被封装在一个充当Factory工厂的静态类中根据Build.VERSION.SDK_INT与Build.VERSION_CODES.JELLY_BEAN对应 Android 4.1 / API level 16的比较结果选择返回对应实现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版本切换完全透明public class GameView extends View implements InputDeviceListener { private InputManagerCompat mInputManager; ... public GameView(Context context, AttributeSet attrs) { mInputManager InputManagerCompat.Factory.getInputManager(this.getContext()); mInputManager.registerInputDeviceListener(this, null); ... } }下一步重写主 View 的onGenericMotionEvent()方法完整的运动事件处理细节可参考 处理控制器输入动作 一课将事件先交给兼容层处理用于旧版本下的设备发现再按常规逻辑处理模拟量输入Override public boolean onGenericMotionEvent(MotionEvent event) { mInputManager.onGenericMotionEvent(event); // Handle analog input from the controller as normal ... return super.onGenericMotionEvent(event); }完成上述步骤后游戏即可在 Android 2.3API level 9及更高版本设备上一致地处理游戏控制器事件。该兼容性的完整代码可在ControllerSample.zip示例的GameView类中找到。架构总览与扩展阅读下面的类图展示了这一兼容层方案的整体结构对应本仓库中的 input/game-controller/backward-compatible-inputmanager.pngFigure 1.接口与特定版本实现的类图图中可以看到完整的设计闭环GameView橙色标注 Your game code只依赖蓝色的InputManagerCompat接口不接触任何系统 APIInputManagerCompat双向分支连接两个实现InputManagerV16标注 Proxy implementation on Android 4.1 and higher与InputManagerV9标注 Implementation on Android 2.3 up to Android 4.0运行时由工厂根据系统版本选择具体实现游戏代码零改动。这一「接口 代理 自实现 工厂」的四件套结构本质上与仓库中 创建向后兼容的 UI 课程TabCompat示例所倡导的抽象化方法论完全一致可作为开发任何需要跨版本兼容能力的组件时的通用范式。从兼容层到完整手柄体验掌握了兼容层之后可以继续在仓库的 支持游戏控制器 课程体系内深入两件事处理控制器输入动作学习如何通过onKeyDown()、onGenericMotionEvent()等回调把 D-pad 按键码如KEYCODE_DPAD_UP、摇杆坐标轴如AXIS_X、AXIS_HAT_X与模拟扳机如AXIS_LTRIGGER、AXIS_RTRIGGER映射为具体游戏动作并处理摇杆居中平面区getFlat()与历史采样点getHistoricalAxisValue()等细节支持多个游戏控制器借助本课InputDeviceListener的onInputDeviceAdded()/onInputDeviceRemoved()回调用SparseArray维护「设备 ID → 玩家化身」的映射实现同一台设备上多名玩家各自独立操作。小结游戏控制器相关能力在 Android 不同版本间差异显著D-pad 全版本可用手柄按键与摇杆从 API level 12 起可用而设备识别与连接监听从 API level 16 起才提供。通过自定义InputManagerCompat接口、InputManagerV16代理实现、InputManagerV9轮询自实现以及Factory运行时切换可以让同一份游戏代码在 Android 2.3 及以上所有版本获得一致的控制器体验。核心工程要点包括V16 侧用适配器包装系统监听器并维护映射以便注销V9 侧用HandlerWeakReference 超时检测模拟断开发现、用MotionEvent被动发现新设备、用DeviceEvent对象池降低通知开销并在onPause()/onResume()中正确启停轮询。相关完整实现与配套课程均收录于本仓库input/game-controller/目录下可作为实战代码的直接参考。赞分享文档教程移动开发【免费下载链接】android-training-course-in-chineseAndroid官方培训课程中文版项目地址https://gitcode.com/gh_mirrors/an/android-training-course-in-chinese点击查看免费下载相关推荐鸣潮自动化终极指南解放双手让游戏回归乐趣鸣潮自动化终极指南解放双手让游戏回归乐趣 你是否曾经计算过自己在《鸣潮》中花费了多少时间在重复的日常任务上每天登录游戏刷副本、收集声骸、完成每日委托这文档教程移动开发Android官方培训课程中文版教程Android官方培训课程中文版教程 1. 项目介绍 Android官方培训课程中文版Android Training Course in Chinese是文档教程移动开发YimMenu终极防护指南5步打造最安全的GTA5游戏体验YimMenu终极防护指南5步打造最安全的GTA5游戏体验 YimMenu是一款专为GTA5在线模式设计的开源辅助工具它不仅提供了丰富的游戏功能增强更重要逆向工程游戏开发上一篇抖音发布触发短信二次验证时如何用 verify_code.txt 提供验证码下一篇MoocDownloader实践教程三步把中国大学MOOC课程存进硬盘离线看创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表