Если вы поддерживаете игровые контроллеры в своей игре, вы обязаны убедиться, что ваша игра одинаково реагирует на контроллеры на устройствах, работающих под управлением разных версий 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 ) | • | • |
Вы можете использовать абстракцию для создания поддержки игровых контроллеров с учетом версий, которая работает на разных платформах. Этот подход включает в себя следующие шаги:
- Определите промежуточный интерфейс Java, который абстрагирует реализацию функций игрового контроллера, необходимых вашей игре.
- Создайте прокси-реализацию вашего интерфейса, использующую API в Android 4.1 и более поздних версиях.
- Создайте собственную реализацию своего интерфейса, использующую API, доступные в версиях от Android 3.1 до Android 4.0.
- Создайте логику переключения между этими реализациями во время выполнения и начните использовать интерфейс в своей игре.
Обзор того, как можно использовать абстракцию для обеспечения обратной совместимости приложений в разных версиях 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. Независимо от того, какая реализация конкретной версии используется во время выполнения, реализация прозрачно передает результаты вызова обратно в игру.
![](https://developer.android.com/static/images/training/backward-compatible-inputmanager.png?authuser=19&hl=ru)
Рисунок 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
, доступном для скачивания выше.