Si vous prenez en charge les manettes dans votre jeu, il est de votre responsabilité de vous assurer qu'il répond de manière cohérente aux manettes sur les appareils fonctionnant sous différentes versions d'Android. Cela permet à votre jeu de toucher une audience plus large et à vos joueurs de bénéficier d'une expérience de jeu fluide avec leurs manettes, même lorsqu'ils changent d'appareil Android ou mettent à niveau leur appareil Android.
Cette leçon explique comment utiliser les API disponibles sous Android 4.1 ou version ultérieure de manière rétrocompatible, afin de permettre à votre jeu de prendre en charge les fonctionnalités suivantes sur les appareils équipés d'Android 3.1 ou version ultérieure:
- Le jeu peut détecter si une nouvelle manette de jeu est ajoutée, modifiée ou supprimée.
- Le jeu peut interroger les fonctionnalités d'une manette de jeu.
- Le jeu peut reconnaître les événements de mouvement entrants provenant d'une manette de jeu.
Les exemples de cette leçon sont basés sur l'implémentation de référence fournie par l'exemple de ControllerSample.zip
disponible en téléchargement ci-dessus. Cet exemple montre comment implémenter l'interface InputManagerCompat
pour prendre en charge différentes versions d'Android. Pour compiler l'exemple, vous devez utiliser Android 4.1 (niveau d'API 16) ou version ultérieure. Une fois compilée, l'application exemple s'exécute sur n'importe quel appareil équipé d'Android 3.1 (niveau d'API 12) ou version ultérieure en tant que cible de compilation.
Se préparer à extraire des API pour la prise en charge des manettes de jeu
Supposons que vous souhaitiez pouvoir déterminer si l'état de connexion d'une manette de jeu a changé sur les appareils équipés d'Android 3.1 (niveau d'API 12). Toutefois, les API ne sont disponibles que sur Android 4.1 (niveau d'API 16) ou version ultérieure. Vous devez donc fournir une implémentation compatible avec Android 4.1 ou version ultérieure, tout en fournissant un mécanisme de secours compatible avec Android 3.1 à Android 4.0.
Pour vous aider à déterminer quelles fonctionnalités nécessitent un tel mécanisme de secours pour les anciennes versions, le tableau 1 liste les différences de prise en charge des manettes de jeu entre Android 3.1 (niveau d'API 12) et 4.1 (niveau d'API 16).
Informations relatives au responsable du traitement | API Controller | Niveau d'API 12 | Niveau d'API 16 |
---|---|---|---|
Identification de l'appareil | getInputDeviceIds() |
• | |
getInputDevice() |
• | ||
getVibrator() |
• | ||
SOURCE_JOYSTICK |
• | • | |
SOURCE_GAMEPAD |
• | • | |
État de la connexion | onInputDeviceAdded() |
• | |
onInputDeviceChanged() |
• | ||
onInputDeviceRemoved() |
• | ||
Identification des événements d'entrée | Appui sur le pavé directionnel (
KEYCODE_DPAD_UP ,
KEYCODE_DPAD_DOWN ,
KEYCODE_DPAD_LEFT ,
KEYCODE_DPAD_RIGHT ,
KEYCODE_DPAD_CENTER ) |
• | • |
Appui sur le bouton de la manette (
BUTTON_A ,
BUTTON_B ,
BUTTON_THUMBL ,
BUTTON_THUMBR ,
BUTTON_SELECT ,
BUTTON_START ,
BUTTON_R1 ,
BUTTON_L1 ,
BUTTON_R2 ,
BUTTON_L2 ) |
• | • | |
Déplacement du joystick et du bouton bascule du chapeau (
AXIS_X ,
AXIS_Y ,
AXIS_Z ,
AXIS_RZ ,
AXIS_HAT_X ,
AXIS_HAT_Y ) |
• | • | |
Appui sur le déclencheur analogique (
AXIS_LTRIGGER ,
AXIS_RTRIGGER ) |
• | • |
Vous pouvez utiliser l'abstraction pour créer une compatibilité avec les manettes de jeu compatibles avec toutes les plates-formes. Cette approche comprend les étapes suivantes:
- Définissez une interface Java intermédiaire qui extrait l'implémentation des fonctionnalités de manette de jeu requises par votre jeu.
- Créez une implémentation de proxy de votre interface qui utilise des API sous Android 4.1 ou version ultérieure.
- Créez une implémentation personnalisée de votre interface utilisant des API disponibles entre Android 3.1 et Android 4.0.
- Créez la logique permettant de basculer entre ces implémentations au moment de l'exécution et commencez à utiliser l'interface dans votre jeu.
Pour découvrir comment utiliser l'abstraction afin de garantir que les applications peuvent fonctionner de manière rétrocompatible sur différentes versions d'Android, consultez la page Créer des interfaces utilisateur rétrocompatibles.
Ajouter une interface pour assurer la rétrocompatibilité
Pour assurer la rétrocompatibilité, vous pouvez créer une interface personnalisée, puis ajouter des implémentations spécifiques à la version. L'un des avantages de cette approche est qu'elle vous permet de dupliquer les interfaces publiques sur Android 4.1 (niveau d'API 16) compatibles avec les manettes de jeu.
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); } ... }
L'interface InputManagerCompat
fournit les méthodes suivantes:
getInputDevice()
- Miroir
getInputDevice()
. Elle obtient l'objetInputDevice
qui représente les fonctionnalités d'une manette de jeu. getInputDeviceIds()
- Miroir
getInputDeviceIds()
. Renvoie un tableau d'entiers, chacun étant un ID pour un périphérique d'entrée différent. Cela est utile si vous créez un jeu acceptant plusieurs joueurs et que vous souhaitez détecter le nombre de manettes connectées. registerInputDeviceListener()
- Miroir
registerInputDeviceListener()
. Vous permet de vous inscrire pour être informé lorsqu'un nouvel appareil est ajouté, modifié ou supprimé. unregisterInputDeviceListener()
- Miroir
unregisterInputDeviceListener()
. Annule l'enregistrement d'un écouteur de périphérique d'entrée. onGenericMotionEvent()
- Miroir
onGenericMotionEvent()
. Permet à votre jeu d'intercepter et de gérer les objetsMotionEvent
et les valeurs d'axe qui représentent des événements tels que les mouvements du joystick et les pressions sur des déclencheurs analogiques. onPause()
- Arrête d'interroger les événements de manette de jeu lorsque l'activité principale est suspendue ou lorsque le jeu n'est plus actif.
onResume()
- Lance la recherche d'événements de manette de jeu lorsque l'activité principale est réactivée, ou lorsque le jeu est lancé et s'exécute au premier plan.
InputDeviceListener
- Reflète l'interface
InputManager.InputDeviceListener
. Indique à votre jeu lorsqu'une manette de jeu a été ajoutée, modifiée ou supprimée.
Ensuite, créez des implémentations pour InputManagerCompat
qui fonctionnent sur différentes versions de la plate-forme. Si votre jeu s'exécute sous Android 4.1 ou version ultérieure et appelle une méthode InputManagerCompat
, l'implémentation du proxy appelle la méthode équivalente dans InputManager
.
Toutefois, si votre jeu s'exécute sur Android 3.1 à Android 4.0, l'implémentation personnalisée traite les appels aux méthodes InputManagerCompat
à l'aide uniquement des API introduites à partir d'Android 3.1. Quelle que soit la version utilisée au moment de l'exécution, l'implémentation transmet les résultats de l'appel de manière transparente au jeu.
Implémenter l'interface sur Android 4.1 ou version ultérieure
InputManagerCompatV16
est une implémentation de l'interface InputManagerCompat
qui sert de proxy pour les appels de méthode à un InputManager
et un InputManager.InputDeviceListener
réels. Le InputManager
est obtenu à partir du système 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 } }
Implémenter l'interface sur les appareils équipés d'Android 3.1 à Android 4.0
Pour créer une implémentation de InputManagerCompat
compatible avec Android 3.1 à Android 4.0, vous pouvez utiliser les objets suivants:
- Un
SparseArray
d'ID d'appareils pour suivre les manettes de jeu connectées à l'appareil. - Un
Handler
pour traiter les événements de l'appareil. Lorsqu'une application est démarrée ou réactivée,Handler
reçoit un message pour commencer à rechercher la déconnexion de la manette de jeu.Handler
démarre une boucle pour vérifier chaque manette de jeu connectée connue et voir si un ID d'appareil est renvoyé. Une valeur renvoyéenull
indique que la manette de jeu est déconnectée. LeHandler
cesse d'interroger lorsque l'application est mise en pause. - Un
Map
d'objetsInputManagerCompat.InputDeviceListener
. Vous utiliserez les écouteurs pour mettre à jour l'état de connexion des manettes de jeu suivies.
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); } }
Implémentez un objet PollingMessageHandler
qui étend Handler
et ignorez la méthode handleMessage()
. Cette méthode vérifie si une manette de jeu associée a été déconnectée et avertit les écouteurs enregistrés.
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; } } }
Pour lancer et arrêter l'interrogation de déconnexion de la manette de jeu, ignorez les méthodes suivantes:
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); }
Pour détecter qu'un périphérique d'entrée a été ajouté, remplacez la méthode onGenericMotionEvent()
. Lorsque le système signale un événement de mouvement, vérifiez si cet événement provient d'un ID d'appareil déjà suivi ou d'un nouvel ID d'appareil. Si l'ID de l'appareil est nouveau, envoyez une notification aux écouteurs enregistrés.
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; }
La notification des écouteurs est implémentée en utilisant l'objet Handler
pour envoyer un objet DeviceEvent
Runnable
à la file d'attente de messages. DeviceEvent
contient une référence à un InputManagerCompat.InputDeviceListener
. Lorsque DeviceEvent
s'exécute, la méthode de rappel appropriée de l'écouteur est appelée pour indiquer si la manette de jeu a été ajoutée, modifiée ou supprimée.
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); } }
Vous disposez maintenant de deux implémentations de InputManagerCompat
: l'une fonctionne sur les appareils équipés d'Android 4.1 ou version ultérieure, et l'autre sur les appareils équipés d'Android 3.1 à Android 4.0.
Utiliser l'implémentation propre à la version
La logique de basculement spécifique à la version est mise en œuvre dans une classe faisant office de usine.
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(); } } }
À présent, il vous suffit d'instancier un objet InputManagerCompat
et d'enregistrer un InputManagerCompat.InputDeviceListener
dans votre View
principale. En raison de la logique de changement de version que vous avez configurée, votre jeu utilise automatiquement l'implémentation adaptée à la version d'Android exécutée sur l'appareil.
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); ... } }
Remplacez ensuite la méthode onGenericMotionEvent()
dans votre vue principale, comme décrit dans la section Gérer un MotionEvent à partir d'une manette de jeu. Votre jeu devrait maintenant pouvoir traiter les événements de manette de jeu de manière cohérente sur les appareils équipés d'Android 3.1 (niveau d'API 12) ou version ultérieure.
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); }
Vous trouverez une implémentation complète de ce code de compatibilité dans la classe GameView
fournie dans l'exemple ControllerSample.zip
disponible en téléchargement ci-dessus.