added right virtual joystick

This commit is contained in:
izzy2lost
2026-02-12 06:45:38 -05:00
parent be2c607893
commit 9c8af01b0e
8 changed files with 272 additions and 42 deletions
+2 -2
View File
@@ -29,8 +29,8 @@ android {
applicationId = "com.izzy2lost.psx2"
minSdk = 26
targetSdk = 36
versionCode 20
versionName "1.1.8"
versionCode 21
versionName "1.1.9"
// APK
base.archivesName = "PSX2_${versionCode}_${new Date().format('yyyyMMddHHmm')}"
+7 -1
View File
@@ -28,6 +28,7 @@
#include "SIO/Pad/PadDualshock2.h"
#include "MTGS.h"
#include "SDL3/SDL.h"
#include <algorithm>
#include <future>
#ifdef __ANDROID__
#include "SDL3/SDL.h"
@@ -400,7 +401,12 @@ Java_com_izzy2lost_psx2_NativeApp_setPadButton(JNIEnv *env, jclass clazz,
default: _key = PadDualshock2::Inputs::PAD_CROSS ; break;
}
Pad::SetControllerState(0, static_cast<u32>(_key), p_keyPressed ? 1.0 : 0.0);
float value = p_keyPressed ? 1.0f : 0.0f;
if (p_keyPressed && p_range > 0) {
const float denom = (p_range > 255) ? 32766.0f : 255.0f;
value = std::clamp(static_cast<float>(p_range) / denom, 0.0f, 1.0f);
}
Pad::SetControllerState(0, static_cast<u32>(_key), value);
}
extern "C" JNIEXPORT void JNICALL
@@ -19,6 +19,7 @@ public class JoystickView extends View {
private final Paint knobPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
private float centerX, centerY, radius, knobX, knobY, knobRadius;
private boolean isDragging = false;
private int activePointerId = MotionEvent.INVALID_POINTER_ID;
private OnMoveListener listener;
public JoystickView(Context ctx) { super(ctx); init(); }
@@ -72,36 +73,44 @@ public class JoystickView extends View {
@Override
public boolean onTouchEvent(MotionEvent event) {
final int action = event.getActionMasked();
final int actionIndex = event.getActionIndex();
switch (action) {
case MotionEvent.ACTION_DOWN:
case MotionEvent.ACTION_POINTER_DOWN:
isDragging = true;
activePointerId = event.getPointerId(0);
// fallthrough to move
case MotionEvent.ACTION_MOVE:
if (isDragging) {
float dx = event.getX() - centerX;
float dy = event.getY() - centerY;
// Clamp to circle
float dist = (float)Math.hypot(dx, dy);
if (dist > radius) {
float scale = radius / dist;
dx *= scale;
dy *= scale;
}
knobX = centerX + dx;
knobY = centerY + dy;
invalidate();
if (listener != null) {
// Normalize to [-1,1], invert Y so up is negative value (screen y grows down)
float nx = dx / radius;
float ny = dy / radius;
listener.onMove(clamp(nx), clamp(ny), MotionEvent.ACTION_MOVE);
int pointerIndex = 0;
if (activePointerId != MotionEvent.INVALID_POINTER_ID) {
int idx = event.findPointerIndex(activePointerId);
if (idx >= 0) pointerIndex = idx;
}
updateFromPointer(event, pointerIndex);
}
return true;
case MotionEvent.ACTION_POINTER_DOWN:
// Ignore additional pointers while dragging; if we aren't dragging, adopt the new pointer.
if (!isDragging) {
isDragging = true;
activePointerId = event.getPointerId(actionIndex);
updateFromPointer(event, actionIndex);
return true;
}
return true;
case MotionEvent.ACTION_POINTER_UP:
// If the active pointer goes up, release.
if (event.getPointerId(actionIndex) == activePointerId) {
activePointerId = MotionEvent.INVALID_POINTER_ID;
isDragging = false;
resetKnob();
if (listener != null) listener.onMove(0f, 0f, MotionEvent.ACTION_UP);
}
return true;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
isDragging = false;
activePointerId = MotionEvent.INVALID_POINTER_ID;
resetKnob();
if (listener != null) listener.onMove(0f, 0f, MotionEvent.ACTION_UP);
return true;
@@ -109,6 +118,26 @@ public class JoystickView extends View {
return super.onTouchEvent(event);
}
private void updateFromPointer(MotionEvent event, int pointerIndex) {
float dx = event.getX(pointerIndex) - centerX;
float dy = event.getY(pointerIndex) - centerY;
// Clamp to circle
float dist = (float) Math.hypot(dx, dy);
if (dist > radius) {
float scale = radius / dist;
dx *= scale;
dy *= scale;
}
knobX = centerX + dx;
knobY = centerY + dy;
invalidate();
if (listener != null) {
float nx = dx / radius;
float ny = dy / radius;
listener.onMove(clamp(nx), clamp(ny), MotionEvent.ACTION_MOVE);
}
}
private static float clamp(float v) { return Math.max(-1f, Math.min(1f, v)); }
private float dp(float d) {
@@ -76,6 +76,8 @@ public class MainActivity extends AppCompatActivity implements GamesCoverDialogF
public static final int ORIENTATION_LANDSCAPE = 1;
public static final int ORIENTATION_PORTRAIT = 2;
private static final String PREF_TOUCH_RIGHT_STICK = "touch_right_stick";
private String m_szGamefile = "";
private boolean mRaLoginPromptScheduled = false;
@@ -91,6 +93,10 @@ public class MainActivity extends AppCompatActivity implements GamesCoverDialogF
private boolean joyDownPressed = false;
private boolean joyLeftPressed = false;
private boolean joyRightPressed = false;
private boolean joyRUpPressed = false;
private boolean joyRDownPressed = false;
private boolean joyRLeftPressed = false;
private boolean joyRRightPressed = false;
private boolean controllerUiApplied = false;
private AlertDialog mBiosPromptDialog = null;
private boolean mControllerHintShowing = false;
@@ -135,6 +141,7 @@ public class MainActivity extends AppCompatActivity implements GamesCoverDialogF
View btnSettings = findViewById(R.id.btn_settings);
// Visibility toggle removed; no dependency on it for constraints
View llJoy = findViewById(R.id.ll_pad_joy);
View llRJoy = findViewById(R.id.ll_pad_rjoy);
View llDpad = findViewById(R.id.ll_pad_dpad);
View llRight = findViewById(R.id.ll_pad_right_buttons);
View llSelectStart = findViewById(R.id.ll_pad_select_start);
@@ -182,6 +189,31 @@ public class MainActivity extends AppCompatActivity implements GamesCoverDialogF
llJoy.setTranslationX(-dp(28));
}
if (llRJoy != null) {
ConstraintLayout.LayoutParams lp = safeCLP(llRJoy);
lp.startToStart = ConstraintLayout.LayoutParams.UNSET;
lp.endToEnd = ConstraintLayout.LayoutParams.UNSET;
lp.endToStart = ConstraintLayout.LayoutParams.UNSET;
lp.topToTop = ConstraintLayout.LayoutParams.UNSET;
lp.topToBottom = ConstraintLayout.LayoutParams.UNSET;
lp.bottomToBottom = ConstraintLayout.LayoutParams.UNSET;
lp.bottomToTop = ConstraintLayout.LayoutParams.UNSET;
if (orientation == Configuration.ORIENTATION_LANDSCAPE) {
lp.bottomToBottom = ConstraintLayout.LayoutParams.PARENT_ID;
if (llRight != null) lp.endToStart = llRight.getId();
else lp.endToEnd = ConstraintLayout.LayoutParams.PARENT_ID;
lp.setMargins(dp(6), dp(6), dp(6), dp(6));
} else {
lp.endToEnd = ConstraintLayout.LayoutParams.PARENT_ID;
if (llRight != null) lp.bottomToTop = llRight.getId();
else if (llSelectStart != null) lp.bottomToTop = llSelectStart.getId();
else lp.bottomToBottom = ConstraintLayout.LayoutParams.PARENT_ID;
lp.setMargins(dp(0), dp(0), dp(12), dp(6));
}
llRJoy.setLayoutParams(lp);
}
if (llDpad != null && llJoy != null) {
ConstraintLayout.LayoutParams lp = safeCLP(llDpad);
// Above-left of joystick for both, with slight spacing
@@ -886,6 +918,44 @@ public class MainActivity extends AppCompatActivity implements GamesCoverDialogF
});
}
// Optional Right JoystickView (right analog stick)
View joystickRight = findViewById(R.id.joystick_view_right);
if (joystickRight instanceof JoystickView) {
JoystickView jv = (JoystickView) joystickRight;
jv.setOnMoveListener((nx, ny, action) -> {
final float T = 0.3f;
boolean up = ny < -T;
boolean down = ny > T;
boolean left = nx < -T;
boolean right = nx > T;
if (up != joyRUpPressed) {
sendKeyAction(jv, up ? MotionEvent.ACTION_DOWN : MotionEvent.ACTION_UP, PAD_R_UP);
joyRUpPressed = up;
}
if (down != joyRDownPressed) {
sendKeyAction(jv, down ? MotionEvent.ACTION_DOWN : MotionEvent.ACTION_UP, PAD_R_DOWN);
joyRDownPressed = down;
}
if (left != joyRLeftPressed) {
sendKeyAction(jv, left ? MotionEvent.ACTION_DOWN : MotionEvent.ACTION_UP, PAD_R_LEFT);
joyRLeftPressed = left;
}
if (right != joyRRightPressed) {
sendKeyAction(jv, right ? MotionEvent.ACTION_DOWN : MotionEvent.ACTION_UP, PAD_R_RIGHT);
joyRRightPressed = right;
}
if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) {
if (joyRUpPressed) sendKeyAction(jv, MotionEvent.ACTION_UP, PAD_R_UP);
if (joyRDownPressed) sendKeyAction(jv, MotionEvent.ACTION_UP, PAD_R_DOWN);
if (joyRLeftPressed) sendKeyAction(jv, MotionEvent.ACTION_UP, PAD_R_LEFT);
if (joyRRightPressed) sendKeyAction(jv, MotionEvent.ACTION_UP, PAD_R_RIGHT);
joyRUpPressed = joyRDownPressed = joyRLeftPressed = joyRRightPressed = false;
}
});
}
//// D-Pad buttons
MaterialButton btn_pad_dir_top = findViewById(R.id.btn_pad_dir_top);
if(btn_pad_dir_top != null) {
@@ -920,16 +990,48 @@ public class MainActivity extends AppCompatActivity implements GamesCoverDialogF
// Visibility toggle removed; no global hidden state flag
private void setControlsVisible(boolean visible) {
if (!visible) {
releaseVirtualStickInputs();
}
int vis = visible ? View.VISIBLE : View.GONE;
View llDpad = findViewById(R.id.ll_pad_dpad);
View llRight = findViewById(R.id.ll_pad_right_buttons);
View llSelectStart = findViewById(R.id.ll_pad_select_start);
View llJoy = findViewById(R.id.ll_pad_joy);
View llRJoy = findViewById(R.id.ll_pad_rjoy);
if (llDpad != null) llDpad.setVisibility(vis);
if (llRight != null) llRight.setVisibility(vis);
if (llSelectStart != null) llSelectStart.setVisibility(vis);
if (llJoy != null) llJoy.setVisibility(vis);
if (llRJoy != null) {
boolean show = visible && getSharedPreferences("app_prefs", MODE_PRIVATE).getBoolean(PREF_TOUCH_RIGHT_STICK, false);
llRJoy.setVisibility(show ? View.VISIBLE : View.GONE);
}
}
private void releaseVirtualStickInputs() {
// Left stick
if (joyUpPressed || joyDownPressed || joyLeftPressed || joyRightPressed) {
try { NativeApp.setPadButton(ControllerInputHandler.PAD_L_UP, 0, false); } catch (Throwable ignored) {}
try { NativeApp.setPadButton(ControllerInputHandler.PAD_L_DOWN, 0, false); } catch (Throwable ignored) {}
try { NativeApp.setPadButton(ControllerInputHandler.PAD_L_LEFT, 0, false); } catch (Throwable ignored) {}
try { NativeApp.setPadButton(ControllerInputHandler.PAD_L_RIGHT, 0, false); } catch (Throwable ignored) {}
joyUpPressed = joyDownPressed = joyLeftPressed = joyRightPressed = false;
}
// Right stick
releaseVirtualRightStickInputs();
}
private void releaseVirtualRightStickInputs() {
if (joyRUpPressed || joyRDownPressed || joyRLeftPressed || joyRRightPressed) {
try { NativeApp.setPadButton(ControllerInputHandler.PAD_R_UP, 0, false); } catch (Throwable ignored) {}
try { NativeApp.setPadButton(ControllerInputHandler.PAD_R_DOWN, 0, false); } catch (Throwable ignored) {}
try { NativeApp.setPadButton(ControllerInputHandler.PAD_R_LEFT, 0, false); } catch (Throwable ignored) {}
try { NativeApp.setPadButton(ControllerInputHandler.PAD_R_RIGHT, 0, false); } catch (Throwable ignored) {}
joyRUpPressed = joyRDownPressed = joyRLeftPressed = joyRRightPressed = false;
}
}
// Removed visibility toggle function
@@ -2183,6 +2285,17 @@ public class MainActivity extends AppCompatActivity implements GamesCoverDialogF
try { NativeApp.setHudVisible(isChecked); } catch (Throwable ignored) {}
});
}
// Touch right stick joystick (optional on-screen control)
com.google.android.material.materialswitch.MaterialSwitch swTouchRightStick = header.findViewById(R.id.drawer_sw_touch_right_stick);
if (swTouchRightStick != null && swTouchRightStick.getTag() == null) {
swTouchRightStick.setTag("setup");
swTouchRightStick.setOnCheckedChangeListener((buttonView, isChecked) -> {
prefs.edit().putBoolean(PREF_TOUCH_RIGHT_STICK, isChecked).apply();
if (!isChecked) releaseVirtualRightStickInputs();
try { updateUiForControllerPresence(); } catch (Throwable ignored) {}
});
}
}
private void refreshDrawerSettings() {
@@ -2326,6 +2439,15 @@ public class MainActivity extends AppCompatActivity implements GamesCoverDialogF
} catch (Exception e) {
android.util.Log.e("MainActivity", "Error refreshing dev HUD switch: " + e.getMessage());
}
try {
com.google.android.material.materialswitch.MaterialSwitch swTouchRightStick = header.findViewById(R.id.drawer_sw_touch_right_stick);
if (swTouchRightStick != null) {
swTouchRightStick.setChecked(prefs.getBoolean(PREF_TOUCH_RIGHT_STICK, false));
}
} catch (Exception e) {
android.util.Log.e("MainActivity", "Error refreshing touch right stick switch: " + e.getMessage());
}
} catch (Throwable t) {
android.util.Log.e("MainActivity", "Unexpected error in refreshDrawerSettings: " + t.getMessage());
@@ -2826,6 +2948,9 @@ public class MainActivity extends AppCompatActivity implements GamesCoverDialogF
private void hideGamepadControls() {
View joy = findViewById(R.id.ll_pad_joy);
if (joy != null) joy.setVisibility(View.GONE);
View rjoy = findViewById(R.id.ll_pad_rjoy);
if (rjoy != null) rjoy.setVisibility(View.GONE);
View dpad = findViewById(R.id.ll_pad_dpad);
if (dpad != null) dpad.setVisibility(View.GONE);
+41 -21
View File
@@ -9,6 +9,7 @@
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:splitMotionEvents="true"
android:keepScreenOn="true"
tools:context=".MainActivity">
@@ -119,29 +120,48 @@
</LinearLayout>
<!-- Left-bottom Joystick -->
<LinearLayout
android:id="@+id/ll_pad_joy"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:orientation="vertical"
android:visibility="visible"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
android:layout_margin="6dp">
<!-- Left-bottom Joystick -->
<LinearLayout
android:id="@+id/ll_pad_joy"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:orientation="vertical"
android:visibility="visible"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
android:layout_margin="6dp">
<com.izzy2lost.psx2.JoystickView
android:id="@+id/joystick_view"
android:layout_width="180dp"
android:layout_height="180dp"
android:layout_margin="2dp" />
</LinearLayout>
<com.izzy2lost.psx2.JoystickView
android:id="@+id/joystick_view"
android:layout_width="180dp"
android:layout_height="180dp"
android:layout_margin="2dp" />
</LinearLayout>
<!-- Optional right stick joystick (disabled by default) -->
<LinearLayout
android:id="@+id/ll_pad_rjoy"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:orientation="vertical"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@id/ll_pad_right_buttons"
android:layout_margin="6dp">
<!-- Right-bottom face and shoulder buttons -->
<LinearLayout
android:id="@+id/ll_pad_right_buttons"
android:layout_width="wrap_content"
<com.izzy2lost.psx2.JoystickView
android:id="@+id/joystick_view_right"
android:layout_width="160dp"
android:layout_height="160dp"
android:layout_margin="2dp" />
</LinearLayout>
<!-- Right-bottom face and shoulder buttons -->
<LinearLayout
android:id="@+id/ll_pad_right_buttons"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:orientation="vertical"
@@ -9,6 +9,7 @@
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:splitMotionEvents="true"
android:keepScreenOn="true"
tools:context=".MainActivity">
@@ -293,6 +294,26 @@
android:layout_margin="2dp" />
</LinearLayout>
<!-- Optional right stick joystick (disabled by default) -->
<LinearLayout
android:id="@+id/ll_pad_rjoy"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:orientation="vertical"
android:visibility="gone"
app:layout_constraintBottom_toTopOf="@id/ll_pad_right_buttons"
app:layout_constraintEnd_toEndOf="parent"
android:layout_marginEnd="12dp"
android:layout_marginBottom="6dp">
<com.izzy2lost.psx2.JoystickView
android:id="@+id/joystick_view_right"
android:layout_width="130dp"
android:layout_height="130dp"
android:layout_margin="2dp" />
</LinearLayout>
<LinearLayout
android:id="@+id/ll_pad_dpad"
android:layout_width="wrap_content"
+21
View File
@@ -9,6 +9,7 @@
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:splitMotionEvents="true"
android:keepScreenOn="true"
tools:context=".MainActivity">
@@ -293,6 +294,26 @@
android:layout_margin="2dp" />
</LinearLayout>
<!-- Optional right stick joystick (disabled by default) -->
<LinearLayout
android:id="@+id/ll_pad_rjoy"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:orientation="vertical"
android:visibility="gone"
app:layout_constraintBottom_toTopOf="@id/ll_pad_right_buttons"
app:layout_constraintEnd_toEndOf="parent"
android:layout_marginEnd="12dp"
android:layout_marginBottom="6dp">
<com.izzy2lost.psx2.JoystickView
android:id="@+id/joystick_view_right"
android:layout_width="130dp"
android:layout_height="130dp"
android:layout_margin="2dp" />
</LinearLayout>
<LinearLayout
android:id="@+id/ll_pad_dpad"
android:layout_width="wrap_content"
@@ -346,6 +346,14 @@
android:textColor="@color/brand_primary"
android:paddingBottom="8dp" />
<com.google.android.material.materialswitch.MaterialSwitch
android:id="@+id/drawer_sw_touch_right_stick"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Touch: Right Stick Joystick"
android:textColor="@android:color/white"
android:layout_marginBottom="8dp"/>
<com.google.android.material.button.MaterialButton
android:id="@+id/drawer_btn_test_controller"
style="@style/Widget.Material3Expressive.Button.OutlinedButton"