Introduce a control panel to edit buttons instead of a fab bar

An unusual big commit, unfortunately needed because none of these changes would make sense nor work individual. A quick list of what have been done follows.
* Introduced a control panel to control buttons replacing the FAB bar
* `ConfigurableButton` and `OnScreenConfiguration` interfaces have been introduced to allow for easier proxying of actions when applying the same changes to all buttons
* Button resize logic has been stripped from the buttons in favor of the new sliders
* General cleanup and renaming of various methods to better reflect their functionality
This commit is contained in:
lynxnb 2023-03-29 17:35:51 +02:00 committed by Billy Laws
parent 49de8a8f38
commit d7e38e9556
19 changed files with 740 additions and 290 deletions

View File

@ -44,7 +44,8 @@ enum class ButtonId(val value : Long, val short : String? = null, val long : Int
LeftSR(1 shl 25, "SR", string.right_shoulder), LeftSR(1 shl 25, "SR", string.right_shoulder),
RightSL(1 shl 26, "SL", string.left_shoulder), RightSL(1 shl 26, "SL", string.left_shoulder),
RightSR(1 shl 27, "SR", string.right_shoulder), RightSR(1 shl 27, "SR", string.right_shoulder),
Menu(1 shl 28, "⌂︎", string.emu_menu_button); Menu(1 shl 28, "⌂︎", string.emu_menu_button),
All(0x1FFFFFFF, "All");
} }
/** /**

View File

@ -0,0 +1,43 @@
/*
* SPDX-License-Identifier: MPL-2.0
* Copyright © 2023 Skyline Team and Contributors (https://github.com/skyline-emu/)
*/
package emu.skyline.input.onscreen
import emu.skyline.input.ButtonId
/**
* This interface is used to allow proxying of [OnScreenButton]s
*/
interface ConfigurableButton {
val buttonId : ButtonId
val config : OnScreenConfiguration
/**
* Starts a button move session
* @param x The x coordinate of the initial touch
* @param y The y coordinate of the initial touch
*/
fun startMove(x : Float, y : Float)
/**
* Moves this button to the given coordinates
*/
fun move(x : Float, y : Float)
/**
* Ends the current move session
*/
fun endMove()
/**
* Resets the button to its default configuration
*/
fun resetConfig()
fun moveUp()
fun moveDown()
fun moveLeft()
fun moveRight()
}

View File

@ -22,14 +22,14 @@ import kotlin.math.roundToInt
*/ */
abstract class OnScreenButton( abstract class OnScreenButton(
onScreenControllerView : OnScreenControllerView, onScreenControllerView : OnScreenControllerView,
val buttonId : ButtonId, final override val buttonId : ButtonId,
private val defaultRelativeX : Float, private val defaultRelativeX : Float,
private val defaultRelativeY : Float, private val defaultRelativeY : Float,
private val defaultRelativeWidth : Float, private val defaultRelativeWidth : Float,
private val defaultRelativeHeight : Float, private val defaultRelativeHeight : Float,
drawableId : Int, drawableId : Int,
private val defaultEnabled : Boolean private val defaultEnabled : Boolean
) { ) : ConfigurableButton {
companion object { companion object {
/** /**
* Aspect ratio the default values were based on * Aspect ratio the default values were based on
@ -37,7 +37,7 @@ abstract class OnScreenButton(
const val CONFIGURED_ASPECT_RATIO = 2074f / 874f const val CONFIGURED_ASPECT_RATIO = 2074f / 874f
} }
val config = OnScreenConfiguration(onScreenControllerView.context, buttonId, defaultRelativeX, defaultRelativeY, defaultEnabled) final override val config = OnScreenConfigurationImpl(onScreenControllerView.context, buttonId, defaultRelativeX, defaultRelativeY, defaultEnabled)
protected val drawable = ContextCompat.getDrawable(onScreenControllerView.context, drawableId)!! protected val drawable = ContextCompat.getDrawable(onScreenControllerView.context, drawableId)!!
@ -50,8 +50,8 @@ abstract class OnScreenButton(
var relativeX = config.relativeX var relativeX = config.relativeX
var relativeY = config.relativeY var relativeY = config.relativeY
private val relativeWidth get() = defaultRelativeWidth * (config.globalScale + config.scale) private val relativeWidth get() = defaultRelativeWidth * config.scale
private val relativeHeight get() = defaultRelativeHeight * (config.globalScale + config.scale) private val relativeHeight get() = defaultRelativeHeight * config.scale
/** /**
* The width of the view this button is in, populated by the view during draw * The width of the view this button is in, populated by the view during draw
@ -158,28 +158,17 @@ abstract class OnScreenButton(
relativeY = config.relativeY relativeY = config.relativeY
} }
/** fun saveConfigValues() {
* Starts an edit session config.relativeX = relativeX
* @param x The x coordinate of the initial touch config.relativeY = relativeY
* @param y The y coordinate of the initial touch }
*/
open fun startEdit(x : Float, y : Float) { override fun startMove(x : Float, y : Float) {
editInitialTouchPoint.set(x, y) editInitialTouchPoint.set(x, y)
editInitialScale = config.scale editInitialScale = config.scale
} }
open fun edit(x : Float, y : Float) { override fun move(x : Float, y : Float) {
when (editInfo.editMode) {
EditMode.Move -> move(x, y)
EditMode.Resize -> resize(x, y)
else -> return
}
}
/**
* Moves this button to the given coordinates
*/
open fun move(x : Float, y : Float) {
var adjustedX = x var adjustedX = x
var adjustedY = y var adjustedY = y
@ -216,34 +205,38 @@ abstract class OnScreenButton(
relativeY = (adjustedY - heightDiff) / adjustedHeight relativeY = (adjustedY - heightDiff) / adjustedHeight
} }
/** override fun endMove() {
* Resizes this button based on the distance of the given Y coordinate from the initial Y coordinate saveConfigValues()
*/
open fun resize(x : Float, y : Float) {
// Invert the distance because the Y coordinate increases as you move down the screen
val verticalDistance = editInitialTouchPoint.y - y
config.scale = editInitialScale + verticalDistance / 200f
} }
/** override fun moveUp() {
* Ends the current edit session move(currentX, currentY - editInfo.arrowKeyMoveAmount)
*/ saveConfigValues()
open fun endEdit() {
config.relativeX = relativeX
config.relativeY = relativeY
} }
open fun resetRelativeValues() { override fun moveDown() {
move(currentX, currentY + editInfo.arrowKeyMoveAmount)
saveConfigValues()
}
override fun moveLeft() {
move(currentX - editInfo.arrowKeyMoveAmount, currentY)
saveConfigValues()
}
override fun moveRight() {
move(currentX + editInfo.arrowKeyMoveAmount, currentY)
saveConfigValues()
}
override fun resetConfig() {
config.enabled = defaultEnabled
config.alpha = OnScreenConfiguration.DefaultAlpha
config.scale = OnScreenConfiguration.DefaultScale
config.relativeX = defaultRelativeX config.relativeX = defaultRelativeX
config.relativeY = defaultRelativeY config.relativeY = defaultRelativeY
relativeX = defaultRelativeX relativeX = defaultRelativeX
relativeY = defaultRelativeY relativeY = defaultRelativeY
} }
open fun resetConfig() {
resetRelativeValues()
config.enabled = defaultEnabled
config.scale = OnScreenConfiguration.DefaultScale
}
} }

View File

@ -10,32 +10,50 @@ import emu.skyline.input.ButtonId
import emu.skyline.utils.SwitchColors import emu.skyline.utils.SwitchColors
import emu.skyline.utils.sharedPreferences import emu.skyline.utils.sharedPreferences
class OnScreenConfiguration(private val context : Context, private val buttonId : ButtonId, defaultRelativeX : Float, defaultRelativeY : Float, defaultEnabled : Boolean) { interface OnScreenConfiguration {
companion object { companion object {
const val DefaultAlpha = 130 const val GroupDisabled = 0
const val DefaultGlobalScale = 1.15f const val GroupEnabled = 1
const val DefaultScale = 0.0f const val GroupIndeterminate = 2
const val MinAlpha = 0
const val MaxAlpha = 255
const val DefaultAlpha = 128
const val MinScale = 0.5f
const val MaxScale = 2.5f
const val DefaultScale = 1.15f
val DefaultTextColor = SwitchColors.BLACK.color
val DefaultBackgroundColor = SwitchColors.WHITE.color
} }
var enabled : Boolean
/**
* The state of a group of buttons, returns an integer that can be used to set the state of a MaterialCheckBox
*/
val groupEnabled get() = if (enabled) GroupEnabled else GroupDisabled
var alpha : Int
var textColor : Int
var backgroundColor : Int
var scale : Float
var relativeX : Float
var relativeY : Float
}
class OnScreenConfigurationImpl(private val context : Context, private val buttonId : ButtonId, defaultRelativeX : Float, defaultRelativeY : Float, defaultEnabled : Boolean) : OnScreenConfiguration {
private inline fun <reified T> config(default : T, prefix : String = "${buttonId.name}_") = sharedPreferences(context, default, prefix, "controller_config") private inline fun <reified T> config(default : T, prefix : String = "${buttonId.name}_") = sharedPreferences(context, default, prefix, "controller_config")
var enabled by config(defaultEnabled) override var enabled by config(defaultEnabled)
var alpha by config(DefaultAlpha, "") override var alpha by config(OnScreenConfiguration.DefaultAlpha)
var textColor by config(SwitchColors.BLACK.color) override var textColor by config(OnScreenConfiguration.DefaultTextColor)
var backgroundColor by config(SwitchColors.WHITE.color) override var backgroundColor by config(OnScreenConfiguration.DefaultBackgroundColor)
/** override var scale by config(OnScreenConfiguration.DefaultScale)
* The global scale applied to all buttons override var relativeX by config(defaultRelativeX)
*/ override var relativeY by config(defaultRelativeY)
var globalScale by config(DefaultGlobalScale, "")
/**
* The scale of each button, this is added to the global scale
* Allows buttons to have their own size, while still be controlled by the global scale
*/
var scale by config(DefaultScale)
var relativeX by config(defaultRelativeX)
var relativeY by config(defaultRelativeY)
} }

View File

@ -17,6 +17,7 @@ import android.util.AttributeSet
import android.view.MotionEvent import android.view.MotionEvent
import android.view.View import android.view.View
import android.view.View.OnTouchListener import android.view.View.OnTouchListener
import androidx.annotation.IntRange
import emu.skyline.input.ButtonId import emu.skyline.input.ButtonId
import emu.skyline.input.ButtonState import emu.skyline.input.ButtonState
import emu.skyline.input.ControllerType import emu.skyline.input.ControllerType
@ -37,14 +38,18 @@ class OnScreenControllerView @JvmOverloads constructor(context : Context, attrs
private val controllerTypeMappings = mapOf(*ControllerType.values().map { private val controllerTypeMappings = mapOf(*ControllerType.values().map {
it to (setOf(*it.buttons) + setOf(*it.optionalButtons) to setOf(*it.sticks)) it to (setOf(*it.buttons) + setOf(*it.optionalButtons) to setOf(*it.sticks))
}.toTypedArray()) }.toTypedArray())
private const val SCALE_STEP = 0.05f
private const val ALPHA_STEP = 25
private val ALPHA_RANGE = 55..255
} }
private var onButtonStateChangedListener : OnButtonStateChangedListener? = null private var onButtonStateChangedListener : OnButtonStateChangedListener? = null
fun setOnButtonStateChangedListener(listener : OnButtonStateChangedListener) {
onButtonStateChangedListener = listener
}
private var onStickStateChangedListener : OnStickStateChangedListener? = null private var onStickStateChangedListener : OnStickStateChangedListener? = null
fun setOnStickStateChangedListener(listener : OnStickStateChangedListener) {
onStickStateChangedListener = listener
}
private val joystickAnimators = mutableMapOf<JoystickButton, Animator?>() private val joystickAnimators = mutableMapOf<JoystickButton, Animator?>()
var controllerType : ControllerType? = null var controllerType : ControllerType? = null
set(value) { set(value) {
@ -62,8 +67,12 @@ class OnScreenControllerView @JvmOverloads constructor(context : Context, attrs
(controls.circularButtons + controls.rectangularButtons + controls.triggerButtons).forEach { it.hapticFeedback = hapticFeedback } (controls.circularButtons + controls.rectangularButtons + controls.triggerButtons).forEach { it.hapticFeedback = hapticFeedback }
} }
val editInfo = OnScreenEditInfo() internal val editInfo = OnScreenEditInfo()
val isEditing get() = editInfo.isEditing val isEditing get() = editInfo.isEditing
val editButton get() = editInfo.editButton
fun setOnEditButtonChangedListener(listener : OnEditButtonChangedListener?) {
editInfo.onEditButtonChangedListener = listener
}
// Populated externally by the activity, as retrieving the vibrator service inside the view crashes the layout editor // Populated externally by the activity, as retrieving the vibrator service inside the view crashes the layout editor
lateinit var vibrator : Vibrator lateinit var vibrator : Vibrator
@ -222,40 +231,52 @@ class OnScreenControllerView @JvmOverloads constructor(context : Context, attrs
handled.also { if (it) invalidate() } handled.also { if (it) invalidate() }
} }
private val editingTouchHandler = OnTouchListener { _, event -> /**
var handled = false * Tracks whether the last pointer down event changed the active edit button
* Avoids moving the button when the user just wants to select it
*/
private var activeEditButtonChanged = false
when (event.actionMasked) { private val editingTouchHandler = OnTouchListener { _, event ->
MotionEvent.ACTION_DOWN, run {
MotionEvent.ACTION_POINTER_DOWN -> { when (event.actionMasked) {
// Handle this event only if no other button is being edited MotionEvent.ACTION_DOWN,
if (editInfo.editButton == null) { MotionEvent.ACTION_POINTER_DOWN -> {
handled = controls.allButtons.any { button -> val touchedButton = controls.allButtons.firstOrNull { it.isTouched(event.x, event.y) } ?: return@OnTouchListener false
if (button.config.enabled && button.isTouched(event.x, event.y)) {
editInfo.editButton = button // Update the selection if the user touched a button other than the selected one
button.startEdit(event.x, event.y) if (touchedButton != editInfo.editButton) {
performClick() activeEditButtonChanged = true
true editInfo.editButton = touchedButton
} else false performClick()
return@run
} }
editInfo.editButton.startMove(event.x, event.y)
}
MotionEvent.ACTION_MOVE -> {
// If the user just selected another button, don't move it yet
if (activeEditButtonChanged)
return@run
editInfo.editButton.move(event.x, event.y)
invalidate()
}
MotionEvent.ACTION_UP,
MotionEvent.ACTION_POINTER_UP,
MotionEvent.ACTION_CANCEL -> {
if (activeEditButtonChanged) {
activeEditButtonChanged = false
return@run
}
editInfo.editButton.endMove()
} }
} }
MotionEvent.ACTION_MOVE -> {
editInfo.editButton?.edit(event.x, event.y)
handled = true
}
MotionEvent.ACTION_UP,
MotionEvent.ACTION_POINTER_UP,
MotionEvent.ACTION_CANCEL -> {
editInfo.editButton?.endEdit()
editInfo.editButton = null
handled = true
}
} }
true
handled.also { if (it) invalidate() }
} }
init { init {
@ -264,39 +285,49 @@ class OnScreenControllerView @JvmOverloads constructor(context : Context, attrs
fun setEditMode(editMode : EditMode) { fun setEditMode(editMode : EditMode) {
editInfo.editMode = editMode editInfo.editMode = editMode
setOnTouchListener(if (isEditing) editingTouchHandler else playingTouchHandler ) setOnTouchListener(if (isEditing) editingTouchHandler else playingTouchHandler)
} }
fun resetControls() { fun selectAllButtons() {
controls.allButtons.forEach { editInfo.editButton = allButtonsProxy
it.resetConfig() }
}
controls.globalScale = OnScreenConfiguration.DefaultGlobalScale fun setButtonEnabled(enabled : Boolean) {
controls.alpha = OnScreenConfiguration.DefaultAlpha editInfo.editButton.config.enabled = enabled
invalidate() invalidate()
} }
fun increaseScale() { fun setButtonScale(@IntRange(from = 0, to = 100) scale : Int) {
controls.globalScale += SCALE_STEP fun toScaleRange(value : Int) : Float = (value / 100f) * (OnScreenConfiguration.MaxScale - OnScreenConfiguration.MinScale) + OnScreenConfiguration.MinScale
editInfo.editButton.config.scale = toScaleRange(scale)
invalidate() invalidate()
} }
fun decreaseScale() { fun setButtonOpacity(@IntRange(from = 0, to = 100) opacity : Int) {
controls.globalScale -= SCALE_STEP fun toAlphaRange(value : Int) : Int = ((value / 100f) * (OnScreenConfiguration.MaxAlpha - OnScreenConfiguration.MinAlpha)).toInt() + OnScreenConfiguration.MinAlpha
editInfo.editButton.config.alpha = toAlphaRange(opacity)
invalidate() invalidate()
} }
fun setSnapToGrid(snap : Boolean) { fun moveButtonUp() {
editInfo.snapToGrid = snap editInfo.editButton.moveUp()
}
fun increaseOpacity() {
controls.alpha = (controls.alpha + ALPHA_STEP).coerceIn(ALPHA_RANGE)
invalidate() invalidate()
} }
fun decreaseOpacity() { fun moveButtonDown() {
controls.alpha = (controls.alpha - ALPHA_STEP).coerceIn(ALPHA_RANGE) editInfo.editButton.moveDown()
invalidate()
}
fun moveButtonLeft() {
editInfo.editButton.moveLeft()
invalidate()
}
fun moveButtonRight() {
editInfo.editButton.moveRight()
invalidate() invalidate()
} }
@ -308,23 +339,6 @@ class OnScreenControllerView @JvmOverloads constructor(context : Context, attrs
return controls.globalBackgroundColor return controls.globalBackgroundColor
} }
fun setOnButtonStateChangedListener(listener : OnButtonStateChangedListener) {
onButtonStateChangedListener = listener
}
fun setOnStickStateChangedListener(listener : OnStickStateChangedListener) {
onStickStateChangedListener = listener
}
data class ButtonProp(val buttonId : ButtonId, val enabled : Boolean)
fun getButtonProps() = controls.allButtons.map { ButtonProp(it.buttonId, it.config.enabled) }
fun setButtonEnabled(buttonId : ButtonId, enabled : Boolean) {
controls.allButtons.first { it.buttonId == buttonId }.config.enabled = enabled
invalidate()
}
fun setTextColor(color : Int) { fun setTextColor(color : Int) {
for (button in controls.allButtons) { for (button in controls.allButtons) {
button.config.textColor = color button.config.textColor = color
@ -338,4 +352,90 @@ class OnScreenControllerView @JvmOverloads constructor(context : Context, attrs
} }
invalidate() invalidate()
} }
fun setSnapToGrid(snap : Boolean) {
editInfo.snapToGrid = snap
editInfo.arrowKeyMoveAmount = if (snap) editInfo.gridSize else OnScreenEditInfo.ArrowKeyMoveAmount
}
fun resetButton() {
editInfo.editButton.resetConfig()
editInfo.onEditButtonChangedListener?.invoke(editInfo.editButton)
invalidate()
}
/**
* A proxy button that is used to apply changes to all buttons
*/
private val allButtonsProxy = object : ConfigurableButton {
override val buttonId : ButtonId = ButtonId.All
override val config = object : OnScreenConfiguration {
override var enabled : Boolean
get() = controls.allButtons.all { it.config.enabled }
set(value) {
controls.allButtons.forEach { it.config.enabled = value }
}
override val groupEnabled : Int
get() {
if (controls.allButtons.all { it.config.enabled })
return OnScreenConfiguration.GroupEnabled
if (controls.allButtons.all { !it.config.enabled })
return OnScreenConfiguration.GroupDisabled
return OnScreenConfiguration.GroupIndeterminate
}
override var alpha : Int
get() = controls.allButtons.sumOf { it.config.alpha } / controls.allButtons.size
set(value) {
controls.allButtons.forEach { it.config.alpha = value }
}
override var textColor : Int
get() = controls.globalTextColor
set(value) {
setTextColor(value)
}
override var backgroundColor : Int
get() = controls.globalBackgroundColor
set(value) {
setBackGroundColor(value)
}
override var scale : Float
get() = (controls.allButtons.sumOf { it.config.scale.toDouble() } / controls.allButtons.size).toFloat()
set(value) {
controls.allButtons.forEach { it.config.scale = value }
}
override var relativeX = 0f
override var relativeY = 0f
}
override fun startMove(x : Float, y : Float) {}
override fun move(x : Float, y : Float) {}
override fun endMove() {}
override fun moveUp() {
controls.allButtons.forEach { it.moveUp() }
}
override fun moveDown() {
controls.allButtons.forEach { it.moveDown() }
}
override fun moveLeft() {
controls.allButtons.forEach { it.moveLeft() }
}
override fun moveRight() {
controls.allButtons.forEach { it.moveRight() }
}
override fun resetConfig() {
controls.allButtons.forEach { it.resetConfig() }
}
}
} }

View File

@ -5,109 +5,35 @@
package emu.skyline.input.onscreen package emu.skyline.input.onscreen
import android.annotation.SuppressLint
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.os.Vibrator import android.os.Vibrator
import android.os.VibratorManager import android.os.VibratorManager
import android.view.* import android.view.*
import androidx.annotation.DrawableRes
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
import androidx.core.view.isGone import androidx.core.view.isGone
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.floatingactionbutton.FloatingActionButton
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import emu.skyline.R import emu.skyline.R
import emu.skyline.databinding.OnScreenEditActivityBinding import emu.skyline.databinding.OnScreenEditActivityBinding
import emu.skyline.databinding.OscSliderBinding
import emu.skyline.settings.AppSettings import emu.skyline.settings.AppSettings
import emu.skyline.utils.SwitchColors import emu.skyline.utils.SwitchColors
import emu.skyline.utils.SwitchColors.* import emu.skyline.utils.SwitchColors.*
import petrov.kristiyan.colorpicker.DoubleColorPicker import petrov.kristiyan.colorpicker.DoubleColorPicker
import petrov.kristiyan.colorpicker.DoubleColorPicker.OnChooseDoubleColorListener import petrov.kristiyan.colorpicker.DoubleColorPicker.OnChooseDoubleColorListener
import javax.inject.Inject import javax.inject.Inject
import kotlin.math.roundToInt
@AndroidEntryPoint @AndroidEntryPoint
class OnScreenEditActivity : AppCompatActivity() { class OnScreenEditActivity : AppCompatActivity() {
private enum class Action(@DrawableRes private val icon : Int, @DrawableRes private val activeIcon : Int = 0) {
Restore(R.drawable.ic_restore),
Toggle(R.drawable.ic_toggle_on),
Move(R.drawable.ic_move),
Resize(R.drawable.ic_resize),
Grid(R.drawable.ic_grid_off, R.drawable.ic_grid_on),
Palette(R.drawable.ic_palette),
ZoomOut(R.drawable.ic_zoom_out),
ZoomIn(R.drawable.ic_zoom_in),
OpacityMinus(R.drawable.ic_opacity_minus),
OpacityPlus(R.drawable.ic_opacity_plus),
Close(R.drawable.ic_close),
;
fun getIcon(active : Boolean) = if (activeIcon != 0 && active) activeIcon else icon
}
private val binding by lazy { OnScreenEditActivityBinding.inflate(layoutInflater) } private val binding by lazy { OnScreenEditActivityBinding.inflate(layoutInflater) }
private var fullEditVisible = true
@Inject @Inject
lateinit var appSettings : AppSettings lateinit var appSettings : AppSettings
private val closeAction : () -> Unit = { private fun paletteAction() {
if (binding.onScreenControllerView.isEditing) {
toggleFabVisibility(true)
binding.onScreenControllerView.setEditMode(EditMode.None)
} else {
fullEditVisible = !fullEditVisible
toggleFabVisibility(fullEditVisible)
fabMapping[Action.Close]!!.animate().rotation(if (fullEditVisible) 0f else 45f)
}
}
private fun toggleFabVisibility(visible : Boolean) {
fabMapping.forEach { (action, fab) ->
if (action != Action.Close) {
if (visible) fab.show()
else fab.hide()
}
}
}
private val moveAction = {
binding.onScreenControllerView.setEditMode(EditMode.Move)
toggleFabVisibility(false)
}
private val resizeAction = {
binding.onScreenControllerView.setEditMode(EditMode.Resize)
toggleFabVisibility(false)
}
private val toggleAction : () -> Unit = {
val buttonProps = binding.onScreenControllerView.getButtonProps()
val checkedButtonsArray = buttonProps.map { it.enabled }.toBooleanArray()
MaterialAlertDialogBuilder(this)
.setMultiChoiceItems(
buttonProps.map { button ->
val longText = getString(button.buttonId.long!!)
if (button.buttonId.short == longText) longText else "$longText: ${button.buttonId.short}"
}.toTypedArray(),
checkedButtonsArray
) { _, which, isChecked ->
checkedButtonsArray[which] = isChecked
}
.setPositiveButton(R.string.confirm) { _, _ ->
buttonProps.forEachIndexed { index, button ->
if (checkedButtonsArray[index] != button.enabled)
binding.onScreenControllerView.setButtonEnabled(button.buttonId, checkedButtonsArray[index])
}
}
.setNegativeButton(R.string.cancel, null)
.setOnDismissListener { fullScreen() }
.show()
}
private val paletteAction : () -> Unit = {
DoubleColorPicker(this@OnScreenEditActivity).apply { DoubleColorPicker(this@OnScreenEditActivity).apply {
setTitle(this@OnScreenEditActivity.getString(R.string.osc_background_color)) setTitle(this@OnScreenEditActivity.getString(R.string.osc_background_color))
setDefaultColorButton(binding.onScreenControllerView.getBackGroundColor()) setDefaultColorButton(binding.onScreenControllerView.getBackGroundColor())
@ -127,43 +53,25 @@ class OnScreenEditActivity : AppCompatActivity() {
} }
} }
private val toggleGridAction : () -> Unit = { private fun toggleGridAction() {
val snapToGrid = !appSettings.onScreenControlSnapToGrid val snapToGrid = !appSettings.onScreenControlSnapToGrid
appSettings.onScreenControlSnapToGrid = snapToGrid appSettings.onScreenControlSnapToGrid = snapToGrid
binding.onScreenControllerView.setSnapToGrid(snapToGrid) binding.onScreenControllerView.setSnapToGrid(snapToGrid)
binding.alignmentGrid.isGone = !snapToGrid binding.alignmentGrid.isGone = !snapToGrid
fabMapping[Action.Grid]!!.setImageResource(Action.Grid.getIcon(!snapToGrid)) binding.gridButton.setIconResource(if (!snapToGrid) R.drawable.ic_grid_on else R.drawable.ic_grid_off)
} }
private val resetAction : () -> Unit = { private fun resetAction() {
MaterialAlertDialogBuilder(this) MaterialAlertDialogBuilder(this)
.setTitle(R.string.osc_reset) .setTitle(getString(R.string.osc_reset, binding.onScreenControllerView.editButton.buttonId.short))
.setMessage(R.string.osc_reset_confirm) .setMessage(R.string.osc_reset_confirm)
.setPositiveButton(R.string.confirm) { _, _ -> binding.onScreenControllerView.resetControls() } .setPositiveButton(R.string.confirm) { _, _ -> binding.onScreenControllerView.resetButton() }
.setNegativeButton(R.string.cancel, null) .setNegativeButton(R.string.cancel, null)
.setOnDismissListener { fullScreen() } .setOnDismissListener { fullScreen() }
.show() .show()
} }
private data class ActionEntry(val action : Action, val callback : () -> Unit)
private val actions : List<ActionEntry> = listOf(
ActionEntry(Action.Restore, resetAction),
ActionEntry(Action.Toggle, toggleAction),
ActionEntry(Action.Move, moveAction),
ActionEntry(Action.Resize, resizeAction),
ActionEntry(Action.Grid, toggleGridAction),
ActionEntry(Action.Palette, paletteAction),
ActionEntry(Action.ZoomOut) { binding.onScreenControllerView.decreaseScale() },
ActionEntry(Action.ZoomIn) { binding.onScreenControllerView.increaseScale() },
ActionEntry(Action.OpacityMinus) { binding.onScreenControllerView.decreaseOpacity() },
ActionEntry(Action.OpacityPlus) { binding.onScreenControllerView.increaseOpacity() },
ActionEntry(Action.Close, closeAction),
)
private val fabMapping = mutableMapOf<Action, FloatingActionButton>()
override fun onCreate(savedInstanceState : Bundle?) { override fun onCreate(savedInstanceState : Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
window.attributes.layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES window.attributes.layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES
@ -194,20 +102,40 @@ class OnScreenEditActivity : AppCompatActivity() {
binding.alignmentGrid.isGone = !snapToGrid binding.alignmentGrid.isGone = !snapToGrid
binding.alignmentGrid.gridSize = OnScreenEditInfo.GridSize binding.alignmentGrid.gridSize = OnScreenEditInfo.GridSize
actions.forEach { (action, callback) -> binding.onScreenControllerView.setOnEditButtonChangedListener { button ->
binding.fabParent.addView(LayoutInflater.from(this).inflate(R.layout.on_screen_edit_mini_fab, binding.fabParent, false).apply { updateActiveButtonDisplayInfo(button)
(this as FloatingActionButton).setImageDrawable(ContextCompat.getDrawable(context, action.getIcon(false)))
setOnClickListener { callback.invoke() }
fabMapping[action] = this
})
} }
fabMapping[Action.Grid]!!.setImageDrawable(ContextCompat.getDrawable(this, Action.Grid.getIcon(!snapToGrid))) binding.selectAllButton.setOnClickListener { binding.onScreenControllerView.selectAllButtons() }
populateSlider(binding.scaleSlider, getString(R.string.osc_scale)) {
binding.onScreenControllerView.setButtonScale(it)
}
populateSlider(binding.opacitySlider, getString(R.string.osc_opacity)) {
binding.onScreenControllerView.setButtonOpacity(it)
}
binding.enabledCheckbox.setOnClickListener { _ ->
binding.onScreenControllerView.setButtonEnabled(binding.enabledCheckbox.isChecked)
}
binding.moveUpButton.setOnClickListener { binding.onScreenControllerView.moveButtonUp() }
binding.moveDownButton.setOnClickListener { binding.onScreenControllerView.moveButtonDown() }
binding.moveLeftButton.setOnClickListener { binding.onScreenControllerView.moveButtonLeft() }
binding.moveRightButton.setOnClickListener { binding.onScreenControllerView.moveButtonRight() }
binding.colorButton.setOnClickListener { paletteAction() }
binding.gridButton.setOnClickListener { toggleGridAction() }
binding.gridButton.setIconResource(if (!snapToGrid) R.drawable.ic_grid_on else R.drawable.ic_grid_off)
binding.resetButton.setOnClickListener { resetAction() }
binding.onScreenControllerView.setEditMode(EditMode.Move)
binding.onScreenControllerView.selectAllButtons()
} }
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
updateActiveButtonDisplayInfo(binding.onScreenControllerView.editButton)
fullScreen() fullScreen()
} }
@ -222,4 +150,36 @@ class OnScreenEditActivity : AppCompatActivity() {
or View.SYSTEM_UI_FLAG_FULLSCREEN) or View.SYSTEM_UI_FLAG_FULLSCREEN)
} }
} }
/**
* Initializes the slider in the range [0,100], with the given label and value listener
*/
@SuppressLint("SetTextI18n")
private fun populateSlider(slider : OscSliderBinding, label : String, valueListener : ((Int) -> Unit)? = null) {
slider.title.text = label
slider.slider.apply {
valueFrom = 0f
valueTo = 100f
stepSize = 0f
// Always update the value label
addOnChangeListener { _, value, _ ->
slider.valueLabel.text = "${value.roundToInt()}%"
}
// Only call the value listener if the user is dragging the slider
addOnChangeListener { _, value, fromUser ->
if (fromUser)
valueListener?.invoke(value.roundToInt())
}
}
}
/**
* Updates the control panel UI elements to reflect the currently selected button
*/
private fun updateActiveButtonDisplayInfo(button : ConfigurableButton) {
binding.enabledCheckbox.checkedState = button.config.groupEnabled
binding.currentButton.text = getString(R.string.osc_current_button, button.buttonId.short)
binding.scaleSlider.slider.value = (button.config.scale - OnScreenConfiguration.MinScale) / (OnScreenConfiguration.MaxScale - OnScreenConfiguration.MinScale) * 100f
binding.opacitySlider.slider.value = (button.config.alpha - OnScreenConfiguration.MinAlpha) / (OnScreenConfiguration.MaxAlpha - OnScreenConfiguration.MinAlpha).toFloat() * 100f
}
} }

View File

@ -10,10 +10,11 @@ import android.util.TypedValue
enum class EditMode { enum class EditMode {
None, None,
Move, Move
Resize
} }
typealias OnEditButtonChangedListener = ((ConfigurableButton) -> Unit)
/** /**
* A small class that holds information about the current edit session * A small class that holds information about the current edit session
* This is used to share information between the [OnScreenControllerView] and the individual [OnScreenButton]s * This is used to share information between the [OnScreenControllerView] and the individual [OnScreenButton]s
@ -27,7 +28,15 @@ class OnScreenEditInfo {
/** /**
* The button that is currently being edited * The button that is currently being edited
*/ */
var editButton : OnScreenButton? = null private lateinit var _editButton : ConfigurableButton
var editButton : ConfigurableButton
get() = _editButton
set(value) {
_editButton = value
onEditButtonChangedListener?.invoke(value)
}
var onEditButtonChangedListener : OnEditButtonChangedListener? = null
/** /**
* Whether the buttons should snap to a grid when in edit mode * Whether the buttons should snap to a grid when in edit mode
@ -36,6 +45,8 @@ class OnScreenEditInfo {
var gridSize : Int = GridSize var gridSize : Int = GridSize
var arrowKeyMoveAmount : Int = ArrowKeyMoveAmount
val isEditing get() = editMode != EditMode.None val isEditing get() = editMode != EditMode.None
companion object { companion object {
@ -43,5 +54,10 @@ class OnScreenEditInfo {
* The size of the grid, calculated from the value of 8dp * The size of the grid, calculated from the value of 8dp
*/ */
var GridSize = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 8f, Resources.getSystem().displayMetrics).toInt() var GridSize = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 8f, Resources.getSystem().displayMetrics).toInt()
/**
* The amount the button will be moved when using the arrow keys
*/
val ArrowKeyMoveAmount = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 2f, Resources.getSystem().displayMetrics).toInt()
} }
} }

View File

@ -136,15 +136,15 @@ class JoystickButton(
fun outerToInnerRelative() = outerToInner().multiply(1f / radius) fun outerToInnerRelative() = outerToInner().multiply(1f / radius)
override fun edit(x : Float, y : Float) { override fun move(x : Float, y : Float) {
super.edit(x, y) super.move(x, y)
innerButton.relativeX = relativeX innerButton.relativeX = relativeX
innerButton.relativeY = relativeY innerButton.relativeY = relativeY
} }
override fun resetRelativeValues() { override fun resetConfig() {
super.resetRelativeValues() super.resetConfig()
innerButton.relativeX = relativeX innerButton.relativeX = relativeX
innerButton.relativeY = relativeY innerButton.relativeY = relativeY
@ -241,24 +241,6 @@ class Controls(onScreenControllerView : OnScreenControllerView) {
val allButtons = circularButtons + joysticks + rectangularButtons + triggerButtons val allButtons = circularButtons + joysticks + rectangularButtons + triggerButtons
/**
* We can take any of the global scale variables from the buttons since the value is shared across all buttons
*/
var globalScale
get() = circularButtons.first().config.globalScale
set(value) {
circularButtons.first().config.globalScale = value
}
/**
* We can take any of the alpha variables from the buttons since the value is shared across all buttons
*/
var alpha
get() = circularButtons.first().config.alpha
set(value) {
circularButtons.first().config.alpha = value
}
/** /**
* We can take any of the global text color variables from the buttons * We can take any of the global text color variables from the buttons
*/ */

View File

@ -1,10 +1,11 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" <vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp" android:width="24dp"
android:height="24dp" android:height="24dp"
android:autoMirrored="true"
android:tint="#000000" android:tint="#000000"
android:viewportWidth="24" android:viewportWidth="24"
android:viewportHeight="24"> android:viewportHeight="24">
<path <path
android:fillColor="@android:color/white" android:fillColor="@android:color/white"
android:pathData="M17,7L7,7c-2.76,0 -5,2.24 -5,5s2.24,5 5,5h10c2.76,0 5,-2.24 5,-5s-2.24,-5 -5,-5zM17,15c-1.66,0 -3,-1.34 -3,-3s1.34,-3 3,-3 3,1.34 3,3 -1.34,3 -3,3z" /> android:pathData="M7,10l5,5 5,-5z" />
</vector> </vector>

View File

@ -0,0 +1,11 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:autoMirrored="true"
android:tint="#000000"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M7.41,8.59L12,13.17l4.59,-4.58L18,10l-6,6 -6,-6 1.41,-1.41z" />
</vector>

View File

@ -0,0 +1,11 @@
<vector android:autoMirrored="true"
android:height="24dp"
android:tint="#000000"
android:viewportHeight="24"
android:viewportWidth="24"
android:width="24dp"
xmlns:android="http://schemas.android.com/apk/res/android">
<path
android:fillColor="@android:color/white"
android:pathData="M15.41,16.59L10.83,12l4.58,-4.59L14,6l-6,6 6,6 1.41,-1.41z" />
</vector>

View File

@ -0,0 +1,11 @@
<vector android:autoMirrored="true"
android:height="24dp"
android:tint="#000000"
android:viewportHeight="24"
android:viewportWidth="24"
android:width="24dp"
xmlns:android="http://schemas.android.com/apk/res/android">
<path
android:fillColor="@android:color/white"
android:pathData="M8.59,16.59L13.17,12 8.59,7.41 10,6l6,6 -6,6 -1.41,-1.41z" />
</vector>

View File

@ -0,0 +1,11 @@
<vector android:autoMirrored="true"
android:height="24dp"
android:tint="#000000"
android:viewportHeight="24"
android:viewportWidth="24"
android:width="24dp"
xmlns:android="http://schemas.android.com/apk/res/android">
<path
android:fillColor="@android:color/white"
android:pathData="M7.41,15.41L12,10.83l4.59,4.58L18,14l-6,-6 -6,6z" />
</vector>

View File

@ -1,10 +0,0 @@
<?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="M7,10A2,2 0 0,1 9,12A2,2 0 0,1 7,14A2,2 0 0,1 5,12A2,2 0 0,1 7,10M17,7A5,5 0 0,1 22,12A5,5 0 0,1 17,17H7A5,5 0 0,1 2,12A5,5 0 0,1 7,7H17M7,9A3,3 0 0,0 4,12A3,3 0 0,0 7,15H17A3,3 0 0,0 20,12A3,3 0 0,0 17,9H7Z" />
</vector>

View File

@ -2,6 +2,8 @@
<shape xmlns:android="http://schemas.android.com/apk/res/android"> <shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="?attr/colorSurface" /> <solid android:color="?attr/colorSurface" />
<corners <corners
android:topLeftRadius="28dp"
android:topRightRadius="28dp"
android:bottomLeftRadius="28dp" android:bottomLeftRadius="28dp"
android:bottomRightRadius="28dp" /> android:bottomRightRadius="28dp" />
</shape> </shape>

View File

@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" <FrameLayout 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:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
@ -8,7 +9,8 @@
<emu.skyline.views.AlignmentGridView <emu.skyline.views.AlignmentGridView
android:id="@+id/alignment_grid" android:id="@+id/alignment_grid"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" /> android:layout_height="match_parent"
tools:visibility="gone" />
<emu.skyline.input.onscreen.OnScreenControllerView <emu.skyline.input.onscreen.OnScreenControllerView
android:id="@+id/on_screen_controller_view" android:id="@+id/on_screen_controller_view"
@ -16,15 +18,261 @@
android:layout_height="match_parent" /> android:layout_height="match_parent" />
<LinearLayout <LinearLayout
android:id="@+id/fab_parent" android:id="@+id/control_panel"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="top|center_horizontal" android:layout_gravity="top|center_horizontal"
android:background="@drawable/top_sheet_bg" android:alpha="0.85"
android:animateLayoutChanges="true"
android:background="@drawable/rounded_background"
android:backgroundTint="?attr/colorPrimaryContainer" android:backgroundTint="?attr/colorPrimaryContainer"
android:clickable="true"
android:clipChildren="false"
android:clipToPadding="false" android:clipToPadding="false"
android:orientation="horizontal" android:orientation="vertical"
android:padding="10dp" android:translationY="12dp">
tools:layout_height="72dp"
tools:layout_width="512dp" /> <!-- Tob Bar -->
<FrameLayout
android:id="@+id/top_bar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingHorizontal="8dp">
<com.google.android.material.bottomsheet.BottomSheetDragHandleView
android:id="@+id/drag_handle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:minHeight="32dp"
android:paddingBottom="0dp" />
<com.google.android.material.button.MaterialButton
android:id="@+id/close_button"
style="@style/Widget.Material3.Button.IconButton"
android:layout_width="32dp"
android:layout_height="32dp"
android:layout_gravity="end"
android:padding="2dp"
app:icon="@drawable/ic_close"
app:iconTint="?attr/colorOnSurfaceVariant"
tools:ignore="ContentDescription,SpeakableTextPresentCheck,TouchTargetSizeCheck" />
</FrameLayout>
<!-- Control Panel Content -->
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/content"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:clipChildren="false"
android:clipToPadding="false"
android:paddingHorizontal="12dp"
android:paddingBottom="12dp">
<TextView
android:id="@+id/current_button"
style="?attr/textAppearanceTitleLarge"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:text="@string/osc_current_button"
android:textSize="20sp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<com.google.android.material.button.MaterialButton
android:id="@+id/select_all_button"
style="@style/Widget.Material3.Button.TextButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:minHeight="42dp"
android:text="@string/select_all"
app:layout_constraintBottom_toBottomOf="@id/current_button"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@id/current_button"
tools:ignore="TextContrastCheck,TouchTargetSizeCheck,VisualLintBounds" />
<androidx.constraintlayout.widget.Barrier
android:id="@+id/title_barrier"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:barrierDirection="bottom"
app:constraint_referenced_ids="current_button,select_all_button" />
<!-- Checkboxes -->
<com.google.android.material.checkbox.MaterialCheckBox
android:id="@+id/enabled_checkbox"
style="?attr/checkboxStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="-6dp"
android:layout_marginTop="4dp"
android:minHeight="0dp"
android:paddingVertical="8dp"
android:text="@string/enabled"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/current_button"
tools:ignore="TouchTargetSizeCheck" />
<com.google.android.material.checkbox.MaterialCheckBox
android:id="@+id/toggle_mode_checkbox"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:minHeight="0dp"
android:paddingVertical="8dp"
android:text="@string/osc_toggle_mode"
android:visibility="gone"
app:layout_constraintStart_toStartOf="@+id/enabled_checkbox"
app:layout_constraintTop_toBottomOf="@id/enabled_checkbox"
tools:ignore="TouchTargetSizeCheck" />
<!-- Sliders -->
<include
android:id="@+id/scale_slider"
layout="@layout/osc_slider"
android:layout_width="160dp"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/toggle_mode_checkbox" />
<include
android:id="@+id/opacity_slider"
layout="@layout/osc_slider"
android:layout_width="160dp"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
app:layout_constraintStart_toStartOf="@id/scale_slider"
app:layout_constraintTop_toBottomOf="@id/scale_slider" />
<androidx.constraintlayout.widget.Barrier
android:id="@+id/sliders_barrier"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:barrierDirection="end"
app:constraint_referenced_ids="scale_slider,opacity_slider,enabled_checkbox,toggle_mode_checkbox" />
<!-- Horizontal (Left and Right) Move Buttons -->
<com.google.android.material.button.MaterialButton
android:id="@+id/move_left_button"
style="@style/Widget.Material3.Button.IconButton.Outlined"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="16dp"
app:icon="@drawable/ic_keyboard_arrow_left"
app:layout_constraintBottom_toBottomOf="@id/actions_barrier"
app:layout_constraintEnd_toStartOf="@id/move_right_button"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintHorizontal_chainStyle="packed"
app:layout_constraintStart_toEndOf="@id/sliders_barrier"
app:layout_constraintTop_toTopOf="@id/title_barrier"
tools:ignore="TouchTargetSizeCheck,SpeakableTextPresentCheck" />
<com.google.android.material.button.MaterialButton
android:id="@+id/move_right_button"
style="@style/Widget.Material3.Button.IconButton.Outlined"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:icon="@drawable/ic_keyboard_arrow_right"
app:layout_constraintBottom_toBottomOf="@id/actions_barrier"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toEndOf="@id/move_left_button"
app:layout_constraintTop_toTopOf="@id/title_barrier"
tools:ignore="TouchTargetSizeCheck,SpeakableTextPresentCheck" />
<!-- Vertical (Up and Down) Move Buttons -->
<com.google.android.material.button.MaterialButton
android:id="@+id/move_up_button"
style="@style/Widget.Material3.Button.IconButton.Outlined"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
app:icon="@drawable/ic_keyboard_arrow_up"
app:layout_constraintBottom_toTopOf="@id/move_down_button"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toEndOf="@id/sliders_barrier"
app:layout_constraintTop_toBottomOf="@id/title_barrier"
app:layout_constraintVertical_chainStyle="packed"
tools:ignore="TouchTargetSizeCheck,SpeakableTextPresentCheck" />
<com.google.android.material.button.MaterialButton
android:id="@+id/move_down_button"
style="@style/Widget.Material3.Button.IconButton.Outlined"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:icon="@drawable/ic_keyboard_arrow_down"
app:layout_constraintBottom_toTopOf="@id/actions_barrier"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toEndOf="@id/sliders_barrier"
app:layout_constraintTop_toBottomOf="@id/move_up_button"
tools:ignore="TouchTargetSizeCheck,SpeakableTextPresentCheck" />
<!-- Action Buttons -->
<com.google.android.material.button.MaterialButton
android:id="@+id/color_button"
style="@style/Widget.Material3.Button.IconButton.Filled"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:tooltipText="@string/button_color"
app:icon="@drawable/ic_palette"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@id/grid_button"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintHorizontal_chainStyle="spread"
app:layout_constraintStart_toEndOf="@id/sliders_barrier"
tools:ignore="TouchTargetSizeCheck,SpeakableTextPresentCheck" />
<com.google.android.material.button.MaterialButton
android:id="@+id/grid_button"
style="@style/Widget.Material3.Button.IconButton.Filled"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:tooltipText="@string/toggle_grid"
app:icon="@drawable/ic_grid_on"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@id/reset_button"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toEndOf="@id/color_button"
tools:ignore="TouchTargetSizeCheck,SpeakableTextPresentCheck" />
<com.google.android.material.button.MaterialButton
android:id="@+id/reset_button"
style="@style/Widget.Material3.Button.IconButton.Filled"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:tooltipText="@string/reset"
app:ensureMinTouchTargetSize="false"
app:icon="@drawable/ic_restore"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toEndOf="@id/grid_button"
tools:ignore="ContentDescription,SpeakableTextPresentCheck,TouchTargetSizeCheck" />
<androidx.constraintlayout.widget.Barrier
android:id="@+id/actions_barrier"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:barrierDirection="top"
app:constraint_referenced_ids="reset_button,grid_button,color_button" />
</androidx.constraintlayout.widget.ConstraintLayout>
</LinearLayout>
</FrameLayout> </FrameLayout>

View File

@ -0,0 +1,41 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:id="@+id/title"
style="?attr/textAppearanceBodyMedium"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="Slider" />
<TextView
android:id="@+id/valueLabel"
style="?attr/textAppearanceBodyMedium"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="50%" />
<com.google.android.material.slider.Slider
android:id="@+id/slider"
style="?attr/sliderStyle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingVertical="8dp"
app:haloRadius="18dp"
app:labelBehavior="gone"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/title"
app:thumbRadius="8dp"
app:trackColorInactive="@null"
tools:ignore="SpeakableTextPresentCheck,TouchTargetSizeCheck"
tools:value="0.5" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -1,7 +1,10 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources xmlns:tools="http://schemas.android.com/tools">
<dimen name="grid_padding">8dp</dimen> <dimen name="grid_padding">8dp</dimen>
<dimen name="onScreenItemHorizontalMargin">10dp</dimen> <dimen name="onScreenItemHorizontalMargin">10dp</dimen>
<dimen name="cornerRadius">6dp</dimen> <dimen name="cornerRadius">6dp</dimen>
<dimen name="cornerRadiusMedium">12dp</dimen> <dimen name="cornerRadiusMedium">12dp</dimen>
<!-- Remove the default material slider vertical padding -->
<dimen name="mtrl_slider_widget_height" tools:override="true">0dp</dimen>
</resources> </resources>

View File

@ -5,6 +5,7 @@
<string name="error">An error has occurred</string> <string name="error">An error has occurred</string>
<string name="copied_to_clipboard">Copied to clipboard</string> <string name="copied_to_clipboard">Copied to clipboard</string>
<string name="emulator">Emulator</string> <string name="emulator">Emulator</string>
<string name="enabled">Enabled</string>
<!-- Toolbar Main --> <!-- Toolbar Main -->
<string name="settings">Settings</string> <string name="settings">Settings</string>
<string name="share_logs">Share Logs</string> <string name="share_logs">Share Logs</string>
@ -160,8 +161,15 @@
<string name="osc_edit">Edit On-Screen Controls layout</string> <string name="osc_edit">Edit On-Screen Controls layout</string>
<string name="osc_text_color">Text color</string> <string name="osc_text_color">Text color</string>
<string name="osc_background_color">Background color</string> <string name="osc_background_color">Background color</string>
<string name="osc_reset">Reset On-Screen Controls</string> <string name="osc_reset">Reset Button: %1$s</string>
<string name="osc_reset_confirm">Are you sure you want to reset the On-Screen Controls?</string> <string name="osc_reset_confirm">Are you sure you want to reset this button?</string>
<string name="osc_toggle_mode">Toggle Mode</string>
<string name="osc_current_button">Current Button: %1$s</string>
<string name="select_all">Select All</string>
<string name="osc_scale">Scale</string>
<string name="osc_opacity">Opacity</string>
<string name="button_color">Button color</string>
<string name="toggle_grid">Toggle grid</string>
<string name="setup_guide">Setup Guide</string> <string name="setup_guide">Setup Guide</string>
<string name="setup_guide_description">Sequentially map every stick and button</string> <string name="setup_guide_description">Sequentially map every stick and button</string>
<string name="joystick">Joystick</string> <string name="joystick">Joystick</string>