Implement OSC stick regions

Stick regions extend the activation area of the sticks to rectangles covering the corresponding half of the screen.
E.g. for the left stick: when any point of the left side of the screen is touched, the stick is repositioned there, and acts as if it was centered in the touched position. When the finger is lifter, the stick is hidden.
This commit is contained in:
lynxnb 2023-04-18 22:17:37 +02:00 committed by Billy Laws
parent 32f995165c
commit d86b2ec3e9
8 changed files with 140 additions and 9 deletions

View File

@ -337,6 +337,7 @@ class EmulationActivity : AppCompatActivity(), SurfaceHolder.Callback, View.OnTo
setOnStickStateChangedListener(::onStickStateChanged)
hapticFeedback = appSettings.onScreenControl && appSettings.onScreenControlFeedback
recenterSticks = appSettings.onScreenControlRecenterSticks
stickRegions = appSettings.onScreenControlUseStickRegions
}
binding.onScreenControllerToggle.apply {

View File

@ -100,6 +100,10 @@ class ControllerActivity : AppCompatActivity() {
appSettings.onScreenControlRecenterSticks = item.checked
})
items.add(ControllerCheckBoxViewItem(getString(R.string.osc_use_stick_regions), getString(R.string.osc_use_stick_regions_desc), appSettings.onScreenControlUseStickRegions) { item, position ->
appSettings.onScreenControlUseStickRegions = item.checked
})
items.add(ControllerViewItem(content = getString(R.string.osc_edit), onClick = {
startActivity(Intent(this, OnScreenEditActivity::class.java))
}))

View File

@ -60,7 +60,7 @@ abstract class OnScreenButton(
}
}
final override val config = OnScreenConfigurationImpl(onScreenControllerView.context, buttonId, defaultRelativeX, defaultRelativeY, defaultEnabled)
final override val config : OnScreenConfiguration = OnScreenConfigurationImpl(onScreenControllerView.context, buttonId, defaultRelativeX, defaultRelativeY, defaultEnabled)
protected val drawable = ContextCompat.getDrawable(onScreenControllerView.context, drawableId)!!
@ -69,7 +69,7 @@ abstract class OnScreenButton(
typeface = Typeface.create(Typeface.DEFAULT, Typeface.BOLD)
isAntiAlias = true
}
private val textBoundsRect = Rect()
protected val textBoundsRect = Rect()
var relativeX = config.relativeX
var relativeY = config.relativeY

View File

@ -65,10 +65,16 @@ class OnScreenControllerView @JvmOverloads constructor(context : Context, attrs
field = value
controls.joysticks.forEach { it.recenterSticks = recenterSticks }
}
var stickRegions = false
set(value) {
field = value
controls.setStickRegions(value)
invalidate()
}
var hapticFeedback = false
set(value) {
field = value
(controls.circularButtons + controls.rectangularButtons + controls.triggerButtons).forEach { it.hapticFeedback = hapticFeedback }
controls.buttons.forEach { it.hapticFeedback = hapticFeedback }
}
internal val editInfo = OnScreenEditInfo()
@ -116,7 +122,7 @@ class OnScreenControllerView @JvmOverloads constructor(context : Context, attrs
val x by lazy { event.getX(actionIndex) }
val y by lazy { event.getY(actionIndex) }
(controls.circularButtons + controls.rectangularButtons + controls.triggerButtons).forEach { button ->
controls.buttons.forEach { button ->
when (event.action and event.actionMasked) {
MotionEvent.ACTION_UP,
MotionEvent.ACTION_POINTER_UP -> {
@ -174,7 +180,12 @@ class OnScreenControllerView @JvmOverloads constructor(context : Context, attrs
}
}
for (joystick in controls.joysticks) {
if (handled) {
invalidate()
return@OnTouchListener true
}
controls.joysticks.forEach { joystick ->
when (event.actionMasked) {
MotionEvent.ACTION_UP,
MotionEvent.ACTION_POINTER_UP,

View File

@ -100,6 +100,7 @@ class OnScreenEditActivity : AppCompatActivity() {
getSystemService(VIBRATOR_SERVICE) as Vibrator
binding.onScreenControllerView.recenterSticks = appSettings.onScreenControlRecenterSticks
binding.onScreenControllerView.stickRegions = appSettings.onScreenControlUseStickRegions
val snapToGrid = appSettings.onScreenControlSnapToGrid
binding.onScreenControllerView.setSnapToGrid(snapToGrid)

View File

@ -6,7 +6,11 @@
package emu.skyline.input.onscreen
import android.graphics.Canvas
import android.graphics.Paint
import android.graphics.PointF
import android.graphics.Rect
import android.graphics.RectF
import android.graphics.Typeface
import android.os.SystemClock
import androidx.core.graphics.minus
import emu.skyline.R
@ -15,6 +19,7 @@ import emu.skyline.input.ButtonId.*
import emu.skyline.input.StickId
import emu.skyline.input.StickId.Left
import emu.skyline.input.StickId.Right
import emu.skyline.utils.SwitchColors
import emu.skyline.utils.add
import emu.skyline.utils.multiply
import kotlin.math.roundToInt
@ -45,7 +50,7 @@ open class CircularButton(
override fun isTouched(x : Float, y : Float) : Boolean = (PointF(currentX, currentY) - (PointF(x, y))).length() <= radius
}
class JoystickButton(
open class JoystickButton(
onScreenControllerView : OnScreenControllerView,
val stickId : StickId,
defaultRelativeX : Float,
@ -61,7 +66,7 @@ class JoystickButton(
) {
private val innerButton = CircularButton(onScreenControllerView, buttonId, config.relativeX, config.relativeY, defaultRelativeRadiusToX * 0.75f, R.drawable.ic_stick)
var recenterSticks = false
open var recenterSticks = false
private var initialTapPosition = PointF()
private var fingerDownTime = 0L
private var fingerUpTime = 0L
@ -157,6 +162,85 @@ class JoystickButton(
}
}
class JoystickRegion(
onScreenControllerView : OnScreenControllerView,
stickId : StickId,
defaultRelativeX : Float,
defaultRelativeY : Float,
defaultRelativeRadiusToX : Float,
private val relativeRegionPosition : RectF,
regionColor : Int
) : JoystickButton(
onScreenControllerView,
stickId,
defaultRelativeX,
defaultRelativeY,
defaultRelativeRadiusToX
) {
/**
* A stick region always re-centers the stick, it always positions the stick at the initial touch position
*/
override var recenterSticks = true
set(_) = Unit
private val left get() = relativeRegionPosition.left * width
private val top get() = relativeRegionPosition.top * height
private val pixelWidth get() = relativeRegionPosition.width() * width
private val pixelHeight get() = relativeRegionPosition.height() * height
private val regionBounds get() = Rect(left.toInt(), top.toInt(), (left + pixelWidth).toInt(), (top + pixelHeight).toInt())
override fun isTouched(x : Float, y : Float) = regionBounds.contains(x.roundToInt(), y.roundToInt())
private val regionPaint = Paint().apply {
this.color = regionColor
alpha = 40
style = Paint.Style.FILL
}
private val buttonSymbolPaint = Paint().apply {
this.color = SwitchColors.WHITE.color
this.alpha = 40
typeface = Typeface.create(Typeface.DEFAULT, Typeface.BOLD)
textAlign = Paint.Align.LEFT
}
/**
* Tracks whether the finger is currently down on the stick region.
* The Stick is only rendered when the finger is down.
*/
private var isFingerDown = false
override fun render(canvas : Canvas) {
if (isFingerDown || editInfo.isEditing)
super.render(canvas)
// Only draw the stick region area when in edit mode
if (!editInfo.isEditing)
return
canvas.drawRect(regionBounds, regionPaint)
val text = stickId.button.short!!
val x = left + pixelWidth / 2f
val y = top + pixelHeight / 2f
buttonSymbolPaint.apply {
textSize = pixelWidth.coerceAtMost(pixelHeight) * 0.4f
getTextBounds(text, 0, text.length, textBoundsRect)
}
canvas.drawText(text, x - textBoundsRect.width() / 2f - textBoundsRect.left, y + textBoundsRect.height() / 2f - textBoundsRect.bottom, buttonSymbolPaint)
}
override fun onFingerDown(x : Float, y : Float) : Boolean {
isFingerDown = true
return super.onFingerDown(x, y)
}
override fun onFingerUp(x : Float, y : Float) : Boolean {
isFingerDown = false
return super.onFingerUp(x, y)
}
}
open class RectangularButton(
onScreenControllerView : OnScreenControllerView,
buttonId : ButtonId,
@ -236,14 +320,41 @@ class Controls(onScreenControllerView : OnScreenControllerView) {
CircularButton(onScreenControllerView, Menu, 0.5f, 0.85f, 0.029f)
)
val joysticks = listOf(
private val joystickRegions = listOf<JoystickButton>(
JoystickRegion(
onScreenControllerView, Left, 0.24f, 0.75f, 0.06f,
RectF(0f, 0f, 0.5f, 1f), SwitchColors.NEON_BLUE.color
),
JoystickRegion(
onScreenControllerView, Right, 0.9f, 0.53f, 0.06f,
RectF(0.5f, 0f, 1f, 1f), SwitchColors.NEON_RED.color
),
)
private val joystickButtons = listOf(
JoystickButton(onScreenControllerView, Left, 0.24f, 0.75f, 0.06f),
JoystickButton(onScreenControllerView, Right, 0.9f, 0.53f, 0.06f)
)
var joysticks = joystickButtons
private set
val rectangularButtons = listOf(buttonL, buttonR)
val triggerButtons = listOf(buttonZL, buttonZR)
val allButtons = circularButtons + joysticks + rectangularButtons + triggerButtons
/**
* All buttons except the joysticks
*/
val buttons = circularButtons + rectangularButtons + triggerButtons
var allButtons = getCurrentButtons()
private set
fun setStickRegions(enabled : Boolean) {
joysticks = if (enabled) joystickRegions else joystickButtons
allButtons = getCurrentButtons()
}
private fun getCurrentButtons() = buttons + joysticks
}

View File

@ -31,6 +31,7 @@ class AppSettings @Inject constructor(@ApplicationContext private val context :
var onScreenControlFeedback by sharedPreferences(context, true)
var onScreenControlRecenterSticks by sharedPreferences(context, true)
var onScreenControlSnapToGrid by sharedPreferences(context, false)
var onScreenControlUseStickRegions by sharedPreferences(context, false)
// Other
var refreshRequired by sharedPreferences(context, false)

View File

@ -176,6 +176,8 @@
<string name="confirm">Confirm</string>
<string name="cancel">Cancel</string>
<string name="osc_recenter_sticks">Recenter Sticks On Touch</string>
<string name="osc_use_stick_regions">Use Stick Regions</string>
<string name="osc_use_stick_regions_desc">When enabled, the stick activaation radius is extended to the corresponding half of the screen.</string>
<string name="controller">Controller</string>
<string name="config_controller">Configure Controller</string>
<string name="controller_type">Controller Type</string>