2020-10-03 12:09:35 +02:00
|
|
|
/*
|
|
|
|
* SPDX-License-Identifier: MPL-2.0
|
|
|
|
* Copyright © 2020 Skyline Team and Contributors (https://github.com/skyline-emu/)
|
|
|
|
*/
|
|
|
|
|
|
|
|
package emu.skyline.input.onscreen
|
|
|
|
|
|
|
|
import android.animation.Animator
|
|
|
|
import android.animation.AnimatorListenerAdapter
|
|
|
|
import android.animation.ValueAnimator
|
|
|
|
import android.content.Context
|
2022-11-21 23:09:53 +01:00
|
|
|
import android.content.Context.VIBRATOR_MANAGER_SERVICE
|
|
|
|
import android.content.Context.VIBRATOR_SERVICE
|
2020-10-03 12:09:35 +02:00
|
|
|
import android.graphics.Canvas
|
|
|
|
import android.graphics.PointF
|
2022-11-21 23:09:53 +01:00
|
|
|
import android.os.Build
|
|
|
|
import android.os.VibrationEffect
|
|
|
|
import android.os.Vibrator
|
|
|
|
import android.os.VibratorManager
|
2020-10-03 12:09:35 +02:00
|
|
|
import android.util.AttributeSet
|
|
|
|
import android.view.MotionEvent
|
|
|
|
import android.view.View
|
|
|
|
import android.view.View.OnTouchListener
|
|
|
|
import emu.skyline.input.ButtonId
|
|
|
|
import emu.skyline.input.ButtonState
|
2021-04-30 21:38:13 +02:00
|
|
|
import emu.skyline.input.ControllerType
|
|
|
|
import emu.skyline.input.StickId
|
2020-10-04 22:29:50 +02:00
|
|
|
import emu.skyline.utils.add
|
|
|
|
import emu.skyline.utils.multiply
|
|
|
|
import emu.skyline.utils.normalize
|
2020-10-03 12:09:35 +02:00
|
|
|
import kotlin.math.roundToLong
|
|
|
|
|
|
|
|
typealias OnButtonStateChangedListener = (buttonId : ButtonId, state : ButtonState) -> Unit
|
2021-04-30 21:38:13 +02:00
|
|
|
typealias OnStickStateChangedListener = (stickId : StickId, position : PointF) -> Unit
|
2020-10-03 12:09:35 +02:00
|
|
|
|
2020-10-05 12:04:57 +02:00
|
|
|
/**
|
|
|
|
* Renders On-Screen Controls as a single view, handles touch inputs and button toggling
|
|
|
|
*/
|
|
|
|
class OnScreenControllerView @JvmOverloads constructor(context : Context, attrs : AttributeSet? = null, defStyleAttr : Int = 0, defStyleRes : Int = 0) : View(context, attrs, defStyleAttr, defStyleRes) {
|
2021-04-30 21:38:13 +02:00
|
|
|
companion object {
|
|
|
|
private val controllerTypeMappings = mapOf(*ControllerType.values().map {
|
|
|
|
it to (setOf(*it.buttons) to setOf(*it.sticks))
|
|
|
|
}.toTypedArray())
|
2022-08-20 17:00:40 +02:00
|
|
|
|
|
|
|
private const val SCALE_STEP = 0.05f
|
|
|
|
private const val ALPHA_STEP = 25
|
|
|
|
private val ALPHA_RANGE = 55..255
|
2021-04-30 21:38:13 +02:00
|
|
|
}
|
|
|
|
|
2020-10-03 12:09:35 +02:00
|
|
|
private val controls = Controls(this)
|
|
|
|
private var onButtonStateChangedListener : OnButtonStateChangedListener? = null
|
|
|
|
private var onStickStateChangedListener : OnStickStateChangedListener? = null
|
|
|
|
private val joystickAnimators = mutableMapOf<JoystickButton, Animator?>()
|
2021-04-30 21:38:13 +02:00
|
|
|
var controllerType : ControllerType? = null
|
|
|
|
set(value) {
|
|
|
|
field = value
|
|
|
|
invalidate()
|
|
|
|
}
|
2020-10-04 22:29:50 +02:00
|
|
|
var recenterSticks = false
|
|
|
|
set(value) {
|
|
|
|
field = value
|
|
|
|
controls.joysticks.forEach { it.recenterSticks = recenterSticks }
|
|
|
|
}
|
2022-11-21 23:09:53 +01:00
|
|
|
var hapticFeedback = false
|
|
|
|
set(value) {
|
|
|
|
field = value
|
|
|
|
(controls.circularButtons + controls.rectangularButtons + controls.triggerButtons).forEach { it.hapticFeedback = hapticFeedback }
|
|
|
|
}
|
2022-11-26 01:43:51 +01:00
|
|
|
private val vibrator: Vibrator =
|
|
|
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
|
|
|
(context.getSystemService(VIBRATOR_MANAGER_SERVICE) as VibratorManager).defaultVibrator
|
|
|
|
} else {
|
|
|
|
@Suppress("DEPRECATION") (context.getSystemService(VIBRATOR_SERVICE) as Vibrator)
|
|
|
|
}
|
|
|
|
private val effectClick = VibrationEffect.createPredefined(VibrationEffect.EFFECT_CLICK)
|
2020-10-03 12:09:35 +02:00
|
|
|
|
|
|
|
override fun onDraw(canvas : Canvas) {
|
|
|
|
super.onDraw(canvas)
|
|
|
|
|
2021-04-30 21:38:13 +02:00
|
|
|
val allowedIds = controllerTypeMappings[controllerType]
|
|
|
|
controls.allButtons.forEach { button ->
|
|
|
|
if (button.config.enabled
|
|
|
|
&& allowedIds?.let { (buttonIds, stickIds) ->
|
|
|
|
if (button is JoystickButton) stickIds.contains(button.stickId) else buttonIds.contains(button.buttonId)
|
|
|
|
} != false
|
|
|
|
) {
|
|
|
|
button.width = width
|
|
|
|
button.height = height
|
|
|
|
button.render(canvas)
|
2020-10-03 19:20:36 +02:00
|
|
|
}
|
2020-10-03 12:09:35 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private val playingTouchHandler = OnTouchListener { _, event ->
|
|
|
|
var handled = false
|
|
|
|
val actionIndex = event.actionIndex
|
|
|
|
val pointerId = event.getPointerId(actionIndex)
|
|
|
|
val x by lazy { event.getX(actionIndex) }
|
|
|
|
val y by lazy { event.getY(actionIndex) }
|
|
|
|
|
|
|
|
(controls.circularButtons + controls.rectangularButtons + controls.triggerButtons).forEach { button ->
|
|
|
|
when (event.action and event.actionMasked) {
|
|
|
|
MotionEvent.ACTION_UP,
|
|
|
|
MotionEvent.ACTION_POINTER_UP -> {
|
|
|
|
if (pointerId == button.touchPointerId) {
|
|
|
|
button.touchPointerId = -1
|
|
|
|
button.onFingerUp(x, y)
|
|
|
|
onButtonStateChangedListener?.invoke(button.buttonId, ButtonState.Released)
|
|
|
|
handled = true
|
2021-02-05 00:04:03 +01:00
|
|
|
} else if (pointerId == button.partnerPointerId) {
|
|
|
|
button.partnerPointerId = -1
|
|
|
|
button.onFingerUp(x, y)
|
|
|
|
onButtonStateChangedListener?.invoke(button.buttonId, ButtonState.Released)
|
|
|
|
handled = true
|
2020-10-03 12:09:35 +02:00
|
|
|
}
|
|
|
|
}
|
2021-02-05 00:04:03 +01:00
|
|
|
|
2020-10-03 12:09:35 +02:00
|
|
|
MotionEvent.ACTION_DOWN,
|
|
|
|
MotionEvent.ACTION_POINTER_DOWN -> {
|
2020-10-03 19:20:36 +02:00
|
|
|
if (button.config.enabled && button.isTouched(x, y)) {
|
2020-10-03 12:09:35 +02:00
|
|
|
button.touchPointerId = pointerId
|
|
|
|
button.onFingerDown(x, y)
|
2022-11-26 01:43:51 +01:00
|
|
|
if (hapticFeedback) vibrator.vibrate(effectClick)
|
2020-10-03 12:09:35 +02:00
|
|
|
performClick()
|
|
|
|
onButtonStateChangedListener?.invoke(button.buttonId, ButtonState.Pressed)
|
|
|
|
handled = true
|
|
|
|
}
|
|
|
|
}
|
2021-02-05 00:04:03 +01:00
|
|
|
|
|
|
|
MotionEvent.ACTION_MOVE -> {
|
2021-02-05 17:37:05 +01:00
|
|
|
for (fingerId in 0 until event.pointerCount) {
|
|
|
|
if (fingerId == button.touchPointerId) {
|
|
|
|
for (buttonPair in controls.buttonPairs) {
|
|
|
|
if (buttonPair.contains(button)) {
|
|
|
|
for (otherButton in buttonPair) {
|
|
|
|
if (otherButton != button && otherButton.config.enabled && otherButton.isTouched(event.getX(fingerId), event.getY(fingerId))) {
|
|
|
|
otherButton.partnerPointerId = fingerId
|
|
|
|
otherButton.onFingerDown(x, y)
|
|
|
|
performClick()
|
|
|
|
onButtonStateChangedListener?.invoke(otherButton.buttonId, ButtonState.Pressed)
|
|
|
|
handled = true
|
|
|
|
}
|
2021-02-05 00:04:03 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2020-10-03 12:09:35 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
for (joystick in controls.joysticks) {
|
|
|
|
when (event.actionMasked) {
|
|
|
|
MotionEvent.ACTION_UP,
|
|
|
|
MotionEvent.ACTION_POINTER_UP,
|
|
|
|
MotionEvent.ACTION_CANCEL -> {
|
|
|
|
if (pointerId == joystick.touchPointerId) {
|
|
|
|
joystick.touchPointerId = -1
|
|
|
|
|
|
|
|
val position = PointF(joystick.currentX, joystick.currentY)
|
|
|
|
val radius = joystick.radius
|
|
|
|
val outerToInner = joystick.outerToInner()
|
|
|
|
val outerToInnerLength = outerToInner.length()
|
|
|
|
val direction = outerToInner.normalize()
|
2020-10-03 12:10:16 +02:00
|
|
|
val duration = (50f * outerToInnerLength / radius).roundToLong()
|
2020-10-03 12:09:35 +02:00
|
|
|
joystickAnimators[joystick] = ValueAnimator.ofFloat(outerToInnerLength, 0f).apply {
|
|
|
|
addUpdateListener { animation ->
|
|
|
|
val value = animation.animatedValue as Float
|
|
|
|
val vector = direction.multiply(value)
|
|
|
|
val newPosition = position.add(vector)
|
2020-10-04 22:29:50 +02:00
|
|
|
joystick.onFingerMoved(newPosition.x, newPosition.y, false)
|
2021-04-30 21:38:13 +02:00
|
|
|
onStickStateChangedListener?.invoke(joystick.stickId, vector.multiply(1f / radius))
|
2020-10-03 12:09:35 +02:00
|
|
|
invalidate()
|
|
|
|
}
|
|
|
|
addListener(object : AnimatorListenerAdapter() {
|
2022-08-17 12:16:26 +02:00
|
|
|
override fun onAnimationCancel(animation : Animator) {
|
2020-10-03 12:09:35 +02:00
|
|
|
super.onAnimationCancel(animation)
|
|
|
|
onAnimationEnd(animation)
|
2021-04-30 21:38:13 +02:00
|
|
|
onStickStateChangedListener?.invoke(joystick.stickId, PointF(0f, 0f))
|
2020-10-03 12:09:35 +02:00
|
|
|
}
|
|
|
|
|
2022-08-17 12:16:26 +02:00
|
|
|
override fun onAnimationEnd(animation : Animator) {
|
2020-10-03 12:09:35 +02:00
|
|
|
super.onAnimationEnd(animation)
|
2020-10-03 12:10:16 +02:00
|
|
|
if (joystick.shortDoubleTapped)
|
|
|
|
onButtonStateChangedListener?.invoke(joystick.buttonId, ButtonState.Released)
|
2020-10-03 12:09:35 +02:00
|
|
|
joystick.onFingerUp(event.x, event.y)
|
|
|
|
invalidate()
|
|
|
|
}
|
|
|
|
})
|
|
|
|
setDuration(duration)
|
|
|
|
start()
|
|
|
|
}
|
|
|
|
handled = true
|
|
|
|
}
|
|
|
|
}
|
2021-02-05 00:04:03 +01:00
|
|
|
|
2020-10-03 12:09:35 +02:00
|
|
|
MotionEvent.ACTION_DOWN,
|
|
|
|
MotionEvent.ACTION_POINTER_DOWN -> {
|
2020-10-03 19:20:36 +02:00
|
|
|
if (joystick.config.enabled && joystick.isTouched(x, y)) {
|
2020-10-03 12:09:35 +02:00
|
|
|
joystickAnimators[joystick]?.cancel()
|
|
|
|
joystickAnimators[joystick] = null
|
|
|
|
joystick.touchPointerId = pointerId
|
|
|
|
joystick.onFingerDown(x, y)
|
2020-10-03 12:10:16 +02:00
|
|
|
if (joystick.shortDoubleTapped)
|
|
|
|
onButtonStateChangedListener?.invoke(joystick.buttonId, ButtonState.Pressed)
|
2020-10-04 22:29:50 +02:00
|
|
|
if (recenterSticks)
|
2021-04-30 21:38:13 +02:00
|
|
|
onStickStateChangedListener?.invoke(joystick.stickId, joystick.outerToInnerRelative())
|
2020-10-03 12:09:35 +02:00
|
|
|
performClick()
|
|
|
|
handled = true
|
|
|
|
}
|
|
|
|
}
|
2021-02-05 00:04:03 +01:00
|
|
|
|
2020-10-03 12:09:35 +02:00
|
|
|
MotionEvent.ACTION_MOVE -> {
|
|
|
|
for (i in 0 until event.pointerCount) {
|
|
|
|
if (event.getPointerId(i) == joystick.touchPointerId) {
|
|
|
|
val centerToPoint = joystick.onFingerMoved(event.getX(i), event.getY(i))
|
2021-04-30 21:38:13 +02:00
|
|
|
onStickStateChangedListener?.invoke(joystick.stickId, centerToPoint)
|
2020-10-03 12:09:35 +02:00
|
|
|
handled = true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2020-10-04 22:29:50 +02:00
|
|
|
handled.also { if (it) invalidate() }
|
2020-10-03 12:09:35 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
private val editingTouchHandler = OnTouchListener { _, event ->
|
2020-10-03 19:20:36 +02:00
|
|
|
controls.allButtons.any { button ->
|
2020-10-03 12:09:35 +02:00
|
|
|
when (event.actionMasked) {
|
|
|
|
MotionEvent.ACTION_UP,
|
|
|
|
MotionEvent.ACTION_POINTER_UP,
|
|
|
|
MotionEvent.ACTION_CANCEL -> {
|
|
|
|
if (button.isEditing) {
|
|
|
|
button.endEdit()
|
|
|
|
return@any true
|
|
|
|
}
|
|
|
|
}
|
2021-02-05 00:04:03 +01:00
|
|
|
|
2020-10-03 12:09:35 +02:00
|
|
|
MotionEvent.ACTION_DOWN,
|
|
|
|
MotionEvent.ACTION_POINTER_DOWN -> {
|
2020-10-03 19:20:36 +02:00
|
|
|
if (button.config.enabled && button.isTouched(event.x, event.y)) {
|
2020-10-03 12:09:35 +02:00
|
|
|
button.startEdit()
|
|
|
|
performClick()
|
|
|
|
return@any true
|
|
|
|
}
|
|
|
|
}
|
2021-02-05 00:04:03 +01:00
|
|
|
|
2020-10-03 12:09:35 +02:00
|
|
|
MotionEvent.ACTION_MOVE -> {
|
|
|
|
if (button.isEditing) {
|
|
|
|
button.edit(event.x, event.y)
|
|
|
|
return@any true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
false
|
2020-10-04 22:29:50 +02:00
|
|
|
}.also { handled -> if (handled) invalidate() }
|
2020-10-03 12:09:35 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
init {
|
|
|
|
setOnTouchListener(playingTouchHandler)
|
|
|
|
}
|
|
|
|
|
|
|
|
fun setEditMode(editMode : Boolean) = setOnTouchListener(if (editMode) editingTouchHandler else playingTouchHandler)
|
|
|
|
|
|
|
|
fun resetControls() {
|
2020-10-03 19:20:36 +02:00
|
|
|
controls.allButtons.forEach {
|
|
|
|
it.resetRelativeValues()
|
|
|
|
it.config.enabled = true
|
|
|
|
}
|
2021-02-05 20:51:15 +01:00
|
|
|
controls.globalScale = 1.15f
|
2022-08-31 23:18:24 +02:00
|
|
|
controls.alpha = 255
|
2020-10-03 12:09:35 +02:00
|
|
|
invalidate()
|
|
|
|
}
|
|
|
|
|
|
|
|
fun increaseScale() {
|
2022-08-20 17:00:40 +02:00
|
|
|
controls.globalScale += SCALE_STEP
|
2020-10-03 12:09:35 +02:00
|
|
|
invalidate()
|
|
|
|
}
|
|
|
|
|
|
|
|
fun decreaseScale() {
|
2022-08-20 17:00:40 +02:00
|
|
|
controls.globalScale -= SCALE_STEP
|
2020-10-03 12:09:35 +02:00
|
|
|
invalidate()
|
|
|
|
}
|
|
|
|
|
2022-08-20 17:00:40 +02:00
|
|
|
fun increaseOpacity() {
|
|
|
|
controls.alpha = (controls.alpha + ALPHA_STEP).coerceIn(ALPHA_RANGE)
|
|
|
|
invalidate()
|
|
|
|
}
|
|
|
|
|
|
|
|
fun decreaseOpacity() {
|
|
|
|
controls.alpha = (controls.alpha - ALPHA_STEP).coerceIn(ALPHA_RANGE)
|
2022-07-31 17:35:28 +02:00
|
|
|
invalidate()
|
|
|
|
}
|
|
|
|
|
2022-08-04 01:25:29 +02:00
|
|
|
fun getTextColor() : Int {
|
|
|
|
return controls.globalTextColor
|
|
|
|
}
|
|
|
|
|
|
|
|
fun getBackGroundColor() : Int {
|
|
|
|
return controls.globalBackgroundColor
|
|
|
|
}
|
|
|
|
|
2020-10-03 12:09:35 +02:00
|
|
|
fun setOnButtonStateChangedListener(listener : OnButtonStateChangedListener) {
|
|
|
|
onButtonStateChangedListener = listener
|
|
|
|
}
|
|
|
|
|
|
|
|
fun setOnStickStateChangedListener(listener : OnStickStateChangedListener) {
|
|
|
|
onStickStateChangedListener = listener
|
|
|
|
}
|
2020-10-03 19:20:36 +02:00
|
|
|
|
|
|
|
fun getButtonProps() = controls.allButtons.map { Pair(it.buttonId, it.config.enabled) }
|
|
|
|
|
|
|
|
fun setButtonEnabled(buttonId : ButtonId, enabled : Boolean) {
|
|
|
|
controls.allButtons.first { it.buttonId == buttonId }.config.enabled = enabled
|
|
|
|
invalidate()
|
|
|
|
}
|
2022-08-04 01:25:29 +02:00
|
|
|
|
|
|
|
fun setTextColor(color : Int) {
|
|
|
|
for (button in controls.allButtons) {
|
|
|
|
button.config.textColor = color
|
|
|
|
}
|
|
|
|
invalidate()
|
|
|
|
}
|
|
|
|
|
|
|
|
fun setBackGroundColor(color : Int) {
|
|
|
|
for (button in controls.allButtons) {
|
|
|
|
button.config.backgroundColor = color
|
|
|
|
}
|
|
|
|
invalidate()
|
|
|
|
}
|
2020-10-05 12:04:57 +02:00
|
|
|
}
|