mirror of
https://github.com/skyline-emu/skyline.git
synced 2024-12-27 15:15:30 +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" />
|
||||
<uses-permission android:name="android.permission.VIBRATE" />
|
||||
<application
|
||||
android:name=".SkylineApplication"
|
||||
android:allowBackup="true"
|
||||
android:extractNativeLibs="true"
|
||||
android:fullBackupContent="@xml/backup_descriptor"
|
||||
|
@ -8,11 +8,13 @@ package emu.skyline
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.graphics.PointF
|
||||
import android.net.Uri
|
||||
import android.os.*
|
||||
import android.util.Log
|
||||
import android.view.*
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.view.isInvisible
|
||||
import androidx.preference.PreferenceManager
|
||||
import emu.skyline.input.*
|
||||
import emu.skyline.loader.getRomFormat
|
||||
@ -22,18 +24,13 @@ import kotlin.math.abs
|
||||
|
||||
class EmulationActivity : AppCompatActivity(), SurfaceHolder.Callback, View.OnTouchListener {
|
||||
companion object {
|
||||
private val Tag = EmulationActivity::class.java.name
|
||||
private val Tag = EmulationActivity::class.java.simpleName
|
||||
}
|
||||
|
||||
init {
|
||||
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]
|
||||
*/
|
||||
@ -144,12 +141,11 @@ class EmulationActivity : AppCompatActivity(), SurfaceHolder.Callback, View.OnTo
|
||||
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() {
|
||||
for (entry in input.controllers) {
|
||||
val controller = entry.value
|
||||
|
||||
for (controller in InputManager.controllers.values) {
|
||||
if (controller.type != ControllerType.None) {
|
||||
val type = when (controller.type) {
|
||||
ControllerType.None -> throw IllegalArgumentException()
|
||||
@ -163,7 +159,7 @@ class EmulationActivity : AppCompatActivity(), SurfaceHolder.Callback, View.OnTo
|
||||
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)
|
||||
}
|
||||
|
||||
input = InputManager(this)
|
||||
|
||||
game_view.holder.addCallback(this)
|
||||
|
||||
val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this)
|
||||
@ -236,6 +230,11 @@ class EmulationActivity : AppCompatActivity(), SurfaceHolder.Callback, View.OnTo
|
||||
|
||||
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!!)
|
||||
}
|
||||
|
||||
@ -310,7 +309,7 @@ class EmulationActivity : AppCompatActivity(), SurfaceHolder.Callback, View.OnTo
|
||||
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 -> {
|
||||
if (guestEvent.button != ButtonId.Menu)
|
||||
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
|
||||
*/
|
||||
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
|
||||
*/
|
||||
override fun onGenericMotionEvent(event : MotionEvent) : Boolean {
|
||||
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) {
|
||||
for (axisItem in MotionHostEvent.axes.withIndex()) {
|
||||
@ -351,11 +350,13 @@ class EmulationActivity : AppCompatActivity(), SurfaceHolder.Callback, View.OnTo
|
||||
if ((event.historySize != 0 && value != event.getHistoricalAxisValue(axis, 0)) || (axesHistory[axisItem.index]?.let { it == value } == false)) {
|
||||
var polarity = value >= 0
|
||||
|
||||
val guestEvent = input.eventMap[MotionHostEvent(event.device.descriptor, axis, polarity)] ?: if (value == 0f) {
|
||||
polarity = false
|
||||
input.eventMap[MotionHostEvent(event.device.descriptor, axis, polarity)]
|
||||
} else {
|
||||
null
|
||||
val guestEvent = MotionHostEvent(event.device.descriptor, axis, polarity).let { hostEvent ->
|
||||
InputManager.eventMap[hostEvent] ?: if (value == 0f) {
|
||||
polarity = false
|
||||
InputManager.eventMap[hostEvent.copy(polarity = false)]
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
when (guestEvent) {
|
||||
@ -388,7 +389,7 @@ class EmulationActivity : AppCompatActivity(), SurfaceHolder.Callback, View.OnTo
|
||||
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
override fun onTouch(view : View, event : MotionEvent) : Boolean {
|
||||
val count = if(event.action != MotionEvent.ACTION_UP && event.action != MotionEvent.ACTION_CANCEL) event.pointerCount else 0
|
||||
val count = if (event.action != MotionEvent.ACTION_UP && event.action != MotionEvent.ACTION_CANCEL) event.pointerCount else 0
|
||||
val points = IntArray(count * 5) // This is an array of skyline::input::TouchScreenPoint in C++ as that allows for efficient transfer of values to it
|
||||
var offset = 0
|
||||
for (index in 0 until count) {
|
||||
@ -410,12 +411,30 @@ class EmulationActivity : AppCompatActivity(), SurfaceHolder.Callback, View.OnTo
|
||||
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")
|
||||
@Suppress("unused")
|
||||
fun vibrateDevice(index : Int, timing : LongArray, amplitude : IntArray) {
|
||||
val vibrator = if (vibrators[index] != null) {
|
||||
vibrators[index]!!
|
||||
} else {
|
||||
input.controllers[index]?.rumbleDeviceDescriptor?.let {
|
||||
InputManager.controllers[index]!!.rumbleDeviceDescriptor?.let {
|
||||
if (it == "builtin") {
|
||||
val vibrator = getSystemService(Context.VIBRATOR_SERVICE) as Vibrator
|
||||
vibrators[index] = vibrator
|
||||
@ -423,7 +442,7 @@ class EmulationActivity : AppCompatActivity(), SurfaceHolder.Callback, View.OnTo
|
||||
} else {
|
||||
for (id in InputDevice.getDeviceIds()) {
|
||||
val device = InputDevice.getDevice(id)
|
||||
if (device.descriptor == input.controllers[index]?.rumbleDeviceDescriptor) {
|
||||
if (device.descriptor == InputManager.controllers[index]!!.rumbleDeviceDescriptor) {
|
||||
vibrators[index] = device.vibrator
|
||||
device.vibrator
|
||||
}
|
||||
@ -437,6 +456,7 @@ class EmulationActivity : AppCompatActivity(), SurfaceHolder.Callback, View.OnTo
|
||||
vibrator.vibrate(effect)
|
||||
}
|
||||
|
||||
@Suppress("unused")
|
||||
fun clearVibrationDevice(index : Int) {
|
||||
vibrators[index]?.cancel()
|
||||
}
|
||||
|
@ -137,7 +137,7 @@ class LogActivity : AppCompatActivity() {
|
||||
private fun uploadAndShareLog() {
|
||||
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
|
||||
|
||||
try {
|
||||
@ -173,7 +173,7 @@ class LogActivity : AppCompatActivity() {
|
||||
} finally {
|
||||
urlConnection!!.disconnect()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
shareThread.start()
|
||||
}
|
||||
|
@ -7,7 +7,6 @@ package emu.skyline
|
||||
|
||||
import android.animation.ObjectAnimator
|
||||
import android.content.Intent
|
||||
import android.content.SharedPreferences
|
||||
import android.content.pm.ActivityInfo
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
@ -45,15 +44,19 @@ class MainActivity : AppCompatActivity() {
|
||||
/**
|
||||
* 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]
|
||||
*/
|
||||
private lateinit var adapter : AppAdapter
|
||||
private val adapter by lazy {
|
||||
AppAdapter(layoutType = layoutType, onClick = ::selectStartGame, onLongClick = ::selectShowGameDialog)
|
||||
}
|
||||
|
||||
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
|
||||
*/
|
||||
@ -160,7 +163,6 @@ class MainActivity : AppCompatActivity() {
|
||||
setSupportActionBar(toolbar)
|
||||
|
||||
PreferenceManager.setDefaultValues(this, R.xml.preferences, false)
|
||||
sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this)
|
||||
|
||||
AppCompatDelegate.setDefaultNightMode(when ((sharedPreferences.getString("app_theme", "2")?.toInt())) {
|
||||
0 -> AppCompatDelegate.MODE_NIGHT_NO
|
||||
@ -221,12 +223,8 @@ class MainActivity : AppCompatActivity() {
|
||||
val metrics = resources.displayMetrics
|
||||
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.layoutManager = when (layoutType) {
|
||||
app_list.layoutManager = when (adapter.layoutType) {
|
||||
LayoutType.List -> LinearLayoutManager(this).also { app_list.addItemDecoration(DividerItemDecoration(this, RecyclerView.VERTICAL)) }
|
||||
|
||||
LayoutType.Grid, LayoutType.GridCompact -> GridLayoutManager(this, gridSpan).apply {
|
||||
@ -340,7 +338,6 @@ class MainActivity : AppCompatActivity() {
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
|
||||
val layoutType = LayoutType.values()[sharedPreferences.getString("layout_type", "1")!!.toInt()]
|
||||
if (layoutType != adapter.layoutType) {
|
||||
setupAppList()
|
||||
}
|
||||
|
@ -11,9 +11,7 @@ import android.view.KeyEvent
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.preference.PreferenceFragmentCompat
|
||||
import androidx.preference.PreferenceGroup
|
||||
import emu.skyline.input.InputManager
|
||||
import emu.skyline.preference.ActivityResultDelegate
|
||||
import emu.skyline.preference.ControllerPreference
|
||||
import emu.skyline.preference.DocumentActivity
|
||||
import kotlinx.android.synthetic.main.settings_activity.*
|
||||
import kotlinx.android.synthetic.main.titlebar.*
|
||||
@ -24,19 +22,12 @@ class SettingsActivity : AppCompatActivity() {
|
||||
*/
|
||||
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
|
||||
*/
|
||||
override fun onCreate(savedInstanceState : Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
inputManager = InputManager(this)
|
||||
|
||||
setContentView(R.layout.settings_activity)
|
||||
|
||||
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]
|
||||
*/
|
||||
fun getSummary(context : ControllerActivity, type : GeneralType) : String {
|
||||
val controller = context.manager.controllers[context.id]!!
|
||||
val controller = InputManager.controllers[context.id]!!
|
||||
|
||||
return when (type) {
|
||||
GeneralType.PartnerJoyCon -> {
|
||||
@ -105,7 +105,7 @@ class ControllerButtonItem(val context : ControllerActivity, val button : Button
|
||||
*/
|
||||
fun getSummary(context : ControllerActivity, button : ButtonId) : String {
|
||||
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 {
|
||||
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)
|
||||
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)
|
||||
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)
|
||||
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)
|
||||
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"
|
||||
}
|
||||
@ -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
|
||||
*/
|
||||
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]
|
||||
*/
|
||||
@ -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]
|
||||
*/
|
||||
override fun onCreateViewHolder(parent : ViewGroup, viewType : Int) : RecyclerView.ViewHolder {
|
||||
val inflater = LayoutInflater.from(context)
|
||||
var holder : RecyclerView.ViewHolder? = null
|
||||
|
||||
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)
|
||||
override fun onCreateViewHolder(parent : ViewGroup, viewType : Int) = when (ElementType.values()[viewType]) {
|
||||
ElementType.Header -> LayoutInflater.from(parent.context).inflate(R.layout.section_item, parent, false).let { view ->
|
||||
HeaderViewHolder(view).apply { 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.subtitle.text = item.subContent
|
||||
|
||||
holder.parent.tag = item
|
||||
holder.parent.setOnClickListener { onItemClickCallback.invoke(item) }
|
||||
} else if (item is BaseHeader && holder is HeaderViewHolder) {
|
||||
holder.header?.text = item.title
|
||||
}
|
||||
|
@ -7,7 +7,6 @@ package emu.skyline.input
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.KeyEvent
|
||||
import android.view.View
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
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
|
||||
*/
|
||||
class ControllerActivity : AppCompatActivity(), View.OnClickListener {
|
||||
class ControllerActivity : AppCompatActivity() {
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
val adapter = ControllerAdapter(this)
|
||||
|
||||
/**
|
||||
* The [InputManager] class handles loading/saving the input data
|
||||
*/
|
||||
lateinit var manager : InputManager
|
||||
private val adapter = ControllerAdapter(::onControllerItemClick)
|
||||
|
||||
/**
|
||||
* 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>()
|
||||
|
||||
/**
|
||||
* This function updates the [adapter] based on information from [manager]
|
||||
* This function updates the [adapter] based on information from [InputManager]
|
||||
*/
|
||||
private fun update() {
|
||||
adapter.clear()
|
||||
|
||||
val controller = manager.controllers[id]!!
|
||||
val controller = InputManager.controllers[id]!!
|
||||
|
||||
adapter.addItem(ControllerTypeItem(this, controller.type))
|
||||
|
||||
@ -134,8 +128,6 @@ class ControllerActivity : AppCompatActivity(), View.OnClickListener {
|
||||
override fun onCreate(state : Bundle?) {
|
||||
super.onCreate(state)
|
||||
|
||||
manager = InputManager(this)
|
||||
|
||||
id = intent.getIntExtra("index", 0)
|
||||
|
||||
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
|
||||
*/
|
||||
override fun onPause() {
|
||||
manager.syncFile()
|
||||
InputManager.syncFile()
|
||||
super.onPause()
|
||||
}
|
||||
|
||||
/**
|
||||
* This handles the onClick events for the items in the activity
|
||||
*/
|
||||
override fun onClick(v : View?) {
|
||||
when (val tag = v!!.tag) {
|
||||
private fun onControllerItemClick(item : ControllerItem) {
|
||||
when (item) {
|
||||
is ControllerTypeItem -> {
|
||||
val controller = manager.controllers[id]!!
|
||||
val controller = InputManager.controllers[id]!!
|
||||
|
||||
val types = ControllerType.values().filter { !it.firstController || id == 0 }
|
||||
val typeNames = types.map { getString(it.stringRes) }.toTypedArray()
|
||||
|
||||
MaterialAlertDialogBuilder(this)
|
||||
.setTitle(tag.content)
|
||||
.setTitle(item.content)
|
||||
.setSingleChoiceItems(typeNames, types.indexOf(controller.type)) { dialog, typeIndex ->
|
||||
val selectedType = types[typeIndex]
|
||||
if (controller.type != selectedType) {
|
||||
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)
|
||||
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.HandheldProController -> HandheldController(id)
|
||||
ControllerType.ProController -> ProController(id)
|
||||
@ -200,26 +189,28 @@ class ControllerActivity : AppCompatActivity(), View.OnClickListener {
|
||||
}
|
||||
|
||||
is ControllerGeneralItem -> {
|
||||
when (tag.type) {
|
||||
when (item.type) {
|
||||
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 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)
|
||||
.setTitle(tag.content)
|
||||
.setTitle(item.content)
|
||||
.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
|
||||
|
||||
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()
|
||||
}
|
||||
@ -227,20 +218,17 @@ class ControllerActivity : AppCompatActivity(), View.OnClickListener {
|
||||
}
|
||||
|
||||
GeneralType.RumbleDevice -> {
|
||||
val dialog = RumbleDialog(tag)
|
||||
dialog.show(supportFragmentManager, null)
|
||||
RumbleDialog(item).show(supportFragmentManager, null)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
is ControllerButtonItem -> {
|
||||
val dialog = ButtonDialog(tag)
|
||||
dialog.show(supportFragmentManager, null)
|
||||
ButtonDialog(item).show(supportFragmentManager, null)
|
||||
}
|
||||
|
||||
is ControllerStickItem -> {
|
||||
val dialog = StickDialog(tag)
|
||||
dialog.show(supportFragmentManager, null)
|
||||
StickDialog(item).show(supportFragmentManager, null)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -8,75 +8,53 @@ package emu.skyline.input
|
||||
import android.view.KeyEvent
|
||||
import android.view.MotionEvent
|
||||
import java.io.Serializable
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
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
|
||||
*/
|
||||
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]
|
||||
*/
|
||||
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]
|
||||
*/
|
||||
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]
|
||||
*/
|
||||
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 {
|
||||
/**
|
||||
* 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]
|
||||
*/
|
||||
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.*
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
private val file = File("${context.applicationInfo.dataDir}/input.bin")
|
||||
private lateinit var file : File
|
||||
|
||||
/**
|
||||
* A [HashMap] of all the controllers that contains their metadata
|
||||
@ -28,33 +28,31 @@ class InputManager constructor(val context : Context) {
|
||||
*/
|
||||
lateinit var eventMap : HashMap<HostEvent?, GuestEvent?>
|
||||
|
||||
init {
|
||||
var readFile = false
|
||||
fun init(context : Context) {
|
||||
file = File("${context.applicationInfo.dataDir}/input.bin")
|
||||
|
||||
try {
|
||||
if (file.exists() && file.length() != 0L) {
|
||||
syncObjects()
|
||||
readFile = true
|
||||
return
|
||||
}
|
||||
} catch (e : Exception) {
|
||||
Log.e(this.toString(), e.localizedMessage ?: "InputManager cannot read \"${file.absolutePath}\"")
|
||||
}
|
||||
|
||||
if (!readFile) {
|
||||
controllers = hashMapOf(
|
||||
0 to Controller(0, ControllerType.None),
|
||||
1 to Controller(1, ControllerType.None),
|
||||
2 to Controller(2, ControllerType.None),
|
||||
3 to Controller(3, ControllerType.None),
|
||||
4 to Controller(4, ControllerType.None),
|
||||
5 to Controller(5, ControllerType.None),
|
||||
6 to Controller(6, ControllerType.None),
|
||||
7 to Controller(7, ControllerType.None))
|
||||
controllers = hashMapOf(
|
||||
0 to Controller(0, ControllerType.None),
|
||||
1 to Controller(1, ControllerType.None),
|
||||
2 to Controller(2, ControllerType.None),
|
||||
3 to Controller(3, ControllerType.None),
|
||||
4 to Controller(4, ControllerType.None),
|
||||
5 to Controller(5, ControllerType.None),
|
||||
6 to Controller(6, ControllerType.None),
|
||||
7 to Controller(7, ControllerType.None))
|
||||
|
||||
eventMap = hashMapOf()
|
||||
eventMap = hashMapOf()
|
||||
|
||||
syncFile()
|
||||
}
|
||||
syncFile()
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -24,7 +24,7 @@ import kotlin.math.abs
|
||||
*
|
||||
* @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
|
||||
*/
|
||||
@ -46,9 +46,9 @@ class ButtonDialog(val item : ControllerButtonItem) : BottomSheetDialogFragment(
|
||||
override fun onActivityCreated(savedInstanceState : Bundle?) {
|
||||
super.onActivityCreated(savedInstanceState)
|
||||
|
||||
if (context is ControllerActivity) {
|
||||
if (item != null && context is 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?.requestFocus()
|
||||
@ -61,7 +61,7 @@ class ButtonDialog(val item : ControllerButtonItem) : BottomSheetDialogFragment(
|
||||
button_reset.setOnClickListener {
|
||||
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()
|
||||
|
||||
@ -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]
|
||||
val hostEvent = KeyHostEvent(event.device.descriptor, event.keyCode)
|
||||
|
||||
var guestEvent = context.manager.eventMap[hostEvent]
|
||||
var guestEvent = InputManager.eventMap[hostEvent]
|
||||
|
||||
if (guestEvent is GuestEvent) {
|
||||
context.manager.eventMap.remove(hostEvent)
|
||||
InputManager.eventMap.remove(hostEvent)
|
||||
|
||||
if (guestEvent is ButtonGuestEvent)
|
||||
context.buttonMap[guestEvent.button]?.update()
|
||||
@ -121,9 +121,9 @@ class ButtonDialog(val item : ControllerButtonItem) : BottomSheetDialogFragment(
|
||||
|
||||
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()
|
||||
|
||||
@ -186,10 +186,10 @@ class ButtonDialog(val item : ControllerButtonItem) : BottomSheetDialogFragment(
|
||||
axisRunnable = Runnable {
|
||||
val hostEvent = MotionHostEvent(event.device.descriptor, inputId!!, axisPolarity)
|
||||
|
||||
var guestEvent = context.manager.eventMap[hostEvent]
|
||||
var guestEvent = InputManager.eventMap[hostEvent]
|
||||
|
||||
if (guestEvent is GuestEvent) {
|
||||
context.manager.eventMap.remove(hostEvent)
|
||||
InputManager.eventMap.remove(hostEvent)
|
||||
|
||||
if (guestEvent is ButtonGuestEvent)
|
||||
context.buttonMap[(guestEvent as ButtonGuestEvent).button]?.update()
|
||||
@ -199,9 +199,9 @@ class ButtonDialog(val item : ControllerButtonItem) : BottomSheetDialogFragment(
|
||||
|
||||
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()
|
||||
|
||||
|
@ -17,6 +17,7 @@ import com.google.android.material.bottomsheet.BottomSheetDialogFragment
|
||||
import emu.skyline.R
|
||||
import emu.skyline.adapter.ControllerGeneralItem
|
||||
import emu.skyline.input.ControllerActivity
|
||||
import emu.skyline.input.InputManager
|
||||
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
|
||||
*/
|
||||
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
|
||||
*/
|
||||
@ -46,9 +47,9 @@ class RumbleDialog(val item : ControllerGeneralItem) : BottomSheetDialogFragment
|
||||
override fun onActivityCreated(savedInstanceState : Bundle?) {
|
||||
super.onActivityCreated(savedInstanceState)
|
||||
|
||||
if (context is ControllerActivity) {
|
||||
if (item != null && context is 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
|
||||
rumble_reset.setOnClickListener {
|
||||
|
@ -28,7 +28,7 @@ import kotlin.math.max
|
||||
*
|
||||
* @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
|
||||
*/
|
||||
@ -219,9 +219,9 @@ class StickDialog(val item : ControllerStickItem) : BottomSheetDialogFragment()
|
||||
override fun onActivityCreated(savedInstanceState : Bundle?) {
|
||||
super.onActivityCreated(savedInstanceState)
|
||||
|
||||
if (context is ControllerActivity) {
|
||||
if (item != null && context is 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?.requestFocus()
|
||||
@ -236,13 +236,13 @@ class StickDialog(val item : ControllerStickItem) : BottomSheetDialogFragment()
|
||||
for (polarity in booleanArrayOf(true, false)) {
|
||||
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)
|
||||
|
||||
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()
|
||||
|
||||
@ -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 -> {
|
||||
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 (val guestEvent = context.manager.eventMap[KeyHostEvent(event.device.descriptor, event.keyCode)]) {
|
||||
when (val guestEvent = InputManager.eventMap[KeyHostEvent(event.device.descriptor, event.keyCode)]) {
|
||||
is ButtonGuestEvent -> {
|
||||
if (guestEvent.button == item.stick.button) {
|
||||
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]
|
||||
val hostEvent = KeyHostEvent(event.device.descriptor, event.keyCode)
|
||||
|
||||
var guestEvent = context.manager.eventMap[hostEvent]
|
||||
var guestEvent = InputManager.eventMap[hostEvent]
|
||||
|
||||
if (guestEvent is GuestEvent) {
|
||||
context.manager.eventMap.remove(hostEvent)
|
||||
InputManager.eventMap.remove(hostEvent)
|
||||
|
||||
if (guestEvent is ButtonGuestEvent)
|
||||
context.buttonMap[guestEvent.button]?.update()
|
||||
@ -364,9 +364,9 @@ class StickDialog(val item : ControllerStickItem) : BottomSheetDialogFragment()
|
||||
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!!))
|
||||
|
||||
@ -420,11 +420,13 @@ class StickDialog(val item : ControllerStickItem) : BottomSheetDialogFragment()
|
||||
axesHistory[axisItem.index] = value
|
||||
|
||||
var polarity = value >= 0
|
||||
val guestEvent = context.manager.eventMap[MotionHostEvent(event.device.descriptor, axis, polarity)] ?: if (value == 0f) {
|
||||
polarity = false
|
||||
context.manager.eventMap[MotionHostEvent(event.device.descriptor, axis, polarity)]
|
||||
} else {
|
||||
null
|
||||
val guestEvent = MotionHostEvent(event.device.descriptor, axis, polarity).let { hostEvent ->
|
||||
InputManager.eventMap[hostEvent] ?: if (value == 0f) {
|
||||
polarity = false
|
||||
InputManager.eventMap[hostEvent.copy(polarity = false)]
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
when (guestEvent) {
|
||||
@ -528,10 +530,10 @@ class StickDialog(val item : ControllerStickItem) : BottomSheetDialogFragment()
|
||||
axisRunnable = Runnable {
|
||||
val hostEvent = MotionHostEvent(event.device.descriptor, inputId!!, axisPolarity)
|
||||
|
||||
var guestEvent = context.manager.eventMap[hostEvent]
|
||||
var guestEvent = InputManager.eventMap[hostEvent]
|
||||
|
||||
if (guestEvent is GuestEvent) {
|
||||
context.manager.eventMap.remove(hostEvent)
|
||||
InputManager.eventMap.remove(hostEvent)
|
||||
|
||||
if (guestEvent is ButtonGuestEvent)
|
||||
context.buttonMap[(guestEvent as ButtonGuestEvent).button]?.update()
|
||||
@ -548,9 +550,9 @@ class StickDialog(val item : ControllerStickItem) : BottomSheetDialogFragment()
|
||||
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))
|
||||
|
||||
|
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.SummaryProvider
|
||||
import emu.skyline.R
|
||||
import emu.skyline.SettingsActivity
|
||||
import emu.skyline.input.ControllerActivity
|
||||
import emu.skyline.input.InputManager
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
private var index = -1
|
||||
|
||||
private var inputManager : InputManager? = null
|
||||
|
||||
override var requestCode = 0
|
||||
|
||||
init {
|
||||
@ -45,12 +42,8 @@ class ControllerPreference @JvmOverloads constructor(context : Context?, attrs :
|
||||
if (key == null)
|
||||
key = "controller_$index"
|
||||
|
||||
title = "${context?.getString(R.string.config_controller)} #${index + 1}"
|
||||
|
||||
if (context is SettingsActivity) {
|
||||
inputManager = context.inputManager
|
||||
summaryProvider = SummaryProvider<ControllerPreference> { context.inputManager.controllers[index]?.type?.stringRes?.let { context.getString(it) } }
|
||||
}
|
||||
title = "${context.getString(R.string.config_controller)} #${index + 1}"
|
||||
summaryProvider = SummaryProvider<ControllerPreference> { 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?) {
|
||||
if (this.requestCode == requestCode) {
|
||||
inputManager?.syncObjects()
|
||||
InputManager.syncObjects()
|
||||
notifyChanged()
|
||||
}
|
||||
}
|
||||
|
@ -8,7 +8,6 @@ package emu.skyline.views
|
||||
import android.content.Context
|
||||
import android.graphics.Rect
|
||||
import android.util.AttributeSet
|
||||
import android.util.Log
|
||||
import android.view.View
|
||||
import android.widget.LinearLayout
|
||||
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
|
@ -1,8 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="oval">
|
||||
<solid android:color="#A0000000" />
|
||||
android:shape="oval">
|
||||
<solid android:color="#50FFFFFF" />
|
||||
<stroke
|
||||
android:width="2.5dp"
|
||||
android:color="@android:color/black" />
|
||||
android:width="2.5dp"
|
||||
android:color="#A0FFFFFF" />
|
||||
</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,31 +2,26 @@
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item>
|
||||
<shape android:shape="oval">
|
||||
<solid android:color="#A0000000" />
|
||||
<solid android:color="#50FFFFFF" />
|
||||
<stroke
|
||||
android:width="2dp"
|
||||
android:color="@android:color/black" />
|
||||
android:width="2dp"
|
||||
android:color="#A0FFFFFF" />
|
||||
<size
|
||||
android:width="25dp"
|
||||
android:height="25dp" />
|
||||
android:width="25dp"
|
||||
android:height="25dp" />
|
||||
<padding
|
||||
android:bottom="10dp"
|
||||
android:left="10dp"
|
||||
android:right="10dp"
|
||||
android:top="10dp" />
|
||||
android:bottom="10dp"
|
||||
android:left="10dp"
|
||||
android:right="10dp"
|
||||
android:top="10dp" />
|
||||
</shape>
|
||||
</item>
|
||||
<item>
|
||||
<shape android:shape="oval">
|
||||
<!--
|
||||
<stroke
|
||||
android:width="2dp"
|
||||
android:color="#B0FFFFFF" />
|
||||
-->
|
||||
<solid android:color="#50000000" />
|
||||
<size
|
||||
android:width="30dp"
|
||||
android:height="30dp" />
|
||||
android:width="30dp"
|
||||
android:height="30dp" />
|
||||
</shape>
|
||||
</item>
|
||||
</layer-list>
|
||||
|
@ -1,8 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="oval">
|
||||
<solid android:color="#A0000000" />
|
||||
android:shape="oval">
|
||||
<solid android:color="#50FFFFFF" />
|
||||
<stroke
|
||||
android:width="2.5dp"
|
||||
android:color="@android:color/black" />
|
||||
android:width="2.5dp"
|
||||
android:color="#A0FFFFFF" />
|
||||
</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"?>
|
||||
<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"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
@ -9,18 +9,18 @@
|
||||
<SurfaceView
|
||||
android:id="@+id/game_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_alignParentStart="true"
|
||||
android:layout_alignParentTop="true" />
|
||||
android:layout_height="match_parent" />
|
||||
|
||||
<emu.skyline.input.onscreen.OnScreenControllerView
|
||||
android:id="@+id/on_screen_controller_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/perf_stats"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentStart="true"
|
||||
android:layout_alignParentTop="true"
|
||||
android:layout_marginStart="5dp"
|
||||
android:layout_marginTop="5dp"
|
||||
android:textColor="#9fffff00" />
|
||||
|
||||
</RelativeLayout>
|
||||
</FrameLayout>
|
||||
|
Loading…
Reference in New Issue
Block a user