跨 Android 版本支持控制器

如果您在游戏中支持游戏控制器,则您有责任确保您的游戏在运行不同 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)之间游戏控制器支持的差异。

表 1. 跨不同 Android 版本的游戏控制器支持 API。

控制器信息 控制器 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)

您可以使用抽象构建跨平台工作的版本感知游戏控制器支持。此方法涉及以下步骤

  1. 定义一个中间 Java 接口,该接口抽象化游戏所需的游戏控制器功能的实现。
  2. 创建接口的代理实现,该实现使用 Android 4.1 及更高版本的 API。
  3. 创建接口的自定义实现,该实现使用 Android 3.1 到 Android 4.0 之间可用的 API。
  4. 创建在运行时在这些实现之间切换的逻辑,并开始在您的游戏中使用该接口。

有关如何使用抽象来确保应用程序可以在不同 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 中的等效方法。但是,如果您的游戏在 Android 3.1 到 Android 4.0 上运行,则自定义实现会通过仅使用在 Android 3.1 之前引入的 API 来处理对 InputManagerCompat 方法的调用。无论在运行时使用哪个特定于版本的实现,实现都会将调用结果透明地传递回游戏。

图 1. 接口和特定于版本的实现的类图。

在 Android 4.1 及更高版本上实现接口

InputManagerCompatV16InputManagerCompat 接口的实现,它将方法调用代理到实际的 InputManagerInputManager.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);
    }
}

实现一个扩展 HandlerPollingMessageHandler 对象,并覆盖 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 类中找到此兼容性代码的完整实现。