At the system level, Android reports input event codes from game controllers as Android key codes and axis values. In your game, you can receive these codes and values and convert them to specific in-game actions.
When players physically connect or wirelessly pair a game controller to
their Android-powered devices, the system auto-detects the controller
as an input device and starts reporting its input events. Your game can receive
these input events by implementing the following callback methods in your active
Activity
or focused View
(you should
implement the callbacks for either the Activity
or
View
, but not both):
- From
Activity
:dispatchGenericMotionEvent(android.view. MotionEvent)
Called to process generic motion events such as joystick movements.
dispatchKeyEvent(android.view.KeyEvent)
Called to process key events such as a press or release of a gamepad or D-pad button.
- From
View
:onGenericMotionEvent(android.view.MotionEvent)
Called to process generic motion events such as joystick movements.
onKeyDown(int, android.view.KeyEvent)
Called to process a press of a physical key such as a gamepad or D-pad button.
onKeyUp(int, android.view.KeyEvent)
Called to process a release of a physical key such as a gamepad or D-pad button.
The recommended approach is to capture the events from the
specific View
object that the user interacts with.
Inspect the following objects provided by the callbacks to get information
about the type of input event received:
KeyEvent
- An object that describes directional
pad (D-pad) and gamepad button events. Key events are accompanied by a
key code that indicates the specific button triggered, such as
DPAD_DOWN
orBUTTON_A
. You can obtain the key code by callinggetKeyCode()
or from key event callbacks such asonKeyDown()
. MotionEvent
- An object that describes input from joystick and shoulder trigger
movements. Motion events are accompanied by an action code and a set of
axis values. The action code specifies the state change that occurred
such as a joystick being moved. The axis values describe the position and other
movement properties for a specific physical control, such as
AXIS_X
orAXIS_RTRIGGER
. You can obtain the action code by callinggetAction()
and the axis value by callinggetAxisValue()
.
This lesson focuses on how you can handle input from the most common types of
physical controls (gamepad buttons, directional pads, and
joysticks) in a game screen by implementing the above-mentioned
View
callback methods and processing
KeyEvent
and MotionEvent
objects.
Verify a game controller is connected
When reporting input events, Android does not distinguish
between events that came from a non-game controller device and events that came
from a game controller. For example, a touch screen action generates an
AXIS_X
event that represents the X
coordinate of the touch surface, but a joystick generates an
AXIS_X
event that represents the X position of the joystick. If
your game cares about handling game-controller input, you should first check
that the input event comes from a relevant source type.
To verify that a connected input device is a game controller, call
getSources()
to obtain a combined bit field of
input source types supported on that device. You can then test to see if
the following fields are set:
- A source type of
SOURCE_GAMEPAD
indicates that the input device has gamepad buttons (for example,BUTTON_A
). Note that this source type does not strictly indicate if the game controller has D-pad buttons, although most gamepads typically have directional controls. - A source type of
SOURCE_DPAD
indicates that the input device has D-pad buttons (for example,DPAD_UP
). - A source type of
SOURCE_JOYSTICK
indicates that the input device has analog control sticks (for example, a joystick that records movements alongAXIS_X
andAXIS_Y
).
The following code snippet shows a helper method that lets you check whether the connected input devices are game controllers. If so, the method retrieves the device IDs for the game controllers. You can then associate each device ID with a player in your game, and process game actions for each connected player separately. To learn more about supporting multiple game controllers that are simultaneously connected on the same Android device, see Support multiple game controllers.
Kotlin
fun getGameControllerIds(): List<Int> { val gameControllerDeviceIds = mutableListOf<Int>() val deviceIds = InputDevice.getDeviceIds() deviceIds.forEach { deviceId -> InputDevice.getDevice(deviceId).apply { // Verify that the device has gamepad buttons, control sticks, or both. if (sources and InputDevice.SOURCE_GAMEPAD == InputDevice.SOURCE_GAMEPAD || sources and InputDevice.SOURCE_JOYSTICK == InputDevice.SOURCE_JOYSTICK) { // This device is a game controller. Store its device ID. gameControllerDeviceIds .takeIf { !it.contains(deviceId) } ?.add(deviceId) } } } return gameControllerDeviceIds }
Java
public ArrayList<Integer> getGameControllerIds() { ArrayList<Integer> gameControllerDeviceIds = new ArrayList<Integer>(); int[] deviceIds = InputDevice.getDeviceIds(); for (int deviceId : deviceIds) { InputDevice dev = InputDevice.getDevice(deviceId); int sources = dev.getSources(); // Verify that the device has gamepad buttons, control sticks, or both. if (((sources & InputDevice.SOURCE_GAMEPAD) == InputDevice.SOURCE_GAMEPAD) || ((sources & InputDevice.SOURCE_JOYSTICK) == InputDevice.SOURCE_JOYSTICK)) { // This device is a game controller. Store its device ID. if (!gameControllerDeviceIds.contains(deviceId)) { gameControllerDeviceIds.add(deviceId); } } } return gameControllerDeviceIds; }
Additionally, you might want to check for individual input capabilities supported by a connected game controller. This might be useful, for example, if you want your game to use only input from the set of physical controls it understands.
To detect if a specific key code or axis code is supported by a connected game controller, use these techniques:
- In Android 4.4 (API level 19) or higher, you can determine if a key code is
supported on a connected game controller by calling
hasKeys(int...)
. - In Android 3.1 (API level 12) or higher, you can find all available axes
supported on a connected game controller by first calling
getMotionRanges()
. Then, on eachInputDevice.MotionRange
object returned, callgetAxis()
to get its axis ID.
Process gamepad button presses
Figure 1 shows how Android maps key codes and axis values to the physical controls on most game controllers.
The callouts in the figure refer to the following:
Common key codes generated by gamepad button presses include
BUTTON_A
,
BUTTON_B
,
BUTTON_SELECT
,
and BUTTON_START
. Some game
controllers also trigger the DPAD_CENTER
key code when the center of the D-pad crossbar is pressed. Your
game can inspect the key code by calling getKeyCode()
or from key event callbacks such as
onKeyDown()
,
and if it represents an event that is relevant to your game, process it as a
game action. Table 1 lists the recommended game actions for the most common
gamepad buttons.
Game Action | Button Key Code |
---|---|
Start game in main menu, or pause/unpause during game | BUTTON_START * |
Display menu | BUTTON_SELECT *
and KEYCODE_MENU * |
Same as Android Back navigation behavior described in the Navigation design guide. | KEYCODE_BACK |
Navigate back to a previous item in a menu | BUTTON_B |
Confirm selection, or perform primary game action | BUTTON_A and
DPAD_CENTER |
* Your game should not rely on the presence of the Start, Select, or Menu buttons.
Tip: Consider providing a configuration screen in your game to allow users to personalize their own game controller mappings for game actions.
The following snippet shows how you might override
onKeyDown()
to
associate the BUTTON_A
and
DPAD_CENTER
button presses
with a game action.
Kotlin
class GameView(...) : View(...) { ... override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean { var handled = false if (event.source and InputDevice.SOURCE_GAMEPAD == InputDevice.SOURCE_GAMEPAD) { if (event.repeatCount == 0) { when (keyCode) { // Handle gamepad and D-pad button presses to navigate the ship ... else -> { keyCode.takeIf { isFireKey(it) }?.run { // Update the ship object to fire lasers ... handled = true } } } } if (handled) { return true } } return super.onKeyDown(keyCode, event) } // Here we treat Button_A and DPAD_CENTER as the primary action // keys for the game. private fun isFireKey(keyCode: Int): Boolean = keyCode == KeyEvent.KEYCODE_DPAD_CENTER || keyCode == KeyEvent.KEYCODE_BUTTON_A }
Java
public class GameView extends View { ... @Override public boolean onKeyDown(int keyCode, KeyEvent event) { boolean handled = false; if ((event.getSource() & InputDevice.SOURCE_GAMEPAD) == InputDevice.SOURCE_GAMEPAD) { if (event.getRepeatCount() == 0) { switch (keyCode) { // Handle gamepad and D-pad button presses to // navigate the ship ... default: if (isFireKey(keyCode)) { // Update the ship object to fire lasers ... handled = true; } break; } } if (handled) { return true; } } return super.onKeyDown(keyCode, event); } private static boolean isFireKey(int keyCode) { // Here we treat Button_A and DPAD_CENTER as the primary action // keys for the game. return keyCode == KeyEvent.KEYCODE_DPAD_CENTER || keyCode == KeyEvent.KEYCODE_BUTTON_A; } }
Note: On Android 4.2 (API
level 17) and lower, the system treats
BUTTON_A
as the Android
Back key by default. If your app supports these Android
versions, make sure to treat
BUTTON_A
as the primary game
action. To determine the current Android SDK
version on the device, refer to the
Build.VERSION.SDK_INT
value.
Process directional pad input
The 4-way directional pad (D-pad) is a common physical control in many game
controllers. Android reports D-pad UP and DOWN presses as
AXIS_HAT_Y
events with a range
from -1.0 (up) to 1.0 (down), and D-pad LEFT or RIGHT presses as
AXIS_HAT_X
events with a range from -1.0
(left) to 1.0 (right).
Some controllers instead report D-pad presses with a key code. If your game cares about D-pad presses, you should treat the hat axis events and the D-pad key codes as the same input events, as recommended in table 2.
Game Action | D-pad Key Code | Hat Axis Code |
---|---|---|
Move Up | KEYCODE_DPAD_UP |
AXIS_HAT_Y (for values 0 to -1.0) |
Move Down | KEYCODE_DPAD_DOWN |
AXIS_HAT_Y (for values 0 to 1.0) |
Move Left | KEYCODE_DPAD_LEFT |
AXIS_HAT_X (for values 0 to -1.0) |
Move Right | KEYCODE_DPAD_RIGHT |
AXIS_HAT_X (for values 0 to 1.0) |
The following code snippet shows a helper class that lets you check the hat axis and key code values from an input event to determine the D-pad direction.
Kotlin
class Dpad { private var directionPressed = -1 // initialized to -1 fun getDirectionPressed(event: InputEvent): Int { if (!isDpadDevice(event)) { return -1 } // If the input event is a MotionEvent, check its hat axis values. (event as? MotionEvent)?.apply { // Use the hat axis value to find the D-pad direction val xaxis: Float = event.getAxisValue(MotionEvent.AXIS_HAT_X) val yaxis: Float = event.getAxisValue(MotionEvent.AXIS_HAT_Y) directionPressed = when { // Check if the AXIS_HAT_X value is -1 or 1, and set the D-pad // LEFT and RIGHT direction accordingly. xaxis.compareTo(-1.0f) == 0 -> Dpad.LEFT xaxis.compareTo(1.0f) == 0 -> Dpad.RIGHT // Check if the AXIS_HAT_Y value is -1 or 1, and set the D-pad // UP and DOWN direction accordingly. yaxis.compareTo(-1.0f) == 0 -> Dpad.UP yaxis.compareTo(1.0f) == 0 -> Dpad.DOWN else -> directionPressed } } // If the input event is a KeyEvent, check its key code. (event as? KeyEvent)?.apply { // Use the key code to find the D-pad direction. directionPressed = when(event.keyCode) { KeyEvent.KEYCODE_DPAD_LEFT -> Dpad.LEFT KeyEvent.KEYCODE_DPAD_RIGHT -> Dpad.RIGHT KeyEvent.KEYCODE_DPAD_UP -> Dpad.UP KeyEvent.KEYCODE_DPAD_DOWN -> Dpad.DOWN KeyEvent.KEYCODE_DPAD_CENTER -> Dpad.CENTER else -> directionPressed } } return directionPressed } companion object { internal const val UP = 0 internal const val LEFT = 1 internal const val RIGHT = 2 internal const val DOWN = 3 internal const val CENTER = 4 fun isDpadDevice(event: InputEvent): Boolean = // Check that input comes from a device with directional pads. event.source and InputDevice.SOURCE_DPAD != InputDevice.SOURCE_DPAD } }
Java
public class Dpad { final static int UP = 0; final static int LEFT = 1; final static int RIGHT = 2; final static int DOWN = 3; final static int CENTER = 4; int directionPressed = -1; // initialized to -1 public int getDirectionPressed(InputEvent event) { if (!isDpadDevice(event)) { return -1; } // If the input event is a MotionEvent, check its hat axis values. if (event instanceof MotionEvent) { // Use the hat axis value to find the D-pad direction MotionEvent motionEvent = (MotionEvent) event; float xaxis = motionEvent.getAxisValue(MotionEvent.AXIS_HAT_X); float yaxis = motionEvent.getAxisValue(MotionEvent.AXIS_HAT_Y); // Check if the AXIS_HAT_X value is -1 or 1, and set the D-pad // LEFT and RIGHT direction accordingly. if (Float.compare(xaxis, -1.0f) == 0) { directionPressed = Dpad.LEFT; } else if (Float.compare(xaxis, 1.0f) == 0) { directionPressed = Dpad.RIGHT; } // Check if the AXIS_HAT_Y value is -1 or 1, and set the D-pad // UP and DOWN direction accordingly. else if (Float.compare(yaxis, -1.0f) == 0) { directionPressed = Dpad.UP; } else if (Float.compare(yaxis, 1.0f) == 0) { directionPressed = Dpad.DOWN; } } // If the input event is a KeyEvent, check its key code. else if (event instanceof KeyEvent) { // Use the key code to find the D-pad direction. KeyEvent keyEvent = (KeyEvent) event; if (keyEvent.getKeyCode() == KeyEvent.KEYCODE_DPAD_LEFT) { directionPressed = Dpad.LEFT; } else if (keyEvent.getKeyCode() == KeyEvent.KEYCODE_DPAD_RIGHT) { directionPressed = Dpad.RIGHT; } else if (keyEvent.getKeyCode() == KeyEvent.KEYCODE_DPAD_UP) { directionPressed = Dpad.UP; } else if (keyEvent.getKeyCode() == KeyEvent.KEYCODE_DPAD_DOWN) { directionPressed = Dpad.DOWN; } else if (keyEvent.getKeyCode() == KeyEvent.KEYCODE_DPAD_CENTER) { directionPressed = Dpad.CENTER; } } return directionPressed; } public static boolean isDpadDevice(InputEvent event) { // Check that input comes from a device with directional pads. if ((event.getSource() & InputDevice.SOURCE_DPAD) != InputDevice.SOURCE_DPAD) { return true; } else { return false; } } }
You can use this helper class in your game wherever you want to process
D-pad input (for example, in the
onGenericMotionEvent()
or
onKeyDown()
callbacks).
For example:
Kotlin
private val dpad = Dpad() ... override fun onGenericMotionEvent(event: MotionEvent): Boolean { if (Dpad.isDpadDevice(event)) { when (dpad.getDirectionPressed(event)) { Dpad.LEFT -> { // Do something for LEFT direction press ... return true } Dpad.RIGHT -> { // Do something for RIGHT direction press ... return true } Dpad.UP -> { // Do something for UP direction press ... return true } ... } } // Check if this event is from a joystick movement and process accordingly. ... }
Java
Dpad dpad = new Dpad(); ... @Override public boolean onGenericMotionEvent(MotionEvent event) { // Check if this event if from a D-pad and process accordingly. if (Dpad.isDpadDevice(event)) { int press = dpad.getDirectionPressed(event); switch (press) { case LEFT: // Do something for LEFT direction press ... return true; case RIGHT: // Do something for RIGHT direction press ... return true; case UP: // Do something for UP direction press ... return true; ... } } // Check if this event is from a joystick movement and process accordingly. ... }
Process joystick movements
When players move a joystick on their game controllers, Android reports a
MotionEvent
that contains the
ACTION_MOVE
action code and the updated
positions of the joystick's axes. Your game can use the data provided by
the MotionEvent
to determine if a joystick movement it
cares about happened.
Note that joystick motion events may batch multiple movement samples together
within a single object. The MotionEvent
object contains
the current position for each joystick axis as well as multiple historical
positions for each axis. When reporting motion events with action code ACTION_MOVE
(such as joystick movements), Android batches up the
axis values for efficiency. The historical values for an axis consists of the
set of distinct values older than the current axis value, and more recent than
values reported in any previous motion events. See the
MotionEvent
reference for details.
You can use the historical information to more accurately render a game
object's movement based on the joystick input. To
retrieve the current and historical values, call
getAxisValue()
or getHistoricalAxisValue()
. You can also find the number of historical
points in the joystick event by calling
getHistorySize()
.
The following snippet shows how you might override the
onGenericMotionEvent()
callback to process joystick input. You should first
process the historical values for an axis, then process its current position.
Kotlin
class GameView(...) : View(...) { override fun onGenericMotionEvent(event: MotionEvent): Boolean { // Check that the event came from a game controller return if (event.source and InputDevice.SOURCE_JOYSTICK == InputDevice.SOURCE_JOYSTICK && event.action == MotionEvent.ACTION_MOVE) { // Process the movements starting from the // earliest historical position in the batch (0 until event.historySize).forEach { i -> // Process the event at historical position i processJoystickInput(event, i) } // Process the current movement sample in the batch (position -1) processJoystickInput(event, -1) true } else { super.onGenericMotionEvent(event) } } }
Java
public class GameView extends View { @Override public boolean onGenericMotionEvent(MotionEvent event) { // Check that the event came from a game controller if ((event.getSource() & InputDevice.SOURCE_JOYSTICK) == InputDevice.SOURCE_JOYSTICK && event.getAction() == MotionEvent.ACTION_MOVE) { // Process all historical movement samples in the batch final int historySize = event.getHistorySize(); // Process the movements starting from the // earliest historical position in the batch for (int i = 0; i < historySize; i++) { // Process the event at historical position i processJoystickInput(event, i); } // Process the current movement sample in the batch (position -1) processJoystickInput(event, -1); return true; } return super.onGenericMotionEvent(event); } }
Before using joystick input, you need to determine if the joystick is centered, then calculate its axis movements accordingly. Joysticks typically have a flat area, that is, a range of values near the (0,0) coordinate at which the axis is considered to be centered. If the axis value reported by Android falls within the flat area, you should treat the controller to be at rest (that is, motionless along both axes).
The snippet below shows a helper method that calculates the movement along
each axis. You invoke this helper in the processJoystickInput()
method
described further below.
Kotlin
private fun getCenteredAxis( event: MotionEvent, device: InputDevice, axis: Int, historyPos: Int ): Float { val range: InputDevice.MotionRange? = device.getMotionRange(axis, event.source) // A joystick at rest does not always report an absolute position of // (0,0). Use the getFlat() method to determine the range of values // bounding the joystick axis center. range?.apply { val value: Float = if (historyPos < 0) { event.getAxisValue(axis) } else { event.getHistoricalAxisValue(axis, historyPos) } // Ignore axis values that are within the 'flat' region of the // joystick axis center. if (Math.abs(value) > flat) { return value } } return 0f }
Java
private static float getCenteredAxis(MotionEvent event, InputDevice device, int axis, int historyPos) { final InputDevice.MotionRange range = device.getMotionRange(axis, event.getSource()); // A joystick at rest does not always report an absolute position of // (0,0). Use the getFlat() method to determine the range of values // bounding the joystick axis center. if (range != null) { final float flat = range.getFlat(); final float value = historyPos < 0 ? event.getAxisValue(axis): event.getHistoricalAxisValue(axis, historyPos); // Ignore axis values that are within the 'flat' region of the // joystick axis center. if (Math.abs(value) > flat) { return value; } } return 0; }
Putting it all together, here is how you might process joystick movements in your game:
Kotlin
private fun processJoystickInput(event: MotionEvent, historyPos: Int) { val inputDevice = event.device // Calculate the horizontal distance to move by // using the input value from one of these physical controls: // the left control stick, hat axis, or the right control stick. var x: Float = getCenteredAxis(event, inputDevice, MotionEvent.AXIS_X, historyPos) if (x == 0f) { x = getCenteredAxis(event, inputDevice, MotionEvent.AXIS_HAT_X, historyPos) } if (x == 0f) { x = getCenteredAxis(event, inputDevice, MotionEvent.AXIS_Z, historyPos) } // Calculate the vertical distance to move by // using the input value from one of these physical controls: // the left control stick, hat switch, or the right control stick. var y: Float = getCenteredAxis(event, inputDevice, MotionEvent.AXIS_Y, historyPos) if (y == 0f) { y = getCenteredAxis(event, inputDevice, MotionEvent.AXIS_HAT_Y, historyPos) } if (y == 0f) { y = getCenteredAxis(event, inputDevice, MotionEvent.AXIS_RZ, historyPos) } // Update the ship object based on the new x and y values }
Java
private void processJoystickInput(MotionEvent event, int historyPos) { InputDevice inputDevice = event.getDevice(); // Calculate the horizontal distance to move by // using the input value from one of these physical controls: // the left control stick, hat axis, or the right control stick. float x = getCenteredAxis(event, inputDevice, MotionEvent.AXIS_X, historyPos); if (x == 0) { x = getCenteredAxis(event, inputDevice, MotionEvent.AXIS_HAT_X, historyPos); } if (x == 0) { x = getCenteredAxis(event, inputDevice, MotionEvent.AXIS_Z, historyPos); } // Calculate the vertical distance to move by // using the input value from one of these physical controls: // the left control stick, hat switch, or the right control stick. float y = getCenteredAxis(event, inputDevice, MotionEvent.AXIS_Y, historyPos); if (y == 0) { y = getCenteredAxis(event, inputDevice, MotionEvent.AXIS_HAT_Y, historyPos); } if (y == 0) { y = getCenteredAxis(event, inputDevice, MotionEvent.AXIS_RZ, historyPos); } // Update the ship object based on the new x and y values }
To support game controllers that have more sophisticated features beyond a single joystick, follow these best practices:
- Handle dual controller sticks. Many game controllers have
both a left and right joystick. For the left stick, Android
reports horizontal movements as
AXIS_X
events and vertical movements asAXIS_Y
events. For the right stick, Android reports horizontal movements asAXIS_Z
events and vertical movements asAXIS_RZ
events. Make sure to handle both controller sticks in your code. - Handle shoulder trigger presses (but provide alternative input
methods). Some controllers have left and right shoulder
triggers. If these triggers are present, Android reports a left trigger press
as an
AXIS_LTRIGGER
event and a right trigger press as anAXIS_RTRIGGER
event. On Android 4.3 (API level 18), a controller that produces aAXIS_LTRIGGER
also reports an identical value for theAXIS_BRAKE
axis. The same is true forAXIS_RTRIGGER
andAXIS_GAS
. Android reports all analog trigger presses with a normalized value from 0.0 (released) to 1.0 (fully pressed). Not all controllers have triggers, so consider allowing players to perform those game actions with other buttons.