Если ваша игра поддерживает игровые контроллеры, вы обязаны обеспечить стабильную реакцию игры на контроллеры на устройствах с разными версиями 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 ) | • | • |
Вы можете использовать абстракцию для создания поддержки игровых контроллеров с учетом версий, которая будет работать на разных платформах. Этот подход включает в себя следующие шаги:
- Определите промежуточный Java-интерфейс, который абстрагирует реализацию функций игрового контроллера, необходимых для вашей игры.
- Создайте прокси-реализацию вашего интерфейса, использующую API из Android 4.1 и выше.
- Создайте собственную реализацию интерфейса, использующую API, доступные в версиях Android от 3.1 до 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)
}
}
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. Независимо от того, какая реализация, специфичная для конкретной версии, используется во время выполнения, реализация прозрачно передает результаты вызова обратно в игру.

Реализуйте интерфейс на 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 доступном для скачивания.