summaryrefslogtreecommitdiff
path: root/src/android
diff options
context:
space:
mode:
authorGravatar Charles Lombardo2023-03-08 17:12:47 -0500
committerGravatar bunnei2023-06-03 00:05:39 -0700
commita1c57de466855bf826262a1613699ce9c468cadc (patch)
tree75acd0979c9542c8efdfcb7922e3f09471ab3368 /src/android
parentandroid: Remove DividerItemDecoration (diff)
downloadyuzu-a1c57de466855bf826262a1613699ce9c468cadc.tar.gz
yuzu-a1c57de466855bf826262a1613699ce9c468cadc.tar.xz
yuzu-a1c57de466855bf826262a1613699ce9c468cadc.zip
android: Convert InputOverlay to Kotlin
Diffstat (limited to 'src/android')
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/overlay/InputOverlay.java656
-rw-r--r--src/android/app/src/main/java/org/yuzu/yuzu_emu/overlay/InputOverlay.kt886
2 files changed, 886 insertions, 656 deletions
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/overlay/InputOverlay.java b/src/android/app/src/main/java/org/yuzu/yuzu_emu/overlay/InputOverlay.java
deleted file mode 100644
index 74119c398..000000000
--- a/src/android/app/src/main/java/org/yuzu/yuzu_emu/overlay/InputOverlay.java
+++ /dev/null
@@ -1,656 +0,0 @@
1/**
2 * Copyright 2013 Dolphin Emulator Project
3 * Licensed under GPLv2+
4 * Refer to the license.txt file included.
5 */
6
7package org.yuzu.yuzu_emu.overlay;
8
9import android.app.Activity;
10import android.content.Context;
11import android.content.SharedPreferences;
12import android.content.res.Configuration;
13import android.content.res.Resources;
14import android.graphics.Bitmap;
15import android.graphics.BitmapFactory;
16import android.graphics.Canvas;
17import android.graphics.Rect;
18import android.graphics.drawable.BitmapDrawable;
19import android.graphics.drawable.Drawable;
20import android.graphics.drawable.VectorDrawable;
21import android.hardware.Sensor;
22import android.hardware.SensorEvent;
23import android.hardware.SensorEventListener;
24import android.hardware.SensorManager;
25import android.preference.PreferenceManager;
26import android.util.AttributeSet;
27import android.util.DisplayMetrics;
28import android.view.Display;
29import android.view.MotionEvent;
30import android.view.SurfaceView;
31import android.view.View;
32import android.view.View.OnTouchListener;
33
34import androidx.core.content.ContextCompat;
35
36import org.yuzu.yuzu_emu.NativeLibrary;
37import org.yuzu.yuzu_emu.NativeLibrary.ButtonType;
38import org.yuzu.yuzu_emu.NativeLibrary.StickType;
39import org.yuzu.yuzu_emu.R;
40import org.yuzu.yuzu_emu.utils.EmulationMenuSettings;
41
42import java.util.HashSet;
43import java.util.Set;
44
45/**
46 * Draws the interactive input overlay on top of the
47 * {@link SurfaceView} that is rendering emulation.
48 */
49public final class InputOverlay extends SurfaceView implements OnTouchListener, SensorEventListener {
50 private final Set<InputOverlayDrawableButton> overlayButtons = new HashSet<>();
51 private final Set<InputOverlayDrawableDpad> overlayDpads = new HashSet<>();
52 private final Set<InputOverlayDrawableJoystick> overlayJoysticks = new HashSet<>();
53
54 private boolean mIsInEditMode = false;
55
56 private SharedPreferences mPreferences;
57
58 private float[] gyro = new float[3];
59 private float[] accel = new float[3];
60
61 private long motionTimestamp;
62
63 /**
64 * Constructor
65 *
66 * @param context The current {@link Context}.
67 * @param attrs {@link AttributeSet} for parsing XML attributes.
68 */
69 public InputOverlay(Context context, AttributeSet attrs) {
70 super(context, attrs);
71
72 mPreferences = PreferenceManager.getDefaultSharedPreferences(getContext());
73 if (!mPreferences.getBoolean("OverlayInit", false)) {
74 defaultOverlay();
75 }
76
77 // Load the controls.
78 refreshControls();
79
80 // Set the on motion sensor listener.
81 setMotionSensorListener(context);
82
83 // Set the on touch listener.
84 setOnTouchListener(this);
85
86 // Force draw
87 setWillNotDraw(false);
88
89 // Request focus for the overlay so it has priority on presses.
90 requestFocus();
91 }
92
93 private void setMotionSensorListener(Context context) {
94 SensorManager sensorManager = (SensorManager) context.getSystemService(Context.SENSOR_SERVICE);
95 Sensor gyro_sensor = sensorManager.getDefaultSensor(Sensor.TYPE_GYROSCOPE);
96 Sensor accel_sensor = sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);
97
98 if (gyro_sensor != null) {
99 sensorManager.registerListener(this, gyro_sensor, SensorManager.SENSOR_DELAY_GAME);
100 }
101 if (accel_sensor != null) {
102 sensorManager.registerListener(this, accel_sensor, SensorManager.SENSOR_DELAY_GAME);
103 }
104 }
105
106
107 /**
108 * Resizes a {@link Bitmap} by a given scale factor
109 *
110 * @param vectorDrawable The {@link Bitmap} to scale.
111 * @param scale The scale factor for the bitmap.
112 * @return The scaled {@link Bitmap}
113 */
114 private static Bitmap getBitmap(VectorDrawable vectorDrawable, float scale) {
115 Bitmap bitmap = Bitmap.createBitmap((int) (vectorDrawable.getIntrinsicWidth() * scale),
116 (int) (vectorDrawable.getIntrinsicHeight() * scale), Bitmap.Config.ARGB_8888);
117 Canvas canvas = new Canvas(bitmap);
118 vectorDrawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight());
119 vectorDrawable.draw(canvas);
120 return bitmap;
121 }
122
123 private static Bitmap getBitmap(Context context, int drawableId, float scale) {
124 Drawable drawable = ContextCompat.getDrawable(context, drawableId);
125 if (drawable instanceof BitmapDrawable) {
126 return BitmapFactory.decodeResource(context.getResources(), drawableId);
127 } else if (drawable instanceof VectorDrawable) {
128 return getBitmap((VectorDrawable) drawable, scale);
129 } else {
130 throw new IllegalArgumentException("unsupported drawable type");
131 }
132 }
133
134 /**
135 * Initializes an InputOverlayDrawableButton, given by resId, with all of the
136 * parameters set for it to be properly shown on the InputOverlay.
137 * <p>
138 * This works due to the way the X and Y coordinates are stored within
139 * the {@link SharedPreferences}.
140 * <p>
141 * In the input overlay configuration menu,
142 * once a touch event begins and then ends (ie. Organizing the buttons to one's own liking for the overlay).
143 * the X and Y coordinates of the button at the END of its touch event
144 * (when you remove your finger/stylus from the touchscreen) are then stored
145 * within a SharedPreferences instance so that those values can be retrieved here.
146 * <p>
147 * This has a few benefits over the conventional way of storing the values
148 * (ie. within the yuzu ini file).
149 * <ul>
150 * <li>No native calls</li>
151 * <li>Keeps Android-only values inside the Android environment</li>
152 * </ul>
153 * <p>
154 * Technically no modifications should need to be performed on the returned
155 * InputOverlayDrawableButton. Simply add it to the HashSet of overlay items and wait
156 * for Android to call the onDraw method.
157 *
158 * @param context The current {@link Context}.
159 * @param defaultResId The resource ID of the {@link Drawable} to get the {@link Bitmap} of (Default State).
160 * @param pressedResId The resource ID of the {@link Drawable} to get the {@link Bitmap} of (Pressed State).
161 * @param buttonId Identifier for determining what type of button the initialized InputOverlayDrawableButton represents.
162 * @return An {@link InputOverlayDrawableButton} with the correct drawing bounds set.
163 */
164 private static InputOverlayDrawableButton initializeOverlayButton(Context context,
165 int defaultResId, int pressedResId, int buttonId, String orientation) {
166 // Resources handle for fetching the initial Drawable resource.
167 final Resources res = context.getResources();
168
169 // SharedPreference to retrieve the X and Y coordinates for the InputOverlayDrawableButton.
170 final SharedPreferences sPrefs = PreferenceManager.getDefaultSharedPreferences(context);
171
172 // Decide scale based on button ID and user preference
173 float scale;
174
175 switch (buttonId) {
176 case ButtonType.BUTTON_HOME:
177 case ButtonType.BUTTON_CAPTURE:
178 case ButtonType.BUTTON_PLUS:
179 case ButtonType.BUTTON_MINUS:
180 scale = 0.35f;
181 break;
182 case ButtonType.TRIGGER_L:
183 case ButtonType.TRIGGER_R:
184 case ButtonType.TRIGGER_ZL:
185 case ButtonType.TRIGGER_ZR:
186 scale = 0.38f;
187 break;
188 default:
189 scale = 0.43f;
190 break;
191 }
192
193 scale *= (sPrefs.getInt("controlScale", 50) + 50);
194 scale /= 100;
195
196 // Initialize the InputOverlayDrawableButton.
197 final Bitmap defaultStateBitmap = getBitmap(context, defaultResId, scale);
198 final Bitmap pressedStateBitmap = getBitmap(context, pressedResId, scale);
199 final InputOverlayDrawableButton overlayDrawable =
200 new InputOverlayDrawableButton(res, defaultStateBitmap, pressedStateBitmap, buttonId);
201
202 // The X and Y coordinates of the InputOverlayDrawableButton on the InputOverlay.
203 // These were set in the input overlay configuration menu.
204 String xKey;
205 String yKey;
206
207 xKey = buttonId + orientation + "-X";
208 yKey = buttonId + orientation + "-Y";
209
210 int drawableX = (int) sPrefs.getFloat(xKey, 0f);
211 int drawableY = (int) sPrefs.getFloat(yKey, 0f);
212
213 int width = overlayDrawable.getWidth();
214 int height = overlayDrawable.getHeight();
215
216 // Now set the bounds for the InputOverlayDrawableButton.
217 // This will dictate where on the screen (and the what the size) the InputOverlayDrawableButton will be.
218 overlayDrawable.setBounds(drawableX - (width / 2), drawableY - (height / 2), drawableX + (width / 2), drawableY + (height / 2));
219
220 // Need to set the image's position
221 overlayDrawable.setPosition(drawableX - (width / 2), drawableY - (height / 2));
222
223 return overlayDrawable;
224 }
225
226 /**
227 * Initializes an {@link InputOverlayDrawableDpad}
228 *
229 * @param context The current {@link Context}.
230 * @param defaultResId The {@link Bitmap} resource ID of the default sate.
231 * @param pressedOneDirectionResId The {@link Bitmap} resource ID of the pressed sate in one direction.
232 * @param pressedTwoDirectionsResId The {@link Bitmap} resource ID of the pressed sate in two directions.
233 * @param buttonUp Identifier for the up button.
234 * @param buttonDown Identifier for the down button.
235 * @param buttonLeft Identifier for the left button.
236 * @param buttonRight Identifier for the right button.
237 * @return the initialized {@link InputOverlayDrawableDpad}
238 */
239 private static InputOverlayDrawableDpad initializeOverlayDpad(Context context,
240 int defaultResId,
241 int pressedOneDirectionResId,
242 int pressedTwoDirectionsResId,
243 int buttonUp,
244 int buttonDown,
245 int buttonLeft,
246 int buttonRight,
247 String orientation) {
248 // Resources handle for fetching the initial Drawable resource.
249 final Resources res = context.getResources();
250
251 // SharedPreference to retrieve the X and Y coordinates for the InputOverlayDrawableDpad.
252 final SharedPreferences sPrefs = PreferenceManager.getDefaultSharedPreferences(context);
253
254 // Decide scale based on button ID and user preference
255 float scale = 0.40f;
256
257 scale *= (sPrefs.getInt("controlScale", 50) + 50);
258 scale /= 100;
259
260 // Initialize the InputOverlayDrawableDpad.
261 final Bitmap defaultStateBitmap = getBitmap(context, defaultResId, scale);
262 final Bitmap pressedOneDirectionStateBitmap = getBitmap(context, pressedOneDirectionResId,
263 scale);
264 final Bitmap pressedTwoDirectionsStateBitmap = getBitmap(context, pressedTwoDirectionsResId,
265 scale);
266 final InputOverlayDrawableDpad overlayDrawable =
267 new InputOverlayDrawableDpad(res, defaultStateBitmap,
268 pressedOneDirectionStateBitmap, pressedTwoDirectionsStateBitmap,
269 buttonUp, buttonDown, buttonLeft, buttonRight);
270
271 // The X and Y coordinates of the InputOverlayDrawableDpad on the InputOverlay.
272 // These were set in the input overlay configuration menu.
273 int drawableX = (int) sPrefs.getFloat(buttonUp + orientation + "-X", 0f);
274 int drawableY = (int) sPrefs.getFloat(buttonUp + orientation + "-Y", 0f);
275
276 int width = overlayDrawable.getWidth();
277 int height = overlayDrawable.getHeight();
278
279 // Now set the bounds for the InputOverlayDrawableDpad.
280 // This will dictate where on the screen (and the what the size) the InputOverlayDrawableDpad will be.
281 overlayDrawable.setBounds(drawableX - (width / 2), drawableY - (height / 2), drawableX + (width / 2), drawableY + (height / 2));
282
283 // Need to set the image's position
284 overlayDrawable.setPosition(drawableX - (width / 2), drawableY - (height / 2));
285
286 return overlayDrawable;
287 }
288
289 /**
290 * Initializes an {@link InputOverlayDrawableJoystick}
291 *
292 * @param context The current {@link Context}
293 * @param resOuter Resource ID for the outer image of the joystick (the static image that shows the circular bounds).
294 * @param defaultResInner Resource ID for the default inner image of the joystick (the one you actually move around).
295 * @param pressedResInner Resource ID for the pressed inner image of the joystick.
296 * @param joystick Identifier for which joystick this is.
297 * @param button Identifier for which joystick button this is.
298 * @return the initialized {@link InputOverlayDrawableJoystick}.
299 */
300 private static InputOverlayDrawableJoystick initializeOverlayJoystick(Context context,
301 int resOuter, int defaultResInner, int pressedResInner, int joystick, int button, String orientation) {
302 // Resources handle for fetching the initial Drawable resource.
303 final Resources res = context.getResources();
304
305 // SharedPreference to retrieve the X and Y coordinates for the InputOverlayDrawableJoystick.
306 final SharedPreferences sPrefs = PreferenceManager.getDefaultSharedPreferences(context);
307
308 // Decide scale based on user preference
309 float scale = 0.40f;
310 scale *= (sPrefs.getInt("controlScale", 50) + 50);
311 scale /= 100;
312
313 // Initialize the InputOverlayDrawableJoystick.
314 final Bitmap bitmapOuter = getBitmap(context, resOuter, scale);
315 final Bitmap bitmapInnerDefault = getBitmap(context, defaultResInner, 1.0f);
316 final Bitmap bitmapInnerPressed = getBitmap(context, pressedResInner, 1.0f);
317
318 // The X and Y coordinates of the InputOverlayDrawableButton on the InputOverlay.
319 // These were set in the input overlay configuration menu.
320 int drawableX = (int) sPrefs.getFloat(button + orientation + "-X", 0f);
321 int drawableY = (int) sPrefs.getFloat(button + orientation + "-Y", 0f);
322
323 float outerScale = 1.66f;
324
325 // Now set the bounds for the InputOverlayDrawableJoystick.
326 // This will dictate where on the screen (and the what the size) the InputOverlayDrawableJoystick will be.
327 int outerSize = bitmapOuter.getWidth();
328 Rect outerRect = new Rect(drawableX - (outerSize / 2), drawableY - (outerSize / 2), drawableX + (outerSize / 2), drawableY + (outerSize / 2));
329 Rect innerRect = new Rect(0, 0, (int) (outerSize / outerScale), (int) (outerSize / outerScale));
330
331 // Send the drawableId to the joystick so it can be referenced when saving control position.
332 final InputOverlayDrawableJoystick overlayDrawable
333 = new InputOverlayDrawableJoystick(res, bitmapOuter,
334 bitmapInnerDefault, bitmapInnerPressed,
335 outerRect, innerRect, joystick, button);
336
337 // Need to set the image's position
338 overlayDrawable.setPosition(drawableX, drawableY);
339
340 return overlayDrawable;
341 }
342
343 @Override
344 public void draw(Canvas canvas) {
345 super.draw(canvas);
346
347 for (InputOverlayDrawableButton button : overlayButtons) {
348 button.draw(canvas);
349 }
350
351 for (InputOverlayDrawableDpad dpad : overlayDpads) {
352 dpad.draw(canvas);
353 }
354
355 for (InputOverlayDrawableJoystick joystick : overlayJoysticks) {
356 joystick.draw(canvas);
357 }
358 }
359
360 @Override
361 public boolean onTouch(View v, MotionEvent event) {
362 if (isInEditMode()) {
363 return onTouchWhileEditing(event);
364 }
365 boolean should_update_view = false;
366 for (InputOverlayDrawableButton button : overlayButtons) {
367 if (!button.updateStatus(event)) {
368 continue;
369 }
370 NativeLibrary.onGamePadButtonEvent(NativeLibrary.Player1Device, button.getId(), button.getStatus());
371 should_update_view = true;
372 }
373
374 for (InputOverlayDrawableDpad dpad : overlayDpads) {
375 if (!dpad.updateStatus(event, EmulationMenuSettings.getDpadSlideEnable())) {
376 continue;
377 }
378 NativeLibrary.onGamePadButtonEvent(NativeLibrary.Player1Device, dpad.getUpId(), dpad.getUpStatus());
379 NativeLibrary.onGamePadButtonEvent(NativeLibrary.Player1Device, dpad.getDownId(), dpad.getDownStatus());
380 NativeLibrary.onGamePadButtonEvent(NativeLibrary.Player1Device, dpad.getLeftId(), dpad.getLeftStatus());
381 NativeLibrary.onGamePadButtonEvent(NativeLibrary.Player1Device, dpad.getRightId(), dpad.getRightStatus());
382 should_update_view = true;
383 }
384
385 for (InputOverlayDrawableJoystick joystick : overlayJoysticks) {
386 if (!joystick.updateStatus(event)) {
387 continue;
388 }
389 int axisID = joystick.getJoystickId();
390 NativeLibrary.onGamePadJoystickEvent(NativeLibrary.Player1Device, axisID, joystick.getXAxis(), joystick.getYAxis());
391 NativeLibrary.onGamePadButtonEvent(NativeLibrary.Player1Device, joystick.getButtonId(), joystick.getButtonStatus());
392 should_update_view = true;
393 }
394
395 if (should_update_view) {
396 invalidate();
397 }
398
399 if (!mPreferences.getBoolean("isTouchEnabled", true)) {
400 return true;
401 }
402
403 int pointerIndex = event.getActionIndex();
404 int xPosition = (int) event.getX(pointerIndex);
405 int yPosition = (int) event.getY(pointerIndex);
406 int pointerId = event.getPointerId(pointerIndex);
407 int motion_event = event.getAction() & MotionEvent.ACTION_MASK;
408 boolean isActionDown = motion_event == MotionEvent.ACTION_DOWN || motion_event == MotionEvent.ACTION_POINTER_DOWN;
409 boolean isActionMove = motion_event == MotionEvent.ACTION_MOVE;
410 boolean isActionUp = motion_event == MotionEvent.ACTION_UP || motion_event == MotionEvent.ACTION_POINTER_UP;
411
412 if (isActionDown && !isTouchInputConsumed(pointerId)) {
413 NativeLibrary.onTouchPressed(pointerId, xPosition, yPosition);
414 }
415
416 if (isActionMove) {
417 for (int i = 0; i < event.getPointerCount(); i++) {
418 int fingerId = event.getPointerId(i);
419 if (isTouchInputConsumed(fingerId)) {
420 continue;
421 }
422 NativeLibrary.onTouchMoved(fingerId, event.getX(i), event.getY(i));
423 }
424 }
425
426 if (isActionUp && !isTouchInputConsumed(pointerId)) {
427 NativeLibrary.onTouchReleased(pointerId);
428 }
429
430 return true;
431 }
432
433 private boolean isTouchInputConsumed(int track_id) {
434 for (InputOverlayDrawableButton button : overlayButtons) {
435 if (button.getTrackId() == track_id) {
436 return true;
437 }
438 }
439 for (InputOverlayDrawableDpad dpad : overlayDpads) {
440 if (dpad.getTrackId() == track_id) {
441 return true;
442 }
443 }
444 for (InputOverlayDrawableJoystick joystick : overlayJoysticks) {
445 if (joystick.getTrackId() == track_id) {
446 return true;
447 }
448 }
449 return false;
450 }
451
452 public boolean onTouchWhileEditing(MotionEvent event) {
453 // TODO: Reimplement this
454 return true;
455 }
456
457 @Override
458 public void onSensorChanged(SensorEvent event) {
459 if (event.sensor.getType() == Sensor.TYPE_ACCELEROMETER) {
460 accel[0] = -event.values[1] / SensorManager.GRAVITY_EARTH;
461 accel[1] = event.values[0] / SensorManager.GRAVITY_EARTH;
462 accel[2] = -event.values[2] / SensorManager.GRAVITY_EARTH;
463 }
464
465 if (event.sensor.getType() == Sensor.TYPE_GYROSCOPE) {
466 // Investigate why sensor value is off by 12x
467 gyro[0] = event.values[1] / 12.0f;
468 gyro[1] = -event.values[0] / 12.0f;
469 gyro[2] = event.values[2] / 12.0f;
470 }
471
472 // Only update state on accelerometer data
473 if (event.sensor.getType() != Sensor.TYPE_ACCELEROMETER) {
474 return;
475 }
476
477 long delta_timestamp = (event.timestamp - motionTimestamp) / 1000;
478 motionTimestamp = event.timestamp;
479 NativeLibrary.onGamePadMotionEvent(NativeLibrary.Player1Device, delta_timestamp, gyro[0], gyro[1], gyro[2], accel[0], accel[1], accel[2]);
480 NativeLibrary.onGamePadMotionEvent(NativeLibrary.ConsoleDevice, delta_timestamp, gyro[0], gyro[1], gyro[2], accel[0], accel[1], accel[2]);
481 }
482
483 @Override
484 public void onAccuracyChanged(Sensor sensor, int i) {
485 }
486
487 private void addOverlayControls(String orientation) {
488 if (mPreferences.getBoolean("buttonToggle0", true)) {
489 overlayButtons.add(initializeOverlayButton(getContext(), R.drawable.facebutton_a,
490 R.drawable.facebutton_a_depressed, ButtonType.BUTTON_A, orientation));
491 }
492 if (mPreferences.getBoolean("buttonToggle1", true)) {
493 overlayButtons.add(initializeOverlayButton(getContext(), R.drawable.facebutton_b,
494 R.drawable.facebutton_b_depressed, ButtonType.BUTTON_B, orientation));
495 }
496 if (mPreferences.getBoolean("buttonToggle2", true)) {
497 overlayButtons.add(initializeOverlayButton(getContext(), R.drawable.facebutton_x,
498 R.drawable.facebutton_x_depressed, ButtonType.BUTTON_X, orientation));
499 }
500 if (mPreferences.getBoolean("buttonToggle3", true)) {
501 overlayButtons.add(initializeOverlayButton(getContext(), R.drawable.facebutton_y,
502 R.drawable.facebutton_y_depressed, ButtonType.BUTTON_Y, orientation));
503 }
504 if (mPreferences.getBoolean("buttonToggle4", true)) {
505 overlayButtons.add(initializeOverlayButton(getContext(), R.drawable.l_shoulder,
506 R.drawable.l_shoulder_depressed, ButtonType.TRIGGER_L, orientation));
507 }
508 if (mPreferences.getBoolean("buttonToggle5", true)) {
509 overlayButtons.add(initializeOverlayButton(getContext(), R.drawable.r_shoulder,
510 R.drawable.r_shoulder_depressed, ButtonType.TRIGGER_R, orientation));
511 }
512 if (mPreferences.getBoolean("buttonToggle6", true)) {
513 overlayButtons.add(initializeOverlayButton(getContext(), R.drawable.zl_trigger,
514 R.drawable.zl_trigger_depressed, ButtonType.TRIGGER_ZL, orientation));
515 }
516 if (mPreferences.getBoolean("buttonToggle7", true)) {
517 overlayButtons.add(initializeOverlayButton(getContext(), R.drawable.zr_trigger,
518 R.drawable.zr_trigger_depressed, ButtonType.TRIGGER_ZR, orientation));
519 }
520 if (mPreferences.getBoolean("buttonToggle8", true)) {
521 overlayButtons.add(initializeOverlayButton(getContext(), R.drawable.facebutton_plus,
522 R.drawable.facebutton_plus_depressed, ButtonType.BUTTON_PLUS, orientation));
523 }
524 if (mPreferences.getBoolean("buttonToggle9", true)) {
525 overlayButtons.add(initializeOverlayButton(getContext(), R.drawable.facebutton_minus,
526 R.drawable.facebutton_minus_depressed, ButtonType.BUTTON_MINUS, orientation));
527 }
528 if (mPreferences.getBoolean("buttonToggle10", true)) {
529 overlayDpads.add(initializeOverlayDpad(getContext(), R.drawable.dpad_standard,
530 R.drawable.dpad_standard_cardinal_depressed,
531 R.drawable.dpad_standard_diagonal_depressed,
532 ButtonType.DPAD_UP, ButtonType.DPAD_DOWN,
533 ButtonType.DPAD_LEFT, ButtonType.DPAD_RIGHT, orientation));
534 }
535 if (mPreferences.getBoolean("buttonToggle11", true)) {
536 overlayJoysticks.add(initializeOverlayJoystick(getContext(), R.drawable.joystick_range,
537 R.drawable.joystick, R.drawable.joystick_depressed,
538 StickType.STICK_L, ButtonType.STICK_L, orientation));
539 }
540 if (mPreferences.getBoolean("buttonToggle12", true)) {
541 overlayJoysticks.add(initializeOverlayJoystick(getContext(), R.drawable.joystick_range,
542 R.drawable.joystick, R.drawable.joystick_depressed, StickType.STICK_R, ButtonType.STICK_R, orientation));
543 }
544 if (mPreferences.getBoolean("buttonToggle13", false)) {
545 overlayButtons.add(initializeOverlayButton(getContext(), R.drawable.facebutton_home,
546 R.drawable.facebutton_home_depressed, ButtonType.BUTTON_HOME, orientation));
547 }
548 if (mPreferences.getBoolean("buttonToggle14", false)) {
549 overlayButtons.add(initializeOverlayButton(getContext(), R.drawable.facebutton_screenshot,
550 R.drawable.facebutton_screenshot_depressed, ButtonType.BUTTON_CAPTURE, orientation));
551 }
552 }
553
554 public void refreshControls() {
555 // Remove all the overlay buttons from the HashSet.
556 overlayButtons.clear();
557 overlayDpads.clear();
558 overlayJoysticks.clear();
559
560 String orientation =
561 getResources().getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT ?
562 "-Portrait" : "";
563
564 // Add all the enabled overlay items back to the HashSet.
565 if (EmulationMenuSettings.getShowOverlay()) {
566 addOverlayControls(orientation);
567 }
568
569 invalidate();
570 }
571
572 private void saveControlPosition(int sharedPrefsId, int x, int y, String orientation) {
573 final SharedPreferences sPrefs = PreferenceManager.getDefaultSharedPreferences(getContext());
574 SharedPreferences.Editor sPrefsEditor = sPrefs.edit();
575 sPrefsEditor.putFloat(sharedPrefsId + orientation + "-X", x);
576 sPrefsEditor.putFloat(sharedPrefsId + orientation + "-Y", y);
577 sPrefsEditor.apply();
578 }
579
580 public void setIsInEditMode(boolean isInEditMode) {
581 mIsInEditMode = isInEditMode;
582 }
583
584 private void defaultOverlay() {
585 if (!mPreferences.getBoolean("OverlayInit", false)) {
586 defaultOverlayLandscape();
587 }
588 resetButtonPlacement();
589 SharedPreferences.Editor sPrefsEditor = mPreferences.edit();
590 sPrefsEditor.putBoolean("OverlayInit", true);
591 sPrefsEditor.apply();
592 }
593
594 public void resetButtonPlacement() {
595 defaultOverlayLandscape();
596 refreshControls();
597 }
598
599 private void defaultOverlayLandscape() {
600 SharedPreferences.Editor sPrefsEditor = mPreferences.edit();
601 // Get screen size
602 Display display = ((Activity) getContext()).getWindowManager().getDefaultDisplay();
603 DisplayMetrics outMetrics = new DisplayMetrics();
604 display.getRealMetrics(outMetrics);
605 float maxX = outMetrics.heightPixels;
606 float maxY = outMetrics.widthPixels;
607 // Height and width changes depending on orientation. Use the larger value for height.
608 if (maxY > maxX) {
609 float tmp = maxX;
610 maxX = maxY;
611 maxY = tmp;
612 }
613
614 Resources res = getResources();
615
616 // Each value is a percent from max X/Y stored as an int. Have to bring that value down
617 // to a decimal before multiplying by MAX X/Y.
618 sPrefsEditor.putFloat(ButtonType.BUTTON_A + "-X", (((float) res.getInteger(R.integer.SWITCH_BUTTON_A_X) / 1000) * maxX));
619 sPrefsEditor.putFloat(ButtonType.BUTTON_A + "-Y", (((float) res.getInteger(R.integer.SWITCH_BUTTON_A_Y) / 1000) * maxY));
620 sPrefsEditor.putFloat(ButtonType.BUTTON_B + "-X", (((float) res.getInteger(R.integer.SWITCH_BUTTON_B_X) / 1000) * maxX));
621 sPrefsEditor.putFloat(ButtonType.BUTTON_B + "-Y", (((float) res.getInteger(R.integer.SWITCH_BUTTON_B_Y) / 1000) * maxY));
622 sPrefsEditor.putFloat(ButtonType.BUTTON_X + "-X", (((float) res.getInteger(R.integer.SWITCH_BUTTON_X_X) / 1000) * maxX));
623 sPrefsEditor.putFloat(ButtonType.BUTTON_X + "-Y", (((float) res.getInteger(R.integer.SWITCH_BUTTON_X_Y) / 1000) * maxY));
624 sPrefsEditor.putFloat(ButtonType.BUTTON_Y + "-X", (((float) res.getInteger(R.integer.SWITCH_BUTTON_Y_X) / 1000) * maxX));
625 sPrefsEditor.putFloat(ButtonType.BUTTON_Y + "-Y", (((float) res.getInteger(R.integer.SWITCH_BUTTON_Y_Y) / 1000) * maxY));
626 sPrefsEditor.putFloat(ButtonType.TRIGGER_ZL + "-X", (((float) res.getInteger(R.integer.SWITCH_TRIGGER_ZL_X) / 1000) * maxX));
627 sPrefsEditor.putFloat(ButtonType.TRIGGER_ZL + "-Y", (((float) res.getInteger(R.integer.SWITCH_TRIGGER_ZL_Y) / 1000) * maxY));
628 sPrefsEditor.putFloat(ButtonType.TRIGGER_ZR + "-X", (((float) res.getInteger(R.integer.SWITCH_TRIGGER_ZR_X) / 1000) * maxX));
629 sPrefsEditor.putFloat(ButtonType.TRIGGER_ZR + "-Y", (((float) res.getInteger(R.integer.SWITCH_TRIGGER_ZR_Y) / 1000) * maxY));
630 sPrefsEditor.putFloat(ButtonType.DPAD_UP + "-X", (((float) res.getInteger(R.integer.SWITCH_BUTTON_DPAD_X) / 1000) * maxX));
631 sPrefsEditor.putFloat(ButtonType.DPAD_UP + "-Y", (((float) res.getInteger(R.integer.SWITCH_BUTTON_DPAD_Y) / 1000) * maxY));
632 sPrefsEditor.putFloat(ButtonType.TRIGGER_L + "-X", (((float) res.getInteger(R.integer.SWITCH_TRIGGER_L_X) / 1000) * maxX));
633 sPrefsEditor.putFloat(ButtonType.TRIGGER_L + "-Y", (((float) res.getInteger(R.integer.SWITCH_TRIGGER_L_Y) / 1000) * maxY));
634 sPrefsEditor.putFloat(ButtonType.TRIGGER_R + "-X", (((float) res.getInteger(R.integer.SWITCH_TRIGGER_R_X) / 1000) * maxX));
635 sPrefsEditor.putFloat(ButtonType.TRIGGER_R + "-Y", (((float) res.getInteger(R.integer.SWITCH_TRIGGER_R_Y) / 1000) * maxY));
636 sPrefsEditor.putFloat(ButtonType.BUTTON_PLUS + "-X", (((float) res.getInteger(R.integer.SWITCH_BUTTON_PLUS_X) / 1000) * maxX));
637 sPrefsEditor.putFloat(ButtonType.BUTTON_PLUS + "-Y", (((float) res.getInteger(R.integer.SWITCH_BUTTON_PLUS_Y) / 1000) * maxY));
638 sPrefsEditor.putFloat(ButtonType.BUTTON_MINUS + "-X", (((float) res.getInteger(R.integer.SWITCH_BUTTON_MINUS_X) / 1000) * maxX));
639 sPrefsEditor.putFloat(ButtonType.BUTTON_MINUS + "-Y", (((float) res.getInteger(R.integer.SWITCH_BUTTON_MINUS_Y) / 1000) * maxY));
640 sPrefsEditor.putFloat(ButtonType.BUTTON_HOME + "-X", (((float) res.getInteger(R.integer.SWITCH_BUTTON_HOME_X) / 1000) * maxX));
641 sPrefsEditor.putFloat(ButtonType.BUTTON_HOME + "-Y", (((float) res.getInteger(R.integer.SWITCH_BUTTON_HOME_Y) / 1000) * maxY));
642 sPrefsEditor.putFloat(ButtonType.BUTTON_CAPTURE + "-X", (((float) res.getInteger(R.integer.SWITCH_BUTTON_CAPTURE_X) / 1000) * maxX));
643 sPrefsEditor.putFloat(ButtonType.BUTTON_CAPTURE + "-Y", (((float) res.getInteger(R.integer.SWITCH_BUTTON_CAPTURE_Y) / 1000) * maxY));
644 sPrefsEditor.putFloat(ButtonType.STICK_R + "-X", (((float) res.getInteger(R.integer.SWITCH_STICK_R_X) / 1000) * maxX));
645 sPrefsEditor.putFloat(ButtonType.STICK_R + "-Y", (((float) res.getInteger(R.integer.SWITCH_STICK_R_Y) / 1000) * maxY));
646 sPrefsEditor.putFloat(ButtonType.STICK_L + "-X", (((float) res.getInteger(R.integer.SWITCH_STICK_L_X) / 1000) * maxX));
647 sPrefsEditor.putFloat(ButtonType.STICK_L + "-Y", (((float) res.getInteger(R.integer.SWITCH_STICK_L_Y) / 1000) * maxY));
648
649 // We want to commit right away, otherwise the overlay could load before this is saved.
650 sPrefsEditor.commit();
651 }
652
653 public boolean isInEditMode() {
654 return mIsInEditMode;
655 }
656}
diff --git a/src/android/app/src/main/java/org/yuzu/yuzu_emu/overlay/InputOverlay.kt b/src/android/app/src/main/java/org/yuzu/yuzu_emu/overlay/InputOverlay.kt
new file mode 100644
index 000000000..a964b6257
--- /dev/null
+++ b/src/android/app/src/main/java/org/yuzu/yuzu_emu/overlay/InputOverlay.kt
@@ -0,0 +1,886 @@
1package org.yuzu.yuzu_emu.overlay
2
3import android.app.Activity
4import android.content.Context
5import android.content.SharedPreferences
6import android.content.res.Configuration
7import android.graphics.Bitmap
8import android.graphics.BitmapFactory
9import android.graphics.Canvas
10import android.graphics.Rect
11import android.graphics.drawable.BitmapDrawable
12import android.graphics.drawable.Drawable
13import android.graphics.drawable.VectorDrawable
14import android.hardware.Sensor
15import android.hardware.SensorEvent
16import android.hardware.SensorEventListener
17import android.hardware.SensorManager
18import android.util.AttributeSet
19import android.util.DisplayMetrics
20import android.view.MotionEvent
21import android.view.SurfaceView
22import android.view.View
23import android.view.View.OnTouchListener
24import androidx.core.content.ContextCompat
25import androidx.preference.PreferenceManager
26import org.yuzu.yuzu_emu.NativeLibrary
27import org.yuzu.yuzu_emu.NativeLibrary.ButtonType
28import org.yuzu.yuzu_emu.NativeLibrary.StickType
29import org.yuzu.yuzu_emu.R
30import org.yuzu.yuzu_emu.YuzuApplication
31import org.yuzu.yuzu_emu.features.settings.model.Settings
32import org.yuzu.yuzu_emu.utils.EmulationMenuSettings
33
34
35/**
36 * Draws the interactive input overlay on top of the
37 * [SurfaceView] that is rendering emulation.
38 */
39class InputOverlay(context: Context, attrs: AttributeSet?) : SurfaceView(context, attrs),
40 OnTouchListener, SensorEventListener {
41 private val overlayButtons: MutableSet<InputOverlayDrawableButton> = HashSet()
42 private val overlayDpads: MutableSet<InputOverlayDrawableDpad> = HashSet()
43 private val overlayJoysticks: MutableSet<InputOverlayDrawableJoystick> = HashSet()
44 private var inEditMode = false
45 private val preferences: SharedPreferences =
46 PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext)
47 private val gyro = FloatArray(3)
48 private val accel = FloatArray(3)
49 private var motionTimestamp: Long = 0
50
51 init {
52 if (!preferences.getBoolean(Settings.PREF_OVERLAY_INIT, false)) {
53 defaultOverlay()
54 }
55
56 // Load the controls.
57 refreshControls()
58
59 // Set the on motion sensor listener.
60 setMotionSensorListener(context)
61
62 // Set the on touch listener.
63 setOnTouchListener(this)
64
65 // Force draw
66 setWillNotDraw(false)
67
68 // Request focus for the overlay so it has priority on presses.
69 requestFocus()
70 }
71
72 private fun setMotionSensorListener(context: Context) {
73 val sensorManager = context.getSystemService(Context.SENSOR_SERVICE) as SensorManager
74 val gyroSensor = sensorManager.getDefaultSensor(Sensor.TYPE_GYROSCOPE)
75 val accelSensor = sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER)
76 if (gyroSensor != null) {
77 sensorManager.registerListener(this, gyroSensor, SensorManager.SENSOR_DELAY_GAME)
78 }
79 if (accelSensor != null) {
80 sensorManager.registerListener(this, accelSensor, SensorManager.SENSOR_DELAY_GAME)
81 }
82 }
83
84 override fun draw(canvas: Canvas) {
85 super.draw(canvas)
86 for (button in overlayButtons) {
87 button.draw(canvas)
88 }
89 for (dpad in overlayDpads) {
90 dpad.draw(canvas)
91 }
92 for (joystick in overlayJoysticks) {
93 joystick.draw(canvas)
94 }
95 }
96
97 override fun onTouch(v: View, event: MotionEvent): Boolean {
98 if (inEditMode) {
99 return onTouchWhileEditing(event)
100 }
101
102 var shouldUpdateView = false
103
104 for (button in overlayButtons) {
105 if (!button.updateStatus(event)) {
106 continue
107 }
108 NativeLibrary.onGamePadButtonEvent(
109 NativeLibrary.Player1Device,
110 button.id,
111 button.status
112 )
113 shouldUpdateView = true
114 }
115
116 for (dpad in overlayDpads) {
117 if (!dpad.updateStatus(event, EmulationMenuSettings.dpadSlideEnable)) {
118 continue
119 }
120 NativeLibrary.onGamePadButtonEvent(
121 NativeLibrary.Player1Device,
122 dpad.upId,
123 dpad.upStatus
124 )
125 NativeLibrary.onGamePadButtonEvent(
126 NativeLibrary.Player1Device,
127 dpad.downId,
128 dpad.downStatus
129 )
130 NativeLibrary.onGamePadButtonEvent(
131 NativeLibrary.Player1Device,
132 dpad.leftId,
133 dpad.leftStatus
134 )
135 NativeLibrary.onGamePadButtonEvent(
136 NativeLibrary.Player1Device,
137 dpad.rightId,
138 dpad.rightStatus
139 )
140 shouldUpdateView = true
141 }
142
143 for (joystick in overlayJoysticks) {
144 if (!joystick.updateStatus(event)) {
145 continue
146 }
147 val axisID = joystick.joystickId
148 NativeLibrary.onGamePadJoystickEvent(
149 NativeLibrary.Player1Device,
150 axisID,
151 joystick.xAxis,
152 joystick.realYAxis
153 )
154 NativeLibrary.onGamePadButtonEvent(
155 NativeLibrary.Player1Device,
156 joystick.buttonId,
157 joystick.buttonStatus
158 )
159 shouldUpdateView = true
160 }
161
162 if (shouldUpdateView)
163 invalidate()
164
165 if (!preferences.getBoolean(Settings.PREF_TOUCH_ENABLED, true)) {
166 return true
167 }
168
169 val pointerIndex = event.actionIndex
170 val xPosition = event.getX(pointerIndex).toInt()
171 val yPosition = event.getY(pointerIndex).toInt()
172 val pointerId = event.getPointerId(pointerIndex)
173 val motionEvent = event.action and MotionEvent.ACTION_MASK
174 val isActionDown =
175 motionEvent == MotionEvent.ACTION_DOWN || motionEvent == MotionEvent.ACTION_POINTER_DOWN
176 val isActionMove = motionEvent == MotionEvent.ACTION_MOVE
177 val isActionUp =
178 motionEvent == MotionEvent.ACTION_UP || motionEvent == MotionEvent.ACTION_POINTER_UP
179
180 if (isActionDown && !isTouchInputConsumed(pointerId)) {
181 NativeLibrary.onTouchPressed(pointerId, xPosition.toFloat(), yPosition.toFloat())
182 }
183
184 if (isActionMove) {
185 for (i in 0 until event.pointerCount) {
186 val fingerId = event.getPointerId(i)
187 if (isTouchInputConsumed(fingerId)) {
188 continue
189 }
190 NativeLibrary.onTouchMoved(fingerId, event.getX(i), event.getY(i))
191 }
192 }
193
194 if (isActionUp && !isTouchInputConsumed(pointerId)) {
195 NativeLibrary.onTouchReleased(pointerId)
196 }
197
198 return true
199 }
200
201 private fun isTouchInputConsumed(track_id: Int): Boolean {
202 for (button in overlayButtons) {
203 if (button.trackId == track_id) {
204 return true
205 }
206 }
207 for (dpad in overlayDpads) {
208 if (dpad.trackId == track_id) {
209 return true
210 }
211 }
212 for (joystick in overlayJoysticks) {
213 if (joystick.trackId == track_id) {
214 return true
215 }
216 }
217 return false
218 }
219
220 private fun onTouchWhileEditing(event: MotionEvent?): Boolean {
221 // TODO: Reimplement this
222 return true
223 }
224
225 override fun onSensorChanged(event: SensorEvent) {
226 if (event.sensor.type == Sensor.TYPE_ACCELEROMETER) {
227 accel[0] = -event.values[1] / SensorManager.GRAVITY_EARTH
228 accel[1] = event.values[0] / SensorManager.GRAVITY_EARTH
229 accel[2] = -event.values[2] / SensorManager.GRAVITY_EARTH
230 }
231 if (event.sensor.type == Sensor.TYPE_GYROSCOPE) {
232 // Investigate why sensor value is off by 12x
233 gyro[0] = event.values[1] / 12.0f
234 gyro[1] = -event.values[0] / 12.0f
235 gyro[2] = event.values[2] / 12.0f
236 }
237
238 // Only update state on accelerometer data
239 if (event.sensor.type != Sensor.TYPE_ACCELEROMETER) {
240 return
241 }
242 val deltaTimestamp = (event.timestamp - motionTimestamp) / 1000
243 motionTimestamp = event.timestamp
244 NativeLibrary.onGamePadMotionEvent(
245 NativeLibrary.Player1Device,
246 deltaTimestamp,
247 gyro[0],
248 gyro[1],
249 gyro[2],
250 accel[0],
251 accel[1],
252 accel[2]
253 )
254 NativeLibrary.onGamePadMotionEvent(
255 NativeLibrary.ConsoleDevice,
256 deltaTimestamp,
257 gyro[0],
258 gyro[1],
259 gyro[2],
260 accel[0],
261 accel[1],
262 accel[2]
263 )
264 }
265
266 override fun onAccuracyChanged(sensor: Sensor, i: Int) {}
267 private fun addOverlayControls(orientation: String) {
268 if (preferences.getBoolean(Settings.PREF_BUTTON_TOGGLE_0, true)) {
269 overlayButtons.add(
270 initializeOverlayButton(
271 context,
272 R.drawable.facebutton_a,
273 R.drawable.facebutton_a_depressed,
274 ButtonType.BUTTON_A,
275 orientation
276 )
277 )
278 }
279 if (preferences.getBoolean(Settings.PREF_BUTTON_TOGGLE_1, true)) {
280 overlayButtons.add(
281 initializeOverlayButton(
282 context,
283 R.drawable.facebutton_b,
284 R.drawable.facebutton_b_depressed,
285 ButtonType.BUTTON_B,
286 orientation
287 )
288 )
289 }
290 if (preferences.getBoolean(Settings.PREF_BUTTON_TOGGLE_2, true)) {
291 overlayButtons.add(
292 initializeOverlayButton(
293 context,
294 R.drawable.facebutton_x,
295 R.drawable.facebutton_x_depressed,
296 ButtonType.BUTTON_X,
297 orientation
298 )
299 )
300 }
301 if (preferences.getBoolean(Settings.PREF_BUTTON_TOGGLE_3, true)) {
302 overlayButtons.add(
303 initializeOverlayButton(
304 context,
305 R.drawable.facebutton_y,
306 R.drawable.facebutton_y_depressed,
307 ButtonType.BUTTON_Y,
308 orientation
309 )
310 )
311 }
312 if (preferences.getBoolean(Settings.PREF_BUTTON_TOGGLE_4, true)) {
313 overlayButtons.add(
314 initializeOverlayButton(
315 context,
316 R.drawable.l_shoulder,
317 R.drawable.l_shoulder_depressed,
318 ButtonType.TRIGGER_L,
319 orientation
320 )
321 )
322 }
323 if (preferences.getBoolean(Settings.PREF_BUTTON_TOGGLE_5, true)) {
324 overlayButtons.add(
325 initializeOverlayButton(
326 context,
327 R.drawable.r_shoulder,
328 R.drawable.r_shoulder_depressed,
329 ButtonType.TRIGGER_R,
330 orientation
331 )
332 )
333 }
334 if (preferences.getBoolean(Settings.PREF_BUTTON_TOGGLE_6, true)) {
335 overlayButtons.add(
336 initializeOverlayButton(
337 context,
338 R.drawable.zl_trigger,
339 R.drawable.zl_trigger_depressed,
340 ButtonType.TRIGGER_ZL,
341 orientation
342 )
343 )
344 }
345 if (preferences.getBoolean(Settings.PREF_BUTTON_TOGGLE_7, true)) {
346 overlayButtons.add(
347 initializeOverlayButton(
348 context,
349 R.drawable.zr_trigger,
350 R.drawable.zr_trigger_depressed,
351 ButtonType.TRIGGER_ZR,
352 orientation
353 )
354 )
355 }
356 if (preferences.getBoolean(Settings.PREF_BUTTON_TOGGLE_8, true)) {
357 overlayButtons.add(
358 initializeOverlayButton(
359 context,
360 R.drawable.facebutton_plus,
361 R.drawable.facebutton_plus_depressed,
362 ButtonType.BUTTON_PLUS,
363 orientation
364 )
365 )
366 }
367 if (preferences.getBoolean(Settings.PREF_BUTTON_TOGGLE_9, true)) {
368 overlayButtons.add(
369 initializeOverlayButton(
370 context,
371 R.drawable.facebutton_minus,
372 R.drawable.facebutton_minus_depressed,
373 ButtonType.BUTTON_MINUS,
374 orientation
375 )
376 )
377 }
378 if (preferences.getBoolean(Settings.PREF_BUTTON_TOGGLE_10, true)) {
379 overlayDpads.add(
380 initializeOverlayDpad(
381 context,
382 R.drawable.dpad_standard,
383 R.drawable.dpad_standard_cardinal_depressed,
384 R.drawable.dpad_standard_diagonal_depressed,
385 orientation
386 )
387 )
388 }
389 if (preferences.getBoolean(Settings.PREF_BUTTON_TOGGLE_11, true)) {
390 overlayJoysticks.add(
391 initializeOverlayJoystick(
392 context,
393 R.drawable.joystick_range,
394 R.drawable.joystick,
395 R.drawable.joystick_depressed,
396 StickType.STICK_L,
397 ButtonType.STICK_L,
398 orientation
399 )
400 )
401 }
402 if (preferences.getBoolean(Settings.PREF_BUTTON_TOGGLE_12, true)) {
403 overlayJoysticks.add(
404 initializeOverlayJoystick(
405 context,
406 R.drawable.joystick_range,
407 R.drawable.joystick,
408 R.drawable.joystick_depressed,
409 StickType.STICK_R,
410 ButtonType.STICK_R,
411 orientation
412 )
413 )
414 }
415 if (preferences.getBoolean(Settings.PREF_BUTTON_TOGGLE_13, false)) {
416 overlayButtons.add(
417 initializeOverlayButton(
418 context,
419 R.drawable.facebutton_home,
420 R.drawable.facebutton_home_depressed,
421 ButtonType.BUTTON_HOME,
422 orientation
423 )
424 )
425 }
426 if (preferences.getBoolean(Settings.PREF_BUTTON_TOGGLE_14, false)) {
427 overlayButtons.add(
428 initializeOverlayButton(
429 context,
430 R.drawable.facebutton_screenshot,
431 R.drawable.facebutton_screenshot_depressed,
432 ButtonType.BUTTON_CAPTURE,
433 orientation
434 )
435 )
436 }
437 }
438
439 fun refreshControls() {
440 // Remove all the overlay buttons from the HashSet.
441 overlayButtons.clear()
442 overlayDpads.clear()
443 overlayJoysticks.clear()
444 val orientation =
445 if (resources.configuration.orientation == Configuration.ORIENTATION_PORTRAIT) "-Portrait" else ""
446
447 // Add all the enabled overlay items back to the HashSet.
448 if (EmulationMenuSettings.showOverlay) {
449 addOverlayControls(orientation)
450 }
451 invalidate()
452 }
453
454 private fun saveControlPosition(sharedPrefsId: Int, x: Int, y: Int, orientation: String) {
455 PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext).edit()
456 .putFloat("$sharedPrefsId$orientation-X", x.toFloat())
457 .putFloat("$sharedPrefsId$orientation-Y", y.toFloat())
458 .apply()
459 }
460
461 fun setIsInEditMode(editMode: Boolean) {
462 inEditMode = editMode
463 }
464
465 private fun defaultOverlay() {
466 if (!preferences.getBoolean(Settings.PREF_OVERLAY_INIT, false)) {
467 defaultOverlayLandscape()
468 }
469
470 resetButtonPlacement()
471 preferences.edit()
472 .putBoolean(Settings.PREF_OVERLAY_INIT, true)
473 .apply()
474 }
475
476 fun resetButtonPlacement() {
477 defaultOverlayLandscape()
478 refreshControls()
479 }
480
481 private fun defaultOverlayLandscape() {
482 // Get screen size
483 val display = (context as Activity).windowManager.defaultDisplay
484 val outMetrics = DisplayMetrics()
485 display.getRealMetrics(outMetrics)
486 var maxX = outMetrics.heightPixels.toFloat()
487 var maxY = outMetrics.widthPixels.toFloat()
488 // Height and width changes depending on orientation. Use the larger value for height.
489 if (maxY > maxX) {
490 val tmp = maxX
491 maxX = maxY
492 maxY = tmp
493 }
494 val res = resources
495
496 // Each value is a percent from max X/Y stored as an int. Have to bring that value down
497 // to a decimal before multiplying by MAX X/Y.
498 preferences.edit()
499 .putFloat(
500 ButtonType.BUTTON_A.toString() + "-X",
501 res.getInteger(R.integer.SWITCH_BUTTON_A_X).toFloat() / 1000 * maxX
502 )
503 .putFloat(
504 ButtonType.BUTTON_A.toString() + "-Y",
505 res.getInteger(R.integer.SWITCH_BUTTON_A_Y).toFloat() / 1000 * maxY
506 )
507 .putFloat(
508 ButtonType.BUTTON_B.toString() + "-X",
509 res.getInteger(R.integer.SWITCH_BUTTON_B_X).toFloat() / 1000 * maxX
510 )
511 .putFloat(
512 ButtonType.BUTTON_B.toString() + "-Y",
513 res.getInteger(R.integer.SWITCH_BUTTON_B_Y).toFloat() / 1000 * maxY
514 )
515 .putFloat(
516 ButtonType.BUTTON_X.toString() + "-X",
517 res.getInteger(R.integer.SWITCH_BUTTON_X_X).toFloat() / 1000 * maxX
518 )
519 .putFloat(
520 ButtonType.BUTTON_X.toString() + "-Y",
521 res.getInteger(R.integer.SWITCH_BUTTON_X_Y).toFloat() / 1000 * maxY
522 )
523 .putFloat(
524 ButtonType.BUTTON_Y.toString() + "-X",
525 res.getInteger(R.integer.SWITCH_BUTTON_Y_X).toFloat() / 1000 * maxX
526 )
527 .putFloat(
528 ButtonType.BUTTON_Y.toString() + "-Y",
529 res.getInteger(R.integer.SWITCH_BUTTON_Y_Y).toFloat() / 1000 * maxY
530 )
531 .putFloat(
532 ButtonType.TRIGGER_ZL.toString() + "-X",
533 res.getInteger(R.integer.SWITCH_TRIGGER_ZL_X).toFloat() / 1000 * maxX
534 )
535 .putFloat(
536 ButtonType.TRIGGER_ZL.toString() + "-Y",
537 res.getInteger(R.integer.SWITCH_TRIGGER_ZL_Y).toFloat() / 1000 * maxY
538 )
539 .putFloat(
540 ButtonType.TRIGGER_ZR.toString() + "-X",
541 res.getInteger(R.integer.SWITCH_TRIGGER_ZR_X).toFloat() / 1000 * maxX
542 )
543 .putFloat(
544 ButtonType.TRIGGER_ZR.toString() + "-Y",
545 res.getInteger(R.integer.SWITCH_TRIGGER_ZR_Y).toFloat() / 1000 * maxY
546 )
547 .putFloat(
548 ButtonType.DPAD_UP.toString() + "-X",
549 res.getInteger(R.integer.SWITCH_BUTTON_DPAD_X).toFloat() / 1000 * maxX
550 )
551 .putFloat(
552 ButtonType.DPAD_UP.toString() + "-Y",
553 res.getInteger(R.integer.SWITCH_BUTTON_DPAD_Y).toFloat() / 1000 * maxY
554 )
555 .putFloat(
556 ButtonType.TRIGGER_L.toString() + "-X",
557 res.getInteger(R.integer.SWITCH_TRIGGER_L_X).toFloat() / 1000 * maxX
558 )
559 .putFloat(
560 ButtonType.TRIGGER_L.toString() + "-Y",
561 res.getInteger(R.integer.SWITCH_TRIGGER_L_Y).toFloat() / 1000 * maxY
562 )
563 .putFloat(
564 ButtonType.TRIGGER_R.toString() + "-X",
565 res.getInteger(R.integer.SWITCH_TRIGGER_R_X).toFloat() / 1000 * maxX
566 )
567 .putFloat(
568 ButtonType.TRIGGER_R.toString() + "-Y",
569 res.getInteger(R.integer.SWITCH_TRIGGER_R_Y).toFloat() / 1000 * maxY
570 )
571 .putFloat(
572 ButtonType.BUTTON_PLUS.toString() + "-X",
573 res.getInteger(R.integer.SWITCH_BUTTON_PLUS_X).toFloat() / 1000 * maxX
574 )
575 .putFloat(
576 ButtonType.BUTTON_PLUS.toString() + "-Y",
577 res.getInteger(R.integer.SWITCH_BUTTON_PLUS_Y).toFloat() / 1000 * maxY
578 )
579 .putFloat(
580 ButtonType.BUTTON_MINUS.toString() + "-X",
581 res.getInteger(R.integer.SWITCH_BUTTON_MINUS_X).toFloat() / 1000 * maxX
582 )
583 .putFloat(
584 ButtonType.BUTTON_MINUS.toString() + "-Y",
585 res.getInteger(R.integer.SWITCH_BUTTON_MINUS_Y).toFloat() / 1000 * maxY
586 )
587 .putFloat(
588 ButtonType.BUTTON_HOME.toString() + "-X",
589 res.getInteger(R.integer.SWITCH_BUTTON_HOME_X).toFloat() / 1000 * maxX
590 )
591 .putFloat(
592 ButtonType.BUTTON_HOME.toString() + "-Y",
593 res.getInteger(R.integer.SWITCH_BUTTON_HOME_Y).toFloat() / 1000 * maxY
594 )
595 .putFloat(
596 ButtonType.BUTTON_CAPTURE.toString() + "-X",
597 res.getInteger(R.integer.SWITCH_BUTTON_CAPTURE_X).toFloat() / 1000 * maxX
598 )
599 .putFloat(
600 ButtonType.BUTTON_CAPTURE.toString() + "-Y",
601 res.getInteger(R.integer.SWITCH_BUTTON_CAPTURE_Y).toFloat() / 1000 * maxY
602 )
603 .putFloat(
604 ButtonType.STICK_R.toString() + "-X",
605 res.getInteger(R.integer.SWITCH_STICK_R_X).toFloat() / 1000 * maxX
606 )
607 .putFloat(
608 ButtonType.STICK_R.toString() + "-Y",
609 res.getInteger(R.integer.SWITCH_STICK_R_Y).toFloat() / 1000 * maxY
610 )
611 .putFloat(
612 ButtonType.STICK_L.toString() + "-X",
613 res.getInteger(R.integer.SWITCH_STICK_L_X).toFloat() / 1000 * maxX
614 )
615 .putFloat(
616 ButtonType.STICK_L.toString() + "-Y",
617 res.getInteger(R.integer.SWITCH_STICK_L_Y).toFloat() / 1000 * maxY
618 )
619 .commit()
620 // We want to commit right away, otherwise the overlay could load before this is saved.
621 }
622
623 override fun isInEditMode(): Boolean {
624 return inEditMode
625 }
626
627 companion object {
628 /**
629 * Resizes a [Bitmap] by a given scale factor
630 *
631 * @param vectorDrawable The {@link Bitmap} to scale.
632 * @param scale The scale factor for the bitmap.
633 * @return The scaled [Bitmap]
634 */
635 private fun getBitmap(vectorDrawable: VectorDrawable, scale: Float): Bitmap {
636 val bitmap = Bitmap.createBitmap(
637 (vectorDrawable.intrinsicWidth * scale).toInt(),
638 (vectorDrawable.intrinsicHeight * scale).toInt(),
639 Bitmap.Config.ARGB_8888
640 )
641 val canvas = Canvas(bitmap)
642 vectorDrawable.setBounds(0, 0, canvas.width, canvas.height)
643 vectorDrawable.draw(canvas)
644 return bitmap
645 }
646
647 private fun getBitmap(context: Context, drawableId: Int, scale: Float): Bitmap {
648 return when (val drawable = ContextCompat.getDrawable(context, drawableId)) {
649 is BitmapDrawable -> BitmapFactory.decodeResource(context.resources, drawableId)
650 is VectorDrawable -> getBitmap(drawable, scale)
651 else -> throw IllegalArgumentException("Unsupported drawable type")
652 }
653 }
654
655 /**
656 * Initializes an InputOverlayDrawableButton, given by resId, with all of the
657 * parameters set for it to be properly shown on the InputOverlay.
658 *
659 *
660 * This works due to the way the X and Y coordinates are stored within
661 * the [SharedPreferences].
662 *
663 *
664 * In the input overlay configuration menu,
665 * once a touch event begins and then ends (ie. Organizing the buttons to one's own liking for the overlay).
666 * the X and Y coordinates of the button at the END of its touch event
667 * (when you remove your finger/stylus from the touchscreen) are then stored
668 * within a SharedPreferences instance so that those values can be retrieved here.
669 *
670 *
671 * This has a few benefits over the conventional way of storing the values
672 * (ie. within the yuzu ini file).
673 *
674 * * No native calls
675 * * Keeps Android-only values inside the Android environment
676 *
677 *
678 *
679 * Technically no modifications should need to be performed on the returned
680 * InputOverlayDrawableButton. Simply add it to the HashSet of overlay items and wait
681 * for Android to call the onDraw method.
682 *
683 * @param context The current [Context].
684 * @param defaultResId The resource ID of the [Drawable] to get the [Bitmap] of (Default State).
685 * @param pressedResId The resource ID of the [Drawable] to get the [Bitmap] of (Pressed State).
686 * @param buttonId Identifier for determining what type of button the initialized InputOverlayDrawableButton represents.
687 * @return An [InputOverlayDrawableButton] with the correct drawing bounds set.
688 */
689 private fun initializeOverlayButton(
690 context: Context,
691 defaultResId: Int,
692 pressedResId: Int,
693 buttonId: Int,
694 orientation: String
695 ): InputOverlayDrawableButton {
696 // Resources handle for fetching the initial Drawable resource.
697 val res = context.resources
698
699 // SharedPreference to retrieve the X and Y coordinates for the InputOverlayDrawableButton.
700 val sPrefs = PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext)
701
702 // Decide scale based on button ID and user preference
703 var scale: Float = when (buttonId) {
704 ButtonType.BUTTON_HOME,
705 ButtonType.BUTTON_CAPTURE,
706 ButtonType.BUTTON_PLUS,
707 ButtonType.BUTTON_MINUS -> 0.35f
708 ButtonType.TRIGGER_L,
709 ButtonType.TRIGGER_R,
710 ButtonType.TRIGGER_ZL,
711 ButtonType.TRIGGER_ZR -> 0.38f
712 else -> 0.43f
713 }
714 scale *= (sPrefs.getInt(Settings.PREF_CONTROL_SCALE, 50) + 50).toFloat()
715 scale /= 100f
716
717 // Initialize the InputOverlayDrawableButton.
718 val defaultStateBitmap = getBitmap(context, defaultResId, scale)
719 val pressedStateBitmap = getBitmap(context, pressedResId, scale)
720 val overlayDrawable =
721 InputOverlayDrawableButton(res, defaultStateBitmap, pressedStateBitmap, buttonId)
722
723 // The X and Y coordinates of the InputOverlayDrawableButton on the InputOverlay.
724 // These were set in the input overlay configuration menu.
725 val xKey = "$buttonId$orientation-X"
726 val yKey = "$buttonId$orientation-Y"
727 val drawableX = sPrefs.getFloat(xKey, 0f).toInt()
728 val drawableY = sPrefs.getFloat(yKey, 0f).toInt()
729 val width = overlayDrawable.width
730 val height = overlayDrawable.height
731
732 // Now set the bounds for the InputOverlayDrawableButton.
733 // This will dictate where on the screen (and the what the size) the InputOverlayDrawableButton will be.
734 overlayDrawable.setBounds(
735 drawableX - (width / 2),
736 drawableY - (height / 2),
737 drawableX + (width / 2),
738 drawableY + (height / 2)
739 )
740
741 // Need to set the image's position
742 overlayDrawable.setPosition(
743 drawableX - (width / 2),
744 drawableY - (height / 2)
745 )
746 return overlayDrawable
747 }
748
749 /**
750 * Initializes an [InputOverlayDrawableDpad]
751 *
752 * @param context The current [Context].
753 * @param defaultResId The [Bitmap] resource ID of the default sate.
754 * @param pressedOneDirectionResId The [Bitmap] resource ID of the pressed sate in one direction.
755 * @param pressedTwoDirectionsResId The [Bitmap] resource ID of the pressed sate in two directions.
756 * @return the initialized [InputOverlayDrawableDpad]
757 */
758 private fun initializeOverlayDpad(
759 context: Context,
760 defaultResId: Int,
761 pressedOneDirectionResId: Int,
762 pressedTwoDirectionsResId: Int,
763 orientation: String
764 ): InputOverlayDrawableDpad {
765 // Resources handle for fetching the initial Drawable resource.
766 val res = context.resources
767
768 // SharedPreference to retrieve the X and Y coordinates for the InputOverlayDrawableDpad.
769 val sPrefs = PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext)
770
771 // Decide scale based on button ID and user preference
772 var scale = 0.40f
773 scale *= (sPrefs.getInt(Settings.PREF_CONTROL_SCALE, 50) + 50).toFloat()
774 scale /= 100f
775
776 // Initialize the InputOverlayDrawableDpad.
777 val defaultStateBitmap =
778 getBitmap(context, defaultResId, scale)
779 val pressedOneDirectionStateBitmap = getBitmap(context, pressedOneDirectionResId, scale)
780 val pressedTwoDirectionsStateBitmap =
781 getBitmap(context, pressedTwoDirectionsResId, scale)
782
783 val overlayDrawable = InputOverlayDrawableDpad(
784 res,
785 defaultStateBitmap,
786 pressedOneDirectionStateBitmap,
787 pressedTwoDirectionsStateBitmap,
788 ButtonType.DPAD_UP,
789 ButtonType.DPAD_DOWN,
790 ButtonType.DPAD_LEFT,
791 ButtonType.DPAD_RIGHT
792 )
793
794 // The X and Y coordinates of the InputOverlayDrawableDpad on the InputOverlay.
795 // These were set in the input overlay configuration menu.
796 val drawableX = sPrefs.getFloat("${ButtonType.DPAD_UP}$orientation-X", 0f).toInt()
797 val drawableY = sPrefs.getFloat("${ButtonType.DPAD_UP}$orientation-Y", 0f).toInt()
798 val width = overlayDrawable.width
799 val height = overlayDrawable.height
800
801 // Now set the bounds for the InputOverlayDrawableDpad.
802 // This will dictate where on the screen (and the what the size) the InputOverlayDrawableDpad will be.
803 overlayDrawable.setBounds(
804 drawableX - (width / 2),
805 drawableY - (height / 2),
806 drawableX + (width / 2),
807 drawableY + (height / 2)
808 )
809
810 // Need to set the image's position
811 overlayDrawable.setPosition(drawableX - (width / 2), drawableY - (height / 2))
812 return overlayDrawable
813 }
814
815 /**
816 * Initializes an [InputOverlayDrawableJoystick]
817 *
818 * @param context The current [Context]
819 * @param resOuter Resource ID for the outer image of the joystick (the static image that shows the circular bounds).
820 * @param defaultResInner Resource ID for the default inner image of the joystick (the one you actually move around).
821 * @param pressedResInner Resource ID for the pressed inner image of the joystick.
822 * @param joystick Identifier for which joystick this is.
823 * @param button Identifier for which joystick button this is.
824 * @return the initialized [InputOverlayDrawableJoystick].
825 */
826 private fun initializeOverlayJoystick(
827 context: Context,
828 resOuter: Int,
829 defaultResInner: Int,
830 pressedResInner: Int,
831 joystick: Int,
832 button: Int,
833 orientation: String
834 ): InputOverlayDrawableJoystick {
835 // Resources handle for fetching the initial Drawable resource.
836 val res = context.resources
837
838 // SharedPreference to retrieve the X and Y coordinates for the InputOverlayDrawableJoystick.
839 val sPrefs = PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext)
840
841 // Decide scale based on user preference
842 var scale = 0.40f
843 scale *= (sPrefs.getInt(Settings.PREF_CONTROL_SCALE, 50) + 50).toFloat()
844 scale /= 100f
845
846 // Initialize the InputOverlayDrawableJoystick.
847 val bitmapOuter = getBitmap(context, resOuter, scale)
848 val bitmapInnerDefault = getBitmap(context, defaultResInner, 1.0f)
849 val bitmapInnerPressed = getBitmap(context, pressedResInner, 1.0f)
850
851 // The X and Y coordinates of the InputOverlayDrawableButton on the InputOverlay.
852 // These were set in the input overlay configuration menu.
853 val drawableX = sPrefs.getFloat("$button$orientation-X", 0f).toInt()
854 val drawableY = sPrefs.getFloat("$button$orientation-Y", 0f).toInt()
855 val outerScale = 1.66f
856
857 // Now set the bounds for the InputOverlayDrawableJoystick.
858 // This will dictate where on the screen (and the what the size) the InputOverlayDrawableJoystick will be.
859 val outerSize = bitmapOuter.width
860 val outerRect = Rect(
861 drawableX - (outerSize / 2),
862 drawableY - (outerSize / 2),
863 drawableX + (outerSize / 2),
864 drawableY + (outerSize / 2)
865 )
866 val innerRect =
867 Rect(0, 0, (outerSize / outerScale).toInt(), (outerSize / outerScale).toInt())
868
869 // Send the drawableId to the joystick so it can be referenced when saving control position.
870 val overlayDrawable = InputOverlayDrawableJoystick(
871 res,
872 bitmapOuter,
873 bitmapInnerDefault,
874 bitmapInnerPressed,
875 outerRect,
876 innerRect,
877 joystick,
878 button
879 )
880
881 // Need to set the image's position
882 overlayDrawable.setPosition(drawableX, drawableY)
883 return overlayDrawable
884 }
885 }
886}