Поддержка контроллеров в разных версиях Android

Если вы поддерживаете игровые контроллеры в своей игре, вы обязаны убедиться, что ваша игра одинаково реагирует на контроллеры на устройствах, работающих под управлением разных версий Android. Это позволит вашей игре охватить более широкую аудиторию, а ваши игроки смогут наслаждаться плавным игровым процессом со своими контроллерами, даже когда они переключают или обновляют свои устройства Android.

В этом уроке показано, как использовать API-интерфейсы, доступные в Android 4.1 и более поздних версиях, с обратной совместимостью, что позволяет вашей игре поддерживать следующие функции на устройствах под управлением 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. API-интерфейсы для поддержки игровых контроллеров в разных версиях Android.

Информация о контроллере API контроллера уровень API 12 уровень API 16
Идентификация устройства getInputDeviceIds()  
getInputDevice()  
getVibrator()  
SOURCE_JOYSTICK
SOURCE_GAMEPAD
Статус подключения onInputDeviceAdded()
onInputDeviceChanged()
onInputDeviceRemoved()
Идентификация входного события Нажмите D-pad ( 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. Создайте прокси-реализацию вашего интерфейса, использующую API в Android 4.1 и более поздних версиях.
  3. Создайте собственную реализацию своего интерфейса, использующую API, доступные в версиях от Android 3.1 до Android 4.0.
  4. Создайте логику переключения между этими реализациями во время выполнения и начните использовать интерфейс в своей игре.

Обзор того, как можно использовать абстракцию для обеспечения обратной совместимости приложений в разных версиях Android, см. в разделе Создание обратно совместимых пользовательских интерфейсов .

Добавьте интерфейс для обратной совместимости

Чтобы обеспечить обратную совместимость, вы можете создать собственный интерфейс, а затем добавить реализации для конкретной версии. Одним из преимуществ этого подхода является то, что он позволяет зеркально отображать общедоступные интерфейсы Android 4.1 (уровень API 16), поддерживающие игровые контроллеры.

Котлин

// 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)
    }
}

Ява

// 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() . Возвращает массив целых чисел, каждое из которых является идентификатором отдельного устройства ввода. Это полезно, если вы создаете игру, поддерживающую несколько игроков, и хотите определить, сколько контроллеров подключено.
registerInputDeviceListener()
Зеркала registerInputDeviceListener() . Позволяет зарегистрироваться, чтобы получать информацию о добавлении, изменении или удалении нового устройства.
unregisterInputDeviceListener()
Зеркала unregisterInputDeviceListener() . Отменяет регистрацию прослушивателя устройства ввода.
onGenericMotionEvent()
Зеркала onGenericMotionEvent() . Позволяет вашей игре перехватывать и обрабатывать объекты MotionEvent и значения осей, которые представляют такие события, как движения джойстика и нажатия аналоговых триггеров.
onPause()
Останавливает опрос событий игрового контроллера, когда основное действие приостановлено или когда игра больше не находится в фокусе.
onResume()
Начинает опрос событий игрового контроллера при возобновлении основного действия или когда игра запускается и работает на переднем плане.
InputDeviceListener
Отражает интерфейс InputManager.InputDeviceListener . Сообщает вашей игре, когда игровой контроллер был добавлен, изменен или удален.

Затем создайте реализации для InputManagerCompat , которые будут работать на разных версиях платформы. Если ваша игра работает на Android 4.1 или более поздней версии и вызывает метод InputManagerCompat , реализация прокси-сервера вызывает эквивалентный метод в InputManager . Однако если ваша игра работает на Android 3.1–4.0, пользовательская реализация обрабатывает вызовы методов InputManagerCompat , используя только API, представленные не позднее Android 3.1. Независимо от того, какая реализация конкретной версии используется во время выполнения, реализация прозрачно передает результаты вызова обратно в игру.

Рисунок 1. Диаграмма классов интерфейса и реализаций для конкретных версий.

Реализовать интерфейс на Android 4.1 и выше

InputManagerCompatV16 — это реализация интерфейса InputManagerCompat , который пересылает вызовы методов к фактическому InputManager и InputManager.InputDeviceListener . InputManager получается из системного Context .

Котлин

// 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
    ...
}

Ява

// 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.

Чтобы создать реализацию InputManagerCompat , поддерживающую версии от Android 3.1 до Android 4.0, вы можете использовать следующие объекты:

  • SparseArray идентификаторов устройств для отслеживания игровых контроллеров, подключенных к устройству.
  • Handler для обработки событий устройства. Когда приложение запускается или возобновляется, Handler получает сообщение о начале опроса на предмет отключения игрового контроллера. Handler запустит цикл для проверки каждого известного подключенного игрового контроллера и проверки, возвращен ли идентификатор устройства. Возвращаемое значение null указывает на то, что игровой контроллер отключен. Handler прекращает опрос, когда приложение приостанавливается.
  • Map объектов InputManagerCompat.InputDeviceListener . Вы будете использовать прослушиватели для обновления состояния подключения отслеживаемых игровых контроллеров.

Котлин

// 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)
    
}

Ява

// 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);
    }
}

Реализуйте объект PollingMessageHandler , расширяющий Handler , и переопределите метод handleMessage() . Этот метод проверяет, был ли отключен подключенный игровой контроллер, и уведомляет зарегистрированных прослушивателей.

Котлин

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)
                }
            }
        }
    }
}

Ява

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;
        }
    }
}

Чтобы запустить и остановить опрос на предмет отключения игрового контроллера, переопределите эти методы:

Котлин

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)
    }
    ...
}

Ява

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() . Когда система сообщает о событии движения, проверьте, произошло ли это событие от идентификатора устройства, которое уже отслеживается, или от нового идентификатора устройства. Если идентификатор устройства новый, уведомите зарегистрированных прослушивателей.

Котлин

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()
}

Ява

@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 вызывается соответствующий метод обратного вызова прослушивателя, чтобы сообщить, был ли игровой контроллер добавлен, изменен или удален.

Котлин

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
            }
        }
    }

}

Ява

@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.

Используйте реализацию для конкретной версии

Логика переключения, зависящая от версии, реализована в классе, который действует как фабрика .

Котлин

object Factory {
    fun getInputManager(context: Context): InputManagerCompat =
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
                InputManagerV16(context)
            } else {
                InputManagerV9()
            }
}

Ява

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 и зарегистрировать InputManagerCompat.InputDeviceListener в своем основном View . Благодаря установленной вами логике переключения версий ваша игра автоматически использует реализацию, соответствующую версии Android, на которой работает устройство.

Котлин

class GameView(context: Context) : View(context), InputManager.InputDeviceListener {
    private val inputManager: InputManagerCompat = Factory.getInputManager(context).apply {
        registerInputDeviceListener(this@GameView, null)
        ...
    }
    ...
}

Ява

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) и выше.

Котлин

override fun onGenericMotionEvent(event: MotionEvent): Boolean {
    inputManager.onGenericMotionEvent(event)

    // Handle analog input from the controller as normal
    ...
    return super.onGenericMotionEvent(event)
}

Ява

@Override
public boolean onGenericMotionEvent(MotionEvent event) {
    inputManager.onGenericMotionEvent(event);

    // Handle analog input from the controller as normal
    ...
    return super.onGenericMotionEvent(event);
}

Полную реализацию этого кода совместимости можно найти в классе GameView , представленном в образце ControllerSample.zip , доступном для скачивания выше.