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

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

В этом уроке показано, как использовать API, доступные в Android 4.1 и выше, обеспечивая обратную совместимость, что позволит вашей игре поддерживать следующие функции на устройствах под управлением Android 3.1 и выше:

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

Подготовьтесь к абстрагированию 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 до 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)
    }
}

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() . Она возвращает массив целых чисел, каждое из которых является идентификатором для отдельного устройства ввода. Это полезно, если вы разрабатываете игру с поддержкой нескольких игроков и хотите определить, сколько контроллеров подключено.
registerInputDeviceListener()
Функция registerInputDeviceListener() позволяет зарегистрироваться для получения уведомлений о добавлении, изменении или удалении нового устройства.
unregisterInputDeviceListener()
Аналогично функции unregisterInputDeviceListener() . Отменяет регистрацию слушателя входного устройства.
onGenericMotionEvent()
Функция `mirrors 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
    ...
}

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

Для создания реализации InputManagerCompat , поддерживающей Android от 3.1 до 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)
    
}

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

Реализуйте объект 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)
                }
            }
        }
    }
}

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

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

Котлин

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

Котлин

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

Котлин

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.

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

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

Котлин

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

Котлин

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

Котлин

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

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