Add on screen controls

* Fix missing default constructor for dialog fragments
This commit is contained in:
Willi Ye 2020-10-03 12:09:35 +02:00 committed by ◱ PixelyIon
parent 85d5dd3619
commit 3057e4b29a
27 changed files with 897 additions and 241 deletions

View File

@ -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"

View File

@ -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,12 +350,14 @@ class EmulationActivity : AppCompatActivity(), SurfaceHolder.Callback, View.OnTo
if ((event.historySize != 0 && value != event.getHistoricalAxisValue(axis, 0)) || (axesHistory[axisItem.index]?.let { it == value } == false)) {
var polarity = value >= 0
val guestEvent = input.eventMap[MotionHostEvent(event.device.descriptor, axis, polarity)] ?: if (value == 0f) {
val guestEvent = MotionHostEvent(event.device.descriptor, axis, polarity).let { hostEvent ->
InputManager.eventMap[hostEvent] ?: if (value == 0f) {
polarity = false
input.eventMap[MotionHostEvent(event.device.descriptor, axis, polarity)]
InputManager.eventMap[hostEvent.copy(polarity = false)]
} else {
null
}
}
when (guestEvent) {
is ButtonGuestEvent -> {
@ -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()
}

View File

@ -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()
}

View File

@ -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()
}

View File

@ -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)

View 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)
}
}

View File

@ -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
}

View File

@ -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)
}
}
}

View File

@ -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)
}

View File

@ -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,19 +28,18 @@ 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),
@ -55,7 +54,6 @@ class InputManager constructor(val context : Context) {
syncFile()
}
}
/**
* This function syncs the class with data from [file]

View File

@ -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()

View File

@ -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 {

View File

@ -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,12 +420,14 @@ 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) {
val guestEvent = MotionHostEvent(event.device.descriptor, axis, polarity).let { hostEvent ->
InputManager.eventMap[hostEvent] ?: if (value == 0f) {
polarity = false
context.manager.eventMap[MotionHostEvent(event.device.descriptor, axis, polarity)]
InputManager.eventMap[hostEvent.copy(polarity = false)]
} else {
null
}
}
when (guestEvent) {
is ButtonGuestEvent -> {
@ -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))

View 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
}
}

View File

@ -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
}

View File

@ -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
}
}

View File

@ -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
}
}

View File

@ -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())

View File

@ -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()
}
}

View File

@ -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

View File

@ -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" />
<solid android:color="#50FFFFFF" />
<stroke
android:width="2.5dp"
android:color="@android:color/black" />
android:color="#A0FFFFFF" />
</shape>

View 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>

View File

@ -2,10 +2,10 @@
<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:color="#A0FFFFFF" />
<size
android:width="25dp"
android:height="25dp" />
@ -18,11 +18,6 @@
</item>
<item>
<shape android:shape="oval">
<!--
<stroke
android:width="2dp"
android:color="#B0FFFFFF" />
-->
<solid android:color="#50000000" />
<size
android:width="30dp"

View File

@ -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" />
<solid android:color="#50FFFFFF" />
<stroke
android:width="2.5dp"
android:color="@android:color/black" />
android:color="#A0FFFFFF" />
</shape>

View 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>

View 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>

View File

@ -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>