Obsługa kontrolerów w różnych wersjach Androida

Jeśli w swojej grze korzystasz z kontrolerów do gier, Twoim obowiązkiem jest upewnienie się, że gra zachowuje spójność na różnych urządzeniach z różnymi wersjami Androida. Dzięki temu Twoja gra może dotrzeć do szerszego grona odbiorców, a gracze mogą płynnie grać za pomocą kontrolerów, nawet jeśli przejdą na inne urządzenie z Androidem lub zmodernizują swoje urządzenie.

Ta lekcja pokazuje, jak korzystać z interfejsów API dostępnych na Androidzie 4.1 i nowszych w sposób zgodny wstecznie, tak by gra obsługiwała te funkcje na urządzeniach z Androidem 3.1 lub nowszym:

  • Gra może wykryć, czy został dodany, zmieniony czy usunięty.
  • Gra może sprawdzać możliwości kontrolera do gier.
  • Gra może rozpoznawać przychodzące zdarzenia ruchu z kontrolera do gier.

Przykłady w tej lekcji opierają się na referencyjnej implementacji, którą znajdziesz powyżej w przykładzie ControllerSample.zip. Ten przykład pokazuje, jak wdrożyć interfejs InputManagerCompat do obsługi różnych wersji Androida. Aby skompilować przykład, musisz używać Androida 4.1 (poziom interfejsu API 16) lub nowszego. Po skompilowaniu przykładowa aplikacja działa na dowolnym urządzeniu z Androidem 3.1 (poziom interfejsu API 12) lub nowszym.

Przygotuj się do abstrakcyjnych interfejsów API do obsługi kontrolera gier

Załóżmy, że chcesz mieć możliwość sprawdzenia, czy stan połączenia kontrolera gier na urządzeniach z Androidem 3.1 (poziom interfejsu API 12) zmienił się. Jednak interfejsy API są dostępne tylko na Androidzie 4.1 (poziom interfejsu API 16) i nowszych, więc musisz udostępnić implementację, która obsługuje Androida 4.1 i nowsze wersje, oraz udostępniać mechanizm kreacji zastępczej, który obsługuje Androida od 3.1 do 4.0.

Aby ułatwić określenie, które funkcje wymagają takiego mechanizmu kreacji zastępczej w przypadku starszych wersji, w tabeli 1 znajdziesz różnice w obsłudze kontrolerów gier między Androidem 3.1 (poziom interfejsu API 12) a 4.1 (poziom interfejsu API 16).

Tabela 1. Interfejsy API do obsługi kontrolera gier w różnych wersjach Androida.

Informacje o administratorze Interfejs API kontrolera Poziom API 12 Poziom API 16
Identyfikacja urządzenia getInputDeviceIds()  
getInputDevice()  
getVibrator()  
SOURCE_JOYSTICK
SOURCE_GAMEPAD
Stan połączenia onInputDeviceAdded()  
onInputDeviceChanged()  
onInputDeviceRemoved()  
Identyfikacja zdarzeń wejściowych Naciśnij na padzie kierunkowym (KEYCODE_DPAD_UP, KEYCODE_DPAD_DOWN, KEYCODE_DPAD_LEFT, KEYCODE_DPAD_RIGHT, KEYCODE_DPAD_CENTER)
Naciśnięcie przycisku na padzie do gier (BUTTON_A, BUTTON_B, BUTTON_THUMBL, BUTTON_THUMBR, BUTTON_SELECT, BUTTON_START, BUTTON_R1, BUTTON_L1, BUTTON_R2, BUTTON_L2)
Ruch joysticka i przełącznika kapelusza (AXIS_X, AXIS_Y, AXIS_Z, AXIS_RZ, AXIS_HAT_X, AXIS_HAT_Y)
Naciśnięcie aktywatora analogowego (AXIS_LTRIGGER, AXIS_RTRIGGER)

Za pomocą abstrakcji możesz zbudować obsługę kontrolera do gier z uwzględnieniem wersji, która działa na różnych platformach. Oto czynności, które musisz wykonać:

  1. Zdefiniuj pośredni interfejs Java, który wyodrębnia implementację funkcji kontrolera do gier wymaganych przez Twoją grę.
  2. Utwórz pośredniczącą implementację interfejsu, która używa interfejsów API na Androidzie 4.1 lub nowszym.
  3. Utwórz niestandardową implementację swojego interfejsu, która korzysta z interfejsów API dostępnych między Androidem 3.1 a 4.0.
  4. Utwórz logikę przełączania między tymi implementacjami w czasie działania i zacznij korzystać z interfejsu w grze.

Omówienie sposobu wykorzystania abstrakcji do zapewnienia zgodności wstecznej aplikacji w różnych wersjach Androida znajdziesz w artykule o tworzeniu interfejsów zgodnych wstecznie.

Dodawanie interfejsu na potrzeby zgodności wstecznej

Aby zapewnić zgodność wsteczną, możesz utworzyć interfejs niestandardowy i dodać implementacje związane z konkretną wersją. Jedną z zalet tego podejścia jest możliwość powielania interfejsów publicznych na Androidzie 4.1 (poziom API 16), które obsługują kontrolery gier.

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

Interfejs InputManagerCompat udostępnia te metody:

getInputDevice()
Odbicia lustrzanegetInputDevice(). Pobiera obiekt InputDevice, który reprezentuje możliwości kontrolera gier.
getInputDeviceIds()
Odbicia lustrzanegetInputDeviceIds(). Zwraca tablicę liczb całkowitych, z których każda jest identyfikatorem innego urządzenia wejściowego. Jest to przydatne, gdy tworzysz grę, w której może grać wielu graczy i chcesz sprawdzić, ile kontrolerów jest podłączonych.
registerInputDeviceListener()
Odbicia lustrzaneregisterInputDeviceListener(). Dzięki tej funkcji możesz się zarejestrować, aby otrzymywać informacje o dodaniu, zmianie lub usunięciu nowego urządzenia.
unregisterInputDeviceListener()
Odbicia lustrzaneunregisterInputDeviceListener(). Wyrejestrowuje detektor urządzenia wejściowego.
onGenericMotionEvent()
Odbicia lustrzaneonGenericMotionEvent(). Pozwala grze na przechwytywanie i obsługę obiektów MotionEvent oraz wartości osi, które reprezentują zdarzenia, takie jak ruchy joysticka czy naciśnięcia spustu analogowego.
onPause()
Zatrzymuje odpytywanie o zdarzenia na kontrolerze do gier, gdy główna aktywność zostanie wstrzymana lub gra nie będzie już koncentrować się na grze.
onResume()
Rozpoczyna odpytywanie zdarzeń kontrolera gier, gdy zostanie wznowiona główna aktywność lub gdy gra już się rozpoczęła i działa na pierwszym planie.
InputDeviceListener
Odzwierciedla interfejs InputManager.InputDeviceListener. Informuje grę o dodaniu, zmianie lub usunięciu kontrolera.

Następnie utwórz implementacje InputManagerCompat, które działają na różnych wersjach platformy. Jeśli Twoja gra działa na Androidzie 4.1 lub nowszym i wywołuje metodę InputManagerCompat, implementacja serwera proxy wywołuje jej odpowiednik w InputManager. Jeśli jednak gra działa na Androidzie od 3.1 do 4.0, implementacja niestandardowa przetwarza wywołania metod InputManagerCompat przy użyciu tylko interfejsów API wprowadzonych nie później niż w Androidzie 3.1. Niezależnie od tego, która implementacja dla konkretnej wersji jest używana w czasie działania, implementacja przekazuje wyniki wywołania z powrotem do gry w przejrzysty sposób.

Rysunek 1. Diagram klas interfejsu i implementacji w danej wersji.

Wdrażanie interfejsu na Androidzie 4.1 lub nowszym

InputManagerCompatV16 to implementacja interfejsu InputManagerCompat, który powoduje wywołanie metody proxy do rzeczywistych InputManager i InputManager.InputDeviceListener. Wartość InputManager jest pobierana z systemu 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
    }

}

Implementacja interfejsu na urządzeniach z Androidem od 3.1 do 4.0

Aby utworzyć implementację InputManagerCompat obsługującą Androida od 3.1 do 4.0, możesz użyć tych obiektów:

  • SparseArray identyfikatorów urządzeń służących do śledzenia kontrolerów gier połączonych z urządzeniem.
  • Handler do przetwarzania zdarzeń dotyczących urządzenia. Po uruchomieniu lub wznowieniu aplikacji Handler otrzyma wiadomość z prośbą o rozpoczęcie odpytywania o odłączenie kontrolera gier. Handler rozpocznie pętlę, aby sprawdzić każdy połączony kontroler gier i sprawdzić, czy zwracany jest identyfikator urządzenia. Zwracana wartość null wskazuje, że kontroler gier jest odłączony. Handler przerywa odpytywanie, gdy aplikacja jest wstrzymana.
  • Map z InputManagerCompat.InputDeviceListener obiektów. Detektory będą aktualizować stan połączenia monitorowanych kontrolerów gier.

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

Zaimplementuj obiekt PollingMessageHandler, który rozszerza zakres Handler, i zastąp metodę handleMessage(). Ta metoda sprawdza, czy podłączony kontroler do gier został odłączony, i powiadamia zarejestrowane detektory.

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

Aby rozpocząć i zatrzymać odpytywanie w celu odłączenia kontrolera do gier, zastąp te metody:

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

Aby wykryć, że zostało dodane urządzenie wejściowe, zastąp metodę onGenericMotionEvent(). Gdy system zgłasza zdarzenie ruchu, sprawdź, czy pochodzi ono z identyfikatora urządzenia, które jest już śledzone, czy z nowego identyfikatora urządzenia. Jeśli identyfikator urządzenia jest nowy, powiadom zarejestrowane detektory.

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

Powiadomienie detektorów wykorzystuje obiekt Handler do wysyłania obiektu DeviceEvent Runnable do kolejki wiadomości. DeviceEvent zawiera odwołanie do InputManagerCompat.InputDeviceListener. Po uruchomieniu DeviceEvent następuje wywołanie odpowiedniej metody wywołania zwrotnego detektora, aby zasygnalizować, czy kontroler gry został dodany, zmieniony lub usunięty.

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

Masz teraz 2 implementacje pakietu InputManagerCompat: jedną, która działa na urządzeniach z Androidem 4.1 lub nowszym, a druga, która działa na urządzeniach z Androidem w wersji od 3.1 do 4.0.

Używanie implementacji zależnie od wersji

Logika przełączania specyficzna dla wersji jest zaimplementowana w klasie, która działa jako fabryka.

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

Teraz możesz po prostu utworzyć instancję obiektu InputManagerCompat i zarejestrować InputManagerCompat.InputDeviceListener w głównym View. Ze względu na logikę zmiany wersji gra automatycznie korzysta z implementacji odpowiedniej dla wersji Androida, na której działa urządzenie.

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

Następnie zastąp metodę onGenericMotionEvent() w widoku głównym zgodnie z opisem w sekcji Obsługa zdarzeń MotionEvent z kontrolera gier. Gra powinna teraz przetwarzać w spójny sposób zdarzenia kontrolera do gier na urządzeniach z Androidem 3.1 (poziom interfejsu API 12) lub nowszym.

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

Pełną implementację tego kodu zgodności znajdziesz w klasie GameView w przykładowym ControllerSample.zip dostępnym do pobrania powyżej.