Se seu jogo é compatível com controles, é sua responsabilidade verificar se ele responde aos controles de forma consistente em dispositivos executados em diferentes versões do Android. Isso permite que o jogo alcance um público mais amplo e que os jogadores possam desfrutar de uma experiência de jogo perfeita com seus controles, mesmo quando mudam ou atualizam os dispositivos Android.
Esta lição demonstra como usar APIs disponíveis no Android 4.1 e versões mais recentes de forma compatível com versões anteriores, permitindo que o jogo ofereça compatibilidade com os seguintes recursos em dispositivos com Android 3.1 e versões mais recentes:
- O jogo pode detectar quando um novo controle de jogo é adicionado, modificado ou removido.
- O jogo pode consultar os recursos de um controle de jogo.
- O jogo pode reconhecer eventos de movimento recebidos de um controle de jogo.
Os exemplos mostrados nesta lição são baseados na implementação de referência fornecida pelo exemplo ControllerSample.zip
, disponível para download acima. Este exemplo mostra como implementar a interface InputManagerCompat
para compatibilidade com diferentes versões do Android. Para compilar a amostra, você precisa usar o Android 4.1 (API de nível 16) ou versões mais recentes. Depois de compilado, o app de amostra será executado em qualquer dispositivo com o Android 3.1 (API de nível 12) ou versões mais recentes como o destino da versão.
Preparar-se para abstrair APIs para oferecer compatibilidade com controle de jogos
Suponha que você queira determinar se o status da conexão de um controle de jogo foi modificado em dispositivos executando o Android 3.1 (API de nível 12). Porém, as API estão disponíveis apenas no Android 4.1 (API de nível 16) e versões mais recentes. Portanto, você precisa fornecer uma implementação que seja compatível com o Android 4.1 e versões mais recentes, oferecendo um mecanismo substituto compatível com o Android 3.1 até o Android 4.0.
Para ajudar a determinar quais recursos exigem esse mecanismo substituto para versões mais antigas, a Tabela 1 lista as diferenças na compatibilidade com controles de jogos entre o Android 3.1 (API de nível 12) e 4.1 (API de nível 16).
Tabela 1. APIs compatíveis com controles de jogos em diferentes versões do Android.
Informações do controle | API do controle | API de nível 12 | API de nível 16 |
---|---|---|---|
Identificação do dispositivo | getInputDeviceIds() |
• | |
getInputDevice() |
• | ||
getVibrator() |
• | ||
SOURCE_JOYSTICK |
• | • | |
SOURCE_GAMEPAD |
• | • | |
Status da conexão | onInputDeviceAdded() |
• | |
onInputDeviceChanged() |
• | ||
onInputDeviceRemoved() |
• | ||
Identificação de eventos de entrada | Pressionamento do botão direcional (KEYCODE_DPAD_UP , KEYCODE_DPAD_DOWN , KEYCODE_DPAD_LEFT , KEYCODE_DPAD_RIGHT , KEYCODE_DPAD_CENTER ) |
• | • |
Pressionamento do botão do gamepad (
BUTTON_A ,
BUTTON_B ,
BUTTON_THUMBL ,
BUTTON_THUMBR ,
BUTTON_SELECT ,
BUTTON_START ,
BUTTON_R1 ,
BUTTON_L1 ,
BUTTON_R2 ,
BUTTON_L2 ) |
• | • | |
Movimento do joystick e do botão de ângulo de visão (
AXIS_X ,
AXIS_Y ,
AXIS_Z ,
AXIS_RZ ,
AXIS_HAT_X ,
AXIS_HAT_Y ) |
• | • | |
Pressionamento do gatilho analógico (
AXIS_LTRIGGER ,
AXIS_RTRIGGER ) |
• | • |
Você pode usar abstração para oferecer compatibilidade com um controle de jogo com uma versão que funcione em várias plataformas. Essa abordagem envolve as seguintes etapas:
- Definir uma interface Java intermediária que abstraia a implementação dos recursos do controle exigidos pelo jogo.
- Criar uma implementação de proxy da sua interface que use APIs no Android 4.1 e versões mais recentes.
- Criar uma implementação personalizada da sua interface que use APIs disponíveis entre o Android 3.1 e o Android 4.0.
- Criar a lógica para alternar entre essas implementações no momento da execução e começar a usar a interface no seu jogo.
Para ter uma visão geral de como a abstração pode ser usada para garantir que os apps funcionem de maneira compatível com versões anteriores do Android, consulte Como criar IUs compatíveis com versões anteriores.
Adicionar uma interface para compatibilidade com versões anteriores
Para oferecer compatibilidade com versões anteriores, você pode criar uma interface personalizada e, em seguida, adicionar implementações específicas da versão. Uma vantagem dessa abordagem é que ela permite espelhar as interfaces públicas no Android 4.1 (API de nível 16) que são compatíveis com controles de jogos.
Kotlin
// The InputManagerCompat interface is a reference example. // The full code is provided in the ControllerSample.zip sample. interface InputManagerCompat { val inputDeviceIds: IntArray fun getInputDevice(id: Int): InputDevice fun registerInputDeviceListener( listener: InputManager.InputDeviceListener, handler: Handler? ) fun unregisterInputDeviceListener(listener:InputManager.InputDeviceListener) fun onGenericMotionEvent(event: MotionEvent) fun onPause() fun onResume() interface InputDeviceListener { fun onInputDeviceAdded(deviceId: Int) fun onInputDeviceChanged(deviceId: Int) fun onInputDeviceRemoved(deviceId: Int) } }
Java
// The InputManagerCompat interface is a reference example. // The full code is provided in the ControllerSample.zip sample. public interface InputManagerCompat { ... public InputDevice getInputDevice(int id); public int[] getInputDeviceIds(); public void registerInputDeviceListener( InputManagerCompat.InputDeviceListener listener, Handler handler); public void unregisterInputDeviceListener( InputManagerCompat.InputDeviceListener listener); public void onGenericMotionEvent(MotionEvent event); public void onPause(); public void onResume(); public interface InputDeviceListener { void onInputDeviceAdded(int deviceId); void onInputDeviceChanged(int deviceId); void onInputDeviceRemoved(int deviceId); } ... }
A interface InputManagerCompat
fornece os seguintes métodos:
getInputDevice()
- Espelha
getInputDevice()
. Retorna o objetoInputDevice
que representa os recursos de um controle de jogo. getInputDeviceIds()
- Espelha
getInputDeviceIds()
. Retorna uma matriz de números inteiros, cada um sendo um código para um dispositivo de entrada diferente. Isso é útil se você está criando um jogo compatível com vários jogadores e quer detectar quantos controles estão conectados. registerInputDeviceListener()
- Espelha
registerInputDeviceListener()
. Permite que você se cadastre para ser informado quando um novo dispositivo é adicionado, modificado ou removido. unregisterInputDeviceListener()
- Espelha
unregisterInputDeviceListener()
. Cancela o registro de um listener de dispositivo de entrada. onGenericMotionEvent()
- Espelha
onGenericMotionEvent()
. Permite que seu jogo intercepte e processe objetos e valores de eixos deMotionEvent
que representam eventos, por exemplo, movimentos do joystick e pressionamento de gatilhos analógicos. onPause()
- Interrompe a pesquisa de eventos do controle de jogo quando a atividade principal está pausada ou quando o jogo não está mais em foco.
onResume()
- Inicia a pesquisa de eventos do controle de jogo quando a atividade principal é retomada ou quando o jogo é iniciado e executado em primeiro plano.
InputDeviceListener
- Espelha a interface
InputManager.InputDeviceListener
. Informa seu jogo quando um controle de jogo é adicionado, modificado ou removido.
Em seguida, crie implementações para InputManagerCompat
que funcionem em diferentes versões da plataforma. Se seu jogo estiver sendo executado no Android 4.1 ou versões mais recentes e chamar um método InputManagerCompat
, a implementação proxy chamará o método equivalente em InputManager
.
No entanto, se seu jogo for executado no Android 3.1 até o Android 4.0, a implementação personalizada processará chamadas para os métodos InputManagerCompat
usando apenas APIs lançadas até o Android 3.1. Independentemente de qual implementação específica da versão é usada no momento da execução, a implementação transmite os resultados da chamada de volta de forma transparente para o jogo.
![](https://developer.android.com/static/images/training/backward-compatible-inputmanager.png?authuser=9&hl=pt)
Figura 1. Diagrama de classes das implementações da interface e da versão específica.
Implementar a interface no Android 4.1 e versões mais recentes
InputManagerCompatV16
é uma implementação da interface InputManagerCompat
que envia proxies de chamadas de método para um InputManager
e um InputManager.InputDeviceListener
reais. O InputManager
é obtido do sistema Context
.
Kotlin
// The InputManagerCompatV16 class is a reference implementation. // The full code is provided in the ControllerSample.zip sample. public class InputManagerV16( context: Context, private val inputManager: InputManager = context.getSystemService(Context.INPUT_SERVICE) as InputManager, private val listeners: MutableMap<InputManager.InputDeviceListener, V16InputDeviceListener> = mutableMapOf() ) : InputManagerCompat { override val inputDeviceIds: IntArray = inputManager.inputDeviceIds override fun getInputDevice(id: Int): InputDevice = inputManager.getInputDevice(id) override fun registerInputDeviceListener( listener: InputManager.InputDeviceListener, handler: Handler? ) { V16InputDeviceListener(listener).also { v16listener -> inputManager.registerInputDeviceListener(v16listener, handler) listeners += listener to v16listener } } // Do the same for unregistering an input device listener ... override fun onGenericMotionEvent(event: MotionEvent) { // unused in V16 } override fun onPause() { // unused in V16 } override fun onResume() { // unused in V16 } } class V16InputDeviceListener( private val idl: InputManager.InputDeviceListener ) : InputManager.InputDeviceListener { override fun onInputDeviceAdded(deviceId: Int) { idl.onInputDeviceAdded(deviceId) } // Do the same for device change and removal ... }
Java
// The InputManagerCompatV16 class is a reference implementation. // The full code is provided in the ControllerSample.zip sample. public class InputManagerV16 implements InputManagerCompat { private final InputManager inputManager; private final Map<InputManagerCompat.InputDeviceListener, V16InputDeviceListener> listeners; public InputManagerV16(Context context) { inputManager = (InputManager) context.getSystemService(Context.INPUT_SERVICE); listeners = new HashMap<InputManagerCompat.InputDeviceListener, V16InputDeviceListener>(); } @Override public InputDevice getInputDevice(int id) { return inputManager.getInputDevice(id); } @Override public int[] getInputDeviceIds() { return inputManager.getInputDeviceIds(); } static class V16InputDeviceListener implements InputManager.InputDeviceListener { final InputManagerCompat.InputDeviceListener mIDL; public V16InputDeviceListener(InputDeviceListener idl) { mIDL = idl; } @Override public void onInputDeviceAdded(int deviceId) { mIDL.onInputDeviceAdded(deviceId); } // Do the same for device change and removal ... } @Override public void registerInputDeviceListener(InputDeviceListener listener, Handler handler) { V16InputDeviceListener v16Listener = new V16InputDeviceListener(listener); inputManager.registerInputDeviceListener(v16Listener, handler); listeners.put(listener, v16Listener); } // Do the same for unregistering an input device listener ... @Override public void onGenericMotionEvent(MotionEvent event) { // unused in V16 } @Override public void onPause() { // unused in V16 } @Override public void onResume() { // unused in V16 } }
Implementar a interface no Android 3.1 até o Android 4.0
Para criar uma implementação de InputManagerCompat
compatível com o Android 3.1 até o Android 4.0, você pode usar os seguintes objetos:
- Um
SparseArray
de IDs de dispositivos para rastrear os controles de jogos conectados ao dispositivo. - Um
Handler
para processar eventos do dispositivo. Quando um app for iniciado ou retomado, oHandler
receberá uma mensagem para iniciar a busca pela desconexão do controle de jogo. OHandler
iniciará um ciclo repetitivo para verificar cada controle de jogo conectado conhecido e verificar se um ID de dispositivo é retornado. Um valor de retornonull
indica que o controle de jogo está desconectado. OHandler
interrompe a pesquisa quando o app é pausado. - Um
Map
de objetosInputManagerCompat.InputDeviceListener
. Você usará os listeners para atualizar o status da conexão de controles de jogos rastreados.
Kotlin
// The InputManagerCompatV9 class is a reference implementation. // The full code is provided in the ControllerSample.zip sample. class InputManagerV9( val devices: SparseArray<Array<Long>> = SparseArray(), private val listeners: MutableMap<InputManager.InputDeviceListener, Handler> = mutableMapOf() ) : InputManagerCompat { private val defaultHandler: Handler = PollingMessageHandler(this) … }
Java
// The InputManagerCompatV9 class is a reference implementation. // The full code is provided in the ControllerSample.zip sample. public class InputManagerV9 implements InputManagerCompat { private final SparseArray<long[]> devices; private final Map<InputDeviceListener, Handler> listeners; private final Handler defaultHandler; … public InputManagerV9() { devices = new SparseArray<long[]>(); listeners = new HashMap<InputDeviceListener, Handler>(); defaultHandler = new PollingMessageHandler(this); } }
Implemente um objeto PollingMessageHandler
que estenda Handler
e substitua o método handleMessage()
. Esse método verifica se um controle de jogo anexado foi desconectado e notifica listeners registrados.
Kotlin
private class PollingMessageHandler( inputManager: InputManagerV9, private val mInputManager: WeakReference<InputManagerV9> = WeakReference(inputManager) ) : Handler() { override fun handleMessage(msg: Message) { super.handleMessage(msg) when (msg.what) { MESSAGE_TEST_FOR_DISCONNECT -> { mInputManager.get()?.also { imv -> val time = SystemClock.elapsedRealtime() val size = imv.devices.size() for (i in 0 until size) { imv.devices.valueAt(i)?.also { lastContact -> if (time - lastContact[0] > CHECK_ELAPSED_TIME) { // check to see if the device has been // disconnected val id = imv.devices.keyAt(i) if (null == InputDevice.getDevice(id)) { // Notify the registered listeners // that the game controller is disconnected imv.devices.remove(id) } else { lastContact[0] = time } } } } sendEmptyMessageDelayed(MESSAGE_TEST_FOR_DISCONNECT, CHECK_ELAPSED_TIME) } } } } }
Java
private static class PollingMessageHandler extends Handler { private final WeakReference<InputManagerV9> inputManager; PollingMessageHandler(InputManagerV9 im) { inputManager = new WeakReference<InputManagerV9>(im); } @Override public void handleMessage(Message msg) { super.handleMessage(msg); switch (msg.what) { case MESSAGE_TEST_FOR_DISCONNECT: InputManagerV9 imv = inputManager.get(); if (null != imv) { long time = SystemClock.elapsedRealtime(); int size = imv.devices.size(); for (int i = 0; i < size; i++) { long[] lastContact = imv.devices.valueAt(i); if (null != lastContact) { if (time - lastContact[0] > CHECK_ELAPSED_TIME) { // check to see if the device has been // disconnected int id = imv.devices.keyAt(i); if (null == InputDevice.getDevice(id)) { // Notify the registered listeners // that the game controller is disconnected imv.devices.remove(id); } else { lastContact[0] = time; } } } } sendEmptyMessageDelayed(MESSAGE_TEST_FOR_DISCONNECT, CHECK_ELAPSED_TIME); } break; } } }
Para iniciar e interromper a sondagem da desconexão do controle de jogo, substitua esses métodos:
Kotlin
private const val MESSAGE_TEST_FOR_DISCONNECT = 101 private const val CHECK_ELAPSED_TIME = 3000L class InputManagerV9( val devices: SparseArray<Array<Long>> = SparseArray(), private val listeners: MutableMap<InputManager.InputDeviceListener, Handler> = mutableMapOf() ) : InputManagerCompat { ... override fun onPause() { defaultHandler.removeMessages(MESSAGE_TEST_FOR_DISCONNECT) } override fun onResume() { defaultHandler.sendEmptyMessageDelayed(MESSAGE_TEST_FOR_DISCONNECT, CHECK_ELAPSED_TIME) } ... }
Java
private static final int MESSAGE_TEST_FOR_DISCONNECT = 101; private static final long CHECK_ELAPSED_TIME = 3000L; @Override public void onPause() { defaultHandler.removeMessages(MESSAGE_TEST_FOR_DISCONNECT); } @Override public void onResume() { defaultHandler.sendEmptyMessageDelayed(MESSAGE_TEST_FOR_DISCONNECT, CHECK_ELAPSED_TIME); }
Para detectar se um dispositivo de entrada foi adicionado, substitua o método onGenericMotionEvent()
. Quando o sistema relatar um evento de movimento, verifique se esse evento veio de um código de dispositivo que já foi rastreado ou de um novo código de dispositivo. Se o código do dispositivo for novo, notifique listeners registrados.
Kotlin
override fun onGenericMotionEvent(event: MotionEvent) { // detect new devices val id = event.deviceId val timeArray: Array<Long> = mDevices.get(id) ?: run { // Notify the registered listeners that a game controller is added ... arrayOf<Long>().also { mDevices.put(id, it) } } timeArray[0] = SystemClock.elapsedRealtime() }
Java
@Override public void onGenericMotionEvent(MotionEvent event) { // detect new devices int id = event.getDeviceId(); long[] timeArray = mDevices.get(id); if (null == timeArray) { // Notify the registered listeners that a game controller is added ... timeArray = new long[1]; mDevices.put(id, timeArray); } long time = SystemClock.elapsedRealtime(); timeArray[0] = time; }
A notificação de listeners é implementada usando o objeto Handler
para enviar um objeto DeviceEvent
Runnable
para a fila de mensagens. O DeviceEvent
contém uma referência a um InputManagerCompat.InputDeviceListener
. Quando o DeviceEvent
é executado, o método de callback apropriado do listener é chamado para sinalizar se o controle de jogo foi adicionado, modificado ou removido.
Kotlin
class InputManagerV9( val devices: SparseArray<Array<Long>> = SparseArray(), private val listeners: MutableMap<InputManager.InputDeviceListener, Handler> = mutableMapOf() ) : InputManagerCompat { ... override fun registerInputDeviceListener( listener: InputManager.InputDeviceListener, handler: Handler? ) { listeners[listener] = handler ?: defaultHandler } override fun unregisterInputDeviceListener(listener: InputManager.InputDeviceListener) { listeners.remove(listener) } private fun notifyListeners(why: Int, deviceId: Int) { // the state of some device has changed listeners.forEach { listener, handler -> DeviceEvent.getDeviceEvent(why, deviceId, listener).also { handler?.post(it) } } } ... } private val sObjectQueue: Queue<DeviceEvent> = ArrayDeque<DeviceEvent>() private class DeviceEvent( private var mMessageType: Int, private var mId: Int, private var mListener: InputManager.InputDeviceListener ) : Runnable { companion object { fun getDeviceEvent(messageType: Int, id: Int, listener: InputManager.InputDeviceListener) = sObjectQueue.poll()?.apply { mMessageType = messageType mId = id mListener = listener } ?: DeviceEvent(messageType, id, listener) } override fun run() { when(mMessageType) { ON_DEVICE_ADDED -> mListener.onInputDeviceAdded(mId) ON_DEVICE_CHANGED -> mListener.onInputDeviceChanged(mId) ON_DEVICE_REMOVED -> mListener.onInputDeviceChanged(mId) else -> { // Handle unknown message type } } } }
Java
@Override public void registerInputDeviceListener(InputDeviceListener listener, Handler handler) { listeners.remove(listener); if (handler == null) { handler = defaultHandler; } listeners.put(listener, handler); } @Override public void unregisterInputDeviceListener(InputDeviceListener listener) { listeners.remove(listener); } private void notifyListeners(int why, int deviceId) { // the state of some device has changed if (!listeners.isEmpty()) { for (InputDeviceListener listener : listeners.keySet()) { Handler handler = listeners.get(listener); DeviceEvent odc = DeviceEvent.getDeviceEvent(why, deviceId, listener); handler.post(odc); } } } private static class DeviceEvent implements Runnable { private int mMessageType; private int mId; private InputDeviceListener mListener; private static Queue<DeviceEvent> sObjectQueue = new ArrayDeque<DeviceEvent>(); ... static DeviceEvent getDeviceEvent(int messageType, int id, InputDeviceListener listener) { DeviceEvent curChanged = sObjectQueue.poll(); if (null == curChanged) { curChanged = new DeviceEvent(); } curChanged.mMessageType = messageType; curChanged.mId = id; curChanged.mListener = listener; return curChanged; } @Override public void run() { switch (mMessageType) { case ON_DEVICE_ADDED: mListener.onInputDeviceAdded(mId); break; case ON_DEVICE_CHANGED: mListener.onInputDeviceChanged(mId); break; case ON_DEVICE_REMOVED: mListener.onInputDeviceRemoved(mId); break; default: // Handle unknown message type ... break; } // Put this runnable back in the queue sObjectQueue.offer(this); } }
Agora você tem duas implementações de InputManagerCompat
: uma que funciona em dispositivos com Android 4.1 e versões mais recentes e outra que funciona em dispositivos com Android 3.1 até Android 4.0.
Usar a implementação específica da versão
A lógica de chave específica da versão é implementada em uma classe que atua como fábrica.
Kotlin
object Factory { fun getInputManager(context: Context): InputManagerCompat = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { InputManagerV16(context) } else { InputManagerV9() } }
Java
public static class Factory { public static InputManagerCompat getInputManager(Context context) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { return new InputManagerV16(context); } else { return new InputManagerV9(); } } }
Agora você pode simplesmente instanciar um objeto InputManagerCompat
e registrar um InputManagerCompat.InputDeviceListener
na sua View
principal. Devido à lógica de alternância de versões que você configurou, seu jogo usa automaticamente a implementação apropriada para a versão do Android em que o dispositivo está sendo executado.
Kotlin
class GameView(context: Context) : View(context), InputManager.InputDeviceListener { private val inputManager: InputManagerCompat = Factory.getInputManager(context).apply { registerInputDeviceListener(this@GameView, null) ... } ... }
Java
public class GameView extends View implements InputDeviceListener { private InputManagerCompat inputManager; ... public GameView(Context context, AttributeSet attrs) { inputManager = InputManagerCompat.Factory.getInputManager(this.getContext()); inputManager.registerInputDeviceListener(this, null); ... } }
Em seguida, substitua o método onGenericMotionEvent()
na visualização principal, conforme descrito em Processar um MotionEvent em um controle de jogo. Agora seu jogo deve conseguir processar eventos do controle de jogo de forma consistente em dispositivos que executam o Android 3.1 (API de nível 12) ou versões mais recentes.
Kotlin
override fun onGenericMotionEvent(event: MotionEvent): Boolean { inputManager.onGenericMotionEvent(event) // Handle analog input from the controller as normal ... return super.onGenericMotionEvent(event) }
Java
@Override public boolean onGenericMotionEvent(MotionEvent event) { inputManager.onGenericMotionEvent(event); // Handle analog input from the controller as normal ... return super.onGenericMotionEvent(event); }
Você pode encontrar uma implementação completa desse código de compatibilidade na classe GameView
fornecida na amostra de ControllerSample.zip
, disponível para download acima.