mirror of
https://github.com/hajimehoshi/ebiten.git
synced 2024-12-27 12:18:55 +01:00
b7dd45c0e4
On Android, MotionEvent with 0 values might come for axes when connecting a gamepad, even though a user didn't touch any axes. This is problematic especially for tirgger axes, where the default value should be -1. This change fixes the issue by adding a new state `axesReady` to check if an axis is really touched or not. If an axis is not touched yet, a button value for a standard (trigger) button always returns 0. This change also removes an old hack to initialize axis values for triggers. Closes #2598
443 lines
18 KiB
Java
443 lines
18 KiB
Java
// Copyright 2022 The Ebitengine Authors
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
package {{.JavaPkg}}.{{.PrefixLower}};
|
|
|
|
import java.util.ArrayList;
|
|
import java.util.Collections;
|
|
import java.util.Comparator;
|
|
import java.util.List;
|
|
|
|
import android.content.Context;
|
|
import android.hardware.input.InputManager;
|
|
import android.os.Handler;
|
|
import android.os.Looper;
|
|
import android.util.AttributeSet;
|
|
import android.util.DisplayMetrics;
|
|
import android.util.Log;
|
|
import android.view.Display;
|
|
import android.view.KeyEvent;
|
|
import android.view.InputDevice;
|
|
import android.view.MotionEvent;
|
|
import android.view.ViewGroup;
|
|
import android.view.WindowManager;
|
|
|
|
import {{.JavaPkg}}.ebitenmobileview.Ebitenmobileview;
|
|
|
|
public class EbitenView extends ViewGroup implements InputManager.InputDeviceListener {
|
|
static class Gamepad {
|
|
public int deviceId;
|
|
public ArrayList<InputDevice.MotionRange> axes;
|
|
public ArrayList<InputDevice.MotionRange> hats;
|
|
}
|
|
|
|
// See https://github.com/libsdl-org/SDL/blob/2df2da11f627299c6e05b7e0aff407c915043372/android-project/app/src/main/java/org/libsdl/app/SDLControllerManager.java#L154-L173
|
|
static class RangeComparator implements Comparator<InputDevice.MotionRange> {
|
|
@Override
|
|
public int compare(InputDevice.MotionRange arg0, InputDevice.MotionRange arg1) {
|
|
int arg0Axis = arg0.getAxis();
|
|
int arg1Axis = arg1.getAxis();
|
|
if (arg0Axis == MotionEvent.AXIS_GAS) {
|
|
arg0Axis = MotionEvent.AXIS_BRAKE;
|
|
} else if (arg0Axis == MotionEvent.AXIS_BRAKE) {
|
|
arg0Axis = MotionEvent.AXIS_GAS;
|
|
}
|
|
if (arg1Axis == MotionEvent.AXIS_GAS) {
|
|
arg1Axis = MotionEvent.AXIS_BRAKE;
|
|
} else if (arg1Axis == MotionEvent.AXIS_BRAKE) {
|
|
arg1Axis = MotionEvent.AXIS_GAS;
|
|
}
|
|
|
|
// Make sure the AXIS_Z is sorted between AXIS_RY and AXIS_RZ.
|
|
//
|
|
// The value ordering on Android otherwise is AXIS_X, AXIS_Y,
|
|
// all kinds of axes used by touchscreens or touchpads only,
|
|
// AXIS_Z, AXIS_RX, AXIS_RY, AXIS_RZ, hats, triggers,
|
|
// flight controls, car controls, misc stuff.
|
|
//
|
|
// This is because the usual pairing are:
|
|
// - AXIS_X, AXIS_Y (left stick).
|
|
// - AXIS_RX, AXIS_RY (sometimes the right stick, sometimes triggers).
|
|
// - AXIS_Z, AXIS_RZ (sometimes the right stick, sometimes triggers).
|
|
//
|
|
// This sorts the axes in the above order, which tends to be correct
|
|
// for Xbox-ish game pads that have the right stick on RX/RY and the
|
|
// triggers on Z/RZ.
|
|
//
|
|
// Gamepads that don't have AXIS_Z/AXIS_RZ but use
|
|
// AXIS_LTRIGGER/AXIS_RTRIGGER are unaffected by this.
|
|
//
|
|
// References:
|
|
// - https://developer.android.com/develop/ui/views/touch-and-input/game-controllers/controller-input
|
|
// - https://www.kernel.org/doc/html/latest/input/gamepad.html
|
|
if (arg0Axis == MotionEvent.AXIS_Z) {
|
|
arg0Axis = MotionEvent.AXIS_RZ - 1;
|
|
} else if (arg0Axis > MotionEvent.AXIS_Z && arg0Axis < MotionEvent.AXIS_RZ) {
|
|
arg0Axis--;
|
|
}
|
|
if (arg1Axis == MotionEvent.AXIS_Z) {
|
|
arg1Axis = MotionEvent.AXIS_RZ - 1;
|
|
} else if (arg1Axis > MotionEvent.AXIS_Z && arg1Axis < MotionEvent.AXIS_RZ) {
|
|
arg1Axis--;
|
|
}
|
|
|
|
return arg0Axis - arg1Axis;
|
|
}
|
|
}
|
|
|
|
private static double pxToDp(double x) {
|
|
return x / Ebitenmobileview.deviceScale();
|
|
}
|
|
|
|
public EbitenView(Context context) {
|
|
super(context);
|
|
initialize(context);
|
|
}
|
|
|
|
public EbitenView(Context context, AttributeSet attrs) {
|
|
super(context, attrs);
|
|
initialize(context);
|
|
}
|
|
|
|
private void initialize(Context context) {
|
|
this.gamepads = new ArrayList<Gamepad>();
|
|
|
|
this.ebitenSurfaceView = new EbitenSurfaceView(getContext());
|
|
LayoutParams params = new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
|
|
addView(this.ebitenSurfaceView, params);
|
|
|
|
this.inputManager = (InputManager)context.getSystemService(Context.INPUT_SERVICE);
|
|
this.inputManager.registerInputDeviceListener(this, null);
|
|
for (int id : this.inputManager.getInputDeviceIds()) {
|
|
this.onInputDeviceAdded(id);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
|
|
this.ebitenSurfaceView.layout(0, 0, right - left, bottom - top);
|
|
double widthInDp = pxToDp(right - left);
|
|
double heightInDp = pxToDp(bottom - top);
|
|
Ebitenmobileview.layout(widthInDp, heightInDp);
|
|
}
|
|
|
|
@Override
|
|
public boolean onKeyDown(int keyCode, KeyEvent event) {
|
|
Ebitenmobileview.onKeyDownOnAndroid(keyCode, event.getUnicodeChar(), event.getSource(), event.getDeviceId());
|
|
return true;
|
|
}
|
|
|
|
@Override
|
|
public boolean onKeyUp(int keyCode, KeyEvent event) {
|
|
Ebitenmobileview.onKeyUpOnAndroid(keyCode, event.getSource(), event.getDeviceId());
|
|
return true;
|
|
}
|
|
|
|
@Override
|
|
public boolean onTouchEvent(MotionEvent e) {
|
|
// getActionIndex returns a valid value only for the action whose index is the returned value of getActionIndex (#2220).
|
|
// See https://developer.android.com/reference/android/view/MotionEvent#getActionMasked().
|
|
// For other pointers, treat their actions as MotionEvent.ACTION_MOVE.
|
|
int touchIndex = e.getActionIndex();
|
|
for (int i = 0; i < e.getPointerCount(); i++) {
|
|
int id = e.getPointerId(i);
|
|
int x = (int)e.getX(i);
|
|
int y = (int)e.getY(i);
|
|
int action = (i == touchIndex) ? e.getActionMasked() : MotionEvent.ACTION_MOVE;
|
|
Ebitenmobileview.updateTouchesOnAndroid(action, id, (int)pxToDp(x), (int)pxToDp(y));
|
|
}
|
|
return true;
|
|
}
|
|
|
|
private Gamepad getGamepad(int deviceId) {
|
|
for (Gamepad gamepad : this.gamepads) {
|
|
if (gamepad.deviceId == deviceId) {
|
|
return gamepad;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
@Override
|
|
public boolean onGenericMotionEvent(MotionEvent event) {
|
|
if ((event.getSource() & InputDevice.SOURCE_JOYSTICK) != InputDevice.SOURCE_JOYSTICK) {
|
|
return super.onGenericMotionEvent(event);
|
|
}
|
|
if (event.getAction() != MotionEvent.ACTION_MOVE) {
|
|
return super.onGenericMotionEvent(event);
|
|
}
|
|
|
|
// See https://github.com/libsdl-org/SDL/blob/2df2da11f627299c6e05b7e0aff407c915043372/android-project/app/src/main/java/org/libsdl/app/SDLControllerManager.java#L256-L277
|
|
Gamepad gamepad = this.getGamepad(event.getDeviceId());
|
|
if (gamepad == null) {
|
|
return true;
|
|
}
|
|
|
|
int actionPointerIndex = event.getActionIndex();
|
|
for (int i = 0; i < gamepad.axes.size(); i++) {
|
|
InputDevice.MotionRange range = gamepad.axes.get(i);
|
|
float axisValue = event.getAxisValue(range.getAxis(), actionPointerIndex);
|
|
float value = (axisValue - range.getMin()) / range.getRange() * 2.0f - 1.0f;
|
|
Ebitenmobileview.onGamepadAxisChanged(gamepad.deviceId, i, value);
|
|
}
|
|
for (int i = 0; i < gamepad.hats.size() / 2; i++) {
|
|
int hatX = Math.round(event.getAxisValue(gamepad.hats.get(2*i).getAxis(), actionPointerIndex));
|
|
int hatY = Math.round(event.getAxisValue(gamepad.hats.get(2*i+1).getAxis(), actionPointerIndex));
|
|
Ebitenmobileview.onGamepadHatChanged(gamepad.deviceId, i, hatX, hatY);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
@Override
|
|
public void onInputDeviceAdded(int deviceId) {
|
|
InputDevice inputDevice = this.inputManager.getInputDevice(deviceId);
|
|
// The InputDevice can be null on some deivces (#1342).
|
|
if (inputDevice == null) {
|
|
return;
|
|
}
|
|
|
|
// A fingerprint reader is unexpectedly recognized as a joystick. Skip this (#1542).
|
|
if (inputDevice.getName().equals("uinput-fpc")) {
|
|
return;
|
|
}
|
|
|
|
int sources = inputDevice.getSources();
|
|
if ((sources & InputDevice.SOURCE_GAMEPAD) != InputDevice.SOURCE_GAMEPAD &&
|
|
(sources & InputDevice.SOURCE_JOYSTICK) != InputDevice.SOURCE_JOYSTICK) {
|
|
return;
|
|
}
|
|
|
|
// See https://github.com/libsdl-org/SDL/blob/2df2da11f627299c6e05b7e0aff407c915043372/android-project/app/src/main/java/org/libsdl/app/SDLControllerManager.java#L182-L216
|
|
List<InputDevice.MotionRange> ranges = inputDevice.getMotionRanges();
|
|
Collections.sort(ranges, new RangeComparator());
|
|
|
|
Gamepad gamepad = new Gamepad();
|
|
gamepad.deviceId = deviceId;
|
|
gamepad.axes = new ArrayList<InputDevice.MotionRange>();
|
|
gamepad.hats = new ArrayList<InputDevice.MotionRange>();
|
|
for (InputDevice.MotionRange range : ranges) {
|
|
if (range.getAxis() == MotionEvent.AXIS_HAT_X || range.getAxis() == MotionEvent.AXIS_HAT_Y) {
|
|
gamepad.hats.add(range);
|
|
} else {
|
|
gamepad.axes.add(range);
|
|
}
|
|
}
|
|
this.gamepads.add(gamepad);
|
|
|
|
String descriptor = inputDevice.getDescriptor();
|
|
int vendorId = inputDevice.getVendorId();
|
|
int productId = inputDevice.getProductId();
|
|
|
|
// These values are required to calculate SDL's GUID.
|
|
int buttonMask = getButtonMask(inputDevice, gamepad.hats.size()/2);
|
|
int axisMask = getAxisMask(inputDevice);
|
|
|
|
Ebitenmobileview.onGamepadAdded(deviceId, inputDevice.getName(), gamepad.axes.size(), gamepad.hats.size()/2, descriptor, vendorId, productId, buttonMask, axisMask);
|
|
}
|
|
|
|
// The implementation is copied from SDL:
|
|
// https://github.com/libsdl-org/SDL/blob/0e9560aea22818884921e5e5064953257bfe7fa7/android-project/app/src/main/java/org/libsdl/app/SDLControllerManager.java#L308
|
|
private static int getButtonMask(InputDevice joystickDevice, int nhats) {
|
|
int buttonMask = 0;
|
|
int[] keys = new int[] {
|
|
KeyEvent.KEYCODE_BUTTON_A,
|
|
KeyEvent.KEYCODE_BUTTON_B,
|
|
KeyEvent.KEYCODE_BUTTON_X,
|
|
KeyEvent.KEYCODE_BUTTON_Y,
|
|
KeyEvent.KEYCODE_BACK,
|
|
KeyEvent.KEYCODE_BUTTON_MODE,
|
|
KeyEvent.KEYCODE_BUTTON_START,
|
|
KeyEvent.KEYCODE_BUTTON_THUMBL,
|
|
KeyEvent.KEYCODE_BUTTON_THUMBR,
|
|
KeyEvent.KEYCODE_BUTTON_L1,
|
|
KeyEvent.KEYCODE_BUTTON_R1,
|
|
KeyEvent.KEYCODE_DPAD_UP,
|
|
KeyEvent.KEYCODE_DPAD_DOWN,
|
|
KeyEvent.KEYCODE_DPAD_LEFT,
|
|
KeyEvent.KEYCODE_DPAD_RIGHT,
|
|
KeyEvent.KEYCODE_BUTTON_SELECT,
|
|
KeyEvent.KEYCODE_DPAD_CENTER,
|
|
|
|
// These don't map into any SDL controller buttons directly
|
|
KeyEvent.KEYCODE_BUTTON_L2,
|
|
KeyEvent.KEYCODE_BUTTON_R2,
|
|
KeyEvent.KEYCODE_BUTTON_C,
|
|
KeyEvent.KEYCODE_BUTTON_Z,
|
|
KeyEvent.KEYCODE_BUTTON_1,
|
|
KeyEvent.KEYCODE_BUTTON_2,
|
|
KeyEvent.KEYCODE_BUTTON_3,
|
|
KeyEvent.KEYCODE_BUTTON_4,
|
|
KeyEvent.KEYCODE_BUTTON_5,
|
|
KeyEvent.KEYCODE_BUTTON_6,
|
|
KeyEvent.KEYCODE_BUTTON_7,
|
|
KeyEvent.KEYCODE_BUTTON_8,
|
|
KeyEvent.KEYCODE_BUTTON_9,
|
|
KeyEvent.KEYCODE_BUTTON_10,
|
|
KeyEvent.KEYCODE_BUTTON_11,
|
|
KeyEvent.KEYCODE_BUTTON_12,
|
|
KeyEvent.KEYCODE_BUTTON_13,
|
|
KeyEvent.KEYCODE_BUTTON_14,
|
|
KeyEvent.KEYCODE_BUTTON_15,
|
|
KeyEvent.KEYCODE_BUTTON_16,
|
|
};
|
|
int[] masks = new int[] {
|
|
(1 << 0), // A -> A
|
|
(1 << 1), // B -> B
|
|
(1 << 2), // X -> X
|
|
(1 << 3), // Y -> Y
|
|
(1 << 4), // BACK -> BACK
|
|
(1 << 5), // MODE -> GUIDE
|
|
(1 << 6), // START -> START
|
|
(1 << 7), // THUMBL -> LEFTSTICK
|
|
(1 << 8), // THUMBR -> RIGHTSTICK
|
|
(1 << 9), // L1 -> LEFTSHOULDER
|
|
(1 << 10), // R1 -> RIGHTSHOULDER
|
|
(1 << 11), // DPAD_UP -> DPAD_UP
|
|
(1 << 12), // DPAD_DOWN -> DPAD_DOWN
|
|
(1 << 13), // DPAD_LEFT -> DPAD_LEFT
|
|
(1 << 14), // DPAD_RIGHT -> DPAD_RIGHT
|
|
(1 << 4), // SELECT -> BACK
|
|
(1 << 0), // DPAD_CENTER -> A
|
|
(1 << 15), // L2 -> ??
|
|
(1 << 16), // R2 -> ??
|
|
(1 << 17), // C -> ??
|
|
(1 << 18), // Z -> ??
|
|
(1 << 20), // 1 -> ??
|
|
(1 << 21), // 2 -> ??
|
|
(1 << 22), // 3 -> ??
|
|
(1 << 23), // 4 -> ??
|
|
(1 << 24), // 5 -> ??
|
|
(1 << 25), // 6 -> ??
|
|
(1 << 26), // 7 -> ??
|
|
(1 << 27), // 8 -> ??
|
|
(1 << 28), // 9 -> ??
|
|
(1 << 29), // 10 -> ??
|
|
(1 << 30), // 11 -> ??
|
|
(1 << 31), // 12 -> ??
|
|
// We're out of room...
|
|
0xFFFFFFFF, // 13 -> ??
|
|
0xFFFFFFFF, // 14 -> ??
|
|
0xFFFFFFFF, // 15 -> ??
|
|
0xFFFFFFFF, // 16 -> ??
|
|
};
|
|
boolean[] hasKeys = joystickDevice.hasKeys(keys);
|
|
for (int i = 0; i < keys.length; ++i) {
|
|
if (hasKeys[i]) {
|
|
buttonMask |= masks[i];
|
|
}
|
|
}
|
|
// https://github.com/libsdl-org/SDL/blob/47f2373dc13b66c48bf4024fcdab53cd0bdd59bb/src/joystick/android/SDL_sysjoystick.c#L360-L367
|
|
if (nhats > 0) {
|
|
// Add Dpad buttons.
|
|
buttonMask |= 1 << 11;
|
|
buttonMask |= 1 << 12;
|
|
buttonMask |= 1 << 13;
|
|
buttonMask |= 1 << 14;
|
|
}
|
|
return buttonMask;
|
|
}
|
|
|
|
private static int getAxisMask(InputDevice joystickDevice) {
|
|
final int SDL_CONTROLLER_AXIS_LEFTX = 0;
|
|
final int SDL_CONTROLLER_AXIS_LEFTY = 1;
|
|
final int SDL_CONTROLLER_AXIS_RIGHTX = 2;
|
|
final int SDL_CONTROLLER_AXIS_RIGHTY = 3;
|
|
final int SDL_CONTROLLER_AXIS_TRIGGERLEFT = 4;
|
|
final int SDL_CONTROLLER_AXIS_TRIGGERRIGHT = 5;
|
|
|
|
int naxes = 0;
|
|
boolean haveZ = false;
|
|
boolean havePastZBeforeRZ = false;
|
|
for (InputDevice.MotionRange range : joystickDevice.getMotionRanges()) {
|
|
if ((range.getSource() & InputDevice.SOURCE_CLASS_JOYSTICK) != 0) {
|
|
int axis = range.getAxis();
|
|
if (axis != MotionEvent.AXIS_HAT_X && axis != MotionEvent.AXIS_HAT_Y) {
|
|
naxes++;
|
|
}
|
|
if (axis == MotionEvent.AXIS_Z) {
|
|
haveZ = true;
|
|
} else if (axis > MotionEvent.AXIS_Z && axis < MotionEvent.AXIS_RZ) {
|
|
havePastZBeforeRZ = true;
|
|
}
|
|
}
|
|
}
|
|
// The variable is_accelerometer seems always false, then skip the checking:
|
|
// https://github.com/libsdl-org/SDL/blob/0e9560aea22818884921e5e5064953257bfe7fa7/android-project/app/src/main/java/org/libsdl/app/SDLControllerManager.java#L207
|
|
int axisMask = 0;
|
|
if (naxes >= 2) {
|
|
axisMask |= ((1 << SDL_CONTROLLER_AXIS_LEFTX) | (1 << SDL_CONTROLLER_AXIS_LEFTY));
|
|
}
|
|
if (naxes >= 4) {
|
|
axisMask |= ((1 << SDL_CONTROLLER_AXIS_RIGHTX) | (1 << SDL_CONTROLLER_AXIS_RIGHTY));
|
|
}
|
|
if (naxes >= 6) {
|
|
axisMask |= ((1 << SDL_CONTROLLER_AXIS_TRIGGERLEFT) | (1 << SDL_CONTROLLER_AXIS_TRIGGERRIGHT));
|
|
}
|
|
// Also add an indicator bit for whether the sorting order has changed.
|
|
// This serves to disable outdated gamecontrollerdb.txt mappings.
|
|
if (haveZ && havePastZBeforeRZ) {
|
|
axisMask |= 0x8000;
|
|
}
|
|
return axisMask;
|
|
}
|
|
|
|
@Override
|
|
public void onInputDeviceChanged(int deviceId) {
|
|
// Do nothing.
|
|
}
|
|
|
|
@Override
|
|
public void onInputDeviceRemoved(int deviceId) {
|
|
// Do not call inputManager.getInputDevice(), which returns null (#1185).
|
|
Ebitenmobileview.onInputDeviceRemoved(deviceId);
|
|
this.gamepads.remove(this.getGamepad(deviceId));
|
|
}
|
|
|
|
// suspendGame suspends the game.
|
|
// It is recommended to call this when the application is being suspended e.g.,
|
|
// Activity's onPause is called.
|
|
public void suspendGame() {
|
|
this.inputManager.unregisterInputDeviceListener(this);
|
|
this.ebitenSurfaceView.onPause();
|
|
try {
|
|
Ebitenmobileview.suspend();
|
|
} catch (final Exception e) {
|
|
onErrorOnGameUpdate(e);
|
|
}
|
|
}
|
|
|
|
// resumeGame resumes the game.
|
|
// It is recommended to call this when the application is being resumed e.g.,
|
|
// Activity's onResume is called.
|
|
public void resumeGame() {
|
|
this.inputManager.registerInputDeviceListener(this, null);
|
|
this.ebitenSurfaceView.onResume();
|
|
try {
|
|
Ebitenmobileview.resume();
|
|
} catch (final Exception e) {
|
|
onErrorOnGameUpdate(e);
|
|
}
|
|
}
|
|
|
|
// onErrorOnGameUpdate is called on the main thread when an error happens when updating a game.
|
|
// You can define your own error handler, e.g., using Crashlytics, by overriding this method.
|
|
protected void onErrorOnGameUpdate(Exception e) {
|
|
Log.e("Go", e.toString());
|
|
}
|
|
|
|
private EbitenSurfaceView ebitenSurfaceView;
|
|
private InputManager inputManager;
|
|
private ArrayList<Gamepad> gamepads;
|
|
}
|