Added 6 different Virtual Controller Presets

Also make the "Use Switch Controller" toggle have an effect on virtual Controllers, too.
This commit is contained in:
BeZide93 2025-10-28 10:34:57 +01:00
parent 78db4c365f
commit fba336f3af
11 changed files with 2094 additions and 27 deletions

View file

@ -37,7 +37,7 @@ typealias GamePadConfig = RadialGamePadConfig
private const val DUMMY_LEFT_STICK_PRESS_ID = 10001
private const val DUMMY_RIGHT_STICK_PRESS_ID = 10002
class GameController(var activity: Activity) {
class GameController(var activity: Activity) : IGameController {
companion object {
private fun init(context: Context, controller: GameController): View {
@ -80,12 +80,13 @@ class GameController(var activity: Activity) {
var leftGamePad: GamePad
var rightGamePad: GamePad
var controllerId: Int = -1
val isVisible: Boolean
override val isVisible: Boolean
get() = controllerView?.isVisible ?: false
init {
leftGamePad = GamePad(generateConfig(true), 16f, activity)
rightGamePad = GamePad(generateConfig(false), 16f, activity)
val useSwitchLayout = QuickSettings(activity).useSwitchLayout
leftGamePad = GamePad(generateConfig(true, useSwitchLayout), 16f, activity)
rightGamePad = GamePad(generateConfig(false, useSwitchLayout), 16f, activity)
leftGamePad.primaryDialMaxSizeDp = 200f
rightGamePad.primaryDialMaxSizeDp = 200f
@ -96,14 +97,14 @@ class GameController(var activity: Activity) {
rightGamePad.gravityY = 1f
}
fun setVisible(isVisible: Boolean) {
override fun setVisible(isVisible: Boolean) {
controllerView?.apply {
this.isVisible = isVisible
if (isVisible) connect()
}
}
fun connect() {
override fun connect() {
if (controllerId == -1)
controllerId = KenjinxNative.inputConnectGamepad(0)
}
@ -177,7 +178,7 @@ suspend fun <T> Flow<T>.safeCollect(block: suspend (T) -> Unit) {
.collect { block(it) }
}
private fun generateConfig(isLeft: Boolean): GamePadConfig {
private fun generateConfig(isLeft: Boolean, useSwitchLayout: Boolean): GamePadConfig {
val distance = 0.3f
val buttonScale = 1f
@ -293,20 +294,23 @@ private fun generateConfig(isLeft: Boolean): GamePadConfig {
/* ringSegments = */ 12,
/* Primary (ABXY) */
PrimaryDialConfig.PrimaryButtons(
listOf(
ButtonConfig(
GamePadButtonInputId.A.ordinal, "A", true, null, "A", setOf(), true, null
),
ButtonConfig(
GamePadButtonInputId.X.ordinal, "X", true, null, "X", setOf(), true, null
),
ButtonConfig(
GamePadButtonInputId.Y.ordinal, "Y", true, null, "Y", setOf(), true, null
),
ButtonConfig(
GamePadButtonInputId.B.ordinal, "B", true, null, "B", setOf(), true, null
if (useSwitchLayout) {
// Switch/Nintendo: A=Right, X=Top, Y=Left, B=Bottom
listOf(
ButtonConfig(GamePadButtonInputId.A.ordinal, "A", true, null, "A", setOf(), true, null), // Right
ButtonConfig(GamePadButtonInputId.X.ordinal, "X", true, null, "X", setOf(), true, null), // Top
ButtonConfig(GamePadButtonInputId.Y.ordinal, "Y", true, null, "Y", setOf(), true, null), // Left
ButtonConfig(GamePadButtonInputId.B.ordinal, "B", true, null, "B", setOf(), true, null) // Bottom
)
),
} else {
// Xbox-Stil: B=Right, Y=Top, X=Left, A=Bottom
listOf(
ButtonConfig(GamePadButtonInputId.B.ordinal, "B", true, null, "B", setOf(), true, null), // Right
ButtonConfig(GamePadButtonInputId.Y.ordinal, "Y", true, null, "Y", setOf(), true, null), // Top
ButtonConfig(GamePadButtonInputId.X.ordinal, "X", true, null, "X", setOf(), true, null), // Left
ButtonConfig(GamePadButtonInputId.A.ordinal, "A", true, null, "A", setOf(), true, null) // Bottom
)
},
null,
0f,
true,

View file

@ -0,0 +1,403 @@
package org.kenjinx.android
import android.app.Activity
import android.content.Context
import android.view.KeyEvent
import android.view.LayoutInflater
import android.view.View
import android.widget.FrameLayout
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.viewinterop.AndroidView
import androidx.core.math.MathUtils
import androidx.core.view.isVisible
import androidx.lifecycle.lifecycleScope
import com.swordfish.radialgamepad.library.RadialGamePad
import com.swordfish.radialgamepad.library.config.ButtonConfig
import com.swordfish.radialgamepad.library.config.CrossConfig
import com.swordfish.radialgamepad.library.config.CrossContentDescription
import com.swordfish.radialgamepad.library.config.PrimaryDialConfig
import com.swordfish.radialgamepad.library.config.RadialGamePadConfig
import com.swordfish.radialgamepad.library.config.SecondaryDialConfig
import com.swordfish.radialgamepad.library.event.Event
import kotlinx.coroutines.flow.merge
import kotlinx.coroutines.launch
import org.kenjinx.android.viewmodels.MainViewModel
import org.kenjinx.android.viewmodels.QuickSettings
// --- Dummy IDs to disconnect legacy L3/R3 (stick double tap/tap) ---
private const val DUMMY_LEFT_STICK_PRESS_ID = 10001
private const val DUMMY_RIGHT_STICK_PRESS_ID = 10002
/**
* GameController2
* Layout 2 aktuell identisch zum Default-Layout (GameController),
* als eigenständige Klasse für Preset-Umschaltung.
*/
class GameController2(var activity: Activity) : IGameController {
companion object {
private fun init(context: Context, controller: GameController2): View {
val inflater = LayoutInflater.from(context)
val parent = FrameLayout(context)
val view = inflater.inflate(R.layout.game_layout, parent, false)
view.findViewById<FrameLayout>(R.id.leftcontainer)!!.addView(controller.leftGamePad)
view.findViewById<FrameLayout>(R.id.rightcontainer)!!.addView(controller.rightGamePad)
return view
}
@Composable
fun Compose(viewModel: MainViewModel) {
AndroidView(
modifier = Modifier.fillMaxSize(),
factory = { context ->
val controller = GameController2(viewModel.activity)
val c = init(context, controller)
viewModel.activity.lifecycleScope.launch {
val events = merge(
controller.leftGamePad.events(),
controller.rightGamePad.events()
)
// safeCollect kommt aus GameController.kt (bitte dort belassen)
events.safeCollect { controller.handleEvent(it) }
}
controller.controllerView = c
viewModel.setGameController(controller)
controller.setVisible(QuickSettings(viewModel.activity).useVirtualController)
c
}
)
}
}
private var controllerView: View? = null
var leftGamePad: RadialGamePad
var rightGamePad: RadialGamePad
var controllerId: Int = -1
override val isVisible: Boolean
get() = controllerView?.isVisible ?: false
init {
val useSwitchLayout = QuickSettings(activity).useSwitchLayout
leftGamePad = RadialGamePad(generateConfig2(true, useSwitchLayout), 16f, activity)
rightGamePad = RadialGamePad(generateConfig2(false, useSwitchLayout), 16f, activity)
leftGamePad.primaryDialMaxSizeDp = 200f
rightGamePad.primaryDialMaxSizeDp = 200f
leftGamePad.gravityX = -1f
leftGamePad.gravityY = 1f
rightGamePad.gravityX = 1f
rightGamePad.gravityY = 1f
}
override fun setVisible(isVisible: Boolean) {
controllerView?.apply {
this.isVisible = isVisible
if (isVisible) connect()
}
}
override fun connect() {
if (controllerId == -1)
controllerId = KenjinxNative.inputConnectGamepad(0)
}
private fun handleEvent(ev: Event) {
if (controllerId == -1)
controllerId = KenjinxNative.inputConnectGamepad(0)
controllerId.apply {
when (ev) {
is Event.Button -> {
// Ignore legacy L3/R3 via stick press (double tap)
if (ev.id == DUMMY_LEFT_STICK_PRESS_ID || ev.id == DUMMY_RIGHT_STICK_PRESS_ID) {
return
}
when (ev.action) {
KeyEvent.ACTION_UP -> KenjinxNative.inputSetButtonReleased(ev.id, this)
KeyEvent.ACTION_DOWN -> KenjinxNative.inputSetButtonPressed(ev.id, this)
}
}
is Event.Direction -> {
when (ev.id) {
GamePadButtonInputId.DpadUp.ordinal -> {
// Horizontal
if (ev.xAxis > 0) {
KenjinxNative.inputSetButtonPressed(GamePadButtonInputId.DpadRight.ordinal, this)
KenjinxNative.inputSetButtonReleased(GamePadButtonInputId.DpadLeft.ordinal, this)
} else if (ev.xAxis < 0) {
KenjinxNative.inputSetButtonPressed(GamePadButtonInputId.DpadLeft.ordinal, this)
KenjinxNative.inputSetButtonReleased(GamePadButtonInputId.DpadRight.ordinal, this)
} else {
KenjinxNative.inputSetButtonReleased(GamePadButtonInputId.DpadLeft.ordinal, this)
KenjinxNative.inputSetButtonReleased(GamePadButtonInputId.DpadRight.ordinal, this)
}
// Vertical
if (ev.yAxis < 0) {
KenjinxNative.inputSetButtonPressed(GamePadButtonInputId.DpadUp.ordinal, this)
KenjinxNative.inputSetButtonReleased(GamePadButtonInputId.DpadDown.ordinal, this)
} else if (ev.yAxis > 0) {
KenjinxNative.inputSetButtonPressed(GamePadButtonInputId.DpadDown.ordinal, this)
KenjinxNative.inputSetButtonReleased(GamePadButtonInputId.DpadUp.ordinal, this)
} else {
KenjinxNative.inputSetButtonReleased(GamePadButtonInputId.DpadDown.ordinal, this)
KenjinxNative.inputSetButtonReleased(GamePadButtonInputId.DpadUp.ordinal, this)
}
}
GamePadButtonInputId.LeftStick.ordinal -> {
val setting = QuickSettings(activity)
val x = MathUtils.clamp(ev.xAxis * setting.controllerStickSensitivity, -1f, 1f)
val y = MathUtils.clamp(ev.yAxis * setting.controllerStickSensitivity, -1f, 1f)
KenjinxNative.inputSetStickAxis(1, x, -y, this)
}
GamePadButtonInputId.RightStick.ordinal -> {
val setting = QuickSettings(activity)
val x = MathUtils.clamp(ev.xAxis * setting.controllerStickSensitivity, -1f, 1f)
val y = MathUtils.clamp(ev.yAxis * setting.controllerStickSensitivity, -1f, 1f)
KenjinxNative.inputSetStickAxis(2, x, -y, this)
}
}
}
}
}
}
}
// --- Lokale Kopie der Config-Erzeugung (Name = generateConfig2, um Konflikte zu vermeiden) ---
private fun generateConfig2(isLeft: Boolean, useSwitchLayout: Boolean): RadialGamePadConfig {
val distance = 0.3f
val buttonScale = 1f
if (isLeft) {
return RadialGamePadConfig(
/* ringSegments = */ 12,
/* Primary (Stick) */
// IMPORTANT: pressButtonId -> DUMMY_LEFT_STICK_PRESS_ID, so that double tap does not trigger L3
PrimaryDialConfig.Stick(
GamePadButtonInputId.LeftStick.ordinal,
DUMMY_LEFT_STICK_PRESS_ID,
setOf(),
"LeftStick",
null
),
listOf(
// D-Pad
SecondaryDialConfig.Cross(
/* sector */ 10,
/* size */ 3,
/* gap */ 2.1f,
distance,
CrossConfig(
GamePadButtonInputId.DpadUp.ordinal,
CrossConfig.Shape.STANDARD,
null,
setOf(),
CrossContentDescription(),
true,
null
),
SecondaryDialConfig.RotationProcessor()
),
// Minus
SecondaryDialConfig.SingleButton(
/* sector */ 9,
buttonScale,
0.3f,
ButtonConfig(
GamePadButtonInputId.Minus.ordinal,
"-",
true,
null,
"Minus",
setOf(),
true,
null
),
null,
SecondaryDialConfig.RotationProcessor()
),
// L-Bumper
SecondaryDialConfig.DoubleButton(
/* sector */ 2,
0.2f,
ButtonConfig(
GamePadButtonInputId.LeftShoulder.ordinal,
"L",
true,
null,
"LeftBumper",
setOf(),
true,
null
),
null,
SecondaryDialConfig.RotationProcessor()
),
// ZL-Trigger
SecondaryDialConfig.DoubleButton(
/* sector */ 2,
1f,
ButtonConfig(
GamePadButtonInputId.LeftTrigger.ordinal,
"ZL",
true,
null,
"LeftTrigger",
setOf(),
true,
null
),
null,
SecondaryDialConfig.RotationProcessor()
),
// L3 separat
SecondaryDialConfig.SingleButton(
/* sector */ 1,
buttonScale,
1.0f,
ButtonConfig(
GamePadButtonInputId.LeftStickButton.ordinal,
"L3",
true,
null,
"LeftStickButton",
setOf(),
true,
null
),
null,
SecondaryDialConfig.RotationProcessor()
),
)
)
} else {
return RadialGamePadConfig(
/* ringSegments = */ 12,
/* Primary (ABXY) */
PrimaryDialConfig.PrimaryButtons(
if (useSwitchLayout) {
// Switch/Nintendo: A=Right, X=Top, Y=Left, B=Bottom
listOf(
ButtonConfig(GamePadButtonInputId.A.ordinal, "A", true, null, "A", setOf(), true, null), // Right
ButtonConfig(GamePadButtonInputId.X.ordinal, "X", true, null, "X", setOf(), true, null), // Top
ButtonConfig(GamePadButtonInputId.Y.ordinal, "Y", true, null, "Y", setOf(), true, null), // Left
ButtonConfig(GamePadButtonInputId.B.ordinal, "B", true, null, "B", setOf(), true, null) // Bottom
)
} else {
// Xbox-Stil: B=Right, Y=Top, X=Left, A=Bottom
listOf(
ButtonConfig(GamePadButtonInputId.B.ordinal, "B", true, null, "B", setOf(), true, null), // Right
ButtonConfig(GamePadButtonInputId.Y.ordinal, "Y", true, null, "Y", setOf(), true, null), // Top
ButtonConfig(GamePadButtonInputId.X.ordinal, "X", true, null, "X", setOf(), true, null), // Left
ButtonConfig(GamePadButtonInputId.A.ordinal, "A", true, null, "A", setOf(), true, null) // Bottom
)
},
null,
0f,
true,
null
),
listOf(
// Right stick
// IMPORTANT: pressButtonId -> DUMMY_RIGHT_STICK_PRESS_ID, so that double tap does not trigger R3
SecondaryDialConfig.Stick(
/* sector */ 6,
/* size */ 3,
/* gap */ 2.7f,
distance,
GamePadButtonInputId.RightStick.ordinal,
DUMMY_RIGHT_STICK_PRESS_ID,
null,
setOf(),
"RightStick",
SecondaryDialConfig.RotationProcessor()
),
// Plus
SecondaryDialConfig.SingleButton(
/* sector */ 9,
buttonScale,
0.3f,
ButtonConfig(
GamePadButtonInputId.Plus.ordinal,
"+",
true,
null,
"Plus",
setOf(),
true,
null
),
null,
SecondaryDialConfig.RotationProcessor()
),
// R-Bumper
SecondaryDialConfig.DoubleButton(
/* sector */ 3,
0.2f,
ButtonConfig(
GamePadButtonInputId.RightShoulder.ordinal,
"R",
true,
null,
"RightBumper",
setOf(),
true,
null
),
null,
SecondaryDialConfig.RotationProcessor()
),
// ZR-Trigger
SecondaryDialConfig.DoubleButton(
/* sector */ 3,
1f,
ButtonConfig(
GamePadButtonInputId.RightTrigger.ordinal,
"ZR",
true,
null,
"RightTrigger",
setOf(),
true,
null
),
null,
SecondaryDialConfig.RotationProcessor()
),
// R3 separat
SecondaryDialConfig.SingleButton(
/* sector */ 5,
buttonScale,
1.0f,
ButtonConfig(
GamePadButtonInputId.RightStickButton.ordinal,
"R3",
true,
null,
"RightStickButton",
setOf(),
true,
null
),
null,
SecondaryDialConfig.RotationProcessor()
),
)
)
}
}

View file

@ -0,0 +1,396 @@
// src/KenjinxAndroid/app/src/main/java/org/kenjinx/android/GameController3.kt
package org.kenjinx.android
import android.app.Activity
import android.content.Context
import android.view.KeyEvent
import android.view.LayoutInflater
import android.view.View
import android.widget.FrameLayout
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.viewinterop.AndroidView
import androidx.core.math.MathUtils
import androidx.core.view.isVisible
import androidx.lifecycle.lifecycleScope
import com.swordfish.radialgamepad.library.RadialGamePad
import com.swordfish.radialgamepad.library.config.ButtonConfig
import com.swordfish.radialgamepad.library.config.CrossConfig
import com.swordfish.radialgamepad.library.config.CrossContentDescription
import com.swordfish.radialgamepad.library.config.PrimaryDialConfig
import com.swordfish.radialgamepad.library.config.RadialGamePadConfig
import com.swordfish.radialgamepad.library.config.SecondaryDialConfig
import com.swordfish.radialgamepad.library.event.Event
import kotlinx.coroutines.flow.merge
import kotlinx.coroutines.launch
import org.kenjinx.android.viewmodels.MainViewModel
import org.kenjinx.android.viewmodels.QuickSettings
// --- Dummy IDs to disconnect legacy L3/R3 (stick double tap/tap) ---
private const val DUMMY_LEFT_STICK_PRESS_ID = 10001
private const val DUMMY_RIGHT_STICK_PRESS_ID = 10002
/**
* GameController3
* Layout 3 aktuell identisch zum Default-Layout, als eigenständige Klasse.
*/
class GameController3(var activity: Activity) : IGameController {
companion object {
private fun init(context: Context, controller: GameController3): View {
val inflater = LayoutInflater.from(context)
val parent = FrameLayout(context)
val view = inflater.inflate(R.layout.game_layout, parent, false)
view.findViewById<FrameLayout>(R.id.leftcontainer)!!.addView(controller.leftGamePad)
view.findViewById<FrameLayout>(R.id.rightcontainer)!!.addView(controller.rightGamePad)
return view
}
@Composable
fun Compose(viewModel: MainViewModel) {
AndroidView(
modifier = Modifier.fillMaxSize(),
factory = { context ->
val controller = GameController3(viewModel.activity)
val c = init(context, controller)
viewModel.activity.lifecycleScope.launch {
val events = merge(
controller.leftGamePad.events(),
controller.rightGamePad.events()
)
events.safeCollect { controller.handleEvent(it) }
}
controller.controllerView = c
viewModel.setGameController(controller)
controller.setVisible(QuickSettings(viewModel.activity).useVirtualController)
c
}
)
}
}
private var controllerView: View? = null
var leftGamePad: RadialGamePad
var rightGamePad: RadialGamePad
var controllerId: Int = -1
override val isVisible: Boolean
get() = controllerView?.isVisible ?: false
init {
val useSwitchLayout = QuickSettings(activity).useSwitchLayout
leftGamePad = RadialGamePad(generateConfig3(true, useSwitchLayout), 16f, activity)
rightGamePad = RadialGamePad(generateConfig3(false, useSwitchLayout), 16f, activity)
leftGamePad.primaryDialMaxSizeDp = 200f
rightGamePad.primaryDialMaxSizeDp = 200f
leftGamePad.gravityX = -1f
leftGamePad.gravityY = 1f
rightGamePad.gravityX = 1f
rightGamePad.gravityY = 1f
}
override fun setVisible(isVisible: Boolean) {
controllerView?.apply {
this.isVisible = isVisible
if (isVisible) connect()
}
}
override fun connect() {
if (controllerId == -1)
controllerId = KenjinxNative.inputConnectGamepad(0)
}
private fun handleEvent(ev: Event) {
if (controllerId == -1)
controllerId = KenjinxNative.inputConnectGamepad(0)
controllerId.apply {
when (ev) {
is Event.Button -> {
// Ignore legacy L3/R3 via stick press (double tap)
if (ev.id == DUMMY_LEFT_STICK_PRESS_ID || ev.id == DUMMY_RIGHT_STICK_PRESS_ID) return
when (ev.action) {
KeyEvent.ACTION_UP -> KenjinxNative.inputSetButtonReleased(ev.id, this)
KeyEvent.ACTION_DOWN -> KenjinxNative.inputSetButtonPressed(ev.id, this)
}
}
is Event.Direction -> {
when (ev.id) {
GamePadButtonInputId.DpadUp.ordinal -> {
// Horizontal
if (ev.xAxis > 0) {
KenjinxNative.inputSetButtonPressed(GamePadButtonInputId.DpadRight.ordinal, this)
KenjinxNative.inputSetButtonReleased(GamePadButtonInputId.DpadLeft.ordinal, this)
} else if (ev.xAxis < 0) {
KenjinxNative.inputSetButtonPressed(GamePadButtonInputId.DpadLeft.ordinal, this)
KenjinxNative.inputSetButtonReleased(GamePadButtonInputId.DpadRight.ordinal, this)
} else {
KenjinxNative.inputSetButtonReleased(GamePadButtonInputId.DpadLeft.ordinal, this)
KenjinxNative.inputSetButtonReleased(GamePadButtonInputId.DpadRight.ordinal, this)
}
// Vertical
if (ev.yAxis < 0) {
KenjinxNative.inputSetButtonPressed(GamePadButtonInputId.DpadUp.ordinal, this)
KenjinxNative.inputSetButtonReleased(GamePadButtonInputId.DpadDown.ordinal, this)
} else if (ev.yAxis > 0) {
KenjinxNative.inputSetButtonPressed(GamePadButtonInputId.DpadDown.ordinal, this)
KenjinxNative.inputSetButtonReleased(GamePadButtonInputId.DpadUp.ordinal, this)
} else {
KenjinxNative.inputSetButtonReleased(GamePadButtonInputId.DpadDown.ordinal, this)
KenjinxNative.inputSetButtonReleased(GamePadButtonInputId.DpadUp.ordinal, this)
}
}
GamePadButtonInputId.LeftStick.ordinal -> {
val setting = QuickSettings(activity)
val x = MathUtils.clamp(ev.xAxis * setting.controllerStickSensitivity, -1f, 1f)
val y = MathUtils.clamp(ev.yAxis * setting.controllerStickSensitivity, -1f, 1f)
KenjinxNative.inputSetStickAxis(1, x, -y, this)
}
GamePadButtonInputId.RightStick.ordinal -> {
val setting = QuickSettings(activity)
val x = MathUtils.clamp(ev.xAxis * setting.controllerStickSensitivity, -1f, 1f)
val y = MathUtils.clamp(ev.yAxis * setting.controllerStickSensitivity, -1f, 1f)
KenjinxNative.inputSetStickAxis(2, x, -y, this)
}
}
}
}
}
}
}
private fun generateConfig3(isLeft: Boolean, useSwitchLayout: Boolean): RadialGamePadConfig {
val distance = 0.3f
val buttonScale = 1f
if (isLeft) {
return RadialGamePadConfig(
/* ringSegments = */ 12,
/* Primary (D-Pad) */
PrimaryDialConfig.Cross(
CrossConfig(
GamePadButtonInputId.DpadUp.ordinal,
CrossConfig.Shape.STANDARD,
null,
setOf(),
CrossContentDescription(),
true,
null
),
),
listOf(
// Left stick
// IMPORTANT: pressButtonId -> DUMMY_LEFT_STICK_PRESS_ID, so that double tap does not trigger L3
SecondaryDialConfig.Stick(
/* sector */ 10,
/* size */ 3,
/* gap */ 2.7f,
distance,
GamePadButtonInputId.LeftStick.ordinal,
DUMMY_LEFT_STICK_PRESS_ID,
null,
setOf(),
"LeftStick",
SecondaryDialConfig.RotationProcessor()
),
// Minus
SecondaryDialConfig.SingleButton(
/* sector */ 9,
buttonScale,
0.3f,
ButtonConfig(
GamePadButtonInputId.Minus.ordinal,
"-",
true,
null,
"Minus",
setOf(),
true,
null
),
null,
SecondaryDialConfig.RotationProcessor()
),
// L-Bumper
SecondaryDialConfig.DoubleButton(
/* sector */ 2,
0.2f,
ButtonConfig(
GamePadButtonInputId.LeftShoulder.ordinal,
"L",
true,
null,
"LeftBumper",
setOf(),
true,
null
),
null,
SecondaryDialConfig.RotationProcessor()
),
// ZL-Trigger
SecondaryDialConfig.DoubleButton(
/* sector */ 2,
1f,
ButtonConfig(
GamePadButtonInputId.LeftTrigger.ordinal,
"ZL",
true,
null,
"LeftTrigger",
setOf(),
true,
null
),
null,
SecondaryDialConfig.RotationProcessor()
),
// L3 separat
SecondaryDialConfig.SingleButton(
/* sector */ 1,
buttonScale,
1.0f,
ButtonConfig(
GamePadButtonInputId.LeftStickButton.ordinal,
"L3",
true,
null,
"LeftStickButton",
setOf(),
true,
null
),
null,
SecondaryDialConfig.RotationProcessor()
),
)
)
} else {
return RadialGamePadConfig(
/* ringSegments = */ 12,
/* Primary (ABXY) */
PrimaryDialConfig.PrimaryButtons(
if (useSwitchLayout) {
// Switch/Nintendo: A=Right, X=Top, Y=Left, B=Bottom
listOf(
ButtonConfig(GamePadButtonInputId.A.ordinal, "A", true, null, "A", setOf(), true, null), // Right
ButtonConfig(GamePadButtonInputId.X.ordinal, "X", true, null, "X", setOf(), true, null), // Top
ButtonConfig(GamePadButtonInputId.Y.ordinal, "Y", true, null, "Y", setOf(), true, null), // Left
ButtonConfig(GamePadButtonInputId.B.ordinal, "B", true, null, "B", setOf(), true, null) // Bottom
)
} else {
// Xbox-Stil: B=Right, Y=Top, X=Left, A=Bottom
listOf(
ButtonConfig(GamePadButtonInputId.B.ordinal, "B", true, null, "B", setOf(), true, null), // Right
ButtonConfig(GamePadButtonInputId.Y.ordinal, "Y", true, null, "Y", setOf(), true, null), // Top
ButtonConfig(GamePadButtonInputId.X.ordinal, "X", true, null, "X", setOf(), true, null), // Left
ButtonConfig(GamePadButtonInputId.A.ordinal, "A", true, null, "A", setOf(), true, null) // Bottom
)
},
null,
0f,
true,
null
),
listOf(
// Right stick
// IMPORTANT: pressButtonId -> DUMMY_RIGHT_STICK_PRESS_ID, so that double tap does not trigger R3
SecondaryDialConfig.Stick(
/* sector */ 6,
/* size */ 3,
/* gap */ 2.7f,
distance,
GamePadButtonInputId.RightStick.ordinal,
DUMMY_RIGHT_STICK_PRESS_ID,
null,
setOf(),
"RightStick",
SecondaryDialConfig.RotationProcessor()
),
// Plus
SecondaryDialConfig.SingleButton(
/* sector */ 9,
buttonScale,
0.3f,
ButtonConfig(
GamePadButtonInputId.Plus.ordinal,
"+",
true,
null,
"Plus",
setOf(),
true,
null
),
null,
SecondaryDialConfig.RotationProcessor()
),
// R-Bumper
SecondaryDialConfig.DoubleButton(
/* sector */ 3,
0.2f,
ButtonConfig(
GamePadButtonInputId.RightShoulder.ordinal,
"R",
true,
null,
"RightBumper",
setOf(),
true,
null
),
null,
SecondaryDialConfig.RotationProcessor()
),
// ZR-Trigger
SecondaryDialConfig.DoubleButton(
/* sector */ 3,
1f,
ButtonConfig(
GamePadButtonInputId.RightTrigger.ordinal,
"ZR",
true,
null,
"RightTrigger",
setOf(),
true,
null
),
null,
SecondaryDialConfig.RotationProcessor()
),
// R3 separat
SecondaryDialConfig.SingleButton(
/* sector */ 5,
buttonScale,
1.0f,
ButtonConfig(
GamePadButtonInputId.RightStickButton.ordinal,
"R3",
true,
null,
"RightStickButton",
setOf(),
true,
null
),
null,
SecondaryDialConfig.RotationProcessor()
),
)
)
}
}

View file

@ -0,0 +1,393 @@
// src/KenjinxAndroid/app/src/main/java/org/kenjinx/android/GameController4.kt
package org.kenjinx.android
import android.app.Activity
import android.content.Context
import android.view.KeyEvent
import android.view.LayoutInflater
import android.view.View
import android.widget.FrameLayout
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.viewinterop.AndroidView
import androidx.core.math.MathUtils
import androidx.core.view.isVisible
import androidx.lifecycle.lifecycleScope
import com.swordfish.radialgamepad.library.RadialGamePad
import com.swordfish.radialgamepad.library.config.ButtonConfig
import com.swordfish.radialgamepad.library.config.CrossConfig
import com.swordfish.radialgamepad.library.config.CrossContentDescription
import com.swordfish.radialgamepad.library.config.PrimaryDialConfig
import com.swordfish.radialgamepad.library.config.RadialGamePadConfig
import com.swordfish.radialgamepad.library.config.SecondaryDialConfig
import com.swordfish.radialgamepad.library.event.Event
import kotlinx.coroutines.flow.merge
import kotlinx.coroutines.launch
import org.kenjinx.android.viewmodels.MainViewModel
import org.kenjinx.android.viewmodels.QuickSettings
private const val DUMMY_LEFT_STICK_PRESS_ID = 10001
private const val DUMMY_RIGHT_STICK_PRESS_ID = 10002
/**
* GameController4
* Layout 4 aktuell identisch zum Default-Layout, als eigenständige Klasse.
*/
class GameController4(var activity: Activity) : IGameController {
companion object {
private fun init(context: Context, controller: GameController4): View {
val inflater = LayoutInflater.from(context)
val parent = FrameLayout(context)
val view = inflater.inflate(R.layout.game_layout, parent, false)
view.findViewById<FrameLayout>(R.id.leftcontainer)!!.addView(controller.leftGamePad)
view.findViewById<FrameLayout>(R.id.rightcontainer)!!.addView(controller.rightGamePad)
return view
}
@Composable
fun Compose(viewModel: MainViewModel) {
AndroidView(
modifier = Modifier.fillMaxSize(),
factory = { context ->
val controller = GameController4(viewModel.activity)
val c = init(context, controller)
viewModel.activity.lifecycleScope.launch {
val events = merge(
controller.leftGamePad.events(),
controller.rightGamePad.events()
)
events.safeCollect { controller.handleEvent(it) }
}
controller.controllerView = c
viewModel.setGameController(controller)
controller.setVisible(QuickSettings(viewModel.activity).useVirtualController)
c
}
)
}
}
private var controllerView: View? = null
var leftGamePad: RadialGamePad
var rightGamePad: RadialGamePad
var controllerId: Int = -1
override val isVisible: Boolean
get() = controllerView?.isVisible ?: false
init {
val useSwitchLayout = QuickSettings(activity).useSwitchLayout
leftGamePad = RadialGamePad(generateConfig4(true, useSwitchLayout), 16f, activity)
rightGamePad = RadialGamePad(generateConfig4(false, useSwitchLayout), 16f, activity)
leftGamePad.primaryDialMaxSizeDp = 200f
rightGamePad.primaryDialMaxSizeDp = 200f
leftGamePad.gravityX = -1f
leftGamePad.gravityY = 1f
rightGamePad.gravityX = 1f
rightGamePad.gravityY = 1f
}
override fun setVisible(isVisible: Boolean) {
controllerView?.apply {
this.isVisible = isVisible
if (isVisible) connect()
}
}
override fun connect() {
if (controllerId == -1)
controllerId = KenjinxNative.inputConnectGamepad(0)
}
private fun handleEvent(ev: Event) {
if (controllerId == -1)
controllerId = KenjinxNative.inputConnectGamepad(0)
controllerId.apply {
when (ev) {
is Event.Button -> {
if (ev.id == DUMMY_LEFT_STICK_PRESS_ID || ev.id == DUMMY_RIGHT_STICK_PRESS_ID) return
when (ev.action) {
KeyEvent.ACTION_UP -> KenjinxNative.inputSetButtonReleased(ev.id, this)
KeyEvent.ACTION_DOWN -> KenjinxNative.inputSetButtonPressed(ev.id, this)
}
}
is Event.Direction -> {
when (ev.id) {
GamePadButtonInputId.DpadUp.ordinal -> {
if (ev.xAxis > 0) {
KenjinxNative.inputSetButtonPressed(GamePadButtonInputId.DpadRight.ordinal, this)
KenjinxNative.inputSetButtonReleased(GamePadButtonInputId.DpadLeft.ordinal, this)
} else if (ev.xAxis < 0) {
KenjinxNative.inputSetButtonPressed(GamePadButtonInputId.DpadLeft.ordinal, this)
KenjinxNative.inputSetButtonReleased(GamePadButtonInputId.DpadRight.ordinal, this)
} else {
KenjinxNative.inputSetButtonReleased(GamePadButtonInputId.DpadLeft.ordinal, this)
KenjinxNative.inputSetButtonReleased(GamePadButtonInputId.DpadRight.ordinal, this)
}
if (ev.yAxis < 0) {
KenjinxNative.inputSetButtonPressed(GamePadButtonInputId.DpadUp.ordinal, this)
KenjinxNative.inputSetButtonReleased(GamePadButtonInputId.DpadDown.ordinal, this)
} else if (ev.yAxis > 0) {
KenjinxNative.inputSetButtonPressed(GamePadButtonInputId.DpadDown.ordinal, this)
KenjinxNative.inputSetButtonReleased(GamePadButtonInputId.DpadUp.ordinal, this)
} else {
KenjinxNative.inputSetButtonReleased(GamePadButtonInputId.DpadDown.ordinal, this)
KenjinxNative.inputSetButtonReleased(GamePadButtonInputId.DpadUp.ordinal, this)
}
}
GamePadButtonInputId.LeftStick.ordinal -> {
val setting = QuickSettings(activity)
val x = MathUtils.clamp(ev.xAxis * setting.controllerStickSensitivity, -1f, 1f)
val y = MathUtils.clamp(ev.yAxis * setting.controllerStickSensitivity, -1f, 1f)
KenjinxNative.inputSetStickAxis(1, x, -y, this)
}
GamePadButtonInputId.RightStick.ordinal -> {
val setting = QuickSettings(activity)
val x = MathUtils.clamp(ev.xAxis * setting.controllerStickSensitivity, -1f, 1f)
val y = MathUtils.clamp(ev.yAxis * setting.controllerStickSensitivity, -1f, 1f)
KenjinxNative.inputSetStickAxis(2, x, -y, this)
}
}
}
}
}
}
}
private fun generateConfig4(isLeft: Boolean, useSwitchLayout: Boolean): RadialGamePadConfig {
val distance = 0.3f
val buttonScale = 1f
if (isLeft) {
return RadialGamePadConfig(
/* ringSegments = */ 12,
/* Primary (Stick) */
// IMPORTANT: pressButtonId -> DUMMY_LEFT_STICK_PRESS_ID, so that double tap does not trigger L3
PrimaryDialConfig.Stick(
GamePadButtonInputId.LeftStick.ordinal,
DUMMY_LEFT_STICK_PRESS_ID,
setOf(),
"LeftStick",
null
),
listOf(
// D-Pad
SecondaryDialConfig.Cross(
/* sector */ 10,
/* size */ 3,
/* gap */ 2.1f,
distance,
CrossConfig(
GamePadButtonInputId.DpadUp.ordinal,
CrossConfig.Shape.STANDARD,
null,
setOf(),
CrossContentDescription(),
true,
null
),
SecondaryDialConfig.RotationProcessor()
),
// Minus
SecondaryDialConfig.SingleButton(
/* sector */ 9,
buttonScale,
0.3f,
ButtonConfig(
GamePadButtonInputId.Minus.ordinal,
"-",
true,
null,
"Minus",
setOf(),
true,
null
),
null,
SecondaryDialConfig.RotationProcessor()
),
// L-Bumper
SecondaryDialConfig.DoubleButton(
/* sector */ 2,
1.2f,
ButtonConfig(
GamePadButtonInputId.LeftShoulder.ordinal,
"L",
true,
null,
"LeftBumper",
setOf(),
true,
null
),
null,
SecondaryDialConfig.RotationProcessor()
),
// ZL-Trigger
SecondaryDialConfig.DoubleButton(
/* sector */ 2,
2f,
ButtonConfig(
GamePadButtonInputId.LeftTrigger.ordinal,
"ZL",
true,
null,
"LeftTrigger",
setOf(),
true,
null
),
null,
SecondaryDialConfig.RotationProcessor()
),
)
)
} else {
return RadialGamePadConfig(
/* ringSegments = */ 12,
/* Primary (ABXY) */
PrimaryDialConfig.PrimaryButtons(
if (useSwitchLayout) {
// Switch/Nintendo: A=Right, X=Top, Y=Left, B=Bottom
listOf(
ButtonConfig(GamePadButtonInputId.A.ordinal, "A", true, null, "A", setOf(), true, null), // Right
ButtonConfig(GamePadButtonInputId.X.ordinal, "X", true, null, "X", setOf(), true, null), // Top
ButtonConfig(GamePadButtonInputId.Y.ordinal, "Y", true, null, "Y", setOf(), true, null), // Left
ButtonConfig(GamePadButtonInputId.B.ordinal, "B", true, null, "B", setOf(), true, null) // Bottom
)
} else {
// Xbox-Stil: B=Right, Y=Top, X=Left, A=Bottom
listOf(
ButtonConfig(GamePadButtonInputId.B.ordinal, "B", true, null, "B", setOf(), true, null), // Right
ButtonConfig(GamePadButtonInputId.Y.ordinal, "Y", true, null, "Y", setOf(), true, null), // Top
ButtonConfig(GamePadButtonInputId.X.ordinal, "X", true, null, "X", setOf(), true, null), // Left
ButtonConfig(GamePadButtonInputId.A.ordinal, "A", true, null, "A", setOf(), true, null) // Bottom
)
},
null,
0f,
true,
null
),
listOf(
// Right stick
// IMPORTANT: pressButtonId -> DUMMY_RIGHT_STICK_PRESS_ID, so that double tap does not trigger R3
SecondaryDialConfig.Stick(
/* sector */ 6,
/* size */ 3,
/* gap */ 2.7f,
distance,
GamePadButtonInputId.RightStick.ordinal,
DUMMY_RIGHT_STICK_PRESS_ID,
null,
setOf(),
"RightStick",
SecondaryDialConfig.RotationProcessor()
),
// Plus
SecondaryDialConfig.SingleButton(
/* sector */ 9,
buttonScale,
0.3f,
ButtonConfig(
GamePadButtonInputId.Plus.ordinal,
"+",
true,
null,
"Plus",
setOf(),
true,
null
),
null,
SecondaryDialConfig.RotationProcessor()
),
// R-Bumper
SecondaryDialConfig.DoubleButton(
/* sector */ 3,
1.2f,
ButtonConfig(
GamePadButtonInputId.RightShoulder.ordinal,
"R",
true,
null,
"RightBumper",
setOf(),
true,
null
),
null,
SecondaryDialConfig.RotationProcessor()
),
// ZR-Trigger
SecondaryDialConfig.DoubleButton(
/* sector */ 3,
2f,
ButtonConfig(
GamePadButtonInputId.RightTrigger.ordinal,
"ZR",
true,
null,
"RightTrigger",
setOf(),
true,
null
),
null,
SecondaryDialConfig.RotationProcessor()
),
// R3 separat
SecondaryDialConfig.SingleButton(
/* sector */ 3,
buttonScale,
0f,
ButtonConfig(
GamePadButtonInputId.RightStickButton.ordinal,
"R3",
true,
null,
"RightStickButton",
setOf(),
true,
null
),
null,
SecondaryDialConfig.RotationProcessor()
),
// L3 separat
SecondaryDialConfig.SingleButton(
/* sector */ 4,
buttonScale,
0f,
ButtonConfig(
GamePadButtonInputId.LeftStickButton.ordinal,
"L3",
true,
null,
"LeftStickButton",
setOf(),
true,
null
),
null,
SecondaryDialConfig.RotationProcessor()
),
)
)
}
}

View file

@ -0,0 +1,396 @@
package org.kenjinx.android
import android.app.Activity
import android.content.Context
import android.view.KeyEvent
import android.view.LayoutInflater
import android.view.View
import android.widget.FrameLayout
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.viewinterop.AndroidView
import androidx.core.math.MathUtils
import androidx.core.view.isVisible
import androidx.lifecycle.lifecycleScope
import com.swordfish.radialgamepad.library.RadialGamePad
import com.swordfish.radialgamepad.library.config.ButtonConfig
import com.swordfish.radialgamepad.library.config.CrossConfig
import com.swordfish.radialgamepad.library.config.CrossContentDescription
import com.swordfish.radialgamepad.library.config.PrimaryDialConfig
import com.swordfish.radialgamepad.library.config.RadialGamePadConfig
import com.swordfish.radialgamepad.library.config.SecondaryDialConfig
import com.swordfish.radialgamepad.library.event.Event
import kotlinx.coroutines.flow.merge
import kotlinx.coroutines.launch
import org.kenjinx.android.viewmodels.MainViewModel
import org.kenjinx.android.viewmodels.QuickSettings
private const val DUMMY_LEFT_STICK_PRESS_ID = 10001
private const val DUMMY_RIGHT_STICK_PRESS_ID = 10002
/**
* GameController5
* Layout 5 aktuell identisch zum Default-Layout, als eigenständige Klasse.
*/
class GameController5(var activity: Activity) : IGameController {
companion object {
private fun init(context: Context, controller: GameController5): View {
val inflater = LayoutInflater.from(context)
val parent = FrameLayout(context)
val view = inflater.inflate(R.layout.game_layout, parent, false)
view.findViewById<FrameLayout>(R.id.leftcontainer)!!.addView(controller.leftGamePad)
view.findViewById<FrameLayout>(R.id.rightcontainer)!!.addView(controller.rightGamePad)
return view
}
@Composable
fun Compose(viewModel: MainViewModel) {
AndroidView(
modifier = Modifier.fillMaxSize(),
factory = { context ->
val controller = GameController5(viewModel.activity)
val c = init(context, controller)
viewModel.activity.lifecycleScope.launch {
val events = merge(
controller.leftGamePad.events(),
controller.rightGamePad.events()
)
events.safeCollect { controller.handleEvent(it) }
}
controller.controllerView = c
viewModel.setGameController(controller)
controller.setVisible(QuickSettings(viewModel.activity).useVirtualController)
c
}
)
}
}
private var controllerView: View? = null
var leftGamePad: RadialGamePad
var rightGamePad: RadialGamePad
var controllerId: Int = -1
override val isVisible: Boolean
get() = controllerView?.isVisible ?: false
init {
val useSwitchLayout = QuickSettings(activity).useSwitchLayout
leftGamePad = RadialGamePad(generateConfig5(true, useSwitchLayout), 16f, activity)
rightGamePad = RadialGamePad(generateConfig5(false, useSwitchLayout), 16f, activity)
leftGamePad.primaryDialMaxSizeDp = 200f
rightGamePad.primaryDialMaxSizeDp = 200f
leftGamePad.gravityX = -1f
leftGamePad.gravityY = 1f
rightGamePad.gravityX = 1f
rightGamePad.gravityY = 1f
}
override fun setVisible(isVisible: Boolean) {
controllerView?.apply {
this.isVisible = isVisible
if (isVisible) connect()
}
}
override fun connect() {
if (controllerId == -1)
controllerId = KenjinxNative.inputConnectGamepad(0)
}
private fun handleEvent(ev: Event) {
if (controllerId == -1)
controllerId = KenjinxNative.inputConnectGamepad(0)
controllerId.apply {
when (ev) {
is Event.Button -> {
if (ev.id == DUMMY_LEFT_STICK_PRESS_ID || ev.id == DUMMY_RIGHT_STICK_PRESS_ID) return
when (ev.action) {
KeyEvent.ACTION_UP -> KenjinxNative.inputSetButtonReleased(ev.id, this)
KeyEvent.ACTION_DOWN -> KenjinxNative.inputSetButtonPressed(ev.id, this)
}
}
is Event.Direction -> {
when (ev.id) {
GamePadButtonInputId.DpadUp.ordinal -> {
if (ev.xAxis > 0) {
KenjinxNative.inputSetButtonPressed(GamePadButtonInputId.DpadRight.ordinal, this)
KenjinxNative.inputSetButtonReleased(GamePadButtonInputId.DpadLeft.ordinal, this)
} else if (ev.xAxis < 0) {
KenjinxNative.inputSetButtonPressed(GamePadButtonInputId.DpadLeft.ordinal, this)
KenjinxNative.inputSetButtonReleased(GamePadButtonInputId.DpadRight.ordinal, this)
} else {
KenjinxNative.inputSetButtonReleased(GamePadButtonInputId.DpadLeft.ordinal, this)
KenjinxNative.inputSetButtonReleased(GamePadButtonInputId.DpadRight.ordinal, this)
}
if (ev.yAxis < 0) {
KenjinxNative.inputSetButtonPressed(GamePadButtonInputId.DpadUp.ordinal, this)
KenjinxNative.inputSetButtonReleased(GamePadButtonInputId.DpadDown.ordinal, this)
} else if (ev.yAxis > 0) {
KenjinxNative.inputSetButtonPressed(GamePadButtonInputId.DpadDown.ordinal, this)
KenjinxNative.inputSetButtonReleased(GamePadButtonInputId.DpadUp.ordinal, this)
} else {
KenjinxNative.inputSetButtonReleased(GamePadButtonInputId.DpadDown.ordinal, this)
KenjinxNative.inputSetButtonReleased(GamePadButtonInputId.DpadUp.ordinal, this)
}
}
GamePadButtonInputId.LeftStick.ordinal -> {
val setting = QuickSettings(activity)
val x = MathUtils.clamp(ev.xAxis * setting.controllerStickSensitivity, -1f, 1f)
val y = MathUtils.clamp(ev.yAxis * setting.controllerStickSensitivity, -1f, 1f)
KenjinxNative.inputSetStickAxis(1, x, -y, this)
}
GamePadButtonInputId.RightStick.ordinal -> {
val setting = QuickSettings(activity)
val x = MathUtils.clamp(ev.xAxis * setting.controllerStickSensitivity, -1f, 1f)
val y = MathUtils.clamp(ev.yAxis * setting.controllerStickSensitivity, -1f, 1f)
KenjinxNative.inputSetStickAxis(2, x, -y, this)
}
}
}
}
}
}
}
private fun generateConfig5(isLeft: Boolean, useSwitchLayout: Boolean): RadialGamePadConfig {
val distance = 0.3f
val buttonScale = 1f
if (isLeft) {
return RadialGamePadConfig(
/* ringSegments = */ 12,
/* Primary (Stick) */
// IMPORTANT: pressButtonId -> DUMMY_LEFT_STICK_PRESS_ID, so that double tap does not trigger L3
PrimaryDialConfig.Stick(
GamePadButtonInputId.LeftStick.ordinal,
DUMMY_LEFT_STICK_PRESS_ID,
setOf(),
"LeftStick",
null
),
listOf(
// D-Pad
SecondaryDialConfig.Cross(
/* sector */ 10,
/* size */ 3,
/* gap */ 2.1f,
distance,
CrossConfig(
GamePadButtonInputId.DpadUp.ordinal,
CrossConfig.Shape.STANDARD,
null,
setOf(),
CrossContentDescription(),
true,
null
),
SecondaryDialConfig.RotationProcessor()
),
// Minus
SecondaryDialConfig.SingleButton(
/* sector */ 9,
buttonScale,
0.3f,
ButtonConfig(
GamePadButtonInputId.Minus.ordinal,
"-",
true,
null,
"Minus",
setOf(),
true,
null
),
null,
SecondaryDialConfig.RotationProcessor()
),
// L-Bumper
SecondaryDialConfig.SingleButton(
/* sector */ 3,
1.5f,
1.0f,
ButtonConfig(
GamePadButtonInputId.LeftShoulder.ordinal,
"L",
true,
null,
"LeftBumper",
setOf(),
true,
null
),
null,
SecondaryDialConfig.RotationProcessor()
),
// ZL-Trigger
SecondaryDialConfig.SingleButton(
/* sector */ 2,
1.5f,
1.1f,
ButtonConfig(
GamePadButtonInputId.LeftTrigger.ordinal,
"ZL",
true,
null,
"LeftTrigger",
setOf(),
true,
null
),
null,
SecondaryDialConfig.RotationProcessor()
),
)
)
} else {
return RadialGamePadConfig(
/* ringSegments = */ 12,
/* Primary (ABXY) */
PrimaryDialConfig.PrimaryButtons(
if (useSwitchLayout) {
// Switch/Nintendo: A=Right, X=Top, Y=Left, B=Bottom
listOf(
ButtonConfig(GamePadButtonInputId.A.ordinal, "A", true, null, "A", setOf(), true, null), // Right
ButtonConfig(GamePadButtonInputId.X.ordinal, "X", true, null, "X", setOf(), true, null), // Top
ButtonConfig(GamePadButtonInputId.Y.ordinal, "Y", true, null, "Y", setOf(), true, null), // Left
ButtonConfig(GamePadButtonInputId.B.ordinal, "B", true, null, "B", setOf(), true, null) // Bottom
)
} else {
// Xbox-Stil: B=Right, Y=Top, X=Left, A=Bottom
listOf(
ButtonConfig(GamePadButtonInputId.B.ordinal, "B", true, null, "B", setOf(), true, null), // Right
ButtonConfig(GamePadButtonInputId.Y.ordinal, "Y", true, null, "Y", setOf(), true, null), // Top
ButtonConfig(GamePadButtonInputId.X.ordinal, "X", true, null, "X", setOf(), true, null), // Left
ButtonConfig(GamePadButtonInputId.A.ordinal, "A", true, null, "A", setOf(), true, null) // Bottom
)
},
null,
0f,
true,
null
),
listOf(
// Right stick
// IMPORTANT: pressButtonId -> DUMMY_RIGHT_STICK_PRESS_ID, so that double tap does not trigger R3
SecondaryDialConfig.Stick(
/* sector */ 6,
/* size */ 3,
/* gap */ 2.7f,
distance,
GamePadButtonInputId.RightStick.ordinal,
DUMMY_RIGHT_STICK_PRESS_ID,
null,
setOf(),
"RightStick",
SecondaryDialConfig.RotationProcessor()
),
// Plus
SecondaryDialConfig.SingleButton(
/* sector */ 9,
buttonScale,
0.3f,
ButtonConfig(
GamePadButtonInputId.Plus.ordinal,
"+",
true,
null,
"Plus",
setOf(),
true,
null
),
null,
SecondaryDialConfig.RotationProcessor()
),
// R-Bumper
SecondaryDialConfig.SingleButton(
/* sector */ 3,
1.5f,
1.0f,
ButtonConfig(
GamePadButtonInputId.RightShoulder.ordinal,
"R",
true,
null,
"RightBumper",
setOf(),
true,
null
),
null,
SecondaryDialConfig.RotationProcessor()
),
// ZR-Trigger
SecondaryDialConfig.SingleButton(
/* sector */ 4,
1.5f,
1.1f,
ButtonConfig(
GamePadButtonInputId.RightTrigger.ordinal,
"ZR",
true,
null,
"RightTrigger",
setOf(),
true,
null
),
null,
SecondaryDialConfig.RotationProcessor()
),
// R3 separat
SecondaryDialConfig.SingleButton(
/* sector */ 3,
1.1f,
0f,
ButtonConfig(
GamePadButtonInputId.RightStickButton.ordinal,
"R3",
true,
null,
"RightStickButton",
setOf(),
true,
null
),
null,
SecondaryDialConfig.RotationProcessor()
),
// L3 separat
SecondaryDialConfig.SingleButton(
/* sector */ 4,
1.1f,
0f,
ButtonConfig(
GamePadButtonInputId.LeftStickButton.ordinal,
"L3",
true,
null,
"LeftStickButton",
setOf(),
true,
null
),
null,
SecondaryDialConfig.RotationProcessor()
),
)
)
}
}

View file

@ -0,0 +1,393 @@
package org.kenjinx.android
import android.app.Activity
import android.content.Context
import android.view.KeyEvent
import android.view.LayoutInflater
import android.view.View
import android.widget.FrameLayout
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.viewinterop.AndroidView
import androidx.core.math.MathUtils
import androidx.core.view.isVisible
import androidx.lifecycle.lifecycleScope
import com.swordfish.radialgamepad.library.RadialGamePad
import com.swordfish.radialgamepad.library.config.ButtonConfig
import com.swordfish.radialgamepad.library.config.CrossConfig
import com.swordfish.radialgamepad.library.config.CrossContentDescription
import com.swordfish.radialgamepad.library.config.PrimaryDialConfig
import com.swordfish.radialgamepad.library.config.RadialGamePadConfig
import com.swordfish.radialgamepad.library.config.SecondaryDialConfig
import com.swordfish.radialgamepad.library.event.Event
import kotlinx.coroutines.flow.merge
import kotlinx.coroutines.launch
import org.kenjinx.android.viewmodels.MainViewModel
import org.kenjinx.android.viewmodels.QuickSettings
private const val DUMMY_LEFT_STICK_PRESS_ID = 10001
private const val DUMMY_RIGHT_STICK_PRESS_ID = 10002
/**
* GameController6
* Layout 6 aktuell identisch zum Default-Layout, als eigenständige Klasse.
*/
class GameController6(var activity: Activity) : IGameController {
companion object {
private fun init(context: Context, controller: GameController6): View {
val inflater = LayoutInflater.from(context)
val parent = FrameLayout(context)
val view = inflater.inflate(R.layout.game_layout, parent, false)
view.findViewById<FrameLayout>(R.id.leftcontainer)!!.addView(controller.leftGamePad)
view.findViewById<FrameLayout>(R.id.rightcontainer)!!.addView(controller.rightGamePad)
return view
}
@Composable
fun Compose(viewModel: MainViewModel) {
AndroidView(
modifier = Modifier.fillMaxSize(),
factory = { context ->
val controller = GameController6(viewModel.activity)
val c = init(context, controller)
viewModel.activity.lifecycleScope.launch {
val events = merge(
controller.leftGamePad.events(),
controller.rightGamePad.events()
)
events.safeCollect { controller.handleEvent(it) }
}
controller.controllerView = c
viewModel.setGameController(controller)
controller.setVisible(QuickSettings(viewModel.activity).useVirtualController)
c
}
)
}
}
private var controllerView: View? = null
var leftGamePad: RadialGamePad
var rightGamePad: RadialGamePad
var controllerId: Int = -1
override val isVisible: Boolean
get() = controllerView?.isVisible ?: false
init {
val useSwitchLayout = QuickSettings(activity).useSwitchLayout
leftGamePad = RadialGamePad(generateConfig6(true, useSwitchLayout), 16f, activity)
rightGamePad = RadialGamePad(generateConfig6(false, useSwitchLayout), 16f, activity)
leftGamePad.primaryDialMaxSizeDp = 200f
rightGamePad.primaryDialMaxSizeDp = 200f
leftGamePad.gravityX = -1f
leftGamePad.gravityY = 1f
rightGamePad.gravityX = 1f
rightGamePad.gravityY = 1f
}
override fun setVisible(isVisible: Boolean) {
controllerView?.apply {
this.isVisible = isVisible
if (isVisible) connect()
}
}
override fun connect() {
if (controllerId == -1)
controllerId = KenjinxNative.inputConnectGamepad(0)
}
private fun handleEvent(ev: Event) {
if (controllerId == -1)
controllerId = KenjinxNative.inputConnectGamepad(0)
controllerId.apply {
when (ev) {
is Event.Button -> {
if (ev.id == DUMMY_LEFT_STICK_PRESS_ID || ev.id == DUMMY_RIGHT_STICK_PRESS_ID) return
when (ev.action) {
KeyEvent.ACTION_UP -> KenjinxNative.inputSetButtonReleased(ev.id, this)
KeyEvent.ACTION_DOWN -> KenjinxNative.inputSetButtonPressed(ev.id, this)
}
}
is Event.Direction -> {
when (ev.id) {
GamePadButtonInputId.DpadUp.ordinal -> {
if (ev.xAxis > 0) {
KenjinxNative.inputSetButtonPressed(GamePadButtonInputId.DpadRight.ordinal, this)
KenjinxNative.inputSetButtonReleased(GamePadButtonInputId.DpadLeft.ordinal, this)
} else if (ev.xAxis < 0) {
KenjinxNative.inputSetButtonPressed(GamePadButtonInputId.DpadLeft.ordinal, this)
KenjinxNative.inputSetButtonReleased(GamePadButtonInputId.DpadRight.ordinal, this)
} else {
KenjinxNative.inputSetButtonReleased(GamePadButtonInputId.DpadLeft.ordinal, this)
KenjinxNative.inputSetButtonReleased(GamePadButtonInputId.DpadRight.ordinal, this)
}
if (ev.yAxis < 0) {
KenjinxNative.inputSetButtonPressed(GamePadButtonInputId.DpadUp.ordinal, this)
KenjinxNative.inputSetButtonReleased(GamePadButtonInputId.DpadDown.ordinal, this)
} else if (ev.yAxis > 0) {
KenjinxNative.inputSetButtonPressed(GamePadButtonInputId.DpadDown.ordinal, this)
KenjinxNative.inputSetButtonReleased(GamePadButtonInputId.DpadUp.ordinal, this)
} else {
KenjinxNative.inputSetButtonReleased(GamePadButtonInputId.DpadDown.ordinal, this)
KenjinxNative.inputSetButtonReleased(GamePadButtonInputId.DpadUp.ordinal, this)
}
}
GamePadButtonInputId.LeftStick.ordinal -> {
val setting = QuickSettings(activity)
val x = MathUtils.clamp(ev.xAxis * setting.controllerStickSensitivity, -1f, 1f)
val y = MathUtils.clamp(ev.yAxis * setting.controllerStickSensitivity, -1f, 1f)
KenjinxNative.inputSetStickAxis(1, x, -y, this)
}
GamePadButtonInputId.RightStick.ordinal -> {
val setting = QuickSettings(activity)
val x = MathUtils.clamp(ev.xAxis * setting.controllerStickSensitivity, -1f, 1f)
val y = MathUtils.clamp(ev.yAxis * setting.controllerStickSensitivity, -1f, 1f)
KenjinxNative.inputSetStickAxis(2, x, -y, this)
}
}
}
}
}
}
}
private fun generateConfig6(isLeft: Boolean, useSwitchLayout: Boolean): GamePadConfig {
val distance = 0.3f
val buttonScale = 1f
if (isLeft) {
return GamePadConfig(
/* ringSegments = */ 12,
/* Primary (Stick) */
// IMPORTANT: pressButtonId -> DUMMY_LEFT_STICK_PRESS_ID, so that double tap does not trigger L3
PrimaryDialConfig.Stick(
GamePadButtonInputId.LeftStick.ordinal,
DUMMY_LEFT_STICK_PRESS_ID,
setOf(),
"LeftStick",
null
),
listOf(
// D-Pad
SecondaryDialConfig.Cross(
/* sector */ 10,
/* size */ 3,
/* gap */ 2.5f,
distance,
CrossConfig(
GamePadButtonInputId.DpadUp.ordinal,
CrossConfig.Shape.STANDARD,
null,
setOf(),
CrossContentDescription(),
true,
null
),
SecondaryDialConfig.RotationProcessor()
),
// Minus
SecondaryDialConfig.SingleButton(
/* sector */ 1,
buttonScale,
2f,
ButtonConfig(
GamePadButtonInputId.Minus.ordinal,
"-",
true,
null,
"Minus",
setOf(),
true,
null
),
null,
SecondaryDialConfig.RotationProcessor()
),
// L-Bumper
SecondaryDialConfig.DoubleButton(
/* sector */ 2,
distance,
ButtonConfig(
GamePadButtonInputId.LeftShoulder.ordinal,
"L",
true,
null,
"LeftBumper",
setOf(),
true,
null
),
null,
SecondaryDialConfig.RotationProcessor()
),
// ZL-Trigger
SecondaryDialConfig.SingleButton(
/* sector */ 9,
buttonScale,
distance,
ButtonConfig(
GamePadButtonInputId.LeftTrigger.ordinal,
"ZL",
true,
null,
"LeftTrigger",
setOf(),
true,
null
),
null,
SecondaryDialConfig.RotationProcessor()
),
)
)
} else {
return GamePadConfig(
/* ringSegments = */ 12,
/* Primary (ABXY) */
PrimaryDialConfig.PrimaryButtons(
if (useSwitchLayout) {
// Switch/Nintendo: A=Right, X=Top, Y=Left, B=Bottom
listOf(
ButtonConfig(GamePadButtonInputId.A.ordinal, "A", true, null, "A", setOf(), true, null), // Right
ButtonConfig(GamePadButtonInputId.X.ordinal, "X", true, null, "X", setOf(), true, null), // Top
ButtonConfig(GamePadButtonInputId.Y.ordinal, "Y", true, null, "Y", setOf(), true, null), // Left
ButtonConfig(GamePadButtonInputId.B.ordinal, "B", true, null, "B", setOf(), true, null) // Bottom
)
} else {
// Xbox-Stil: B=Right, Y=Top, X=Left, A=Bottom
listOf(
ButtonConfig(GamePadButtonInputId.B.ordinal, "B", true, null, "B", setOf(), true, null), // Right
ButtonConfig(GamePadButtonInputId.Y.ordinal, "Y", true, null, "Y", setOf(), true, null), // Top
ButtonConfig(GamePadButtonInputId.X.ordinal, "X", true, null, "X", setOf(), true, null), // Left
ButtonConfig(GamePadButtonInputId.A.ordinal, "A", true, null, "A", setOf(), true, null) // Bottom
)
},
null,
0f,
true,
null
),
listOf(
// Right stick (unchanged)
// IMPORTANT: pressButtonId -> DUMMY_RIGHT_STICK_PRESS_ID, so that double tap does not trigger R3
SecondaryDialConfig.Stick(
/* sector */ 6,
/* size */ 3,
/* gap */ 2.5f,
0.7f,
GamePadButtonInputId.RightStick.ordinal,
DUMMY_RIGHT_STICK_PRESS_ID,
null,
setOf(),
"RightStick",
SecondaryDialConfig.RotationProcessor()
),
// Plus
SecondaryDialConfig.SingleButton(
/* sector */ 5,
buttonScale,
2f,
ButtonConfig(
GamePadButtonInputId.Plus.ordinal,
"+",
true,
null,
"Plus",
setOf(),
true,
null
),
null,
SecondaryDialConfig.RotationProcessor()
),
// R-Bumper
SecondaryDialConfig.DoubleButton(
/* sector */ 3,
distance,
ButtonConfig(
GamePadButtonInputId.RightShoulder.ordinal,
"R",
true,
null,
"RightBumper",
setOf(),
true,
null
),
null,
SecondaryDialConfig.RotationProcessor()
),
// ZR-Trigger
SecondaryDialConfig.SingleButton(
/* sector */ 9,
buttonScale,
distance,
ButtonConfig(
GamePadButtonInputId.RightTrigger.ordinal,
"ZR",
true,
null,
"RightTrigger",
setOf(),
true,
null
),
null,
SecondaryDialConfig.RotationProcessor()
),
// NEW (remains): R3 as a separate button
SecondaryDialConfig.SingleButton(
/* sector */ 5,
buttonScale,
0.0f,
ButtonConfig(
GamePadButtonInputId.RightStickButton.ordinal,
"R3",
true,
null,
"RightStickButton",
setOf(),
true,
null
),
null,
SecondaryDialConfig.RotationProcessor()
),
// NEW (remains): L3 as a separate button
SecondaryDialConfig.SingleButton(
/* sector */ 6,
buttonScale,
0.0f,
ButtonConfig(
GamePadButtonInputId.LeftStickButton.ordinal,
"L3",
true,
null,
"LeftStickButton",
setOf(),
true,
null
),
null,
SecondaryDialConfig.RotationProcessor()
),
)
)
}
}

View file

@ -0,0 +1,7 @@
package org.kenjinx.android
interface IGameController {
val isVisible: Boolean
fun setVisible(isVisible: Boolean)
fun connect()
}

View file

@ -8,7 +8,7 @@ import androidx.preference.PreferenceManager
import com.anggrayudi.storage.extension.launchOnUiThread
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.sync.Semaphore
import org.kenjinx.android.GameController
import org.kenjinx.android.IGameController
import org.kenjinx.android.GameHost
import org.kenjinx.android.Logging
import org.kenjinx.android.MainActivity
@ -30,7 +30,7 @@ class MainViewModel(val activity: MainActivity) {
var physicalControllerManager: PhysicalControllerManager? = null
var motionSensorManager: MotionSensorManager? = null
var gameModel: GameModel? = null
var controller: GameController? = null
var controller: IGameController? = null
var performanceManager: PerformanceManager? = null
var selected: GameModel? = null
val loadGameModel: MutableState<GameModel?> = mutableStateOf(null)
@ -422,7 +422,7 @@ class MainViewModel(val activity: MainActivity) {
frequenciesState?.let { PerformanceMonitor.getFrequencies(it) }
}
fun setGameController(controller: GameController) {
fun setGameController(controller: IGameController) {
this.controller = controller
}

View file

@ -24,6 +24,13 @@ class QuickSettings(val activity: Activity) {
BottomMiddle, BottomLeft, BottomRight, TopMiddle, TopLeft, TopRight
}
// --- Virtual Controller Preset
enum class VirtualControllerPreset {
Default, Layout2, Layout3, Layout4, Layout5, Layout6
}
var virtualControllerPreset: VirtualControllerPreset
var orientationPreference: OrientationPreference
// --- Overlay Settings
@ -94,6 +101,9 @@ class QuickSettings(val activity: Activity) {
resScale = sharedPref.getFloat("resScale", 1f)
maxAnisotropy = sharedPref.getFloat("maxAnisotropy", 0f)
useVirtualController = sharedPref.getBoolean("useVirtualController", true)
virtualControllerPreset = VirtualControllerPreset.entries[
sharedPref.getInt("virtualControllerPreset", VirtualControllerPreset.Default.ordinal)
]
isGrid = sharedPref.getBoolean("isGrid", true)
useSwitchLayout = sharedPref.getBoolean("useSwitchLayout", true)
enableMotion = sharedPref.getBoolean("enableMotion", true)
@ -151,6 +161,7 @@ class QuickSettings(val activity: Activity) {
putBoolean("enableTraceLogs", enableTraceLogs)
putBoolean("enableDebugLogs", enableDebugLogs)
putBoolean("enableGraphicsLogs", enableGraphicsLogs)
putInt("virtualControllerPreset", virtualControllerPreset.ordinal)
}
}

View file

@ -38,6 +38,11 @@ import androidx.compose.ui.draw.alpha
import compose.icons.CssGgIcons
import compose.icons.cssggicons.ToolbarBottom
import org.kenjinx.android.GameController
import org.kenjinx.android.GameController2
import org.kenjinx.android.GameController3
import org.kenjinx.android.GameController4
import org.kenjinx.android.GameController5
import org.kenjinx.android.GameController6
import org.kenjinx.android.GameHost
import org.kenjinx.android.Icons
import org.kenjinx.android.MainActivity
@ -48,6 +53,10 @@ import org.kenjinx.android.viewmodels.VSyncMode
import org.kenjinx.android.widgets.SimpleAlertDialog
import java.util.Locale
import kotlin.math.roundToInt
import android.net.Uri
import android.widget.Toast
import org.kenjinx.android.viewmodels.QuickSettings.VirtualControllerPreset
class GameViews {
companion object {
@ -155,7 +164,17 @@ class GameViews {
}
if (!showLoading.value) {
GameController.Compose(mainViewModel)
// Aktuelles Preset aus QuickSettings holen (bei jeder Recomposition neu so greift auch ein Wechsel nach dem Speichern)
val preset = QuickSettings(mainViewModel.activity).virtualControllerPreset
when (preset) {
VirtualControllerPreset.Default -> GameController.Compose(mainViewModel)
VirtualControllerPreset.Layout2 -> GameController2.Compose(mainViewModel)
VirtualControllerPreset.Layout3 -> GameController3.Compose(mainViewModel)
VirtualControllerPreset.Layout4 -> GameController4.Compose(mainViewModel)
VirtualControllerPreset.Layout5 -> GameController5.Compose(mainViewModel)
VirtualControllerPreset.Layout6 -> GameController6.Compose(mainViewModel)
}
// --- Button at any corner/edge + transparency
Row(

View file

@ -89,6 +89,8 @@ import org.kenjinx.android.viewmodels.QuickSettings.OverlayMenuPosition // ← N
import org.kenjinx.android.SystemLanguage
import org.kenjinx.android.RegionCode
import org.kenjinx.android.viewmodels.QuickSettings.VirtualControllerPreset
class SettingViews {
companion object {
const val EXPANSTION_TRANSITION_DURATION = 450
@ -129,6 +131,9 @@ class SettingViews {
val isGrid = remember { mutableStateOf(true) }
val useSwitchLayout = remember { mutableStateOf(true) }
val enableMotion = remember { mutableStateOf(true) }
val vcPreset = remember {
mutableStateOf(QuickSettings(mainViewModel.activity).virtualControllerPreset)
}
val enablePerformanceMode = remember { mutableStateOf(true) }
val controllerStickSensitivity = remember { mutableFloatStateOf(1.0f) }
val enableStubLogs = remember { mutableStateOf(true) }
@ -1224,6 +1229,17 @@ class SettingViews {
Column(modifier = Modifier.fillMaxWidth()) {
useVirtualController.SwitchSelector(label = "Use Virtual Controller")
useSwitchLayout.SwitchSelector(label = "Use Switch Controller Layout")
if (useVirtualController.value) {
VirtualControllerPresetDropdown(
selectedPreset = vcPreset.value,
onPresetSelected = { preset ->
vcPreset.value = preset
val qs = QuickSettings(mainViewModel.activity)
qs.virtualControllerPreset = preset
qs.save() // sofort persistieren, wie bei Overlay-Settings
}
)
}
val interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }
@ -1503,8 +1519,6 @@ class SettingViews {
)
}
// ---- Existing dropdowns ----
// ---- Dropdown for orientation ----
@Composable
fun OrientationDropdown(
@ -1532,7 +1546,38 @@ class SettingViews {
)
}
// ---- Existing dropdowns ----
@Composable
fun VirtualControllerPresetDropdown(
selectedPreset: VirtualControllerPreset,
onPresetSelected: (VirtualControllerPreset) -> Unit
) {
val options = listOf(
VirtualControllerPreset.Default,
VirtualControllerPreset.Layout2,
VirtualControllerPreset.Layout3,
VirtualControllerPreset.Layout4,
VirtualControllerPreset.Layout5,
VirtualControllerPreset.Layout6
)
DropdownSelector(
label = "Controller Layout",
selectedValue = selectedPreset,
options = options,
getDisplayText = { opt ->
when (opt) {
VirtualControllerPreset.Default -> "Default"
VirtualControllerPreset.Layout2 -> "Layout 2"
VirtualControllerPreset.Layout3 -> "Layout 3"
VirtualControllerPreset.Layout4 -> "Layout 4"
VirtualControllerPreset.Layout5 -> "Layout 5"
VirtualControllerPreset.Layout6 -> "Layout 6"
}
},
onOptionSelected = onPresetSelected
)
}
@Composable
fun MemoryModeDropdown(