mirror of
https://github.com/skyline-emu/skyline.git
synced 2024-12-28 22:15:29 +03:00
Add on screen controls
* Fix missing default constructor for dialog fragments
This commit is contained in:
parent
85d5dd3619
commit
3057e4b29a
@ -10,6 +10,7 @@
|
|||||||
android:required="true" />
|
android:required="true" />
|
||||||
<uses-permission android:name="android.permission.VIBRATE" />
|
<uses-permission android:name="android.permission.VIBRATE" />
|
||||||
<application
|
<application
|
||||||
|
android:name=".SkylineApplication"
|
||||||
android:allowBackup="true"
|
android:allowBackup="true"
|
||||||
android:extractNativeLibs="true"
|
android:extractNativeLibs="true"
|
||||||
android:fullBackupContent="@xml/backup_descriptor"
|
android:fullBackupContent="@xml/backup_descriptor"
|
||||||
|
@ -8,11 +8,13 @@ package emu.skyline
|
|||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.graphics.PointF
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.*
|
import android.os.*
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.view.*
|
import android.view.*
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import androidx.core.view.isInvisible
|
||||||
import androidx.preference.PreferenceManager
|
import androidx.preference.PreferenceManager
|
||||||
import emu.skyline.input.*
|
import emu.skyline.input.*
|
||||||
import emu.skyline.loader.getRomFormat
|
import emu.skyline.loader.getRomFormat
|
||||||
@ -22,18 +24,13 @@ import kotlin.math.abs
|
|||||||
|
|
||||||
class EmulationActivity : AppCompatActivity(), SurfaceHolder.Callback, View.OnTouchListener {
|
class EmulationActivity : AppCompatActivity(), SurfaceHolder.Callback, View.OnTouchListener {
|
||||||
companion object {
|
companion object {
|
||||||
private val Tag = EmulationActivity::class.java.name
|
private val Tag = EmulationActivity::class.java.simpleName
|
||||||
}
|
}
|
||||||
|
|
||||||
init {
|
init {
|
||||||
System.loadLibrary("skyline") // libskyline.so
|
System.loadLibrary("skyline") // libskyline.so
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* The [InputManager] class handles loading/saving the input data
|
|
||||||
*/
|
|
||||||
private lateinit var input : InputManager
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A map of [Vibrator]s that correspond to [InputManager.controllers]
|
* A map of [Vibrator]s that correspond to [InputManager.controllers]
|
||||||
*/
|
*/
|
||||||
@ -144,12 +141,11 @@ class EmulationActivity : AppCompatActivity(), SurfaceHolder.Callback, View.OnTo
|
|||||||
private external fun setTouchState(points : IntArray)
|
private external fun setTouchState(points : IntArray)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This initializes all of the controllers from [input] on the guest
|
* This initializes all of the controllers from [InputManager] on the guest
|
||||||
*/
|
*/
|
||||||
|
@Suppress("unused")
|
||||||
private fun initializeControllers() {
|
private fun initializeControllers() {
|
||||||
for (entry in input.controllers) {
|
for (controller in InputManager.controllers.values) {
|
||||||
val controller = entry.value
|
|
||||||
|
|
||||||
if (controller.type != ControllerType.None) {
|
if (controller.type != ControllerType.None) {
|
||||||
val type = when (controller.type) {
|
val type = when (controller.type) {
|
||||||
ControllerType.None -> throw IllegalArgumentException()
|
ControllerType.None -> throw IllegalArgumentException()
|
||||||
@ -163,7 +159,7 @@ class EmulationActivity : AppCompatActivity(), SurfaceHolder.Callback, View.OnTo
|
|||||||
else -> null
|
else -> null
|
||||||
}
|
}
|
||||||
|
|
||||||
setController(entry.key, type, partnerIndex ?: -1)
|
setController(controller.id, type, partnerIndex ?: -1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -214,8 +210,6 @@ class EmulationActivity : AppCompatActivity(), SurfaceHolder.Callback, View.OnTo
|
|||||||
or View.SYSTEM_UI_FLAG_FULLSCREEN)
|
or View.SYSTEM_UI_FLAG_FULLSCREEN)
|
||||||
}
|
}
|
||||||
|
|
||||||
input = InputManager(this)
|
|
||||||
|
|
||||||
game_view.holder.addCallback(this)
|
game_view.holder.addCallback(this)
|
||||||
|
|
||||||
val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this)
|
val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this)
|
||||||
@ -236,6 +230,11 @@ class EmulationActivity : AppCompatActivity(), SurfaceHolder.Callback, View.OnTo
|
|||||||
|
|
||||||
game_view.setOnTouchListener(this)
|
game_view.setOnTouchListener(this)
|
||||||
|
|
||||||
|
// Hide on screen controls when first controller is not set
|
||||||
|
on_screen_controller_view.isInvisible = InputManager.controllers[0]!!.type == ControllerType.None
|
||||||
|
on_screen_controller_view.setOnButtonStateChangedListener(::onButtonStateChanged)
|
||||||
|
on_screen_controller_view.setOnStickStateChangedListener(::onStickStateChanged)
|
||||||
|
|
||||||
executeApplication(intent.data!!)
|
executeApplication(intent.data!!)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -310,7 +309,7 @@ class EmulationActivity : AppCompatActivity(), SurfaceHolder.Callback, View.OnTo
|
|||||||
else -> return super.dispatchKeyEvent(event)
|
else -> return super.dispatchKeyEvent(event)
|
||||||
}
|
}
|
||||||
|
|
||||||
return when (val guestEvent = input.eventMap[KeyHostEvent(event.device.descriptor, event.keyCode)]) {
|
return when (val guestEvent = InputManager.eventMap[KeyHostEvent(event.device.descriptor, event.keyCode)]) {
|
||||||
is ButtonGuestEvent -> {
|
is ButtonGuestEvent -> {
|
||||||
if (guestEvent.button != ButtonId.Menu)
|
if (guestEvent.button != ButtonId.Menu)
|
||||||
setButtonState(guestEvent.id, guestEvent.button.value(), action.state)
|
setButtonState(guestEvent.id, guestEvent.button.value(), action.state)
|
||||||
@ -334,14 +333,14 @@ class EmulationActivity : AppCompatActivity(), SurfaceHolder.Callback, View.OnTo
|
|||||||
/**
|
/**
|
||||||
* The last value of the HAT axes so it can be ignored in [onGenericMotionEvent] so they are handled by [dispatchKeyEvent] instead
|
* The last value of the HAT axes so it can be ignored in [onGenericMotionEvent] so they are handled by [dispatchKeyEvent] instead
|
||||||
*/
|
*/
|
||||||
private var oldHat = Pair(0.0f, 0.0f)
|
private var oldHat = PointF()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This handles translating any [MotionHostEvent]s to a [GuestEvent] that is passed into libskyline
|
* This handles translating any [MotionHostEvent]s to a [GuestEvent] that is passed into libskyline
|
||||||
*/
|
*/
|
||||||
override fun onGenericMotionEvent(event : MotionEvent) : Boolean {
|
override fun onGenericMotionEvent(event : MotionEvent) : Boolean {
|
||||||
if ((event.isFromSource(InputDevice.SOURCE_CLASS_JOYSTICK) || event.isFromSource(InputDevice.SOURCE_CLASS_BUTTON)) && event.action == MotionEvent.ACTION_MOVE) {
|
if ((event.isFromSource(InputDevice.SOURCE_CLASS_JOYSTICK) || event.isFromSource(InputDevice.SOURCE_CLASS_BUTTON)) && event.action == MotionEvent.ACTION_MOVE) {
|
||||||
val hat = Pair(event.getAxisValue(MotionEvent.AXIS_HAT_X), event.getAxisValue(MotionEvent.AXIS_HAT_Y))
|
val hat = PointF(event.getAxisValue(MotionEvent.AXIS_HAT_X), event.getAxisValue(MotionEvent.AXIS_HAT_Y))
|
||||||
|
|
||||||
if (hat == oldHat) {
|
if (hat == oldHat) {
|
||||||
for (axisItem in MotionHostEvent.axes.withIndex()) {
|
for (axisItem in MotionHostEvent.axes.withIndex()) {
|
||||||
@ -351,12 +350,14 @@ class EmulationActivity : AppCompatActivity(), SurfaceHolder.Callback, View.OnTo
|
|||||||
if ((event.historySize != 0 && value != event.getHistoricalAxisValue(axis, 0)) || (axesHistory[axisItem.index]?.let { it == value } == false)) {
|
if ((event.historySize != 0 && value != event.getHistoricalAxisValue(axis, 0)) || (axesHistory[axisItem.index]?.let { it == value } == false)) {
|
||||||
var polarity = value >= 0
|
var polarity = value >= 0
|
||||||
|
|
||||||
val guestEvent = input.eventMap[MotionHostEvent(event.device.descriptor, axis, polarity)] ?: if (value == 0f) {
|
val guestEvent = MotionHostEvent(event.device.descriptor, axis, polarity).let { hostEvent ->
|
||||||
|
InputManager.eventMap[hostEvent] ?: if (value == 0f) {
|
||||||
polarity = false
|
polarity = false
|
||||||
input.eventMap[MotionHostEvent(event.device.descriptor, axis, polarity)]
|
InputManager.eventMap[hostEvent.copy(polarity = false)]
|
||||||
} else {
|
} else {
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
when (guestEvent) {
|
when (guestEvent) {
|
||||||
is ButtonGuestEvent -> {
|
is ButtonGuestEvent -> {
|
||||||
@ -410,12 +411,30 @@ class EmulationActivity : AppCompatActivity(), SurfaceHolder.Callback, View.OnTo
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun onButtonStateChanged(buttonId : ButtonId, state : ButtonState) {
|
||||||
|
setButtonState(0, buttonId.value(), state.state)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onStickStateChanged(buttonId : ButtonId, position : PointF) {
|
||||||
|
val stickId = when (buttonId) {
|
||||||
|
ButtonId.LeftStick -> StickId.Left
|
||||||
|
|
||||||
|
ButtonId.RightStick -> StickId.Right
|
||||||
|
|
||||||
|
else -> error("Invalid button id")
|
||||||
|
}
|
||||||
|
Log.i("blaa", "$position")
|
||||||
|
setAxisValue(0, stickId.xAxis.ordinal, (position.x * Short.MAX_VALUE).toInt())
|
||||||
|
setAxisValue(0, stickId.yAxis.ordinal, (-position.y * Short.MAX_VALUE).toInt()) // Y is inverted
|
||||||
|
}
|
||||||
|
|
||||||
@SuppressLint("WrongConstant")
|
@SuppressLint("WrongConstant")
|
||||||
|
@Suppress("unused")
|
||||||
fun vibrateDevice(index : Int, timing : LongArray, amplitude : IntArray) {
|
fun vibrateDevice(index : Int, timing : LongArray, amplitude : IntArray) {
|
||||||
val vibrator = if (vibrators[index] != null) {
|
val vibrator = if (vibrators[index] != null) {
|
||||||
vibrators[index]!!
|
vibrators[index]!!
|
||||||
} else {
|
} else {
|
||||||
input.controllers[index]?.rumbleDeviceDescriptor?.let {
|
InputManager.controllers[index]!!.rumbleDeviceDescriptor?.let {
|
||||||
if (it == "builtin") {
|
if (it == "builtin") {
|
||||||
val vibrator = getSystemService(Context.VIBRATOR_SERVICE) as Vibrator
|
val vibrator = getSystemService(Context.VIBRATOR_SERVICE) as Vibrator
|
||||||
vibrators[index] = vibrator
|
vibrators[index] = vibrator
|
||||||
@ -423,7 +442,7 @@ class EmulationActivity : AppCompatActivity(), SurfaceHolder.Callback, View.OnTo
|
|||||||
} else {
|
} else {
|
||||||
for (id in InputDevice.getDeviceIds()) {
|
for (id in InputDevice.getDeviceIds()) {
|
||||||
val device = InputDevice.getDevice(id)
|
val device = InputDevice.getDevice(id)
|
||||||
if (device.descriptor == input.controllers[index]?.rumbleDeviceDescriptor) {
|
if (device.descriptor == InputManager.controllers[index]!!.rumbleDeviceDescriptor) {
|
||||||
vibrators[index] = device.vibrator
|
vibrators[index] = device.vibrator
|
||||||
device.vibrator
|
device.vibrator
|
||||||
}
|
}
|
||||||
@ -437,6 +456,7 @@ class EmulationActivity : AppCompatActivity(), SurfaceHolder.Callback, View.OnTo
|
|||||||
vibrator.vibrate(effect)
|
vibrator.vibrate(effect)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Suppress("unused")
|
||||||
fun clearVibrationDevice(index : Int) {
|
fun clearVibrationDevice(index : Int) {
|
||||||
vibrators[index]?.cancel()
|
vibrators[index]?.cancel()
|
||||||
}
|
}
|
||||||
|
@ -137,7 +137,7 @@ class LogActivity : AppCompatActivity() {
|
|||||||
private fun uploadAndShareLog() {
|
private fun uploadAndShareLog() {
|
||||||
Snackbar.make(findViewById(android.R.id.content), getString(R.string.upload_logs), Snackbar.LENGTH_SHORT).show()
|
Snackbar.make(findViewById(android.R.id.content), getString(R.string.upload_logs), Snackbar.LENGTH_SHORT).show()
|
||||||
|
|
||||||
val shareThread = Thread(Runnable {
|
val shareThread = Thread {
|
||||||
var urlConnection : HttpsURLConnection? = null
|
var urlConnection : HttpsURLConnection? = null
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@ -173,7 +173,7 @@ class LogActivity : AppCompatActivity() {
|
|||||||
} finally {
|
} finally {
|
||||||
urlConnection!!.disconnect()
|
urlConnection!!.disconnect()
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
|
|
||||||
shareThread.start()
|
shareThread.start()
|
||||||
}
|
}
|
||||||
|
@ -7,7 +7,6 @@ package emu.skyline
|
|||||||
|
|
||||||
import android.animation.ObjectAnimator
|
import android.animation.ObjectAnimator
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.SharedPreferences
|
|
||||||
import android.content.pm.ActivityInfo
|
import android.content.pm.ActivityInfo
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
@ -45,15 +44,19 @@ class MainActivity : AppCompatActivity() {
|
|||||||
/**
|
/**
|
||||||
* This is used to get/set shared preferences
|
* This is used to get/set shared preferences
|
||||||
*/
|
*/
|
||||||
private lateinit var sharedPreferences : SharedPreferences
|
private val sharedPreferences by lazy { PreferenceManager.getDefaultSharedPreferences(this) }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The adapter used for adding elements to [app_list]
|
* The adapter used for adding elements to [app_list]
|
||||||
*/
|
*/
|
||||||
private lateinit var adapter : AppAdapter
|
private val adapter by lazy {
|
||||||
|
AppAdapter(layoutType = layoutType, onClick = ::selectStartGame, onLongClick = ::selectShowGameDialog)
|
||||||
|
}
|
||||||
|
|
||||||
private var reloading = AtomicBoolean()
|
private var reloading = AtomicBoolean()
|
||||||
|
|
||||||
|
private val layoutType get() = LayoutType.values()[sharedPreferences.getString("layout_type", "1")!!.toInt()]
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This adds all files in [directory] with [extension] as an entry in [adapter] using [RomFile] to load metadata
|
* This adds all files in [directory] with [extension] as an entry in [adapter] using [RomFile] to load metadata
|
||||||
*/
|
*/
|
||||||
@ -160,7 +163,6 @@ class MainActivity : AppCompatActivity() {
|
|||||||
setSupportActionBar(toolbar)
|
setSupportActionBar(toolbar)
|
||||||
|
|
||||||
PreferenceManager.setDefaultValues(this, R.xml.preferences, false)
|
PreferenceManager.setDefaultValues(this, R.xml.preferences, false)
|
||||||
sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this)
|
|
||||||
|
|
||||||
AppCompatDelegate.setDefaultNightMode(when ((sharedPreferences.getString("app_theme", "2")?.toInt())) {
|
AppCompatDelegate.setDefaultNightMode(when ((sharedPreferences.getString("app_theme", "2")?.toInt())) {
|
||||||
0 -> AppCompatDelegate.MODE_NIGHT_NO
|
0 -> AppCompatDelegate.MODE_NIGHT_NO
|
||||||
@ -221,12 +223,8 @@ class MainActivity : AppCompatActivity() {
|
|||||||
val metrics = resources.displayMetrics
|
val metrics = resources.displayMetrics
|
||||||
val gridSpan = ceil((metrics.widthPixels / metrics.density) / itemWidth).toInt()
|
val gridSpan = ceil((metrics.widthPixels / metrics.density) / itemWidth).toInt()
|
||||||
|
|
||||||
val layoutType = LayoutType.values()[sharedPreferences.getString("layout_type", "1")!!.toInt()]
|
|
||||||
|
|
||||||
adapter = AppAdapter(layoutType = layoutType, onClick = ::selectStartGame, onLongClick = ::selectShowGameDialog)
|
|
||||||
app_list.adapter = adapter
|
app_list.adapter = adapter
|
||||||
|
app_list.layoutManager = when (adapter.layoutType) {
|
||||||
app_list.layoutManager = when (layoutType) {
|
|
||||||
LayoutType.List -> LinearLayoutManager(this).also { app_list.addItemDecoration(DividerItemDecoration(this, RecyclerView.VERTICAL)) }
|
LayoutType.List -> LinearLayoutManager(this).also { app_list.addItemDecoration(DividerItemDecoration(this, RecyclerView.VERTICAL)) }
|
||||||
|
|
||||||
LayoutType.Grid, LayoutType.GridCompact -> GridLayoutManager(this, gridSpan).apply {
|
LayoutType.Grid, LayoutType.GridCompact -> GridLayoutManager(this, gridSpan).apply {
|
||||||
@ -340,7 +338,6 @@ class MainActivity : AppCompatActivity() {
|
|||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
super.onResume()
|
super.onResume()
|
||||||
|
|
||||||
val layoutType = LayoutType.values()[sharedPreferences.getString("layout_type", "1")!!.toInt()]
|
|
||||||
if (layoutType != adapter.layoutType) {
|
if (layoutType != adapter.layoutType) {
|
||||||
setupAppList()
|
setupAppList()
|
||||||
}
|
}
|
||||||
|
@ -11,9 +11,7 @@ import android.view.KeyEvent
|
|||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.preference.PreferenceFragmentCompat
|
import androidx.preference.PreferenceFragmentCompat
|
||||||
import androidx.preference.PreferenceGroup
|
import androidx.preference.PreferenceGroup
|
||||||
import emu.skyline.input.InputManager
|
|
||||||
import emu.skyline.preference.ActivityResultDelegate
|
import emu.skyline.preference.ActivityResultDelegate
|
||||||
import emu.skyline.preference.ControllerPreference
|
|
||||||
import emu.skyline.preference.DocumentActivity
|
import emu.skyline.preference.DocumentActivity
|
||||||
import kotlinx.android.synthetic.main.settings_activity.*
|
import kotlinx.android.synthetic.main.settings_activity.*
|
||||||
import kotlinx.android.synthetic.main.titlebar.*
|
import kotlinx.android.synthetic.main.titlebar.*
|
||||||
@ -24,19 +22,12 @@ class SettingsActivity : AppCompatActivity() {
|
|||||||
*/
|
*/
|
||||||
private val preferenceFragment = PreferenceFragment()
|
private val preferenceFragment = PreferenceFragment()
|
||||||
|
|
||||||
/**
|
|
||||||
* This is an instance of [InputManager] used by [ControllerPreference]
|
|
||||||
*/
|
|
||||||
lateinit var inputManager : InputManager
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This initializes all of the elements in the activity and displays the settings fragment
|
* This initializes all of the elements in the activity and displays the settings fragment
|
||||||
*/
|
*/
|
||||||
override fun onCreate(savedInstanceState : Bundle?) {
|
override fun onCreate(savedInstanceState : Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
inputManager = InputManager(this)
|
|
||||||
|
|
||||||
setContentView(R.layout.settings_activity)
|
setContentView(R.layout.settings_activity)
|
||||||
|
|
||||||
setSupportActionBar(toolbar)
|
setSupportActionBar(toolbar)
|
||||||
|
16
app/src/main/java/emu/skyline/SkylineApplication.kt
Normal file
16
app/src/main/java/emu/skyline/SkylineApplication.kt
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
/*
|
||||||
|
* SPDX-License-Identifier: MPL-2.0
|
||||||
|
* Copyright © 2020 Skyline Team and Contributors (https://github.com/skyline-emu/)
|
||||||
|
*/
|
||||||
|
|
||||||
|
package emu.skyline
|
||||||
|
|
||||||
|
import android.app.Application
|
||||||
|
import emu.skyline.input.InputManager
|
||||||
|
|
||||||
|
class SkylineApplication : Application() {
|
||||||
|
override fun onCreate() {
|
||||||
|
super.onCreate()
|
||||||
|
InputManager.init(applicationContext)
|
||||||
|
}
|
||||||
|
}
|
@ -72,7 +72,7 @@ class ControllerGeneralItem(val context : ControllerActivity, val type : General
|
|||||||
* This returns the summary for [type] by using data encapsulated within [Controller]
|
* This returns the summary for [type] by using data encapsulated within [Controller]
|
||||||
*/
|
*/
|
||||||
fun getSummary(context : ControllerActivity, type : GeneralType) : String {
|
fun getSummary(context : ControllerActivity, type : GeneralType) : String {
|
||||||
val controller = context.manager.controllers[context.id]!!
|
val controller = InputManager.controllers[context.id]!!
|
||||||
|
|
||||||
return when (type) {
|
return when (type) {
|
||||||
GeneralType.PartnerJoyCon -> {
|
GeneralType.PartnerJoyCon -> {
|
||||||
@ -105,7 +105,7 @@ class ControllerButtonItem(val context : ControllerActivity, val button : Button
|
|||||||
*/
|
*/
|
||||||
fun getSummary(context : ControllerActivity, button : ButtonId) : String {
|
fun getSummary(context : ControllerActivity, button : ButtonId) : String {
|
||||||
val guestEvent = ButtonGuestEvent(context.id, button)
|
val guestEvent = ButtonGuestEvent(context.id, button)
|
||||||
return context.manager.eventMap.filter { it.value is ButtonGuestEvent && it.value == guestEvent }.keys.firstOrNull()?.toString() ?: context.getString(R.string.none)
|
return InputManager.eventMap.filter { it.value is ButtonGuestEvent && it.value == guestEvent }.keys.firstOrNull()?.toString() ?: context.getString(R.string.none)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -125,19 +125,19 @@ class ControllerStickItem(val context : ControllerActivity, val stick : StickId)
|
|||||||
*/
|
*/
|
||||||
fun getSummary(context : ControllerActivity, stick : StickId) : String {
|
fun getSummary(context : ControllerActivity, stick : StickId) : String {
|
||||||
val buttonGuestEvent = ButtonGuestEvent(context.id, stick.button)
|
val buttonGuestEvent = ButtonGuestEvent(context.id, stick.button)
|
||||||
val button = context.manager.eventMap.filter { it.value is ButtonGuestEvent && it.value == buttonGuestEvent }.keys.firstOrNull()?.toString() ?: context.getString(R.string.none)
|
val button = InputManager.eventMap.filter { it.value is ButtonGuestEvent && it.value == buttonGuestEvent }.keys.firstOrNull()?.toString() ?: context.getString(R.string.none)
|
||||||
|
|
||||||
var axisGuestEvent = AxisGuestEvent(context.id, stick.yAxis, true)
|
var axisGuestEvent = AxisGuestEvent(context.id, stick.yAxis, true)
|
||||||
val yAxisPlus = context.manager.eventMap.filter { it.value is AxisGuestEvent && it.value == axisGuestEvent }.keys.firstOrNull()?.toString() ?: context.getString(R.string.none)
|
val yAxisPlus = InputManager.eventMap.filter { it.value is AxisGuestEvent && it.value == axisGuestEvent }.keys.firstOrNull()?.toString() ?: context.getString(R.string.none)
|
||||||
|
|
||||||
axisGuestEvent = AxisGuestEvent(context.id, stick.yAxis, false)
|
axisGuestEvent = AxisGuestEvent(context.id, stick.yAxis, false)
|
||||||
val yAxisMinus = context.manager.eventMap.filter { it.value is AxisGuestEvent && it.value == axisGuestEvent }.keys.firstOrNull()?.toString() ?: context.getString(R.string.none)
|
val yAxisMinus = InputManager.eventMap.filter { it.value is AxisGuestEvent && it.value == axisGuestEvent }.keys.firstOrNull()?.toString() ?: context.getString(R.string.none)
|
||||||
|
|
||||||
axisGuestEvent = AxisGuestEvent(context.id, stick.xAxis, true)
|
axisGuestEvent = AxisGuestEvent(context.id, stick.xAxis, true)
|
||||||
val xAxisPlus = context.manager.eventMap.filter { it.value is AxisGuestEvent && it.value == axisGuestEvent }.keys.firstOrNull()?.toString() ?: context.getString(R.string.none)
|
val xAxisPlus = InputManager.eventMap.filter { it.value is AxisGuestEvent && it.value == axisGuestEvent }.keys.firstOrNull()?.toString() ?: context.getString(R.string.none)
|
||||||
|
|
||||||
axisGuestEvent = AxisGuestEvent(context.id, stick.xAxis, false)
|
axisGuestEvent = AxisGuestEvent(context.id, stick.xAxis, false)
|
||||||
val xAxisMinus = context.manager.eventMap.filter { it.value is AxisGuestEvent && it.value == axisGuestEvent }.keys.firstOrNull()?.toString() ?: context.getString(R.string.none)
|
val xAxisMinus = InputManager.eventMap.filter { it.value is AxisGuestEvent && it.value == axisGuestEvent }.keys.firstOrNull()?.toString() ?: context.getString(R.string.none)
|
||||||
|
|
||||||
return "${context.getString(R.string.button)}: $button\n${context.getString(R.string.up)}: $yAxisPlus\n${context.getString(R.string.down)}: $yAxisMinus\n${context.getString(R.string.left)}: $xAxisMinus\n${context.getString(R.string.right)}: $xAxisPlus"
|
return "${context.getString(R.string.button)}: $button\n${context.getString(R.string.up)}: $yAxisPlus\n${context.getString(R.string.down)}: $yAxisMinus\n${context.getString(R.string.left)}: $xAxisMinus\n${context.getString(R.string.right)}: $xAxisPlus"
|
||||||
}
|
}
|
||||||
@ -152,7 +152,7 @@ class ControllerStickItem(val context : ControllerActivity, val stick : StickId)
|
|||||||
/**
|
/**
|
||||||
* This adapter is used to create a list which handles having a simple view
|
* This adapter is used to create a list which handles having a simple view
|
||||||
*/
|
*/
|
||||||
class ControllerAdapter(val context : Context) : HeaderAdapter<ControllerItem?, BaseHeader, RecyclerView.ViewHolder>() {
|
class ControllerAdapter(private val onItemClickCallback : (item : ControllerItem) -> Unit) : HeaderAdapter<ControllerItem?, BaseHeader, RecyclerView.ViewHolder>() {
|
||||||
/**
|
/**
|
||||||
* This adds a header to the view with the contents of [string]
|
* This adds a header to the view with the contents of [string]
|
||||||
*/
|
*/
|
||||||
@ -189,24 +189,14 @@ class ControllerAdapter(val context : Context) : HeaderAdapter<ControllerItem?,
|
|||||||
/**
|
/**
|
||||||
* This function creates the view-holder of type [viewType] with the layout parent as [parent]
|
* This function creates the view-holder of type [viewType] with the layout parent as [parent]
|
||||||
*/
|
*/
|
||||||
override fun onCreateViewHolder(parent : ViewGroup, viewType : Int) : RecyclerView.ViewHolder {
|
override fun onCreateViewHolder(parent : ViewGroup, viewType : Int) = when (ElementType.values()[viewType]) {
|
||||||
val inflater = LayoutInflater.from(context)
|
ElementType.Header -> LayoutInflater.from(parent.context).inflate(R.layout.section_item, parent, false).let { view ->
|
||||||
var holder : RecyclerView.ViewHolder? = null
|
HeaderViewHolder(view).apply { header = view.findViewById(R.id.text_title) }
|
||||||
|
|
||||||
if (viewType == ElementType.Item.ordinal) {
|
|
||||||
val view = inflater.inflate(R.layout.controller_item, parent, false)
|
|
||||||
holder = ItemViewHolder(view, view.findViewById(R.id.text_title), view.findViewById(R.id.text_subtitle), view.findViewById(R.id.controller_item))
|
|
||||||
|
|
||||||
if (context is View.OnClickListener)
|
|
||||||
holder.item.setOnClickListener(context as View.OnClickListener)
|
|
||||||
} else if (viewType == ElementType.Header.ordinal) {
|
|
||||||
val view = inflater.inflate(R.layout.section_item, parent, false)
|
|
||||||
holder = HeaderViewHolder(view)
|
|
||||||
|
|
||||||
holder.header = view.findViewById(R.id.text_title)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return holder!!
|
ElementType.Item -> LayoutInflater.from(parent.context).inflate(R.layout.controller_item, parent, false).let { view ->
|
||||||
|
ItemViewHolder(view, view.findViewById(R.id.text_title), view.findViewById(R.id.text_subtitle), view.findViewById(R.id.controller_item))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -221,7 +211,7 @@ class ControllerAdapter(val context : Context) : HeaderAdapter<ControllerItem?,
|
|||||||
holder.title.text = item.content
|
holder.title.text = item.content
|
||||||
holder.subtitle.text = item.subContent
|
holder.subtitle.text = item.subContent
|
||||||
|
|
||||||
holder.parent.tag = item
|
holder.parent.setOnClickListener { onItemClickCallback.invoke(item) }
|
||||||
} else if (item is BaseHeader && holder is HeaderViewHolder) {
|
} else if (item is BaseHeader && holder is HeaderViewHolder) {
|
||||||
holder.header?.text = item.title
|
holder.header?.text = item.title
|
||||||
}
|
}
|
||||||
|
@ -7,7 +7,6 @@ package emu.skyline.input
|
|||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.KeyEvent
|
import android.view.KeyEvent
|
||||||
import android.view.View
|
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||||
@ -22,7 +21,7 @@ import kotlinx.android.synthetic.main.titlebar.*
|
|||||||
/**
|
/**
|
||||||
* This activity is used to change the settings for a specific controller
|
* This activity is used to change the settings for a specific controller
|
||||||
*/
|
*/
|
||||||
class ControllerActivity : AppCompatActivity(), View.OnClickListener {
|
class ControllerActivity : AppCompatActivity() {
|
||||||
/**
|
/**
|
||||||
* The index of the controller this activity manages
|
* The index of the controller this activity manages
|
||||||
*/
|
*/
|
||||||
@ -31,12 +30,7 @@ class ControllerActivity : AppCompatActivity(), View.OnClickListener {
|
|||||||
/**
|
/**
|
||||||
* The adapter used by [controller_list] to hold all the items
|
* The adapter used by [controller_list] to hold all the items
|
||||||
*/
|
*/
|
||||||
val adapter = ControllerAdapter(this)
|
private val adapter = ControllerAdapter(::onControllerItemClick)
|
||||||
|
|
||||||
/**
|
|
||||||
* The [InputManager] class handles loading/saving the input data
|
|
||||||
*/
|
|
||||||
lateinit var manager : InputManager
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This is a map between a button and it's corresponding [ControllerItem] in [adapter]
|
* This is a map between a button and it's corresponding [ControllerItem] in [adapter]
|
||||||
@ -49,12 +43,12 @@ class ControllerActivity : AppCompatActivity(), View.OnClickListener {
|
|||||||
val axisMap = mutableMapOf<AxisId, ControllerStickItem>()
|
val axisMap = mutableMapOf<AxisId, ControllerStickItem>()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This function updates the [adapter] based on information from [manager]
|
* This function updates the [adapter] based on information from [InputManager]
|
||||||
*/
|
*/
|
||||||
private fun update() {
|
private fun update() {
|
||||||
adapter.clear()
|
adapter.clear()
|
||||||
|
|
||||||
val controller = manager.controllers[id]!!
|
val controller = InputManager.controllers[id]!!
|
||||||
|
|
||||||
adapter.addItem(ControllerTypeItem(this, controller.type))
|
adapter.addItem(ControllerTypeItem(this, controller.type))
|
||||||
|
|
||||||
@ -134,8 +128,6 @@ class ControllerActivity : AppCompatActivity(), View.OnClickListener {
|
|||||||
override fun onCreate(state : Bundle?) {
|
override fun onCreate(state : Bundle?) {
|
||||||
super.onCreate(state)
|
super.onCreate(state)
|
||||||
|
|
||||||
manager = InputManager(this)
|
|
||||||
|
|
||||||
id = intent.getIntExtra("index", 0)
|
id = intent.getIntExtra("index", 0)
|
||||||
|
|
||||||
if (id < 0 || id > 7)
|
if (id < 0 || id > 7)
|
||||||
@ -158,32 +150,29 @@ class ControllerActivity : AppCompatActivity(), View.OnClickListener {
|
|||||||
* This causes the input file to be synced when the activity has been paused
|
* This causes the input file to be synced when the activity has been paused
|
||||||
*/
|
*/
|
||||||
override fun onPause() {
|
override fun onPause() {
|
||||||
manager.syncFile()
|
InputManager.syncFile()
|
||||||
super.onPause()
|
super.onPause()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
private fun onControllerItemClick(item : ControllerItem) {
|
||||||
* This handles the onClick events for the items in the activity
|
when (item) {
|
||||||
*/
|
|
||||||
override fun onClick(v : View?) {
|
|
||||||
when (val tag = v!!.tag) {
|
|
||||||
is ControllerTypeItem -> {
|
is ControllerTypeItem -> {
|
||||||
val controller = manager.controllers[id]!!
|
val controller = InputManager.controllers[id]!!
|
||||||
|
|
||||||
val types = ControllerType.values().filter { !it.firstController || id == 0 }
|
val types = ControllerType.values().filter { !it.firstController || id == 0 }
|
||||||
val typeNames = types.map { getString(it.stringRes) }.toTypedArray()
|
val typeNames = types.map { getString(it.stringRes) }.toTypedArray()
|
||||||
|
|
||||||
MaterialAlertDialogBuilder(this)
|
MaterialAlertDialogBuilder(this)
|
||||||
.setTitle(tag.content)
|
.setTitle(item.content)
|
||||||
.setSingleChoiceItems(typeNames, types.indexOf(controller.type)) { dialog, typeIndex ->
|
.setSingleChoiceItems(typeNames, types.indexOf(controller.type)) { dialog, typeIndex ->
|
||||||
val selectedType = types[typeIndex]
|
val selectedType = types[typeIndex]
|
||||||
if (controller.type != selectedType) {
|
if (controller.type != selectedType) {
|
||||||
if (controller is JoyConLeftController)
|
if (controller is JoyConLeftController)
|
||||||
controller.partnerId?.let { (manager.controllers[it] as JoyConRightController).partnerId = null }
|
controller.partnerId?.let { (InputManager.controllers[it] as JoyConRightController).partnerId = null }
|
||||||
else if (controller is JoyConRightController)
|
else if (controller is JoyConRightController)
|
||||||
controller.partnerId?.let { (manager.controllers[it] as JoyConLeftController).partnerId = null }
|
controller.partnerId?.let { (InputManager.controllers[it] as JoyConLeftController).partnerId = null }
|
||||||
|
|
||||||
manager.controllers[id] = when (selectedType) {
|
InputManager.controllers[id] = when (selectedType) {
|
||||||
ControllerType.None -> Controller(id, ControllerType.None)
|
ControllerType.None -> Controller(id, ControllerType.None)
|
||||||
ControllerType.HandheldProController -> HandheldController(id)
|
ControllerType.HandheldProController -> HandheldController(id)
|
||||||
ControllerType.ProController -> ProController(id)
|
ControllerType.ProController -> ProController(id)
|
||||||
@ -200,26 +189,28 @@ class ControllerActivity : AppCompatActivity(), View.OnClickListener {
|
|||||||
}
|
}
|
||||||
|
|
||||||
is ControllerGeneralItem -> {
|
is ControllerGeneralItem -> {
|
||||||
when (tag.type) {
|
when (item.type) {
|
||||||
GeneralType.PartnerJoyCon -> {
|
GeneralType.PartnerJoyCon -> {
|
||||||
val controller = manager.controllers[id] as JoyConLeftController
|
val controller = InputManager.controllers[id] as JoyConLeftController
|
||||||
|
|
||||||
val rJoyCons = manager.controllers.values.filter { it.type == ControllerType.JoyConRight }
|
val rJoyCons = InputManager.controllers.values.filter { it.type == ControllerType.JoyConRight }
|
||||||
val rJoyConNames = (listOf(getString(R.string.none)) + rJoyCons.map { "${getString(R.string.controller)} #${it.id + 1}" }).toTypedArray()
|
val rJoyConNames = (listOf(getString(R.string.none)) + rJoyCons.map { "${getString(R.string.controller)} #${it.id + 1}" }).toTypedArray()
|
||||||
|
|
||||||
val partnerNameIndex = if (controller.partnerId == null) 0 else rJoyCons.withIndex().single { it.value.id == controller.partnerId }.index + 1
|
val partnerNameIndex = controller.partnerId?.let { partnerId ->
|
||||||
|
rJoyCons.withIndex().single { it.value.id == partnerId }.index + 1
|
||||||
|
} ?: 0
|
||||||
|
|
||||||
MaterialAlertDialogBuilder(this)
|
MaterialAlertDialogBuilder(this)
|
||||||
.setTitle(tag.content)
|
.setTitle(item.content)
|
||||||
.setSingleChoiceItems(rJoyConNames, partnerNameIndex) { dialog, index ->
|
.setSingleChoiceItems(rJoyConNames, partnerNameIndex) { dialog, index ->
|
||||||
(manager.controllers[controller.partnerId ?: -1] as JoyConRightController?)?.partnerId = null
|
(InputManager.controllers[controller.partnerId ?: -1] as JoyConRightController?)?.partnerId = null
|
||||||
|
|
||||||
controller.partnerId = if (index == 0) null else rJoyCons[index - 1].id
|
controller.partnerId = if (index == 0) null else rJoyCons[index - 1].id
|
||||||
|
|
||||||
if (controller.partnerId != null)
|
if (controller.partnerId != null)
|
||||||
(manager.controllers[controller.partnerId ?: -1] as JoyConRightController?)?.partnerId = controller.id
|
(InputManager.controllers[controller.partnerId ?: -1] as JoyConRightController?)?.partnerId = controller.id
|
||||||
|
|
||||||
tag.update()
|
item.update()
|
||||||
|
|
||||||
dialog.dismiss()
|
dialog.dismiss()
|
||||||
}
|
}
|
||||||
@ -227,20 +218,17 @@ class ControllerActivity : AppCompatActivity(), View.OnClickListener {
|
|||||||
}
|
}
|
||||||
|
|
||||||
GeneralType.RumbleDevice -> {
|
GeneralType.RumbleDevice -> {
|
||||||
val dialog = RumbleDialog(tag)
|
RumbleDialog(item).show(supportFragmentManager, null)
|
||||||
dialog.show(supportFragmentManager, null)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
is ControllerButtonItem -> {
|
is ControllerButtonItem -> {
|
||||||
val dialog = ButtonDialog(tag)
|
ButtonDialog(item).show(supportFragmentManager, null)
|
||||||
dialog.show(supportFragmentManager, null)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
is ControllerStickItem -> {
|
is ControllerStickItem -> {
|
||||||
val dialog = StickDialog(tag)
|
StickDialog(item).show(supportFragmentManager, null)
|
||||||
dialog.show(supportFragmentManager, null)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -8,75 +8,53 @@ package emu.skyline.input
|
|||||||
import android.view.KeyEvent
|
import android.view.KeyEvent
|
||||||
import android.view.MotionEvent
|
import android.view.MotionEvent
|
||||||
import java.io.Serializable
|
import java.io.Serializable
|
||||||
import java.util.*
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This an abstract class for all host events that is inherited by all other event classes
|
* This an abstract class for all host events that is inherited by all other event classes
|
||||||
*
|
*
|
||||||
* @param descriptor The device descriptor of the device this event originates from
|
* @param descriptor The device descriptor of the device this event originates from
|
||||||
*/
|
*/
|
||||||
abstract class HostEvent(val descriptor : String = "") : Serializable {
|
sealed class HostEvent(open val descriptor : String = "") : Serializable {
|
||||||
/**
|
/**
|
||||||
* The [toString] function is abstract so that the derived classes can return a proper string
|
* The [toString] function is abstract so that the derived classes can return a proper string
|
||||||
*/
|
*/
|
||||||
abstract override fun toString() : String
|
abstract override fun toString() : String
|
||||||
|
|
||||||
/**
|
|
||||||
* The equality function is abstract so that equality checking will be for the derived classes rather than this abstract class
|
|
||||||
*/
|
|
||||||
abstract override fun equals(other : Any?) : Boolean
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The hash function is abstract so that hashes will be generated for the derived classes rather than this abstract class
|
|
||||||
*/
|
|
||||||
abstract override fun hashCode() : Int
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This class represents all events on the host that arise from a [KeyEvent]
|
* This class represents all events on the host that arise from a [KeyEvent]
|
||||||
*/
|
*/
|
||||||
class KeyHostEvent(descriptor : String = "", val keyCode : Int) : HostEvent(descriptor) {
|
data class KeyHostEvent(override val descriptor : String = "", val keyCode : Int) : HostEvent(descriptor) {
|
||||||
/**
|
/**
|
||||||
* This returns the string representation of [keyCode]
|
* This returns the string representation of [keyCode]
|
||||||
*/
|
*/
|
||||||
override fun toString() : String = KeyEvent.keyCodeToString(keyCode)
|
override fun toString() : String = KeyEvent.keyCodeToString(keyCode)
|
||||||
|
|
||||||
/**
|
|
||||||
* This does some basic equality checking for the type of [other] and all members in the class
|
|
||||||
*/
|
|
||||||
override fun equals(other : Any?) : Boolean = if (other is KeyHostEvent) this.descriptor == other.descriptor && this.keyCode == other.keyCode else false
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This computes the hash for all members of the class
|
|
||||||
*/
|
|
||||||
override fun hashCode() : Int = Objects.hash(descriptor, keyCode)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This class represents all events on the host that arise from a [MotionEvent]
|
* This class represents all events on the host that arise from a [MotionEvent]
|
||||||
*/
|
*/
|
||||||
class MotionHostEvent(descriptor : String = "", val axis : Int, val polarity : Boolean) : HostEvent(descriptor) {
|
data class MotionHostEvent(override val descriptor : String = "", val axis : Int, val polarity : Boolean) : HostEvent(descriptor) {
|
||||||
companion object {
|
companion object {
|
||||||
/**
|
/**
|
||||||
* This is an array of all the axes that are checked during a [MotionEvent]
|
* This is an array of all the axes that are checked during a [MotionEvent]
|
||||||
*/
|
*/
|
||||||
val axes = arrayOf(MotionEvent.AXIS_X, MotionEvent.AXIS_Y, MotionEvent.AXIS_Z, MotionEvent.AXIS_RZ, MotionEvent.AXIS_LTRIGGER, MotionEvent.AXIS_RTRIGGER, MotionEvent.AXIS_THROTTLE, MotionEvent.AXIS_RUDDER, MotionEvent.AXIS_WHEEL, MotionEvent.AXIS_GAS, MotionEvent.AXIS_BRAKE).plus(IntRange(MotionEvent.AXIS_GENERIC_1, MotionEvent.AXIS_GENERIC_16).toList())
|
val axes = arrayOf(
|
||||||
|
MotionEvent.AXIS_X,
|
||||||
|
MotionEvent.AXIS_Y,
|
||||||
|
MotionEvent.AXIS_Z,
|
||||||
|
MotionEvent.AXIS_RZ,
|
||||||
|
MotionEvent.AXIS_LTRIGGER,
|
||||||
|
MotionEvent.AXIS_RTRIGGER,
|
||||||
|
MotionEvent.AXIS_THROTTLE,
|
||||||
|
MotionEvent.AXIS_RUDDER,
|
||||||
|
MotionEvent.AXIS_WHEEL,
|
||||||
|
MotionEvent.AXIS_GAS,
|
||||||
|
MotionEvent.AXIS_BRAKE) + (IntRange(MotionEvent.AXIS_GENERIC_1, MotionEvent.AXIS_GENERIC_16).toList())
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This returns the string representation of [axis] combined with [polarity]
|
* This returns the string representation of [axis] combined with [polarity]
|
||||||
*/
|
*/
|
||||||
override fun toString() : String = MotionEvent.axisToString(axis) + if (polarity) "+" else "-"
|
override fun toString() : String = MotionEvent.axisToString(axis) + if (polarity) "+" else "-"
|
||||||
|
|
||||||
/**
|
|
||||||
* This does some basic equality checking for the type of [other] and all members in the class
|
|
||||||
*/
|
|
||||||
override fun equals(other : Any?) : Boolean {
|
|
||||||
return if (other is MotionHostEvent) this.descriptor == other.descriptor && this.axis == other.axis && this.polarity == other.polarity else false
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This computes the hash for all members of the class
|
|
||||||
*/
|
|
||||||
override fun hashCode() : Int = Objects.hash(descriptor, axis, polarity)
|
|
||||||
}
|
}
|
||||||
|
@ -10,13 +10,13 @@ import android.util.Log
|
|||||||
import java.io.*
|
import java.io.*
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This class is used to manage all transactions with storing/retrieving data in relation to input
|
* This object is used to manage all transactions with storing/retrieving data in relation to input
|
||||||
*/
|
*/
|
||||||
class InputManager constructor(val context : Context) {
|
object InputManager {
|
||||||
/**
|
/**
|
||||||
* The underlying [File] object with the input data
|
* The underlying [File] object with the input data
|
||||||
*/
|
*/
|
||||||
private val file = File("${context.applicationInfo.dataDir}/input.bin")
|
private lateinit var file : File
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A [HashMap] of all the controllers that contains their metadata
|
* A [HashMap] of all the controllers that contains their metadata
|
||||||
@ -28,19 +28,18 @@ class InputManager constructor(val context : Context) {
|
|||||||
*/
|
*/
|
||||||
lateinit var eventMap : HashMap<HostEvent?, GuestEvent?>
|
lateinit var eventMap : HashMap<HostEvent?, GuestEvent?>
|
||||||
|
|
||||||
init {
|
fun init(context : Context) {
|
||||||
var readFile = false
|
file = File("${context.applicationInfo.dataDir}/input.bin")
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (file.exists() && file.length() != 0L) {
|
if (file.exists() && file.length() != 0L) {
|
||||||
syncObjects()
|
syncObjects()
|
||||||
readFile = true
|
return
|
||||||
}
|
}
|
||||||
} catch (e : Exception) {
|
} catch (e : Exception) {
|
||||||
Log.e(this.toString(), e.localizedMessage ?: "InputManager cannot read \"${file.absolutePath}\"")
|
Log.e(this.toString(), e.localizedMessage ?: "InputManager cannot read \"${file.absolutePath}\"")
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!readFile) {
|
|
||||||
controllers = hashMapOf(
|
controllers = hashMapOf(
|
||||||
0 to Controller(0, ControllerType.None),
|
0 to Controller(0, ControllerType.None),
|
||||||
1 to Controller(1, ControllerType.None),
|
1 to Controller(1, ControllerType.None),
|
||||||
@ -55,7 +54,6 @@ class InputManager constructor(val context : Context) {
|
|||||||
|
|
||||||
syncFile()
|
syncFile()
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This function syncs the class with data from [file]
|
* This function syncs the class with data from [file]
|
||||||
|
@ -24,7 +24,7 @@ import kotlin.math.abs
|
|||||||
*
|
*
|
||||||
* @param item This is used to hold the [ControllerButtonItem] between instances
|
* @param item This is used to hold the [ControllerButtonItem] between instances
|
||||||
*/
|
*/
|
||||||
class ButtonDialog(val item : ControllerButtonItem) : BottomSheetDialogFragment() {
|
class ButtonDialog @JvmOverloads constructor(private val item : ControllerButtonItem? = null) : BottomSheetDialogFragment() {
|
||||||
/**
|
/**
|
||||||
* This inflates the layout of the dialog after initial view creation
|
* This inflates the layout of the dialog after initial view creation
|
||||||
*/
|
*/
|
||||||
@ -46,9 +46,9 @@ class ButtonDialog(val item : ControllerButtonItem) : BottomSheetDialogFragment(
|
|||||||
override fun onActivityCreated(savedInstanceState : Bundle?) {
|
override fun onActivityCreated(savedInstanceState : Bundle?) {
|
||||||
super.onActivityCreated(savedInstanceState)
|
super.onActivityCreated(savedInstanceState)
|
||||||
|
|
||||||
if (context is ControllerActivity) {
|
if (item != null && context is ControllerActivity) {
|
||||||
val context = requireContext() as ControllerActivity
|
val context = requireContext() as ControllerActivity
|
||||||
val controller = context.manager.controllers[context.id]!!
|
val controller = InputManager.controllers[context.id]!!
|
||||||
|
|
||||||
// View focus handling so all input is always directed to this view
|
// View focus handling so all input is always directed to this view
|
||||||
view?.requestFocus()
|
view?.requestFocus()
|
||||||
@ -61,7 +61,7 @@ class ButtonDialog(val item : ControllerButtonItem) : BottomSheetDialogFragment(
|
|||||||
button_reset.setOnClickListener {
|
button_reset.setOnClickListener {
|
||||||
val guestEvent = ButtonGuestEvent(context.id, item.button)
|
val guestEvent = ButtonGuestEvent(context.id, item.button)
|
||||||
|
|
||||||
context.manager.eventMap.filterValues { it is ButtonGuestEvent && it == guestEvent }.keys.forEach { context.manager.eventMap.remove(it) }
|
InputManager.eventMap.filterValues { it is ButtonGuestEvent && it == guestEvent }.keys.forEach { InputManager.eventMap.remove(it) }
|
||||||
|
|
||||||
item.update()
|
item.update()
|
||||||
|
|
||||||
@ -108,10 +108,10 @@ class ButtonDialog(val item : ControllerButtonItem) : BottomSheetDialogFragment(
|
|||||||
// We serialize the current [deviceId] and [inputId] into a [KeyHostEvent] and map it to a corresponding [GuestEvent] on [KeyEvent.ACTION_UP]
|
// We serialize the current [deviceId] and [inputId] into a [KeyHostEvent] and map it to a corresponding [GuestEvent] on [KeyEvent.ACTION_UP]
|
||||||
val hostEvent = KeyHostEvent(event.device.descriptor, event.keyCode)
|
val hostEvent = KeyHostEvent(event.device.descriptor, event.keyCode)
|
||||||
|
|
||||||
var guestEvent = context.manager.eventMap[hostEvent]
|
var guestEvent = InputManager.eventMap[hostEvent]
|
||||||
|
|
||||||
if (guestEvent is GuestEvent) {
|
if (guestEvent is GuestEvent) {
|
||||||
context.manager.eventMap.remove(hostEvent)
|
InputManager.eventMap.remove(hostEvent)
|
||||||
|
|
||||||
if (guestEvent is ButtonGuestEvent)
|
if (guestEvent is ButtonGuestEvent)
|
||||||
context.buttonMap[guestEvent.button]?.update()
|
context.buttonMap[guestEvent.button]?.update()
|
||||||
@ -121,9 +121,9 @@ class ButtonDialog(val item : ControllerButtonItem) : BottomSheetDialogFragment(
|
|||||||
|
|
||||||
guestEvent = ButtonGuestEvent(context.id, item.button)
|
guestEvent = ButtonGuestEvent(context.id, item.button)
|
||||||
|
|
||||||
context.manager.eventMap.filterValues { it == guestEvent }.keys.forEach { context.manager.eventMap.remove(it) }
|
InputManager.eventMap.filterValues { it == guestEvent }.keys.forEach { InputManager.eventMap.remove(it) }
|
||||||
|
|
||||||
context.manager.eventMap[hostEvent] = guestEvent
|
InputManager.eventMap[hostEvent] = guestEvent
|
||||||
|
|
||||||
item.update()
|
item.update()
|
||||||
|
|
||||||
@ -186,10 +186,10 @@ class ButtonDialog(val item : ControllerButtonItem) : BottomSheetDialogFragment(
|
|||||||
axisRunnable = Runnable {
|
axisRunnable = Runnable {
|
||||||
val hostEvent = MotionHostEvent(event.device.descriptor, inputId!!, axisPolarity)
|
val hostEvent = MotionHostEvent(event.device.descriptor, inputId!!, axisPolarity)
|
||||||
|
|
||||||
var guestEvent = context.manager.eventMap[hostEvent]
|
var guestEvent = InputManager.eventMap[hostEvent]
|
||||||
|
|
||||||
if (guestEvent is GuestEvent) {
|
if (guestEvent is GuestEvent) {
|
||||||
context.manager.eventMap.remove(hostEvent)
|
InputManager.eventMap.remove(hostEvent)
|
||||||
|
|
||||||
if (guestEvent is ButtonGuestEvent)
|
if (guestEvent is ButtonGuestEvent)
|
||||||
context.buttonMap[(guestEvent as ButtonGuestEvent).button]?.update()
|
context.buttonMap[(guestEvent as ButtonGuestEvent).button]?.update()
|
||||||
@ -199,9 +199,9 @@ class ButtonDialog(val item : ControllerButtonItem) : BottomSheetDialogFragment(
|
|||||||
|
|
||||||
guestEvent = ButtonGuestEvent(controller.id, item.button, threshold)
|
guestEvent = ButtonGuestEvent(controller.id, item.button, threshold)
|
||||||
|
|
||||||
context.manager.eventMap.filterValues { it == guestEvent }.keys.forEach { context.manager.eventMap.remove(it) }
|
InputManager.eventMap.filterValues { it == guestEvent }.keys.forEach { InputManager.eventMap.remove(it) }
|
||||||
|
|
||||||
context.manager.eventMap[hostEvent] = guestEvent
|
InputManager.eventMap[hostEvent] = guestEvent
|
||||||
|
|
||||||
item.update()
|
item.update()
|
||||||
|
|
||||||
|
@ -17,6 +17,7 @@ import com.google.android.material.bottomsheet.BottomSheetDialogFragment
|
|||||||
import emu.skyline.R
|
import emu.skyline.R
|
||||||
import emu.skyline.adapter.ControllerGeneralItem
|
import emu.skyline.adapter.ControllerGeneralItem
|
||||||
import emu.skyline.input.ControllerActivity
|
import emu.skyline.input.ControllerActivity
|
||||||
|
import emu.skyline.input.InputManager
|
||||||
import kotlinx.android.synthetic.main.rumble_dialog.*
|
import kotlinx.android.synthetic.main.rumble_dialog.*
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -24,7 +25,7 @@ import kotlinx.android.synthetic.main.rumble_dialog.*
|
|||||||
*
|
*
|
||||||
* @param item This is used to hold the [ControllerGeneralItem] between instances
|
* @param item This is used to hold the [ControllerGeneralItem] between instances
|
||||||
*/
|
*/
|
||||||
class RumbleDialog(val item : ControllerGeneralItem) : BottomSheetDialogFragment() {
|
class RumbleDialog @JvmOverloads constructor(val item : ControllerGeneralItem? = null) : BottomSheetDialogFragment() {
|
||||||
/**
|
/**
|
||||||
* This inflates the layout of the dialog after initial view creation
|
* This inflates the layout of the dialog after initial view creation
|
||||||
*/
|
*/
|
||||||
@ -46,9 +47,9 @@ class RumbleDialog(val item : ControllerGeneralItem) : BottomSheetDialogFragment
|
|||||||
override fun onActivityCreated(savedInstanceState : Bundle?) {
|
override fun onActivityCreated(savedInstanceState : Bundle?) {
|
||||||
super.onActivityCreated(savedInstanceState)
|
super.onActivityCreated(savedInstanceState)
|
||||||
|
|
||||||
if (context is ControllerActivity) {
|
if (item != null && context is ControllerActivity) {
|
||||||
val context = requireContext() as ControllerActivity
|
val context = requireContext() as ControllerActivity
|
||||||
val controller = context.manager.controllers[context.id]!!
|
val controller = InputManager.controllers[context.id]!!
|
||||||
|
|
||||||
// Set up the reset button to clear out [Controller.rumbleDevice] when pressed
|
// Set up the reset button to clear out [Controller.rumbleDevice] when pressed
|
||||||
rumble_reset.setOnClickListener {
|
rumble_reset.setOnClickListener {
|
||||||
|
@ -28,7 +28,7 @@ import kotlin.math.max
|
|||||||
*
|
*
|
||||||
* @param item This is used to hold the [ControllerStickItem] between instances
|
* @param item This is used to hold the [ControllerStickItem] between instances
|
||||||
*/
|
*/
|
||||||
class StickDialog(val item : ControllerStickItem) : BottomSheetDialogFragment() {
|
class StickDialog @JvmOverloads constructor(val item : ControllerStickItem? = null) : BottomSheetDialogFragment() {
|
||||||
/**
|
/**
|
||||||
* This enumerates all of the stages this dialog can be in
|
* This enumerates all of the stages this dialog can be in
|
||||||
*/
|
*/
|
||||||
@ -219,9 +219,9 @@ class StickDialog(val item : ControllerStickItem) : BottomSheetDialogFragment()
|
|||||||
override fun onActivityCreated(savedInstanceState : Bundle?) {
|
override fun onActivityCreated(savedInstanceState : Bundle?) {
|
||||||
super.onActivityCreated(savedInstanceState)
|
super.onActivityCreated(savedInstanceState)
|
||||||
|
|
||||||
if (context is ControllerActivity) {
|
if (item != null && context is ControllerActivity) {
|
||||||
val context = requireContext() as ControllerActivity
|
val context = requireContext() as ControllerActivity
|
||||||
val controller = context.manager.controllers[context.id]!!
|
val controller = InputManager.controllers[context.id]!!
|
||||||
|
|
||||||
// View focus handling so all input is always directed to this view
|
// View focus handling so all input is always directed to this view
|
||||||
view?.requestFocus()
|
view?.requestFocus()
|
||||||
@ -236,13 +236,13 @@ class StickDialog(val item : ControllerStickItem) : BottomSheetDialogFragment()
|
|||||||
for (polarity in booleanArrayOf(true, false)) {
|
for (polarity in booleanArrayOf(true, false)) {
|
||||||
val guestEvent = AxisGuestEvent(context.id, axis, polarity)
|
val guestEvent = AxisGuestEvent(context.id, axis, polarity)
|
||||||
|
|
||||||
context.manager.eventMap.filterValues { it == guestEvent }.keys.forEach { context.manager.eventMap.remove(it) }
|
InputManager.eventMap.filterValues { it == guestEvent }.keys.forEach { InputManager.eventMap.remove(it) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val guestEvent = ButtonGuestEvent(context.id, item.stick.button)
|
val guestEvent = ButtonGuestEvent(context.id, item.stick.button)
|
||||||
|
|
||||||
context.manager.eventMap.filterValues { it == guestEvent }.keys.forEach { context.manager.eventMap.remove(it) }
|
InputManager.eventMap.filterValues { it == guestEvent }.keys.forEach { InputManager.eventMap.remove(it) }
|
||||||
|
|
||||||
item.update()
|
item.update()
|
||||||
|
|
||||||
@ -281,7 +281,7 @@ class StickDialog(val item : ControllerStickItem) : BottomSheetDialogFragment()
|
|||||||
((event.isFromSource(InputDevice.SOURCE_CLASS_BUTTON) && event.keyCode != KeyEvent.KEYCODE_BACK) || event.isFromSource(InputDevice.SOURCE_CLASS_JOYSTICK)) && event.repeatCount == 0 -> {
|
((event.isFromSource(InputDevice.SOURCE_CLASS_BUTTON) && event.keyCode != KeyEvent.KEYCODE_BACK) || event.isFromSource(InputDevice.SOURCE_CLASS_JOYSTICK)) && event.repeatCount == 0 -> {
|
||||||
if (stage == DialogStage.Stick) {
|
if (stage == DialogStage.Stick) {
|
||||||
// When the stick is being previewed after everything is mapped we do a lookup into [InputManager.eventMap] to find a corresponding [GuestEvent] and animate the stick correspondingly
|
// When the stick is being previewed after everything is mapped we do a lookup into [InputManager.eventMap] to find a corresponding [GuestEvent] and animate the stick correspondingly
|
||||||
when (val guestEvent = context.manager.eventMap[KeyHostEvent(event.device.descriptor, event.keyCode)]) {
|
when (val guestEvent = InputManager.eventMap[KeyHostEvent(event.device.descriptor, event.keyCode)]) {
|
||||||
is ButtonGuestEvent -> {
|
is ButtonGuestEvent -> {
|
||||||
if (guestEvent.button == item.stick.button) {
|
if (guestEvent.button == item.stick.button) {
|
||||||
if (event.action == KeyEvent.ACTION_DOWN) {
|
if (event.action == KeyEvent.ACTION_DOWN) {
|
||||||
@ -346,10 +346,10 @@ class StickDialog(val item : ControllerStickItem) : BottomSheetDialogFragment()
|
|||||||
// We serialize the current [deviceId] and [inputId] into a [KeyHostEvent] and map it to a corresponding [GuestEvent] and add it to [ignoredEvents] on [KeyEvent.ACTION_UP]
|
// We serialize the current [deviceId] and [inputId] into a [KeyHostEvent] and map it to a corresponding [GuestEvent] and add it to [ignoredEvents] on [KeyEvent.ACTION_UP]
|
||||||
val hostEvent = KeyHostEvent(event.device.descriptor, event.keyCode)
|
val hostEvent = KeyHostEvent(event.device.descriptor, event.keyCode)
|
||||||
|
|
||||||
var guestEvent = context.manager.eventMap[hostEvent]
|
var guestEvent = InputManager.eventMap[hostEvent]
|
||||||
|
|
||||||
if (guestEvent is GuestEvent) {
|
if (guestEvent is GuestEvent) {
|
||||||
context.manager.eventMap.remove(hostEvent)
|
InputManager.eventMap.remove(hostEvent)
|
||||||
|
|
||||||
if (guestEvent is ButtonGuestEvent)
|
if (guestEvent is ButtonGuestEvent)
|
||||||
context.buttonMap[guestEvent.button]?.update()
|
context.buttonMap[guestEvent.button]?.update()
|
||||||
@ -364,9 +364,9 @@ class StickDialog(val item : ControllerStickItem) : BottomSheetDialogFragment()
|
|||||||
else -> null
|
else -> null
|
||||||
}
|
}
|
||||||
|
|
||||||
context.manager.eventMap.filterValues { it == guestEvent }.keys.forEach { context.manager.eventMap.remove(it) }
|
InputManager.eventMap.filterValues { it == guestEvent }.keys.forEach { InputManager.eventMap.remove(it) }
|
||||||
|
|
||||||
context.manager.eventMap[hostEvent] = guestEvent
|
InputManager.eventMap[hostEvent] = guestEvent
|
||||||
|
|
||||||
ignoredEvents.add(Objects.hash(deviceId!!, inputId!!))
|
ignoredEvents.add(Objects.hash(deviceId!!, inputId!!))
|
||||||
|
|
||||||
@ -420,12 +420,14 @@ class StickDialog(val item : ControllerStickItem) : BottomSheetDialogFragment()
|
|||||||
axesHistory[axisItem.index] = value
|
axesHistory[axisItem.index] = value
|
||||||
|
|
||||||
var polarity = value >= 0
|
var polarity = value >= 0
|
||||||
val guestEvent = context.manager.eventMap[MotionHostEvent(event.device.descriptor, axis, polarity)] ?: if (value == 0f) {
|
val guestEvent = MotionHostEvent(event.device.descriptor, axis, polarity).let { hostEvent ->
|
||||||
|
InputManager.eventMap[hostEvent] ?: if (value == 0f) {
|
||||||
polarity = false
|
polarity = false
|
||||||
context.manager.eventMap[MotionHostEvent(event.device.descriptor, axis, polarity)]
|
InputManager.eventMap[hostEvent.copy(polarity = false)]
|
||||||
} else {
|
} else {
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
when (guestEvent) {
|
when (guestEvent) {
|
||||||
is ButtonGuestEvent -> {
|
is ButtonGuestEvent -> {
|
||||||
@ -528,10 +530,10 @@ class StickDialog(val item : ControllerStickItem) : BottomSheetDialogFragment()
|
|||||||
axisRunnable = Runnable {
|
axisRunnable = Runnable {
|
||||||
val hostEvent = MotionHostEvent(event.device.descriptor, inputId!!, axisPolarity)
|
val hostEvent = MotionHostEvent(event.device.descriptor, inputId!!, axisPolarity)
|
||||||
|
|
||||||
var guestEvent = context.manager.eventMap[hostEvent]
|
var guestEvent = InputManager.eventMap[hostEvent]
|
||||||
|
|
||||||
if (guestEvent is GuestEvent) {
|
if (guestEvent is GuestEvent) {
|
||||||
context.manager.eventMap.remove(hostEvent)
|
InputManager.eventMap.remove(hostEvent)
|
||||||
|
|
||||||
if (guestEvent is ButtonGuestEvent)
|
if (guestEvent is ButtonGuestEvent)
|
||||||
context.buttonMap[(guestEvent as ButtonGuestEvent).button]?.update()
|
context.buttonMap[(guestEvent as ButtonGuestEvent).button]?.update()
|
||||||
@ -548,9 +550,9 @@ class StickDialog(val item : ControllerStickItem) : BottomSheetDialogFragment()
|
|||||||
else -> null
|
else -> null
|
||||||
}
|
}
|
||||||
|
|
||||||
context.manager.eventMap.filterValues { it == guestEvent }.keys.forEach { context.manager.eventMap.remove(it) }
|
InputManager.eventMap.filterValues { it == guestEvent }.keys.forEach { InputManager.eventMap.remove(it) }
|
||||||
|
|
||||||
context.manager.eventMap[hostEvent] = guestEvent
|
InputManager.eventMap[hostEvent] = guestEvent
|
||||||
|
|
||||||
ignoredEvents.add(Objects.hash(deviceId!!, inputId!!, axisPolarity))
|
ignoredEvents.add(Objects.hash(deviceId!!, inputId!!, axisPolarity))
|
||||||
|
|
||||||
|
137
app/src/main/java/emu/skyline/input/onscreen/OnScreenButton.kt
Normal file
137
app/src/main/java/emu/skyline/input/onscreen/OnScreenButton.kt
Normal file
@ -0,0 +1,137 @@
|
|||||||
|
/*
|
||||||
|
* SPDX-License-Identifier: MPL-2.0
|
||||||
|
* Copyright © 2020 Skyline Team and Contributors (https://github.com/skyline-emu/)
|
||||||
|
*/
|
||||||
|
|
||||||
|
package emu.skyline.input.onscreen
|
||||||
|
|
||||||
|
import android.graphics.Canvas
|
||||||
|
import android.graphics.Color
|
||||||
|
import android.graphics.Paint
|
||||||
|
import android.graphics.Rect
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
|
import emu.skyline.input.ButtonId
|
||||||
|
import kotlin.math.absoluteValue
|
||||||
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
|
abstract class OnScreenButton(
|
||||||
|
onScreenControllerView : OnScreenControllerView,
|
||||||
|
val buttonId : ButtonId,
|
||||||
|
private val defaultRelativeX : Float,
|
||||||
|
private val defaultRelativeY : Float,
|
||||||
|
private val defaultRelativeWidth : Float,
|
||||||
|
private val defaultRelativeHeight : Float,
|
||||||
|
drawableId : Int
|
||||||
|
) {
|
||||||
|
companion object {
|
||||||
|
/**
|
||||||
|
* Aspect ratio the default values were based on
|
||||||
|
*/
|
||||||
|
const val CONFIGURED_ASPECT_RATIO = 2074f / 874f
|
||||||
|
}
|
||||||
|
|
||||||
|
val config = if (onScreenControllerView.isInEditMode) ControllerConfigurationDummy(defaultRelativeX, defaultRelativeY)
|
||||||
|
else ControllerConfigurationImpl(onScreenControllerView.context, buttonId, defaultRelativeX, defaultRelativeY)
|
||||||
|
|
||||||
|
protected val drawable = ContextCompat.getDrawable(onScreenControllerView.context, drawableId)!!
|
||||||
|
|
||||||
|
private val buttonSymbolPaint = Paint().apply { color = Color.GRAY }
|
||||||
|
private val textBoundsRect = Rect()
|
||||||
|
|
||||||
|
var relativeX = config.relativeX
|
||||||
|
var relativeY = config.relativeY
|
||||||
|
private val relativeWidth get() = defaultRelativeWidth * config.globalScale
|
||||||
|
private val relativeHeight get() = defaultRelativeHeight * config.globalScale
|
||||||
|
|
||||||
|
var width = 0
|
||||||
|
var height = 0
|
||||||
|
|
||||||
|
protected val adjustedHeight get() = width / CONFIGURED_ASPECT_RATIO
|
||||||
|
protected val heightDiff get() = (height - adjustedHeight).absoluteValue
|
||||||
|
|
||||||
|
protected val itemWidth get() = width * relativeWidth
|
||||||
|
private val itemHeight get() = adjustedHeight * relativeHeight
|
||||||
|
|
||||||
|
val currentX get() = width * relativeX
|
||||||
|
val currentY get() = adjustedHeight * relativeY + heightDiff
|
||||||
|
|
||||||
|
private val left get() = currentX - itemWidth / 2f
|
||||||
|
private val top get() = currentY - itemHeight / 2f
|
||||||
|
|
||||||
|
protected val currentBounds
|
||||||
|
get() = Rect(
|
||||||
|
left.roundToInt(),
|
||||||
|
top.roundToInt(),
|
||||||
|
(left + itemWidth).roundToInt(),
|
||||||
|
(top + itemHeight).roundToInt()
|
||||||
|
)
|
||||||
|
|
||||||
|
var touchPointerId = -1
|
||||||
|
|
||||||
|
var isEditing = false
|
||||||
|
private set
|
||||||
|
|
||||||
|
protected open fun renderCenteredText(
|
||||||
|
canvas : Canvas,
|
||||||
|
text : String,
|
||||||
|
size : Float,
|
||||||
|
x : Float,
|
||||||
|
y : Float
|
||||||
|
) {
|
||||||
|
buttonSymbolPaint.apply {
|
||||||
|
textSize = size
|
||||||
|
textAlign = Paint.Align.LEFT
|
||||||
|
getTextBounds(text, 0, text.length, textBoundsRect)
|
||||||
|
}
|
||||||
|
canvas.drawText(
|
||||||
|
text,
|
||||||
|
x - textBoundsRect.width() / 2f - textBoundsRect.left,
|
||||||
|
y + textBoundsRect.height() / 2f - textBoundsRect.bottom,
|
||||||
|
buttonSymbolPaint
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
open fun render(canvas : Canvas) {
|
||||||
|
val bounds = currentBounds
|
||||||
|
drawable.apply {
|
||||||
|
this.bounds = bounds
|
||||||
|
draw(canvas)
|
||||||
|
}
|
||||||
|
|
||||||
|
renderCenteredText(canvas, buttonId.short!!, itemWidth.coerceAtMost(itemHeight) * 0.4f, bounds.centerX().toFloat(), bounds.centerY().toFloat())
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract fun isTouched(x : Float, y : Float) : Boolean
|
||||||
|
|
||||||
|
abstract fun onFingerDown(x : Float, y : Float)
|
||||||
|
|
||||||
|
abstract fun onFingerUp(x : Float, y : Float)
|
||||||
|
|
||||||
|
fun loadConfigValues() {
|
||||||
|
relativeX = config.relativeX
|
||||||
|
relativeY = config.relativeY
|
||||||
|
}
|
||||||
|
|
||||||
|
fun startEdit() {
|
||||||
|
isEditing = true
|
||||||
|
}
|
||||||
|
|
||||||
|
open fun edit(x : Float, y : Float) {
|
||||||
|
relativeX = x / width
|
||||||
|
relativeY = (y - heightDiff) / adjustedHeight
|
||||||
|
}
|
||||||
|
|
||||||
|
fun endEdit() {
|
||||||
|
config.relativeX = relativeX
|
||||||
|
config.relativeY = relativeY
|
||||||
|
isEditing = false
|
||||||
|
}
|
||||||
|
|
||||||
|
open fun resetRelativeValues() {
|
||||||
|
config.relativeX = defaultRelativeX
|
||||||
|
config.relativeY = defaultRelativeY
|
||||||
|
|
||||||
|
relativeX = defaultRelativeX
|
||||||
|
relativeY = defaultRelativeY
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,68 @@
|
|||||||
|
/*
|
||||||
|
* SPDX-License-Identifier: MPL-2.0
|
||||||
|
* Copyright © 2020 Skyline Team and Contributors (https://github.com/skyline-emu/)
|
||||||
|
*/
|
||||||
|
|
||||||
|
package emu.skyline.input.onscreen
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import emu.skyline.input.ButtonId
|
||||||
|
import kotlin.properties.ReadWriteProperty
|
||||||
|
import kotlin.reflect.KProperty
|
||||||
|
|
||||||
|
interface ControllerConfiguration {
|
||||||
|
var globalScale : Float
|
||||||
|
var relativeX : Float
|
||||||
|
var relativeY : Float
|
||||||
|
}
|
||||||
|
|
||||||
|
class ControllerConfigurationDummy(
|
||||||
|
defaultRelativeX : Float,
|
||||||
|
defaultRelativeY : Float
|
||||||
|
) : ControllerConfiguration {
|
||||||
|
override var globalScale = 1f
|
||||||
|
override var relativeX = defaultRelativeX
|
||||||
|
override var relativeY = defaultRelativeY
|
||||||
|
}
|
||||||
|
|
||||||
|
class ControllerConfigurationImpl(
|
||||||
|
private val context : Context,
|
||||||
|
private val buttonId : ButtonId,
|
||||||
|
defaultRelativeX : Float,
|
||||||
|
defaultRelativeY : Float
|
||||||
|
) : ControllerConfiguration {
|
||||||
|
override var globalScale by ControllerPrefs(context, "on_screen_controller_", Float::class.java, 1f)
|
||||||
|
|
||||||
|
private inline fun <reified T> config(default : T) = ControllerPrefs(context, "${buttonId.name}_", T::class.java, default)
|
||||||
|
|
||||||
|
override var relativeX by config(defaultRelativeX)
|
||||||
|
override var relativeY by config(defaultRelativeY)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
private class ControllerPrefs<T>(context : Context, private val prefix : String, private val clazz : Class<T>, private val default : T) : ReadWriteProperty<Any, T> {
|
||||||
|
companion object {
|
||||||
|
const val CONTROLLER_CONFIG = "controller_config"
|
||||||
|
}
|
||||||
|
|
||||||
|
private val prefs = context.getSharedPreferences(CONTROLLER_CONFIG, Context.MODE_PRIVATE)
|
||||||
|
|
||||||
|
override fun setValue(thisRef : Any, property : KProperty<*>, value : T) {
|
||||||
|
prefs.edit().apply {
|
||||||
|
when (clazz) {
|
||||||
|
Float::class.java,
|
||||||
|
java.lang.Float::class.java -> putFloat(prefix + property.name, value as Float)
|
||||||
|
else -> error("Unsupported type $clazz ${Float::class.java}")
|
||||||
|
}
|
||||||
|
}.apply()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getValue(thisRef : Any, property : KProperty<*>) : T =
|
||||||
|
prefs.let {
|
||||||
|
when (clazz) {
|
||||||
|
Float::class.java,
|
||||||
|
java.lang.Float::class.java -> it.getFloat(prefix + property.name, default as Float)
|
||||||
|
else -> error("Unsupported type $clazz ${Float::class.java}")
|
||||||
|
}
|
||||||
|
} as T
|
||||||
|
}
|
@ -0,0 +1,204 @@
|
|||||||
|
/*
|
||||||
|
* 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
|
||||||
|
import android.graphics.Canvas
|
||||||
|
import android.graphics.PointF
|
||||||
|
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
|
||||||
|
import kotlin.math.roundToLong
|
||||||
|
|
||||||
|
typealias OnButtonStateChangedListener = (buttonId : ButtonId, state : ButtonState) -> Unit
|
||||||
|
typealias OnStickStateChangedListener = (buttonId : ButtonId, position : PointF) -> Unit
|
||||||
|
|
||||||
|
class OnScreenControllerView @JvmOverloads constructor(
|
||||||
|
context : Context,
|
||||||
|
attrs : AttributeSet? = null,
|
||||||
|
defStyleAttr : Int = 0,
|
||||||
|
defStyleRes : Int = 0
|
||||||
|
) : View(context, attrs, defStyleAttr, defStyleRes) {
|
||||||
|
private val controls = Controls(this)
|
||||||
|
private var onButtonStateChangedListener : OnButtonStateChangedListener? = null
|
||||||
|
private var onStickStateChangedListener : OnStickStateChangedListener? = null
|
||||||
|
private val joystickAnimators = mutableMapOf<JoystickButton, Animator?>()
|
||||||
|
|
||||||
|
override fun onDraw(canvas : Canvas) {
|
||||||
|
super.onDraw(canvas)
|
||||||
|
|
||||||
|
(controls.circularButtons + controls.rectangularButtons + controls.triggerButtons + controls.joysticks).forEach {
|
||||||
|
it.width = width
|
||||||
|
it.height = height
|
||||||
|
it.render(canvas)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
MotionEvent.ACTION_DOWN,
|
||||||
|
MotionEvent.ACTION_POINTER_DOWN -> {
|
||||||
|
if (button.isTouched(x, y)) {
|
||||||
|
button.touchPointerId = pointerId
|
||||||
|
button.onFingerDown(x, y)
|
||||||
|
performClick()
|
||||||
|
onButtonStateChangedListener?.invoke(button.buttonId, ButtonState.Pressed)
|
||||||
|
handled = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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()
|
||||||
|
val duration = (150f * outerToInnerLength / radius).roundToLong()
|
||||||
|
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)
|
||||||
|
joystick.onFingerMoved(newPosition.x, newPosition.y)
|
||||||
|
onStickStateChangedListener?.invoke(joystick.buttonId, vector.multiply(1f / radius))
|
||||||
|
invalidate()
|
||||||
|
}
|
||||||
|
addListener(object : AnimatorListenerAdapter() {
|
||||||
|
override fun onAnimationCancel(animation : Animator?) {
|
||||||
|
super.onAnimationCancel(animation)
|
||||||
|
onAnimationEnd(animation)
|
||||||
|
onStickStateChangedListener?.invoke(joystick.buttonId, PointF(0f, 0f))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onAnimationEnd(animation : Animator?) {
|
||||||
|
super.onAnimationEnd(animation)
|
||||||
|
joystick.onFingerUp(event.x, event.y)
|
||||||
|
invalidate()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
setDuration(duration)
|
||||||
|
start()
|
||||||
|
}
|
||||||
|
handled = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
MotionEvent.ACTION_DOWN,
|
||||||
|
MotionEvent.ACTION_POINTER_DOWN -> {
|
||||||
|
if (joystick.isTouched(x, y)) {
|
||||||
|
joystickAnimators[joystick]?.cancel()
|
||||||
|
joystickAnimators[joystick] = null
|
||||||
|
joystick.touchPointerId = pointerId
|
||||||
|
joystick.onFingerDown(x, y)
|
||||||
|
performClick()
|
||||||
|
handled = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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))
|
||||||
|
onStickStateChangedListener?.invoke(joystick.buttonId, centerToPoint)
|
||||||
|
handled = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handled.also { if (it) invalidate() }
|
||||||
|
}
|
||||||
|
|
||||||
|
private val editingTouchHandler = OnTouchListener { _, event ->
|
||||||
|
(controls.circularButtons + controls.rectangularButtons + controls.triggerButtons + controls.joysticks).any { button ->
|
||||||
|
when (event.actionMasked) {
|
||||||
|
MotionEvent.ACTION_UP,
|
||||||
|
MotionEvent.ACTION_POINTER_UP,
|
||||||
|
MotionEvent.ACTION_CANCEL -> {
|
||||||
|
if (button.isEditing) {
|
||||||
|
button.endEdit()
|
||||||
|
return@any true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
MotionEvent.ACTION_DOWN,
|
||||||
|
MotionEvent.ACTION_POINTER_DOWN -> {
|
||||||
|
if (button.isTouched(event.x, event.y)) {
|
||||||
|
button.startEdit()
|
||||||
|
performClick()
|
||||||
|
return@any true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
MotionEvent.ACTION_MOVE -> {
|
||||||
|
if (button.isEditing) {
|
||||||
|
button.edit(event.x, event.y)
|
||||||
|
return@any true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
false
|
||||||
|
}.also { handled -> if (handled) invalidate() }
|
||||||
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
|
setOnTouchListener(playingTouchHandler)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setEditMode(editMode : Boolean) = setOnTouchListener(if (editMode) editingTouchHandler else playingTouchHandler)
|
||||||
|
|
||||||
|
fun resetControls() {
|
||||||
|
(controls.circularButtons + controls.rectangularButtons + controls.triggerButtons + controls.joysticks).forEach { it.resetRelativeValues() }
|
||||||
|
controls.globalScale = 1f
|
||||||
|
invalidate()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun increaseScale() {
|
||||||
|
controls.globalScale *= 1.1f
|
||||||
|
invalidate()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun decreaseScale() {
|
||||||
|
controls.globalScale *= 0.9f
|
||||||
|
invalidate()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setOnButtonStateChangedListener(listener : OnButtonStateChangedListener) {
|
||||||
|
onButtonStateChangedListener = listener
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setOnStickStateChangedListener(listener : OnStickStateChangedListener) {
|
||||||
|
onStickStateChangedListener = listener
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,205 @@
|
|||||||
|
/*
|
||||||
|
* SPDX-License-Identifier: MPL-2.0
|
||||||
|
* Copyright © 2020 Skyline Team and Contributors (https://github.com/skyline-emu/)
|
||||||
|
*/
|
||||||
|
|
||||||
|
package emu.skyline.input.onscreen
|
||||||
|
|
||||||
|
import android.graphics.Canvas
|
||||||
|
import android.graphics.PointF
|
||||||
|
import androidx.core.graphics.minus
|
||||||
|
import emu.skyline.R
|
||||||
|
import emu.skyline.input.ButtonId
|
||||||
|
import emu.skyline.input.ButtonId.*
|
||||||
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
|
open class CircularButton(
|
||||||
|
onScreenControllerView : OnScreenControllerView,
|
||||||
|
buttonId : ButtonId,
|
||||||
|
defaultRelativeX : Float,
|
||||||
|
defaultRelativeY : Float,
|
||||||
|
defaultRelativeRadiusToX : Float,
|
||||||
|
drawableId : Int = R.drawable.ic_button
|
||||||
|
) : OnScreenButton(
|
||||||
|
onScreenControllerView,
|
||||||
|
buttonId,
|
||||||
|
defaultRelativeX,
|
||||||
|
defaultRelativeY,
|
||||||
|
defaultRelativeRadiusToX * 2f,
|
||||||
|
defaultRelativeRadiusToX * CONFIGURED_ASPECT_RATIO * 2f,
|
||||||
|
drawableId
|
||||||
|
) {
|
||||||
|
val radius get() = itemWidth / 2f
|
||||||
|
|
||||||
|
override fun isTouched(x : Float, y : Float) : Boolean = PointF(currentX, currentY).minus(PointF(x, y)).length() <= radius
|
||||||
|
|
||||||
|
override fun onFingerDown(x : Float, y : Float) {
|
||||||
|
drawable.alpha = (255 * 0.5f).roundToInt()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onFingerUp(x : Float, y : Float) {
|
||||||
|
drawable.alpha = 255
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class JoystickButton(
|
||||||
|
onScreenControllerView : OnScreenControllerView,
|
||||||
|
buttonId : ButtonId,
|
||||||
|
defaultRelativeX : Float,
|
||||||
|
defaultRelativeY : Float,
|
||||||
|
defaultRelativeRadiusToX : Float
|
||||||
|
) : CircularButton(
|
||||||
|
onScreenControllerView,
|
||||||
|
buttonId,
|
||||||
|
defaultRelativeX,
|
||||||
|
defaultRelativeY,
|
||||||
|
defaultRelativeRadiusToX,
|
||||||
|
R.drawable.ic_stick_circle
|
||||||
|
) {
|
||||||
|
|
||||||
|
private val innerButton = CircularButton(onScreenControllerView, buttonId, config.relativeX, config.relativeY, defaultRelativeRadiusToX * 0.75f, R.drawable.ic_stick)
|
||||||
|
|
||||||
|
override fun renderCenteredText(canvas : Canvas, text : String, size : Float, x : Float, y : Float) = Unit
|
||||||
|
|
||||||
|
override fun render(canvas : Canvas) {
|
||||||
|
super.render(canvas)
|
||||||
|
|
||||||
|
innerButton.width = width
|
||||||
|
innerButton.height = height
|
||||||
|
innerButton.render(canvas)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onFingerDown(x : Float, y : Float) {
|
||||||
|
relativeX = x / width
|
||||||
|
relativeY = (y - heightDiff) / adjustedHeight
|
||||||
|
innerButton.relativeX = relativeX
|
||||||
|
innerButton.relativeY = relativeY
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onFingerUp(x : Float, y : Float) {
|
||||||
|
loadConfigValues()
|
||||||
|
innerButton.relativeX = relativeX
|
||||||
|
innerButton.relativeY = relativeY
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onFingerMoved(x : Float, y : Float) : PointF {
|
||||||
|
val position = PointF(currentX, currentY)
|
||||||
|
var finger = PointF(x, y)
|
||||||
|
val outerToInner = finger.minus(position)
|
||||||
|
val distance = outerToInner.length()
|
||||||
|
if (distance >= radius) {
|
||||||
|
finger = position.add(outerToInner.multiply(1f / distance * radius))
|
||||||
|
}
|
||||||
|
|
||||||
|
innerButton.relativeX = finger.x / width
|
||||||
|
innerButton.relativeY = (finger.y - heightDiff) / adjustedHeight
|
||||||
|
return finger.minus(position).multiply(1f / radius)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun outerToInner() = PointF(innerButton.currentX, innerButton.currentY).minus(PointF(currentX, currentY))
|
||||||
|
|
||||||
|
override fun edit(x : Float, y : Float) {
|
||||||
|
super.edit(x, y)
|
||||||
|
innerButton.relativeX = relativeX
|
||||||
|
innerButton.relativeY = relativeY
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun resetRelativeValues() {
|
||||||
|
super.resetRelativeValues()
|
||||||
|
|
||||||
|
innerButton.relativeX = relativeX
|
||||||
|
innerButton.relativeY = relativeY
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
open class RectangularButton(
|
||||||
|
onScreenControllerView : OnScreenControllerView,
|
||||||
|
buttonId : ButtonId,
|
||||||
|
defaultRelativeX : Float,
|
||||||
|
defaultRelativeY : Float,
|
||||||
|
defaultRelativeWidth : Float,
|
||||||
|
defaultRelativeHeight : Float,
|
||||||
|
drawableId : Int = R.drawable.ic_rectangular_button
|
||||||
|
) : OnScreenButton(
|
||||||
|
onScreenControllerView,
|
||||||
|
buttonId,
|
||||||
|
defaultRelativeX,
|
||||||
|
defaultRelativeY,
|
||||||
|
defaultRelativeWidth,
|
||||||
|
defaultRelativeHeight,
|
||||||
|
drawableId
|
||||||
|
) {
|
||||||
|
override fun isTouched(x : Float, y : Float) =
|
||||||
|
currentBounds.contains(x.roundToInt(), y.roundToInt())
|
||||||
|
|
||||||
|
override fun onFingerDown(x : Float, y : Float) {
|
||||||
|
drawable.alpha = (255 * 0.5f).roundToInt()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onFingerUp(x : Float, y : Float) {
|
||||||
|
drawable.alpha = 255
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class TriggerButton(
|
||||||
|
onScreenControllerView : OnScreenControllerView,
|
||||||
|
buttonId : ButtonId,
|
||||||
|
defaultRelativeX : Float,
|
||||||
|
defaultRelativeY : Float,
|
||||||
|
defaultRelativeWidth : Float,
|
||||||
|
defaultRelativeHeight : Float
|
||||||
|
) : RectangularButton(
|
||||||
|
onScreenControllerView,
|
||||||
|
buttonId,
|
||||||
|
defaultRelativeX,
|
||||||
|
defaultRelativeY,
|
||||||
|
defaultRelativeWidth,
|
||||||
|
defaultRelativeHeight,
|
||||||
|
when (buttonId) {
|
||||||
|
ZL -> R.drawable.ic_trigger_button_left
|
||||||
|
|
||||||
|
ZR -> R.drawable.ic_trigger_button_right
|
||||||
|
|
||||||
|
else -> error("Unsupported trigger button")
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
class Controls(onScreenControllerView : OnScreenControllerView) {
|
||||||
|
val circularButtons = listOf(
|
||||||
|
CircularButton(onScreenControllerView, A, 0.95f, 0.65f, 0.025f),
|
||||||
|
CircularButton(onScreenControllerView, B, 0.9f, 0.75f, 0.025f),
|
||||||
|
CircularButton(onScreenControllerView, X, 0.9f, 0.55f, 0.025f),
|
||||||
|
CircularButton(onScreenControllerView, Y, 0.85f, 0.65f, 0.025f),
|
||||||
|
CircularButton(onScreenControllerView, DpadLeft, 0.2f, 0.65f, 0.025f),
|
||||||
|
CircularButton(onScreenControllerView, DpadUp, 0.25f, 0.55f, 0.025f),
|
||||||
|
CircularButton(onScreenControllerView, DpadRight, 0.3f, 0.65f, 0.025f),
|
||||||
|
CircularButton(onScreenControllerView, DpadDown, 0.25f, 0.75f, 0.025f),
|
||||||
|
CircularButton(onScreenControllerView, Plus, 0.57f, 0.75f, 0.025f),
|
||||||
|
CircularButton(onScreenControllerView, Minus, 0.43f, 0.75f, 0.025f),
|
||||||
|
CircularButton(onScreenControllerView, Menu, 0.5f, 0.75f, 0.025f)
|
||||||
|
)
|
||||||
|
|
||||||
|
val joysticks = listOf(
|
||||||
|
JoystickButton(onScreenControllerView, LeftStick, 0.1f, 0.8f, 0.05f),
|
||||||
|
JoystickButton(onScreenControllerView, RightStick, 0.75f, 0.6f, 0.05f)
|
||||||
|
)
|
||||||
|
|
||||||
|
val rectangularButtons = listOf(
|
||||||
|
RectangularButton(onScreenControllerView, L, 0.1f, 0.25f, 0.075f, 0.08f),
|
||||||
|
RectangularButton(onScreenControllerView, R, 0.9f, 0.25f, 0.075f, 0.08f)
|
||||||
|
)
|
||||||
|
|
||||||
|
val triggerButtons = listOf(
|
||||||
|
TriggerButton(onScreenControllerView, ZL, 0.1f, 0.1f, 0.075f, 0.08f),
|
||||||
|
TriggerButton(onScreenControllerView, ZR, 0.9f, 0.1f, 0.075f, 0.08f)
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* We can take any of the global scale variables from the buttons
|
||||||
|
*/
|
||||||
|
var globalScale
|
||||||
|
get() = circularButtons[0].config.globalScale
|
||||||
|
set(value) {
|
||||||
|
circularButtons[0].config.globalScale = value
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,20 @@
|
|||||||
|
/*
|
||||||
|
* SPDX-License-Identifier: MPL-2.0
|
||||||
|
* Copyright © 2020 Skyline Team and Contributors (https://github.com/skyline-emu/)
|
||||||
|
*/
|
||||||
|
|
||||||
|
package emu.skyline.input.onscreen
|
||||||
|
|
||||||
|
import android.graphics.PointF
|
||||||
|
|
||||||
|
fun PointF.add(p : PointF) = PointF(x, y).apply {
|
||||||
|
x += p.x
|
||||||
|
y += p.y
|
||||||
|
}
|
||||||
|
|
||||||
|
fun PointF.multiply(scalar : Float) = PointF(x, y).apply {
|
||||||
|
x *= scalar
|
||||||
|
y *= scalar
|
||||||
|
}
|
||||||
|
|
||||||
|
fun PointF.normalize() = multiply(1f / length())
|
@ -12,21 +12,18 @@ import android.util.AttributeSet
|
|||||||
import androidx.preference.Preference
|
import androidx.preference.Preference
|
||||||
import androidx.preference.Preference.SummaryProvider
|
import androidx.preference.Preference.SummaryProvider
|
||||||
import emu.skyline.R
|
import emu.skyline.R
|
||||||
import emu.skyline.SettingsActivity
|
|
||||||
import emu.skyline.input.ControllerActivity
|
import emu.skyline.input.ControllerActivity
|
||||||
import emu.skyline.input.InputManager
|
import emu.skyline.input.InputManager
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This preference is used to launch [ControllerActivity] using a preference
|
* This preference is used to launch [ControllerActivity] using a preference
|
||||||
*/
|
*/
|
||||||
class ControllerPreference @JvmOverloads constructor(context : Context?, attrs : AttributeSet? = null, defStyleAttr : Int = R.attr.preferenceStyle) : Preference(context, attrs, defStyleAttr), ActivityResultDelegate {
|
class ControllerPreference @JvmOverloads constructor(context : Context, attrs : AttributeSet? = null, defStyleAttr : Int = R.attr.preferenceStyle) : Preference(context, attrs, defStyleAttr), ActivityResultDelegate {
|
||||||
/**
|
/**
|
||||||
* The index of the controller this preference manages
|
* The index of the controller this preference manages
|
||||||
*/
|
*/
|
||||||
private var index = -1
|
private var index = -1
|
||||||
|
|
||||||
private var inputManager : InputManager? = null
|
|
||||||
|
|
||||||
override var requestCode = 0
|
override var requestCode = 0
|
||||||
|
|
||||||
init {
|
init {
|
||||||
@ -45,12 +42,8 @@ class ControllerPreference @JvmOverloads constructor(context : Context?, attrs :
|
|||||||
if (key == null)
|
if (key == null)
|
||||||
key = "controller_$index"
|
key = "controller_$index"
|
||||||
|
|
||||||
title = "${context?.getString(R.string.config_controller)} #${index + 1}"
|
title = "${context.getString(R.string.config_controller)} #${index + 1}"
|
||||||
|
summaryProvider = SummaryProvider<ControllerPreference> { InputManager.controllers[index]!!.type.stringRes.let { context.getString(it) } }
|
||||||
if (context is SettingsActivity) {
|
|
||||||
inputManager = context.inputManager
|
|
||||||
summaryProvider = SummaryProvider<ControllerPreference> { context.inputManager.controllers[index]?.type?.stringRes?.let { context.getString(it) } }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -62,7 +55,7 @@ class ControllerPreference @JvmOverloads constructor(context : Context?, attrs :
|
|||||||
|
|
||||||
override fun onActivityResult(requestCode : Int, resultCode : Int, data : Intent?) {
|
override fun onActivityResult(requestCode : Int, resultCode : Int, data : Intent?) {
|
||||||
if (this.requestCode == requestCode) {
|
if (this.requestCode == requestCode) {
|
||||||
inputManager?.syncObjects()
|
InputManager.syncObjects()
|
||||||
notifyChanged()
|
notifyChanged()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -8,7 +8,6 @@ package emu.skyline.views
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.graphics.Rect
|
import android.graphics.Rect
|
||||||
import android.util.AttributeSet
|
import android.util.AttributeSet
|
||||||
import android.util.Log
|
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.widget.LinearLayout
|
import android.widget.LinearLayout
|
||||||
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
android:shape="oval">
|
android:shape="oval">
|
||||||
<solid android:color="#A0000000" />
|
<solid android:color="#50FFFFFF" />
|
||||||
<stroke
|
<stroke
|
||||||
android:width="2.5dp"
|
android:width="2.5dp"
|
||||||
android:color="@android:color/black" />
|
android:color="#A0FFFFFF" />
|
||||||
</shape>
|
</shape>
|
||||||
|
15
app/src/main/res/drawable/ic_rectangular_button.xml
Normal file
15
app/src/main/res/drawable/ic_rectangular_button.xml
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<item>
|
||||||
|
<shape android:shape="rectangle">
|
||||||
|
<solid android:color="#50FFFFFF" />
|
||||||
|
<stroke
|
||||||
|
android:width="2dp"
|
||||||
|
android:color="#A0FFFFFF" />
|
||||||
|
<size
|
||||||
|
android:width="25dp"
|
||||||
|
android:height="25dp" />
|
||||||
|
<corners android:radius="10dp" />
|
||||||
|
</shape>
|
||||||
|
</item>
|
||||||
|
</layer-list>
|
@ -2,10 +2,10 @@
|
|||||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
<item>
|
<item>
|
||||||
<shape android:shape="oval">
|
<shape android:shape="oval">
|
||||||
<solid android:color="#A0000000" />
|
<solid android:color="#50FFFFFF" />
|
||||||
<stroke
|
<stroke
|
||||||
android:width="2dp"
|
android:width="2dp"
|
||||||
android:color="@android:color/black" />
|
android:color="#A0FFFFFF" />
|
||||||
<size
|
<size
|
||||||
android:width="25dp"
|
android:width="25dp"
|
||||||
android:height="25dp" />
|
android:height="25dp" />
|
||||||
@ -18,11 +18,6 @@
|
|||||||
</item>
|
</item>
|
||||||
<item>
|
<item>
|
||||||
<shape android:shape="oval">
|
<shape android:shape="oval">
|
||||||
<!--
|
|
||||||
<stroke
|
|
||||||
android:width="2dp"
|
|
||||||
android:color="#B0FFFFFF" />
|
|
||||||
-->
|
|
||||||
<solid android:color="#50000000" />
|
<solid android:color="#50000000" />
|
||||||
<size
|
<size
|
||||||
android:width="30dp"
|
android:width="30dp"
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
android:shape="oval">
|
android:shape="oval">
|
||||||
<solid android:color="#A0000000" />
|
<solid android:color="#50FFFFFF" />
|
||||||
<stroke
|
<stroke
|
||||||
android:width="2.5dp"
|
android:width="2.5dp"
|
||||||
android:color="@android:color/black" />
|
android:color="#A0FFFFFF" />
|
||||||
</shape>
|
</shape>
|
||||||
|
19
app/src/main/res/drawable/ic_trigger_button_left.xml
Normal file
19
app/src/main/res/drawable/ic_trigger_button_left.xml
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<item>
|
||||||
|
<shape android:shape="rectangle">
|
||||||
|
<solid android:color="#50FFFFFF" />
|
||||||
|
<stroke
|
||||||
|
android:width="2dp"
|
||||||
|
android:color="#A0FFFFFF" />
|
||||||
|
<size
|
||||||
|
android:width="25dp"
|
||||||
|
android:height="25dp" />
|
||||||
|
<corners
|
||||||
|
android:bottomLeftRadius="10dp"
|
||||||
|
android:bottomRightRadius="10dp"
|
||||||
|
android:topLeftRadius="50dp"
|
||||||
|
android:topRightRadius="10dp" />
|
||||||
|
</shape>
|
||||||
|
</item>
|
||||||
|
</layer-list>
|
19
app/src/main/res/drawable/ic_trigger_button_right.xml
Normal file
19
app/src/main/res/drawable/ic_trigger_button_right.xml
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<item>
|
||||||
|
<shape android:shape="rectangle">
|
||||||
|
<solid android:color="#50FFFFFF" />
|
||||||
|
<stroke
|
||||||
|
android:width="2dp"
|
||||||
|
android:color="#A0FFFFFF" />
|
||||||
|
<size
|
||||||
|
android:width="25dp"
|
||||||
|
android:height="25dp" />
|
||||||
|
<corners
|
||||||
|
android:bottomLeftRadius="10dp"
|
||||||
|
android:bottomRightRadius="10dp"
|
||||||
|
android:topLeftRadius="10dp"
|
||||||
|
android:topRightRadius="50dp" />
|
||||||
|
</shape>
|
||||||
|
</item>
|
||||||
|
</layer-list>
|
@ -1,5 +1,5 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
@ -9,18 +9,18 @@
|
|||||||
<SurfaceView
|
<SurfaceView
|
||||||
android:id="@+id/game_view"
|
android:id="@+id/game_view"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent" />
|
||||||
android:layout_alignParentStart="true"
|
|
||||||
android:layout_alignParentTop="true" />
|
<emu.skyline.input.onscreen.OnScreenControllerView
|
||||||
|
android:id="@+id/on_screen_controller_view"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent" />
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/perf_stats"
|
android:id="@+id/perf_stats"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_alignParentStart="true"
|
|
||||||
android:layout_alignParentTop="true"
|
|
||||||
android:layout_marginStart="5dp"
|
android:layout_marginStart="5dp"
|
||||||
android:layout_marginTop="5dp"
|
android:layout_marginTop="5dp"
|
||||||
android:textColor="#9fffff00" />
|
android:textColor="#9fffff00" />
|
||||||
|
</FrameLayout>
|
||||||
</RelativeLayout>
|
|
||||||
|
Loading…
Reference in New Issue
Block a user