diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index af3091ea..52be86d7 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -10,6 +10,7 @@ android:required="true" /> 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() } diff --git a/app/src/main/java/emu/skyline/LogActivity.kt b/app/src/main/java/emu/skyline/LogActivity.kt index 4be36cdb..b214e5bc 100644 --- a/app/src/main/java/emu/skyline/LogActivity.kt +++ b/app/src/main/java/emu/skyline/LogActivity.kt @@ -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() } diff --git a/app/src/main/java/emu/skyline/MainActivity.kt b/app/src/main/java/emu/skyline/MainActivity.kt index 0b5b3085..2d3fa610 100644 --- a/app/src/main/java/emu/skyline/MainActivity.kt +++ b/app/src/main/java/emu/skyline/MainActivity.kt @@ -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() } diff --git a/app/src/main/java/emu/skyline/SettingsActivity.kt b/app/src/main/java/emu/skyline/SettingsActivity.kt index 4b18b555..5092147e 100644 --- a/app/src/main/java/emu/skyline/SettingsActivity.kt +++ b/app/src/main/java/emu/skyline/SettingsActivity.kt @@ -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) diff --git a/app/src/main/java/emu/skyline/SkylineApplication.kt b/app/src/main/java/emu/skyline/SkylineApplication.kt new file mode 100644 index 00000000..f6768da9 --- /dev/null +++ b/app/src/main/java/emu/skyline/SkylineApplication.kt @@ -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) + } +} \ No newline at end of file diff --git a/app/src/main/java/emu/skyline/adapter/ControllerAdapter.kt b/app/src/main/java/emu/skyline/adapter/ControllerAdapter.kt index a5a8dd9e..504be4f1 100644 --- a/app/src/main/java/emu/skyline/adapter/ControllerAdapter.kt +++ b/app/src/main/java/emu/skyline/adapter/ControllerAdapter.kt @@ -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() { +class ControllerAdapter(private val onItemClickCallback : (item : ControllerItem) -> Unit) : HeaderAdapter() { /** * This adds a header to the view with the contents of [string] */ @@ -189,24 +189,14 @@ class ControllerAdapter(val context : Context) : HeaderAdapter 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() /** - * 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) } } } diff --git a/app/src/main/java/emu/skyline/input/HostEvent.kt b/app/src/main/java/emu/skyline/input/HostEvent.kt index 10e6fe18..c0e85ac7 100644 --- a/app/src/main/java/emu/skyline/input/HostEvent.kt +++ b/app/src/main/java/emu/skyline/input/HostEvent.kt @@ -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) } diff --git a/app/src/main/java/emu/skyline/input/InputManager.kt b/app/src/main/java/emu/skyline/input/InputManager.kt index ce3fdbf7..dc173ffc 100644 --- a/app/src/main/java/emu/skyline/input/InputManager.kt +++ b/app/src/main/java/emu/skyline/input/InputManager.kt @@ -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 - 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() } /** diff --git a/app/src/main/java/emu/skyline/input/dialog/ButtonDialog.kt b/app/src/main/java/emu/skyline/input/dialog/ButtonDialog.kt index 4bcd6ac5..b0ca1130 100644 --- a/app/src/main/java/emu/skyline/input/dialog/ButtonDialog.kt +++ b/app/src/main/java/emu/skyline/input/dialog/ButtonDialog.kt @@ -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() diff --git a/app/src/main/java/emu/skyline/input/dialog/RumbleDialog.kt b/app/src/main/java/emu/skyline/input/dialog/RumbleDialog.kt index c3493111..410ddcc1 100644 --- a/app/src/main/java/emu/skyline/input/dialog/RumbleDialog.kt +++ b/app/src/main/java/emu/skyline/input/dialog/RumbleDialog.kt @@ -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 { diff --git a/app/src/main/java/emu/skyline/input/dialog/StickDialog.kt b/app/src/main/java/emu/skyline/input/dialog/StickDialog.kt index 4a61f677..ae2f6d81 100644 --- a/app/src/main/java/emu/skyline/input/dialog/StickDialog.kt +++ b/app/src/main/java/emu/skyline/input/dialog/StickDialog.kt @@ -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)) diff --git a/app/src/main/java/emu/skyline/input/onscreen/OnScreenButton.kt b/app/src/main/java/emu/skyline/input/onscreen/OnScreenButton.kt new file mode 100644 index 00000000..9573d939 --- /dev/null +++ b/app/src/main/java/emu/skyline/input/onscreen/OnScreenButton.kt @@ -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 + } +} diff --git a/app/src/main/java/emu/skyline/input/onscreen/OnScreenConfiguration.kt b/app/src/main/java/emu/skyline/input/onscreen/OnScreenConfiguration.kt new file mode 100644 index 00000000..9d716ccf --- /dev/null +++ b/app/src/main/java/emu/skyline/input/onscreen/OnScreenConfiguration.kt @@ -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 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(context : Context, private val prefix : String, private val clazz : Class, private val default : T) : ReadWriteProperty { + 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 +} diff --git a/app/src/main/java/emu/skyline/input/onscreen/OnScreenControllerView.kt b/app/src/main/java/emu/skyline/input/onscreen/OnScreenControllerView.kt new file mode 100644 index 00000000..f12d4d54 --- /dev/null +++ b/app/src/main/java/emu/skyline/input/onscreen/OnScreenControllerView.kt @@ -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() + + 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 + } +} \ No newline at end of file diff --git a/app/src/main/java/emu/skyline/input/onscreen/OnScreenItemDefinitions.kt b/app/src/main/java/emu/skyline/input/onscreen/OnScreenItemDefinitions.kt new file mode 100644 index 00000000..2ed0c01b --- /dev/null +++ b/app/src/main/java/emu/skyline/input/onscreen/OnScreenItemDefinitions.kt @@ -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 + } +} diff --git a/app/src/main/java/emu/skyline/input/onscreen/PointExtensions.kt b/app/src/main/java/emu/skyline/input/onscreen/PointExtensions.kt new file mode 100644 index 00000000..282353b5 --- /dev/null +++ b/app/src/main/java/emu/skyline/input/onscreen/PointExtensions.kt @@ -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()) diff --git a/app/src/main/java/emu/skyline/preference/ControllerPreference.kt b/app/src/main/java/emu/skyline/preference/ControllerPreference.kt index 045ddbbd..4005ded0 100644 --- a/app/src/main/java/emu/skyline/preference/ControllerPreference.kt +++ b/app/src/main/java/emu/skyline/preference/ControllerPreference.kt @@ -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 { context.inputManager.controllers[index]?.type?.stringRes?.let { context.getString(it) } } - } + title = "${context.getString(R.string.config_controller)} #${index + 1}" + summaryProvider = SummaryProvider { 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() } } diff --git a/app/src/main/java/emu/skyline/views/CustomLinearLayout.kt b/app/src/main/java/emu/skyline/views/CustomLinearLayout.kt index 25831e5a..2cdcd705 100644 --- a/app/src/main/java/emu/skyline/views/CustomLinearLayout.kt +++ b/app/src/main/java/emu/skyline/views/CustomLinearLayout.kt @@ -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 diff --git a/app/src/main/res/drawable/ic_button.xml b/app/src/main/res/drawable/ic_button.xml index ba8ea434..84e5688b 100644 --- a/app/src/main/res/drawable/ic_button.xml +++ b/app/src/main/res/drawable/ic_button.xml @@ -1,8 +1,8 @@ - + android:shape="oval"> + + android:width="2.5dp" + android:color="#A0FFFFFF" /> diff --git a/app/src/main/res/drawable/ic_rectangular_button.xml b/app/src/main/res/drawable/ic_rectangular_button.xml new file mode 100644 index 00000000..251af221 --- /dev/null +++ b/app/src/main/res/drawable/ic_rectangular_button.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_stick.xml b/app/src/main/res/drawable/ic_stick.xml index 45db0273..6b1011a1 100644 --- a/app/src/main/res/drawable/ic_stick.xml +++ b/app/src/main/res/drawable/ic_stick.xml @@ -2,31 +2,26 @@ - + + android:width="2dp" + android:color="#A0FFFFFF" /> + android:width="25dp" + android:height="25dp" /> + android:bottom="10dp" + android:left="10dp" + android:right="10dp" + android:top="10dp" /> - + android:width="30dp" + android:height="30dp" /> diff --git a/app/src/main/res/drawable/ic_stick_circle.xml b/app/src/main/res/drawable/ic_stick_circle.xml index ba8ea434..84e5688b 100644 --- a/app/src/main/res/drawable/ic_stick_circle.xml +++ b/app/src/main/res/drawable/ic_stick_circle.xml @@ -1,8 +1,8 @@ - + android:shape="oval"> + + android:width="2.5dp" + android:color="#A0FFFFFF" /> diff --git a/app/src/main/res/drawable/ic_trigger_button_left.xml b/app/src/main/res/drawable/ic_trigger_button_left.xml new file mode 100644 index 00000000..6b2ab5f7 --- /dev/null +++ b/app/src/main/res/drawable/ic_trigger_button_left.xml @@ -0,0 +1,19 @@ + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_trigger_button_right.xml b/app/src/main/res/drawable/ic_trigger_button_right.xml new file mode 100644 index 00000000..5403d742 --- /dev/null +++ b/app/src/main/res/drawable/ic_trigger_button_right.xml @@ -0,0 +1,19 @@ + + + + + + + + + + + diff --git a/app/src/main/res/layout/emu_activity.xml b/app/src/main/res/layout/emu_activity.xml index 3f3c368f..1c083858 100644 --- a/app/src/main/res/layout/emu_activity.xml +++ b/app/src/main/res/layout/emu_activity.xml @@ -1,5 +1,5 @@ - + android:layout_height="match_parent" /> + + - - +