Use property delegate to handle preferences

* Add option to disable joystick recentering
This commit is contained in:
Willi Ye 2020-10-04 22:29:50 +02:00 committed by ◱ PixelyIon
parent 5c4aa95da6
commit 7526a985fb
25 changed files with 223 additions and 162 deletions

View File

@ -15,9 +15,9 @@ import android.util.Log
import android.view.* import android.view.*
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.isInvisible import androidx.core.view.isInvisible
import androidx.preference.PreferenceManager
import emu.skyline.input.* import emu.skyline.input.*
import emu.skyline.loader.getRomFormat import emu.skyline.loader.getRomFormat
import emu.skyline.utils.Settings
import kotlinx.android.synthetic.main.emu_activity.* import kotlinx.android.synthetic.main.emu_activity.*
import java.io.File import java.io.File
import kotlin.math.abs import kotlin.math.abs
@ -36,11 +36,6 @@ class EmulationActivity : AppCompatActivity(), SurfaceHolder.Callback, View.OnTo
*/ */
private var vibrators = HashMap<Int, Vibrator>() private var vibrators = HashMap<Int, Vibrator>()
/**
* A boolean flag denoting the current operation mode of the emulator (Docked = true/Handheld = false)
*/
private var operationMode = true
/** /**
* The surface object used for displaying frames * The surface object used for displaying frames
*/ */
@ -63,6 +58,8 @@ class EmulationActivity : AppCompatActivity(), SurfaceHolder.Callback, View.OnTo
*/ */
private lateinit var emulationThread : Thread private lateinit var emulationThread : Thread
private val settings by lazy { Settings(this) }
/** /**
* This is the entry point into the emulation code for libskyline * This is the entry point into the emulation code for libskyline
* *
@ -149,7 +146,7 @@ class EmulationActivity : AppCompatActivity(), SurfaceHolder.Callback, View.OnTo
if (controller.type != ControllerType.None) { if (controller.type != ControllerType.None) {
val type = when (controller.type) { val type = when (controller.type) {
ControllerType.None -> throw IllegalArgumentException() ControllerType.None -> throw IllegalArgumentException()
ControllerType.HandheldProController -> if (operationMode) ControllerType.ProController.id else ControllerType.HandheldProController.id ControllerType.HandheldProController -> if (settings.operationMode) ControllerType.ProController.id else ControllerType.HandheldProController.id
ControllerType.ProController, ControllerType.JoyConLeft, ControllerType.JoyConRight -> controller.type.id ControllerType.ProController, ControllerType.JoyConLeft, ControllerType.JoyConRight -> controller.type.id
} }
@ -212,9 +209,7 @@ class EmulationActivity : AppCompatActivity(), SurfaceHolder.Callback, View.OnTo
game_view.holder.addCallback(this) game_view.holder.addCallback(this)
val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this) if (settings.perfStats) {
if (sharedPreferences.getBoolean("perf_stats", false)) {
perf_stats.postDelayed(object : Runnable { perf_stats.postDelayed(object : Runnable {
override fun run() { override fun run() {
perf_stats.text = "${getFps()} FPS\n${getFrametime()}ms" perf_stats.text = "${getFps()} FPS\n${getFrametime()}ms"
@ -223,15 +218,13 @@ class EmulationActivity : AppCompatActivity(), SurfaceHolder.Callback, View.OnTo
}, 250) }, 250)
} }
operationMode = sharedPreferences.getBoolean("operation_mode", operationMode)
@Suppress("DEPRECATION") val display = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) display!! else windowManager.defaultDisplay @Suppress("DEPRECATION") val display = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) display!! else windowManager.defaultDisplay
display?.supportedModes?.maxBy { it.refreshRate + (it.physicalHeight * it.physicalWidth) }?.let { window.attributes.preferredDisplayModeId = it.modeId } display?.supportedModes?.maxBy { it.refreshRate + (it.physicalHeight * it.physicalWidth) }?.let { window.attributes.preferredDisplayModeId = it.modeId }
game_view.setOnTouchListener(this) game_view.setOnTouchListener(this)
// Hide on screen controls when first controller is not set // Hide on screen controls when first controller is not set
on_screen_controller_view.isInvisible = !InputManager.controllers[0]!!.type.firstController || !sharedPreferences.getBoolean("on_screen_control", false) on_screen_controller_view.isInvisible = !InputManager.controllers[0]!!.type.firstController || !settings.onScreenControl
on_screen_controller_view.setOnButtonStateChangedListener(::onButtonStateChanged) on_screen_controller_view.setOnButtonStateChangedListener(::onButtonStateChanged)
on_screen_controller_view.setOnStickStateChangedListener(::onStickStateChanged) on_screen_controller_view.setOnStickStateChangedListener(::onStickStateChanged)

View File

@ -14,13 +14,13 @@ import android.view.MenuItem
import android.widget.Toast import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.widget.SearchView import androidx.appcompat.widget.SearchView
import androidx.preference.PreferenceManager
import androidx.recyclerview.widget.DividerItemDecoration import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import emu.skyline.adapter.GenericAdapter import emu.skyline.adapter.GenericAdapter
import emu.skyline.adapter.HeaderViewItem import emu.skyline.adapter.HeaderViewItem
import emu.skyline.adapter.LogViewItem import emu.skyline.adapter.LogViewItem
import emu.skyline.utils.Settings
import kotlinx.android.synthetic.main.log_activity.* import kotlinx.android.synthetic.main.log_activity.*
import kotlinx.android.synthetic.main.titlebar.* import kotlinx.android.synthetic.main.titlebar.*
import org.json.JSONObject import org.json.JSONObject
@ -52,9 +52,10 @@ class LogActivity : AppCompatActivity() {
setSupportActionBar(toolbar) setSupportActionBar(toolbar)
supportActionBar?.setDisplayHomeAsUpEnabled(true) supportActionBar?.setDisplayHomeAsUpEnabled(true)
val prefs = PreferenceManager.getDefaultSharedPreferences(this) val settings = Settings(this)
val compact = prefs.getBoolean("log_compact", false)
val logLevel = prefs.getString("log_level", "3")!!.toInt() val compact = settings.logCompact
val logLevel = settings.logLevel.toInt()
val logLevels = resources.getStringArray(R.array.log_level) val logLevels = resources.getStringArray(R.array.log_level)
log_list.adapter = adapter log_list.adapter = adapter

View File

@ -37,6 +37,9 @@ import emu.skyline.data.ElementType
import emu.skyline.loader.LoaderResult import emu.skyline.loader.LoaderResult
import emu.skyline.loader.RomFile import emu.skyline.loader.RomFile
import emu.skyline.loader.RomFormat import emu.skyline.loader.RomFormat
import emu.skyline.utils.Settings
import emu.skyline.utils.loadSerializedList
import emu.skyline.utils.serialize
import kotlinx.android.synthetic.main.main_activity.* import kotlinx.android.synthetic.main.main_activity.*
import kotlinx.android.synthetic.main.titlebar.* import kotlinx.android.synthetic.main.titlebar.*
import java.io.File import java.io.File
@ -50,10 +53,7 @@ class MainActivity : AppCompatActivity() {
private val TAG = MainActivity::class.java.simpleName private val TAG = MainActivity::class.java.simpleName
} }
/** private val settings by lazy { Settings(this) }
* This is used to get/set shared preferences
*/
private val sharedPreferences by lazy { PreferenceManager.getDefaultSharedPreferences(this) }
/** /**
* The adapter used for adding elements to [app_list] * The adapter used for adding elements to [app_list]
@ -62,7 +62,7 @@ class MainActivity : AppCompatActivity() {
private var reloading = AtomicBoolean() private var reloading = AtomicBoolean()
private val layoutType get() = LayoutType.values()[sharedPreferences.getString("layout_type", "1")!!.toInt()] private val layoutType get() = LayoutType.values()[settings.layoutType.toInt()]
private val missingIcon by lazy { ContextCompat.getDrawable(this, R.drawable.default_icon)!!.toBitmap(256, 256) } private val missingIcon by lazy { ContextCompat.getDrawable(this, R.drawable.default_icon)!!.toBitmap(256, 256) }
@ -134,7 +134,7 @@ class MainActivity : AppCompatActivity() {
try { try {
runOnUiThread { adapter.removeAllItems() } runOnUiThread { adapter.removeAllItems() }
val searchLocation = DocumentFile.fromTreeUri(this, Uri.parse(sharedPreferences.getString("search_location", "")))!! val searchLocation = DocumentFile.fromTreeUri(this, Uri.parse(settings.searchLocation))!!
val romElements = ArrayList<BaseElement>() val romElements = ArrayList<BaseElement>()
addEntries("nro", RomFormat.NRO, searchLocation, romElements) addEntries("nro", RomFormat.NRO, searchLocation, romElements)
@ -155,10 +155,10 @@ class MainActivity : AppCompatActivity() {
} }
} }
sharedPreferences.edit().putBoolean("refresh_required", false).apply() settings.refreshRequired = false
} catch (e : IllegalArgumentException) { } catch (e : IllegalArgumentException) {
runOnUiThread { runOnUiThread {
sharedPreferences.edit().remove("search_location").apply() settings.searchLocation = ""
val intent = intent val intent = intent
finish() finish()
@ -191,7 +191,7 @@ class MainActivity : AppCompatActivity() {
PreferenceManager.setDefaultValues(this, R.xml.preferences, false) PreferenceManager.setDefaultValues(this, R.xml.preferences, false)
AppCompatDelegate.setDefaultNightMode(when ((sharedPreferences.getString("app_theme", "2")?.toInt())) { AppCompatDelegate.setDefaultNightMode(when ((settings.appTheme.toInt())) {
0 -> AppCompatDelegate.MODE_NIGHT_NO 0 -> AppCompatDelegate.MODE_NIGHT_NO
1 -> AppCompatDelegate.MODE_NIGHT_YES 1 -> AppCompatDelegate.MODE_NIGHT_YES
2 -> AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM 2 -> AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM
@ -267,13 +267,13 @@ class MainActivity : AppCompatActivity() {
} }
setAppListDecoration() setAppListDecoration()
if (sharedPreferences.getString("search_location", "") == "") { if (settings.searchLocation.isEmpty()) {
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE) val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE)
intent.flags = Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION or Intent.FLAG_GRANT_PREFIX_URI_PERMISSION or Intent.FLAG_GRANT_READ_URI_PERMISSION intent.flags = Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION or Intent.FLAG_GRANT_PREFIX_URI_PERMISSION or Intent.FLAG_GRANT_READ_URI_PERMISSION
startActivityForResult(intent, 1) startActivityForResult(intent, 1)
} else { } else {
refreshAdapter(!sharedPreferences.getBoolean("refresh_required", false)) refreshAdapter(!settings.refreshRequired)
} }
} }
@ -301,7 +301,7 @@ class MainActivity : AppCompatActivity() {
} }
private fun selectStartGame(appItem : AppItem) { private fun selectStartGame(appItem : AppItem) {
if (sharedPreferences.getBoolean("select_action", false)) if (settings.selectAction)
AppDialog.newInstance(appItem).show(supportFragmentManager, "game") AppDialog.newInstance(appItem).show(supportFragmentManager, "game")
else if (appItem.loaderResult == LoaderResult.Success) else if (appItem.loaderResult == LoaderResult.Success)
startActivity(Intent(this, EmulationActivity::class.java).apply { data = appItem.uri }) startActivity(Intent(this, EmulationActivity::class.java).apply { data = appItem.uri })
@ -341,9 +341,9 @@ class MainActivity : AppCompatActivity() {
1 -> { 1 -> {
val uri = intent!!.data!! val uri = intent!!.data!!
contentResolver.takePersistableUriPermission(uri, Intent.FLAG_GRANT_READ_URI_PERMISSION) contentResolver.takePersistableUriPermission(uri, Intent.FLAG_GRANT_READ_URI_PERMISSION)
sharedPreferences.edit().putString("search_location", uri.toString()).apply() settings.searchLocation = uri.toString()
refreshAdapter(!sharedPreferences.getBoolean("refresh_required", false)) refreshAdapter(!settings.refreshRequired)
} }
2 -> { 2 -> {
@ -361,8 +361,7 @@ class MainActivity : AppCompatActivity() {
} }
3 -> { 3 -> {
if (sharedPreferences.getBoolean("refresh_required", false)) if (settings.refreshRequired) refreshAdapter(false)
refreshAdapter(false)
} }
} }
} }

View File

@ -8,6 +8,7 @@ package emu.skyline.adapter.controller
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.core.view.isGone
import emu.skyline.R import emu.skyline.R
import emu.skyline.adapter.GenericLayoutFactory import emu.skyline.adapter.GenericLayoutFactory
import emu.skyline.adapter.GenericViewHolder import emu.skyline.adapter.GenericViewHolder
@ -22,7 +23,9 @@ class ControllerCheckBoxViewItem(var title : String, var summary : String, var c
override fun getLayoutFactory() : GenericLayoutFactory = ControllerCheckBoxLayoutFactory override fun getLayoutFactory() : GenericLayoutFactory = ControllerCheckBoxLayoutFactory
override fun bind(holder : GenericViewHolder, position : Int) { override fun bind(holder : GenericViewHolder, position : Int) {
holder.text_title.isGone = title.isEmpty()
holder.text_title.text = title holder.text_title.text = title
holder.text_subtitle.isGone = summary.isEmpty()
holder.text_subtitle.text = summary holder.text_subtitle.text = summary
holder.checkbox.isChecked = checked holder.checkbox.isChecked = checked
holder.itemView.setOnClickListener { holder.itemView.setOnClickListener {

View File

@ -9,7 +9,6 @@ import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.view.KeyEvent import android.view.KeyEvent
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.preference.PreferenceManager
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
import emu.skyline.R import emu.skyline.R
@ -20,6 +19,7 @@ import emu.skyline.input.dialog.ButtonDialog
import emu.skyline.input.dialog.RumbleDialog import emu.skyline.input.dialog.RumbleDialog
import emu.skyline.input.dialog.StickDialog import emu.skyline.input.dialog.StickDialog
import emu.skyline.input.onscreen.OnScreenEditActivity import emu.skyline.input.onscreen.OnScreenEditActivity
import emu.skyline.utils.Settings
import kotlinx.android.synthetic.main.controller_activity.* import kotlinx.android.synthetic.main.controller_activity.*
import kotlinx.android.synthetic.main.titlebar.* import kotlinx.android.synthetic.main.titlebar.*
@ -47,7 +47,7 @@ class ControllerActivity : AppCompatActivity() {
*/ */
val axisMap = mutableMapOf<AxisId, ControllerStickViewItem>() val axisMap = mutableMapOf<AxisId, ControllerStickViewItem>()
private val sharedPrefs by lazy { PreferenceManager.getDefaultSharedPreferences(this) } private val settings by lazy { Settings(this) }
/** /**
* This function updates the [adapter] based on information from [InputManager] * This function updates the [adapter] based on information from [InputManager]
@ -65,9 +65,15 @@ class ControllerActivity : AppCompatActivity() {
if (id == 0 && controller.type.firstController) { if (id == 0 && controller.type.firstController) {
adapter.addItem(HeaderViewItem(getString(R.string.osc))) adapter.addItem(HeaderViewItem(getString(R.string.osc)))
adapter.addItem(ControllerCheckBoxViewItem(getString(R.string.osc_enable), getString(R.string.osc_not_shown), sharedPrefs.getBoolean("on_screen_control", false)) { item, position -> val oscSummary = { checked : Boolean -> getString(if (checked) R.string.osc_shown else R.string.osc_not_shown) }
item.summary = getString(if (item.checked) R.string.osc_shown else R.string.osc_not_shown) adapter.addItem(ControllerCheckBoxViewItem(getString(R.string.osc_enable), oscSummary.invoke(settings.onScreenControl), settings.onScreenControl) { item, position ->
sharedPrefs.edit().putBoolean("on_screen_control", item.checked).apply() item.summary = oscSummary.invoke(item.checked)
settings.onScreenControl = item.checked
adapter.notifyItemChanged(position)
})
adapter.addItem(ControllerCheckBoxViewItem(getString(R.string.osc_recenter_sticks), "", settings.onScreenControlRecenterSticks) { item, position ->
settings.onScreenControlRecenterSticks = item.checked
adapter.notifyItemChanged(position) adapter.notifyItemChanged(position)
}) })

View File

@ -24,7 +24,7 @@ import kotlin.math.abs
* *
* @param item This is used to hold the [ControllerButtonViewItem] between instances * @param item This is used to hold the [ControllerButtonViewItem] between instances
*/ */
class ButtonDialog @JvmOverloads constructor(private val item : ControllerButtonViewItem? = null, private val position : Int? = null) : BottomSheetDialogFragment() { class ButtonDialog @JvmOverloads constructor(private val item : ControllerButtonViewItem? = null) : BottomSheetDialogFragment() {
/** /**
* This inflates the layout of the dialog after initial view creation * This inflates the layout of the dialog after initial view creation
*/ */

View File

@ -7,8 +7,7 @@ package emu.skyline.input.onscreen
import android.content.Context import android.content.Context
import emu.skyline.input.ButtonId import emu.skyline.input.ButtonId
import kotlin.properties.ReadWriteProperty import emu.skyline.utils.sharedPreferences
import kotlin.reflect.KProperty
interface ControllerConfiguration { interface ControllerConfiguration {
var enabled : Boolean var enabled : Boolean
@ -17,55 +16,18 @@ interface ControllerConfiguration {
var relativeY : Float var relativeY : Float
} }
class ControllerConfigurationDummy( class ControllerConfigurationDummy(defaultRelativeX : Float, defaultRelativeY : Float) : ControllerConfiguration {
defaultRelativeX : Float,
defaultRelativeY : Float
) : ControllerConfiguration {
override var enabled = true override var enabled = true
override var globalScale = 1f override var globalScale = 1f
override var relativeX = defaultRelativeX override var relativeX = defaultRelativeX
override var relativeY = defaultRelativeY override var relativeY = defaultRelativeY
} }
class ControllerConfigurationImpl( class ControllerConfigurationImpl(private val context : Context, private val buttonId : ButtonId, defaultRelativeX : Float, defaultRelativeY : Float) : ControllerConfiguration {
private val context : Context, private inline fun <reified T> config(default : T, prefix : String = "${buttonId.name}_") = sharedPreferences(context, default, prefix, "controller_config")
private val buttonId : ButtonId,
defaultRelativeX : Float,
defaultRelativeY : Float
) : ControllerConfiguration {
private inline fun <reified T> config(default : T) = ControllerPrefs(context, "${buttonId.name}_", T::class.java, default)
override var enabled by config(true) override var enabled by config(true)
override var globalScale by ControllerPrefs(context, "on_screen_controller_", Float::class.java, 1f) override var globalScale by config(1f, "")
override var relativeX by config(defaultRelativeX) override var relativeX by config(defaultRelativeX)
override var relativeY by config(defaultRelativeY) 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)
Boolean::class.java, java.lang.Boolean::class.java -> putBoolean(prefix + property.name, value as Boolean)
else -> error("Unsupported type $clazz ${Float::class.java}")
}
}.apply()
}
override fun getValue(thisRef : Any, property : KProperty<*>) : T =
prefs.let {
@Suppress("IMPLICIT_CAST_TO_ANY")
when (clazz) {
Float::class.java, java.lang.Float::class.java -> it.getFloat(prefix + property.name, default as Float)
Boolean::class.java, java.lang.Boolean::class.java -> it.getBoolean(prefix + property.name, default as Boolean)
else -> error("Unsupported type $clazz")
}
} as T
}

View File

@ -17,6 +17,9 @@ import android.view.View
import android.view.View.OnTouchListener import android.view.View.OnTouchListener
import emu.skyline.input.ButtonId import emu.skyline.input.ButtonId
import emu.skyline.input.ButtonState import emu.skyline.input.ButtonState
import emu.skyline.utils.add
import emu.skyline.utils.multiply
import emu.skyline.utils.normalize
import kotlin.math.roundToLong import kotlin.math.roundToLong
typealias OnButtonStateChangedListener = (buttonId : ButtonId, state : ButtonState) -> Unit typealias OnButtonStateChangedListener = (buttonId : ButtonId, state : ButtonState) -> Unit
@ -32,6 +35,11 @@ class OnScreenControllerView @JvmOverloads constructor(
private var onButtonStateChangedListener : OnButtonStateChangedListener? = null private var onButtonStateChangedListener : OnButtonStateChangedListener? = null
private var onStickStateChangedListener : OnStickStateChangedListener? = null private var onStickStateChangedListener : OnStickStateChangedListener? = null
private val joystickAnimators = mutableMapOf<JoystickButton, Animator?>() private val joystickAnimators = mutableMapOf<JoystickButton, Animator?>()
var recenterSticks = false
set(value) {
field = value
controls.joysticks.forEach { it.recenterSticks = recenterSticks }
}
override fun onDraw(canvas : Canvas) { override fun onDraw(canvas : Canvas) {
super.onDraw(canvas) super.onDraw(canvas)
@ -95,7 +103,7 @@ class OnScreenControllerView @JvmOverloads constructor(
val value = animation.animatedValue as Float val value = animation.animatedValue as Float
val vector = direction.multiply(value) val vector = direction.multiply(value)
val newPosition = position.add(vector) val newPosition = position.add(vector)
joystick.onFingerMoved(newPosition.x, newPosition.y) joystick.onFingerMoved(newPosition.x, newPosition.y, false)
onStickStateChangedListener?.invoke(joystick.buttonId, vector.multiply(1f / radius)) onStickStateChangedListener?.invoke(joystick.buttonId, vector.multiply(1f / radius))
invalidate() invalidate()
} }
@ -129,6 +137,8 @@ class OnScreenControllerView @JvmOverloads constructor(
joystick.onFingerDown(x, y) joystick.onFingerDown(x, y)
if (joystick.shortDoubleTapped) if (joystick.shortDoubleTapped)
onButtonStateChangedListener?.invoke(joystick.buttonId, ButtonState.Pressed) onButtonStateChangedListener?.invoke(joystick.buttonId, ButtonState.Pressed)
if (recenterSticks)
onStickStateChangedListener?.invoke(joystick.buttonId, joystick.outerToInnerRelative())
performClick() performClick()
handled = true handled = true
} }
@ -144,8 +154,7 @@ class OnScreenControllerView @JvmOverloads constructor(
} }
} }
} }
handled.also { if (it) invalidate() }
handled.also { if (it) invalidate() else super.onTouchEvent(event) }
} }
private val editingTouchHandler = OnTouchListener { _, event -> private val editingTouchHandler = OnTouchListener { _, event ->
@ -175,7 +184,7 @@ class OnScreenControllerView @JvmOverloads constructor(
} }
} }
false false
}.also { handled -> if (handled) invalidate() else super.onTouchEvent(event) } }.also { handled -> if (handled) invalidate() }
} }
init { init {
@ -194,12 +203,12 @@ class OnScreenControllerView @JvmOverloads constructor(
} }
fun increaseScale() { fun increaseScale() {
controls.globalScale *= 1.1f controls.globalScale += 0.05f
invalidate() invalidate()
} }
fun decreaseScale() { fun decreaseScale() {
controls.globalScale *= 0.9f controls.globalScale -= 0.05f
invalidate() invalidate()
} }

View File

@ -16,6 +16,7 @@ import androidx.core.content.ContextCompat
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.floatingactionbutton.FloatingActionButton import com.google.android.material.floatingactionbutton.FloatingActionButton
import emu.skyline.R import emu.skyline.R
import emu.skyline.utils.Settings
import kotlinx.android.synthetic.main.main_activity.fab_parent import kotlinx.android.synthetic.main.main_activity.fab_parent
import kotlinx.android.synthetic.main.on_screen_edit_activity.* import kotlinx.android.synthetic.main.on_screen_edit_activity.*
@ -31,7 +32,7 @@ class OnScreenEditActivity : AppCompatActivity() {
} else { } else {
fullEditVisible = !fullEditVisible fullEditVisible = !fullEditVisible
toggleFabVisibility(fullEditVisible) toggleFabVisibility(fullEditVisible)
fabMapping[R.drawable.ic_close]!!.animate().rotationBy(if (fullEditVisible) -45f else 45f) fabMapping[R.drawable.ic_close]!!.animate().rotation(if (fullEditVisible) 0f else 45f)
} }
} }
@ -70,7 +71,7 @@ class OnScreenEditActivity : AppCompatActivity() {
} }
private val actions : List<Pair<Int, () -> Unit>> = listOf( private val actions : List<Pair<Int, () -> Unit>> = listOf(
Pair(R.drawable.ic_refresh, { on_screen_controller_view.resetControls() }), Pair(R.drawable.ic_restore, { on_screen_controller_view.resetControls() }),
Pair(R.drawable.ic_toggle, toggleAction), Pair(R.drawable.ic_toggle, toggleAction),
Pair(R.drawable.ic_edit, editAction), Pair(R.drawable.ic_edit, editAction),
Pair(R.drawable.ic_zoom_out, { on_screen_controller_view.decreaseScale() }), Pair(R.drawable.ic_zoom_out, { on_screen_controller_view.decreaseScale() }),
@ -83,6 +84,7 @@ class OnScreenEditActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState : Bundle?) { override fun onCreate(savedInstanceState : Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContentView(R.layout.on_screen_edit_activity) setContentView(R.layout.on_screen_edit_activity)
on_screen_controller_view.recenterSticks = Settings(this).onScreenControlRecenterSticks
actions.forEach { pair -> actions.forEach { pair ->
fab_parent.addView(FloatingActionButton(this).apply { fab_parent.addView(FloatingActionButton(this).apply {

View File

@ -12,6 +12,8 @@ import androidx.core.graphics.minus
import emu.skyline.R import emu.skyline.R
import emu.skyline.input.ButtonId import emu.skyline.input.ButtonId
import emu.skyline.input.ButtonId.* import emu.skyline.input.ButtonId.*
import emu.skyline.utils.add
import emu.skyline.utils.multiply
import kotlin.math.roundToInt import kotlin.math.roundToInt
open class CircularButton( open class CircularButton(
@ -55,11 +57,12 @@ class JoystickButton(
defaultRelativeX, defaultRelativeX,
defaultRelativeY, defaultRelativeY,
defaultRelativeRadiusToX, defaultRelativeRadiusToX,
R.drawable.ic_stick_circle R.drawable.ic_button
) { ) {
private val innerButton = CircularButton(onScreenControllerView, buttonId, config.relativeX, config.relativeY, defaultRelativeRadiusToX * 0.75f, R.drawable.ic_stick) private val innerButton = CircularButton(onScreenControllerView, buttonId, config.relativeX, config.relativeY, defaultRelativeRadiusToX * 0.75f, R.drawable.ic_stick)
var recenterSticks = false
private lateinit var initialTapPosition : PointF
private var fingerDownTime = 0L private var fingerDownTime = 0L
private var fingerUpTime = 0L private var fingerUpTime = 0L
var shortDoubleTapped = false var shortDoubleTapped = false
@ -76,12 +79,17 @@ class JoystickButton(
} }
override fun onFingerDown(x : Float, y : Float) { override fun onFingerDown(x : Float, y : Float) {
relativeX = x / width val relativeX = x / width
relativeY = (y - heightDiff) / adjustedHeight val relativeY = (y - heightDiff) / adjustedHeight
if (!recenterSticks) {
this.relativeX = relativeX
this.relativeY = relativeY
}
innerButton.relativeX = relativeX innerButton.relativeX = relativeX
innerButton.relativeY = relativeY innerButton.relativeY = relativeY
val currentTime = SystemClock.elapsedRealtime() val currentTime = SystemClock.elapsedRealtime()
initialTapPosition = PointF(x, y)
val firstTapDiff = fingerUpTime - fingerDownTime val firstTapDiff = fingerUpTime - fingerDownTime
val secondTapDiff = currentTime - fingerUpTime val secondTapDiff = currentTime - fingerUpTime
if (firstTapDiff in 0..500 && secondTapDiff in 0..500) { if (firstTapDiff in 0..500 && secondTapDiff in 0..500) {
@ -101,16 +109,17 @@ class JoystickButton(
drawable.alpha = 255 drawable.alpha = 255
} }
fun onFingerMoved(x : Float, y : Float) : PointF { fun onFingerMoved(x : Float, y : Float, manualMove : Boolean = true) : PointF {
val position = PointF(currentX, currentY) val position = PointF(currentX, currentY)
var finger = PointF(x, y) var finger = PointF(x, y)
val outerToInner = finger.minus(position) val outerToInner = finger.minus(position)
val distance = outerToInner.length() val distance = outerToInner.length()
if (distance >= radius) { if (distance > radius) {
finger = position.add(outerToInner.multiply(1f / distance * radius)) finger = position.add(outerToInner.multiply(1f / distance * radius))
} }
if (distance > radius * 0.075f) { // If finger get moved to much, then don't trigger as joystick being pressed
if (manualMove && initialTapPosition.minus(finger).length() > radius * 0.075f) {
fingerDownTime = 0 fingerDownTime = 0
fingerUpTime = 0 fingerUpTime = 0
} }
@ -122,6 +131,8 @@ class JoystickButton(
fun outerToInner() = PointF(innerButton.currentX, innerButton.currentY).minus(PointF(currentX, currentY)) fun outerToInner() = PointF(innerButton.currentX, innerButton.currentY).minus(PointF(currentX, currentY))
fun outerToInnerRelative() = outerToInner().multiply(1f / radius)
override fun edit(x : Float, y : Float) { override fun edit(x : Float, y : Float) {
super.edit(x, y) super.edit(x, y)
@ -209,13 +220,13 @@ class Controls(onScreenControllerView : OnScreenControllerView) {
) )
val rectangularButtons = listOf( val rectangularButtons = listOf(
RectangularButton(onScreenControllerView, L, 0.1f, 0.25f, 0.075f, 0.08f), RectangularButton(onScreenControllerView, L, 0.1f, 0.25f, 0.09f, 0.1f),
RectangularButton(onScreenControllerView, R, 0.9f, 0.25f, 0.075f, 0.08f) RectangularButton(onScreenControllerView, R, 0.9f, 0.25f, 0.09f, 0.1f)
) )
val triggerButtons = listOf( val triggerButtons = listOf(
TriggerButton(onScreenControllerView, ZL, 0.1f, 0.1f, 0.075f, 0.08f), TriggerButton(onScreenControllerView, ZL, 0.1f, 0.1f, 0.09f, 0.1f),
TriggerButton(onScreenControllerView, ZR, 0.9f, 0.1f, 0.075f, 0.08f) TriggerButton(onScreenControllerView, ZR, 0.9f, 0.1f, 0.09f, 0.1f)
) )
val allButtons = circularButtons + joysticks + rectangularButtons + triggerButtons val allButtons = circularButtons + joysticks + rectangularButtons + triggerButtons

View File

@ -3,7 +3,7 @@
* Copyright © 2020 Skyline Team and Contributors (https://github.com/skyline-emu/) * Copyright © 2020 Skyline Team and Contributors (https://github.com/skyline-emu/)
*/ */
package emu.skyline.input.onscreen package emu.skyline.utils
import android.graphics.PointF import android.graphics.PointF

View File

@ -3,7 +3,7 @@
* Copyright © 2020 Skyline Team and Contributors (https://github.com/skyline-emu/) * Copyright © 2020 Skyline Team and Contributors (https://github.com/skyline-emu/)
*/ */
package emu.skyline package emu.skyline.utils
import java.io.File import java.io.File
import java.io.ObjectInputStream import java.io.ObjectInputStream

View File

@ -0,0 +1,32 @@
/*
* SPDX-License-Identifier: MPL-2.0
* Copyright © 2020 Skyline Team and Contributors (https://github.com/skyline-emu/)
*/
package emu.skyline.utils
import android.content.Context
class Settings(context : Context) {
var layoutType by sharedPreferences(context, "1")
var searchLocation by sharedPreferences(context, "")
var refreshRequired by sharedPreferences(context, false)
var appTheme by sharedPreferences(context, "2")
var selectAction by sharedPreferences(context, false)
var perfStats by sharedPreferences(context, false)
var operationMode by sharedPreferences(context, true)
var onScreenControl by sharedPreferences(context, false)
var onScreenControlRecenterSticks by sharedPreferences(context, false)
var logCompact by sharedPreferences(context, false)
var logLevel by sharedPreferences(context, "3")
}

View File

@ -0,0 +1,48 @@
/*
* SPDX-License-Identifier: MPL-2.0
* Copyright © 2020 Skyline Team and Contributors (https://github.com/skyline-emu/)
*/
package emu.skyline.utils
import android.content.Context
import androidx.preference.PreferenceManager
import kotlin.properties.ReadWriteProperty
import kotlin.reflect.KProperty
inline fun <reified T> sharedPreferences(context : Context, default : T, prefix : String = "", prefName : String? = null) = SharedPreferencesDelegate(context, T::class.java, default, prefix, prefName)
@Suppress("UNCHECKED_CAST")
class SharedPreferencesDelegate<T>(context : Context, private val clazz : Class<T>, private val default : T, private val prefix : String, prefName : String?) : ReadWriteProperty<Any, T> {
private val prefs = prefName?.let { context.getSharedPreferences(prefName, Context.MODE_PRIVATE) } ?: PreferenceManager.getDefaultSharedPreferences(context)
override fun setValue(thisRef : Any, property : KProperty<*>, value : T) = (prefix + pascalToSnakeCase(property.name)).let { keyName ->
prefs.edit().apply {
when (clazz) {
Float::class.java, java.lang.Float::class.java -> putFloat(keyName, value as Float)
Boolean::class.java, java.lang.Boolean::class.java -> putBoolean(keyName, value as Boolean)
String::class.java, java.lang.String::class.java -> putString(keyName, value as String)
else -> error("Unsupported type $clazz ${Float::class.java}")
}
}.apply()
}
override fun getValue(thisRef : Any, property : KProperty<*>) : T = (prefix + pascalToSnakeCase(property.name)).let { keyName ->
prefs.let {
@Suppress("IMPLICIT_CAST_TO_ANY")
when (clazz) {
Float::class.java, java.lang.Float::class.java -> it.getFloat(keyName, default as Float)
Boolean::class.java, java.lang.Boolean::class.java -> it.getBoolean(keyName, default as Boolean)
String::class.java, java.lang.String::class.java -> it.getString(keyName, default as String)
else -> error("Unsupported type $clazz")
}
} as T
}
private fun pascalToSnakeCase(text : String) = StringBuilder().apply {
text.forEachIndexed { index, c ->
if (index != 0 && c.isUpperCase()) append('_')
append(c.toLowerCase())
}.toString()
}
}

View File

@ -1,8 +1,8 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android" <shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval"> android:shape="oval">
<solid android:color="#50FFFFFF" /> <solid android:color="#20FFFFFF" />
<stroke <stroke
android:width="2.5dp" android:width="2.5dp"
android:color="#A0FFFFFF" /> android:color="#25FFFFFF" />
</shape> </shape>

View File

@ -2,10 +2,10 @@
<layer-list xmlns:android="http://schemas.android.com/apk/res/android"> <layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item> <item>
<shape android:shape="rectangle"> <shape android:shape="rectangle">
<solid android:color="#50FFFFFF" /> <solid android:color="#20FFFFFF" />
<stroke <stroke
android:width="2dp" android:width="2dp"
android:color="#A0FFFFFF" /> android:color="#25FFFFFF" />
<size <size
android:width="25dp" android:width="25dp"
android:height="25dp" /> android:height="25dp" />

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#000"
android:pathData="M12,3A9,9 0 0,0 3,12H0L4,16L8,12H5A7,7 0 0,1 12,5A7,7 0 0,1 19,12A7,7 0 0,1 12,19C10.5,19 9.09,18.5 7.94,17.7L6.5,19.14C8.04,20.3 9.94,21 12,21A9,9 0 0,0 21,12A9,9 0 0,0 12,3M14,12A2,2 0 0,0 12,10A2,2 0 0,0 10,12A2,2 0 0,0 12,14A2,2 0 0,0 14,12Z" />
</vector>

View File

@ -2,10 +2,10 @@
<layer-list xmlns:android="http://schemas.android.com/apk/res/android"> <layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item> <item>
<shape android:shape="oval"> <shape android:shape="oval">
<solid android:color="#50FFFFFF" /> <solid android:color="#10FFFFFF" />
<stroke <stroke
android:width="2dp" android:width="2dp"
android:color="#A0FFFFFF" /> android:color="#25FFFFFF" />
<size <size
android:width="25dp" android:width="25dp"
android:height="25dp" /> android:height="25dp" />
@ -18,7 +18,7 @@
</item> </item>
<item> <item>
<shape android:shape="oval"> <shape android:shape="oval">
<solid android:color="#50000000" /> <solid android:color="#30000000" />
<size <size
android:width="30dp" android:width="30dp"
android:height="30dp" /> android:height="30dp" />

View File

@ -1,8 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval">
<solid android:color="#50FFFFFF" />
<stroke
android:width="2.5dp"
android:color="#A0FFFFFF" />
</shape>

View File

@ -2,10 +2,10 @@
<layer-list xmlns:android="http://schemas.android.com/apk/res/android"> <layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item> <item>
<shape android:shape="rectangle"> <shape android:shape="rectangle">
<solid android:color="#50FFFFFF" /> <solid android:color="#20FFFFFF" />
<stroke <stroke
android:width="2dp" android:width="2dp"
android:color="#A0FFFFFF" /> android:color="#25FFFFFF" />
<size <size
android:width="25dp" android:width="25dp"
android:height="25dp" /> android:height="25dp" />

View File

@ -2,10 +2,10 @@
<layer-list xmlns:android="http://schemas.android.com/apk/res/android"> <layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item> <item>
<shape android:shape="rectangle"> <shape android:shape="rectangle">
<solid android:color="#50FFFFFF" /> <solid android:color="#20FFFFFF" />
<stroke <stroke
android:width="2dp" android:width="2dp"
android:color="#A0FFFFFF" /> android:color="#25FFFFFF" />
<size <size
android:width="25dp" android:width="25dp"
android:height="25dp" /> android:height="25dp" />

View File

@ -36,10 +36,10 @@
android:id="@+id/button_icon" android:id="@+id/button_icon"
android:layout_width="50dp" android:layout_width="50dp"
android:layout_height="50dp" android:layout_height="50dp"
android:alpha="0.15"
android:contentDescription="@string/buttons" android:contentDescription="@string/buttons"
android:outlineProvider="bounds" android:outlineProvider="bounds"
android:src="@drawable/ic_button" /> android:src="@drawable/ic_button"
android:tint="?android:attr/textColorPrimary" />
<TextView <TextView
android:id="@+id/button_text" android:id="@+id/button_text"

View File

@ -1,52 +1,42 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/controller_item"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:background="?attr/selectableItemBackground" android:background="?attr/selectableItemBackground"
android:clickable="true" android:clickable="true"
android:focusable="true"> android:focusable="true"
android:orientation="horizontal">
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:layout_marginBottom="16dp"
android:layout_weight="1"
android:orientation="vertical">
<TextView <TextView
android:id="@+id/text_title" android:id="@+id/text_title"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:textAppearance="?android:attr/textAppearanceListItem" android:textAppearance="?android:attr/textAppearanceListItem"
app:layout_constrainedWidth="true"
app:layout_constraintBottom_toTopOf="@id/text_subtitle"
app:layout_constraintEnd_toStartOf="@id/checkbox"
app:layout_constraintHorizontal_bias="0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="Title" /> tools:text="Title" />
<TextView <TextView
android:id="@+id/text_subtitle" android:id="@+id/text_subtitle"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginBottom="16dp"
android:textAppearance="?android:attr/textAppearanceListItemSecondary" android:textAppearance="?android:attr/textAppearanceListItemSecondary"
android:textColor="@android:color/tertiary_text_light" android:textColor="@android:color/tertiary_text_light"
app:layout_constrainedWidth="true"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@id/checkbox"
app:layout_constraintHorizontal_bias="0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/text_title"
tools:text="Summary" /> tools:text="Summary" />
</LinearLayout>
<com.google.android.material.checkbox.MaterialCheckBox <com.google.android.material.checkbox.MaterialCheckBox
android:id="@+id/checkbox" android:id="@+id/checkbox"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:clickable="false" android:layout_gravity="center_vertical"
app:layout_constraintBottom_toBottomOf="parent" android:clickable="false" />
app:layout_constraintEnd_toEndOf="parent" </LinearLayout>
app:layout_constraintHorizontal_bias="1"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -50,7 +50,8 @@
android:alpha="0.4" android:alpha="0.4"
android:contentDescription="@string/buttons" android:contentDescription="@string/buttons"
android:outlineProvider="bounds" android:outlineProvider="bounds"
android:src="@drawable/ic_stick_circle" /> android:src="@drawable/ic_button"
android:tint="?android:attr/textColorPrimary" />
<RelativeLayout <RelativeLayout
@ -70,7 +71,8 @@
android:alpha="0.4" android:alpha="0.4"
android:contentDescription="@string/buttons" android:contentDescription="@string/buttons"
android:outlineProvider="bounds" android:outlineProvider="bounds"
android:src="@drawable/ic_stick" /> android:src="@drawable/ic_stick"
android:tint="?android:attr/textColorPrimary" />
<TextView <TextView
android:id="@+id/stick_name" android:id="@+id/stick_name"

View File

@ -60,6 +60,7 @@
<string name="joystick">Joystick</string> <string name="joystick">Joystick</string>
<string name="confirm">Confirm</string> <string name="confirm">Confirm</string>
<string name="cancel">Cancel</string> <string name="cancel">Cancel</string>
<string name="osc_recenter_sticks">Recenter sticks on touch</string>
<string name="controller">Controller</string> <string name="controller">Controller</string>
<string name="config_controller">Configure Controller</string> <string name="config_controller">Configure Controller</string>
<string name="controller_type">Controller Type</string> <string name="controller_type">Controller Type</string>