跨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_UPKEYCODE_DPAD_DOWNKEYCODE_DPAD_LEFTKEYCODE_DPAD_RIGHTKEYCODE_DPAD_CENTER
游戏手柄按钮按下(BUTTON_ABUTTON_BBUTTON_THUMBLBUTTON_THUMBRBUTTON_SELECTBUTTON_STARTBUTTON_R1BUTTON_L1BUTTON_R2BUTTON_L2
摇杆和帽开关移动(AXIS_XAXIS_YAXIS_ZAXIS_RZAXIS_HAT_XAXIS_HAT_Y
模拟触发器按下(AXIS_LTRIGGERAXIS_RTRIGGER

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

  1. 定义一个中间 Java 接口,该接口抽象化游戏所需的 game 控制器功能的实现。
  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方法,则代理实现将调用InputManagerInputManager.InputDeviceListener中的等效方法。但是,如果您的游戏在 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 类中找到此兼容性代码的完整实现。