Android: Show input indicators in controller settings

This commit is contained in:
JosJuice
2025-05-14 18:14:38 +02:00
parent 0dd601577d
commit 1002f29691
10 changed files with 218 additions and 25 deletions

View File

@ -2,13 +2,15 @@
package org.dolphinemu.dolphinemu.features.input.ui package org.dolphinemu.dolphinemu.features.input.ui
import android.view.View
import androidx.lifecycle.Lifecycle import androidx.lifecycle.Lifecycle
import org.dolphinemu.dolphinemu.databinding.ListItemAdvancedMappingControlBinding import org.dolphinemu.dolphinemu.databinding.ListItemAdvancedMappingControlBinding
import org.dolphinemu.dolphinemu.features.input.model.ControllerInterface import org.dolphinemu.dolphinemu.features.input.model.ControllerInterface
import org.dolphinemu.dolphinemu.features.input.model.CoreDevice import org.dolphinemu.dolphinemu.features.input.model.CoreDevice
import org.dolphinemu.dolphinemu.utils.LifecycleViewHolder import org.dolphinemu.dolphinemu.utils.LifecycleViewHolder
import org.dolphinemu.dolphinemu.utils.Log import java.text.DecimalFormat
import java.util.function.Consumer import java.util.function.Consumer
import kotlin.math.abs
class AdvancedMappingControlViewHolder( class AdvancedMappingControlViewHolder(
private val binding: ListItemAdvancedMappingControlBinding, private val binding: ListItemAdvancedMappingControlBinding,
@ -19,12 +21,16 @@ class AdvancedMappingControlViewHolder(
private lateinit var control: CoreDevice.Control private lateinit var control: CoreDevice.Control
private var previousState = Float.POSITIVE_INFINITY
init { init {
binding.root.setOnClickListener { onClickCallback.accept(control.getName()) } binding.root.setOnClickListener { onClickCallback.accept(control.getName()) }
if (isInput) { if (isInput) {
ControllerInterface.inputStateChanged.observe(this) { ControllerInterface.inputStateChanged.observe(this) {
updateInputValue() updateInputValue()
} }
} else {
binding.layoutState.visibility = View.GONE
} }
} }
@ -41,8 +47,23 @@ class AdvancedMappingControlViewHolder(
throw IllegalStateException("AdvancedMappingControlViewHolder leak") throw IllegalStateException("AdvancedMappingControlViewHolder leak")
} }
// TODO var state = control.getState().toFloat()
Log.info("AdvancedMappingControlViewHolder: Value of " + control.getName() + " is " + if (abs(state - previousState) >= stateUpdateThreshold) {
control.getState()) previousState = state
// Don't print a minus sign for signed zeroes
if (state == -0.0f)
state = 0.0f
binding.textState.setText(stateFormat.format(state))
binding.controlStateBar.state = state
}
}
companion object {
private val stateFormat = DecimalFormat("#.####")
// For performance, require state to change by a certain threshold before we update the UI
private val stateUpdateThreshold = 0.00005f
} }
} }

View File

@ -0,0 +1,25 @@
package org.dolphinemu.dolphinemu.features.input.ui
import android.content.Context
import android.util.AttributeSet
import android.view.LayoutInflater
import android.widget.LinearLayout
import org.dolphinemu.dolphinemu.databinding.ViewControlStateBarHorizontalBinding
class ControlStateBarHorizontal @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : LinearLayout(context, attrs, defStyleAttr) {
private val binding =
ViewControlStateBarHorizontalBinding.inflate(LayoutInflater.from(context), this)
private val impl = ControlStateBarImpl(binding.viewFilled, binding.viewUnfilled)
var state: Float
get() = impl.state
set(value) {
impl.state = value
}
}

View File

@ -0,0 +1,28 @@
package org.dolphinemu.dolphinemu.features.input.ui
import android.view.View
import android.widget.LinearLayout
import androidx.core.math.MathUtils
class ControlStateBarImpl(private val filledView: View, private val unfilledView: View) {
private var _state = 0.0f
var state: Float
get() = _state
set(value) {
val clampedState = MathUtils.clamp(value, 0.0f, 1.0f)
if (clampedState != _state) {
setLinearLayoutWeight(filledView, clampedState)
setLinearLayoutWeight(unfilledView, 1.0f - clampedState)
}
_state = clampedState
}
companion object {
private fun setLinearLayoutWeight(view: View, weight: Float) {
val layoutParams = view.layoutParams as LinearLayout.LayoutParams
layoutParams.weight = weight
view.layoutParams = layoutParams
}
}
}

View File

@ -0,0 +1,30 @@
package org.dolphinemu.dolphinemu.features.input.ui
import android.content.Context
import android.util.AttributeSet
import android.view.LayoutInflater
import android.widget.LinearLayout
import org.dolphinemu.dolphinemu.databinding.ViewControlStateBarVerticalBinding
class ControlStateBarVertical @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : LinearLayout(context, attrs, defStyleAttr) {
init {
// Setting this in the XML file has no effect for some reason
orientation = VERTICAL
}
private val binding =
ViewControlStateBarVerticalBinding.inflate(LayoutInflater.from(context), this)
private val impl = ControlStateBarImpl(binding.viewFilled, binding.viewUnfilled)
var state: Float
get() = impl.state
set(value) {
impl.state = value
}
}

View File

@ -10,14 +10,17 @@ import org.dolphinemu.dolphinemu.features.input.model.view.InputMappingControlSe
import org.dolphinemu.dolphinemu.features.settings.model.view.SettingsItem import org.dolphinemu.dolphinemu.features.settings.model.view.SettingsItem
import org.dolphinemu.dolphinemu.features.settings.ui.SettingsAdapter import org.dolphinemu.dolphinemu.features.settings.ui.SettingsAdapter
import org.dolphinemu.dolphinemu.features.settings.ui.viewholder.SettingViewHolder import org.dolphinemu.dolphinemu.features.settings.ui.viewholder.SettingViewHolder
import org.dolphinemu.dolphinemu.utils.Log import kotlin.math.abs
class InputMappingControlSettingViewHolder( class InputMappingControlSettingViewHolder(
private val binding: ListItemMappingBinding, private val binding: ListItemMappingBinding,
adapter: SettingsAdapter adapter: SettingsAdapter
) : SettingViewHolder(binding.getRoot(), adapter) { ) : SettingViewHolder(binding.getRoot(), adapter) {
lateinit var setting: InputMappingControlSetting lateinit var setting: InputMappingControlSetting
private var previousState = Float.POSITIVE_INFINITY
init { init {
ControllerInterface.inputStateChanged.observe(this) { ControllerInterface.inputStateChanged.observe(this) {
updateInputValue() updateInputValue()
@ -34,6 +37,10 @@ class InputMappingControlSettingViewHolder(
binding.textSettingDescription.text = setting.value binding.textSettingDescription.text = setting.value
binding.buttonAdvancedSettings.setOnClickListener { clicked: View -> onLongClick(clicked) } binding.buttonAdvancedSettings.setOnClickListener { clicked: View -> onLongClick(clicked) }
if (!setting.isInput) {
binding.controlStateBar.visibility = View.INVISIBLE
}
setStyle(binding.textSettingName, setting) setStyle(binding.textSettingName, setting)
updateInputValue() updateInputValue()
} }
@ -69,9 +76,16 @@ class InputMappingControlSettingViewHolder(
} }
if (setting.isInput) { if (setting.isInput) {
// TODO val state = setting.state.toFloat()
Log.info("InputMappingControlSettingViewHolder: Value of " + setting.name + " is " + if (abs(state - previousState) >= stateUpdateThreshold) {
setting.state) previousState = state
binding.controlStateBar.state = state
}
} }
} }
companion object {
// For performance, require state to change by a certain threshold before we update the UI
private val stateUpdateThreshold = 0.01f
}
} }

View File

@ -22,7 +22,7 @@
android:layout_marginTop="@dimen/spacing_large" android:layout_marginTop="@dimen/spacing_large"
android:textSize="16sp" android:textSize="16sp"
android:textAlignment="viewStart" android:textAlignment="viewStart"
android:layout_toStartOf="@+id/button_advanced_settings" android:layout_toStartOf="@id/control_state_bar"
tools:text="Setting Name" /> tools:text="Setting Name" />
<TextView <TextView
@ -30,16 +30,23 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_alignParentStart="true" android:layout_alignParentStart="true"
android:layout_alignStart="@+id/text_setting_name" android:layout_alignStart="@id/text_setting_name"
android:layout_below="@+id/text_setting_name" android:layout_below="@id/text_setting_name"
android:layout_marginBottom="@dimen/spacing_large" android:layout_marginBottom="@dimen/spacing_large"
android:layout_marginEnd="@dimen/spacing_large" android:layout_marginEnd="@dimen/spacing_large"
android:layout_marginStart="@dimen/spacing_large" android:layout_marginStart="@dimen/spacing_large"
android:layout_marginTop="@dimen/spacing_small" android:layout_marginTop="@dimen/spacing_small"
android:layout_toStartOf="@+id/button_advanced_settings" android:layout_toStartOf="@id/control_state_bar"
android:textAlignment="viewStart" android:textAlignment="viewStart"
tools:text="@string/overclock_enable_description" /> tools:text="@string/overclock_enable_description" />
<org.dolphinemu.dolphinemu.features.input.ui.ControlStateBarVertical
android:id="@+id/control_state_bar"
android:layout_width="4dp"
android:layout_height="16dp"
android:layout_centerVertical="true"
android:layout_toStartOf="@id/button_advanced_settings" />
<Button <Button
android:id="@+id/button_advanced_settings" android:id="@+id/button_advanced_settings"
style="?attr/materialIconButtonStyle" style="?attr/materialIconButtonStyle"

View File

@ -1,27 +1,48 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<RelativeLayout <LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android" xmlns:android="http://schemas.android.com/apk/res/android"
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="wrap_content" android:layout_height="wrap_content"
android:orientation="horizontal"
android:minHeight="54dp" android:minHeight="54dp"
android:background="?android:attr/selectableItemBackground" android:background="?android:attr/selectableItemBackground"
android:focusable="true" android:focusable="true"
android:clickable="true"> android:clickable="true">
<TextView <TextView
android:layout_width="match_parent" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_weight="2"
style="@style/TextAppearance.MaterialComponents.Headline5" style="@style/TextAppearance.MaterialComponents.Headline5"
tools:text="Button A" tools:text="Button A"
android:layout_alignParentEnd="true" android:padding="@dimen/spacing_large"
android:layout_alignParentStart="true"
android:layout_alignParentTop="true"
android:layout_marginStart="@dimen/spacing_large"
android:layout_marginEnd="@dimen/spacing_large"
android:layout_marginTop="@dimen/spacing_large"
android:id="@+id/text_name" android:id="@+id/text_name"
android:textAlignment="viewStart" android:textAlignment="viewStart"
android:textSize="16sp" /> android:textSize="16sp" />
</RelativeLayout> <FrameLayout
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:id="@+id/layout_state">
<org.dolphinemu.dolphinemu.features.input.ui.ControlStateBarHorizontal
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/control_state_bar" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal|center_vertical"
style="@style/TextAppearance.MaterialComponents.Headline5"
tools:text="0.1234"
android:padding="@dimen/spacing_large"
android:id="@+id/text_state"
android:textAlignment="center"
android:textSize="16sp" />
</FrameLayout>
</LinearLayout>

View File

@ -22,7 +22,7 @@
android:layout_marginTop="@dimen/spacing_large" android:layout_marginTop="@dimen/spacing_large"
android:textSize="16sp" android:textSize="16sp"
android:textAlignment="viewStart" android:textAlignment="viewStart"
android:layout_toStartOf="@+id/button_advanced_settings" android:layout_toStartOf="@id/control_state_bar"
tools:text="Setting Name" /> tools:text="Setting Name" />
<TextView <TextView
@ -30,16 +30,23 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_alignParentStart="true" android:layout_alignParentStart="true"
android:layout_alignStart="@+id/text_setting_name" android:layout_alignStart="@id/text_setting_name"
android:layout_below="@+id/text_setting_name" android:layout_below="@id/text_setting_name"
android:layout_marginBottom="@dimen/spacing_large" android:layout_marginBottom="@dimen/spacing_large"
android:layout_marginEnd="@dimen/spacing_large" android:layout_marginEnd="@dimen/spacing_large"
android:layout_marginStart="@dimen/spacing_large" android:layout_marginStart="@dimen/spacing_large"
android:layout_marginTop="@dimen/spacing_small" android:layout_marginTop="@dimen/spacing_small"
android:layout_toStartOf="@+id/button_advanced_settings" android:layout_toStartOf="@id/control_state_bar"
android:textAlignment="viewStart" android:textAlignment="viewStart"
tools:text="@string/overclock_enable_description" /> tools:text="@string/overclock_enable_description" />
<org.dolphinemu.dolphinemu.features.input.ui.ControlStateBarVertical
android:id="@+id/control_state_bar"
android:layout_width="4dp"
android:layout_height="16dp"
android:layout_centerVertical="true"
android:layout_toStartOf="@id/button_advanced_settings" />
<Button <Button
android:id="@+id/button_advanced_settings" android:id="@+id/button_advanced_settings"
style="?attr/materialIconButtonStyle" style="?attr/materialIconButtonStyle"

View File

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<merge
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
tools:parentTag="LinearLayout">
<View
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="0"
android:id="@+id/view_filled"
android:background="@color/dolphin_errorContainer" />
<View
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:id="@+id/view_unfilled" />
</merge>

View File

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<merge
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
tools:parentTag="LinearLayout">
<View
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:id="@+id/view_unfilled" />
<View
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="0"
android:id="@+id/view_filled"
android:background="@color/dolphin_error" />
</merge>