Cómo brindar compatibilidad con controles en diferentes versiones de Android

Si admites controles para videojuegos en tu juego, debes asegurarte de que este responda a los controles de manera coherente en dispositivos que ejecutan diferentes versiones de Android. De esta manera, tu juego puede alcanzar un público más amplio y tus jugadores pueden disfrutar de una experiencia de juego sin interrupciones incluso cuando cambian o actualizan sus dispositivos Android.

En esta lección, se muestra cómo usar las API disponibles en Android 4.1 y versiones posteriores de modo que sean compatibles con versiones anteriores, lo cual permite que tu juego admita las siguientes funciones en dispositivos que ejecutan Android 3.1 y versiones posteriores:

  • El juego puede detectar si se agrega, cambia o quita un nuevo control para videojuegos.
  • El juego puede consultar las capacidades de un control para videojuegos.
  • El juego puede reconocer eventos de movimiento entrantes de un control para videojuegos.

Los ejemplos de esta lección se basan en la implementación de referencia que proporciona el archivo ControllerSample.zip de ejemplo que se puede descargar en la parte superior de la página. En este ejemplo, se muestra cómo implementar la interfaz InputManagerCompat para admitir diferentes versiones de Android. Para compilar el ejemplo, debes usar Android 4.1 (API nivel 16) o una versión posterior. Una vez que se compila, la app de ejemplo se ejecuta en cualquier dispositivo con Android 3.1 (API nivel 12) o una versión posterior a la que se oriente la compilación.

Cómo prepararse para abstraer las API a fin de agregar compatibilidad con controles para videojuegos

Supongamos que quieres poder determinar si cambió el estado de la conexión de un control para videojuegos en dispositivos que ejecutan Android 3.1 (API nivel 12). No obstante, las API solo están disponibles en Android 4.1 (API nivel 16) y versiones posteriores, por lo que necesitas proporcionar una implementación que admita Android 4.1 y versiones posteriores, y, al mismo tiempo, brindar un mecanismo de resguardo que admita desde Android 3.1 hasta Android 4.0.

Para ayudarte a determinar qué funciones requieren ese mecanismo de resguardo de versiones anteriores, en la tabla 1, se enumeran las diferencias de compatibilidad con los controles para videojuegos entre Android 3.1 (API nivel 12) y 4.1 (API nivel 16).

Tabla 1: API que brindan compatibilidad con controles para videojuegos en diferentes versiones de Android

Información del control API del control API nivel 12 API nivel 16
Identificación del dispositivo getInputDeviceIds()  
getInputDevice()  
getVibrator()  
SOURCE_JOYSTICK
SOURCE_GAMEPAD
Estado de la conexión onInputDeviceAdded()  
onInputDeviceChanged()  
onInputDeviceRemoved()  
Identificación del evento de entrada Pulsación en el pad direccional (KEYCODE_DPAD_UP, KEYCODE_DPAD_DOWN, KEYCODE_DPAD_LEFT, KEYCODE_DPAD_RIGHT, KEYCODE_DPAD_CENTER)
Pulsación de botones en el control (BUTTON_A, BUTTON_B, BUTTON_THUMBL, BUTTON_THUMBR, BUTTON_SELECT, BUTTON_START, BUTTON_R1, BUTTON_L1, BUTTON_R2, BUTTON_L2)
Movimiento del joystick y del interruptor de punto de vista (AXIS_X, AXIS_Y, AXIS_Z, AXIS_RZ, AXIS_HAT_X, AXIS_HAT_Y)
Pulsación del gatillo analógico (AXIS_LTRIGGER, AXIS_RTRIGGER)

Puedes usar la abstracción para compilar una compatibilidad con controles para videojuegos con reconocimiento de versiones que funcione en todas las plataformas. Este enfoque requiere los siguientes pasos:

  1. Define una interfaz de Java intermediaria que abstraiga la implementación de las funciones de controles para videojuegos que requiere tu juego.
  2. Crea una implementación de proxy de tu interfaz que use las API en Android 4.1 y versiones posteriores.
  3. Crea una implementación personalizada de tu interfaz que use las API disponibles entre Android 3.1 y Android 4.0.
  4. Crea la lógica para alternar entre estas implementaciones en el tiempo de ejecución y comenzar a usar la interfaz en tu juego.

Para obtener una descripción general de cómo se puede usar la abstracción para garantizar que las apps puedan funcionar con versiones anteriores de Android, consulta Cómo crear IU compatibles con versiones anteriores.

Cómo agregar una interfaz para lograr la compatibilidad con versiones anteriores

A fin de brindar compatibilidad con versiones anteriores, puedes crear una interfaz personalizada y, luego, agregar implementaciones específicas de cada versión. Una ventaja de este enfoque es que te permite duplicar las interfaces públicas de Android 4.1 (API nivel 16) que son compatibles con los controles para videojuegos.

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

La interfaz InputManagerCompat proporciona los siguientes métodos:

getInputDevice()
Duplica getInputDevice(). Obtiene el objeto InputDevice que representa las capacidades de un control para videojuegos.
getInputDeviceIds()
Duplica getInputDeviceIds(). Muestra un conjunto de números enteros, cada uno de los cuales es un ID para un dispositivo de entrada diferente. Este enfoque resulta útil si compilas un juego que admite varios jugadores y quieres detectar cuántos controles se conectan.
registerInputDeviceListener()
Duplica registerInputDeviceListener(). Te permite registrarte para saber cuándo se agrega, cambia o quita un dispositivo.
unregisterInputDeviceListener()
Duplica unregisterInputDeviceListener(). Anula el registro de un objeto de escucha de dispositivo de entrada.
onGenericMotionEvent()
Duplica onGenericMotionEvent(). Te permite interceptar y administrar objetos MotionEvent y valores de ejes que representan eventos tales como movimientos de joysticks y pulsaciones de gatillos analógicos.
onPause()
Deja de realizar el sondeo de eventos de controles para juegos cuando se detiene la actividad principal o cuando el juego ya no está enfocado.
onResume()
Inicia el sondeo de los eventos de controles para videojuegos cuando se reanuda la actividad principal o cuando se inicia el juego y se ejecuta en primer plano.
InputDeviceListener
Duplica la interfaz InputManager.InputDeviceListener. Permite a tu juego saber cuándo se agregó, cambió o quitó un control para videojuegos.

A continuación, crea implementaciones para InputManagerCompat que funcionen en diferentes versiones de la plataforma. Si tu juego se ejecuta en Android 4.1 o versiones posteriores y llama a un método InputManagerCompat, la implementación de proxy llama al método equivalente en InputManager. Sin embargo, si tu juego se ejecuta en Android 3.1 hasta Android 4.0, la implementación personalizada procesa las llamadas a los métodos InputManagerCompat únicamente por medio de las API que se introdujeron antes de Android 3.1. Independientemente de qué implementación específica de la versión se usa en el tiempo de ejecución, la implementación pasa los resultados de la llamada al juego de manera transparente.

Figura 1: Diagrama de clase de la interfaz y las implementaciones específicas de la versión

Cómo implementar la interfaz en Android 4.1 y versiones posteriores

InputManagerCompatV16 es una implementación de la interfaz InputManagerCompat que redirige los métodos de llamada mediante un proxy a un InputManager y un InputManager.InputDeviceListener reales. El InputManager se obtiene del Context del sistema.

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
        }

    }
    

Cómo implementar la interfaz en Android 3.1 hasta Android 4.0

Para crear una implementación de InputManagerCompat que admita desde Android 3.1 hasta Android 4.0, puedes usar los siguientes objetos:

  • Un SparseArray de ID de dispositivos a fin de registrar los controles para videojuegos que se conectan al dispositivo.
  • Un Handler para procesar eventos de dispositivos. Cuando se inicia o reanuda una app, el Handler recibe un mensaje a fin de iniciar el sondeo de la desconexión del control para videojuegos. El Handler comenzará un bucle a fin de comprobar cada control para videojuegos conocido conectado y ver si se muestra el ID de dispositivo. Un valor de retorno null indica que el control para videojuegos está desconectado. El Handler deja de realizar el sondeo cuando se detiene la app.
  • Un Map de objetos InputManagerCompat.InputDeviceListener. Usarás los objetos de escucha a fin de actualizar el estado de conexión de los controles para videojuegos registrados.

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

Implementa un objeto PollingMessageHandler que extienda el Handler; luego, anula el método handleMessage(). Este método verifica si se desconectó un control para videojuegos y notifica a los objetos de escucha 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;
            }
        }
    }
    

Si quieres iniciar y detener el sondeo de desconexión de controles para videojuegos, anula los siguientes 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 si se agregó un dispositivo de entrada, anula el método onGenericMotionEvent(). Cuando el sistema informe un evento de movimiento, verifica si este proviene de un ID de dispositivo que ya está registrado o de uno nuevo. Si es nuevo, infórmalo a los objetos de escucha 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;
    }
    

Si quieres enviar notificaciones a los objetos de escucha, usa el objeto Handler para enviar un objeto Runnable de DeviceEvent a la cola de mensajes. El DeviceEvent contiene una referencia a un InputManagerCompat.InputDeviceListener. Cuando se ejecuta el DeviceEvent, se llama al método de devolución de llamada apropiado del objeto de escucha para indicar que se agregó, cambió o quitó el control para videojuegos.

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

Ahora tienes dos implementaciones de InputManagerCompat: una que funciona en dispositivos que ejecutan Android 4.1 y versiones posteriores, y otra que funciona en dispositivos que ejecutan desde Android 3.1 hasta Android 4.0.

Cómo usar la implementación específica de la versión

La lógica de alternancia específica de la versión se implementa en una clase que actúa como un objeto factory.

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

Ahora simplemente debes crear instancias de un objeto InputManagerCompat y registrar un InputManagerCompat.InputDeviceListener en tu View principal. Debido a la lógica de alternancia de versiones que configuraste, tu juego usa automáticamente la implementación adecuada para la versión de Android del dispositivo.

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

A continuación, anula el método onGenericMotionEvent() en tu vista principal, como se describe en Cómo administrar un MotionEvent desde un control para videojuegos. Tu juego ya debería poder procesar eventos de controles para videojuegos de manera coherente en dispositivos que ejecuten Android 3.1 (API nivel 12) y versiones posteriores.

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

Encontrarás una implementación completa de este código de compatibilidad en la clase GameView que se proporciona en el archivo ControllerSample.zip de ejemplo que puedes descargar en la parte superior de la pantalla.