Compatibilidade de controles com as versões do Android

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:

  1. Definir uma interface Java intermediária que abstraia a implementação dos recursos do controle exigidos pelo jogo.
  2. Criar uma implementação de proxy da sua interface que use APIs no Android 4.1 e versões mais recentes.
  3. Criar uma implementação personalizada da sua interface que use APIs disponíveis entre o Android 3.1 e o Android 4.0.
  4. 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 objeto InputDevice 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 de MotionEvent 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.

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, o Handler receberá uma mensagem para iniciar a busca pela desconexão do controle de jogo. O Handler iniciará um ciclo repetitivo para verificar cada controle de jogo conectado conhecido e verificar se um ID de dispositivo é retornado. Um valor de retorno null indica que o controle de jogo está desconectado. O Handler interrompe a pesquisa quando o app é pausado.
  • Um Map de objetos InputManagerCompat.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.