From 4214cb6eb8828e319b90cfde6604cc36eb236f19 Mon Sep 17 00:00:00 2001 From: JosJuice Date: Tue, 13 May 2025 16:55:48 +0200 Subject: [PATCH 1/8] Android: Make input state changes observable --- .../input/model/ControllerInterface.kt | 47 +++++++++++++++++-- .../ControllerInterface/Android/Android.cpp | 6 +-- 2 files changed, 47 insertions(+), 6 deletions(-) diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/model/ControllerInterface.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/model/ControllerInterface.kt index 3bca59f7b7..d5945f2784 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/model/ControllerInterface.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/model/ControllerInterface.kt @@ -6,6 +6,7 @@ import android.content.Context import android.hardware.input.InputManager import android.os.Build import android.os.Handler +import android.os.Looper import android.os.VibrationEffect import android.os.Vibrator import android.os.VibratorManager @@ -13,8 +14,11 @@ import android.view.InputDevice import android.view.KeyEvent import android.view.MotionEvent import androidx.annotation.Keep +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData import org.dolphinemu.dolphinemu.DolphinApplication import org.dolphinemu.dolphinemu.utils.LooperThread +import java.util.concurrent.atomic.AtomicBoolean /** * This class interfaces with the native ControllerInterface, @@ -24,6 +28,12 @@ object ControllerInterface { private var inputDeviceListener: InputDeviceListener? = null private lateinit var looperThread: LooperThread + private var inputStateUpdatePending = AtomicBoolean(false) + private val inputStateVersion = MutableLiveData(0) + + val inputStateChanged: LiveData + get() = inputStateVersion + /** * Activities which want to pass on inputs to native code * should call this in their own dispatchKeyEvent method. @@ -31,7 +41,13 @@ object ControllerInterface { * @return true if the emulator core seems to be interested in this event. * false if the event should be passed on to the default dispatchKeyEvent. */ - external fun dispatchKeyEvent(event: KeyEvent): Boolean + fun dispatchKeyEvent(event: KeyEvent): Boolean { + val result = dispatchKeyEventNative(event) + onInputStateChanged() + return result + } + + private external fun dispatchKeyEventNative(event: KeyEvent): Boolean /** * Activities which want to pass on inputs to native code @@ -40,7 +56,13 @@ object ControllerInterface { * @return true if the emulator core seems to be interested in this event. * false if the event should be passed on to the default dispatchGenericMotionEvent. */ - external fun dispatchGenericMotionEvent(event: MotionEvent): Boolean + fun dispatchGenericMotionEvent(event: MotionEvent): Boolean { + val result = dispatchGenericMotionEventNative(event) + onInputStateChanged() + return result + } + + private external fun dispatchGenericMotionEventNative(event: MotionEvent): Boolean /** * [DolphinSensorEventListener] calls this for each axis of a received SensorEvent. @@ -48,7 +70,13 @@ object ControllerInterface { * @return true if the emulator core seems to be interested in this event. * false if the sensor can be suspended to save battery. */ - external fun dispatchSensorEvent( + fun dispatchSensorEvent(deviceQualifier: String, axisName: String, value: Float): Boolean { + val result = dispatchSensorEventNative(deviceQualifier, axisName, value) + onInputStateChanged() + return result + } + + private external fun dispatchSensorEventNative( deviceQualifier: String, axisName: String, value: Float @@ -76,6 +104,19 @@ object ControllerInterface { external fun getDevice(deviceString: String): CoreDevice? + private fun onInputStateChanged() { + // When a single SensorEvent is dispatched, this method is likely to get called many times. + // For the sake of performance, let's batch input state updates so that observers only have + // to process one update. + if (!inputStateUpdatePending.getAndSet(true)) { + Handler(Looper.getMainLooper()).post { + if (inputStateUpdatePending.getAndSet(false)) { + inputStateVersion.value = inputStateVersion.value?.plus(1) + } + } + } + } + @Keep @JvmStatic private fun registerInputDeviceListener() { diff --git a/Source/Core/InputCommon/ControllerInterface/Android/Android.cpp b/Source/Core/InputCommon/ControllerInterface/Android/Android.cpp index b6e03e73a0..72d7239f9a 100644 --- a/Source/Core/InputCommon/ControllerInterface/Android/Android.cpp +++ b/Source/Core/InputCommon/ControllerInterface/Android/Android.cpp @@ -1002,7 +1002,7 @@ void InputBackend::PopulateDevices() extern "C" { JNIEXPORT jboolean JNICALL -Java_org_dolphinemu_dolphinemu_features_input_model_ControllerInterface_dispatchKeyEvent( +Java_org_dolphinemu_dolphinemu_features_input_model_ControllerInterface_dispatchKeyEventNative( JNIEnv* env, jclass, jobject key_event) { const jint action = env->CallIntMethod(key_event, s_key_event_get_action); @@ -1046,7 +1046,7 @@ Java_org_dolphinemu_dolphinemu_features_input_model_ControllerInterface_dispatch } JNIEXPORT jboolean JNICALL -Java_org_dolphinemu_dolphinemu_features_input_model_ControllerInterface_dispatchGenericMotionEvent( +Java_org_dolphinemu_dolphinemu_features_input_model_ControllerInterface_dispatchGenericMotionEventNative( JNIEnv* env, jclass, jobject motion_event) { const jint device_id = env->CallIntMethod(motion_event, s_input_event_get_device_id); @@ -1090,7 +1090,7 @@ Java_org_dolphinemu_dolphinemu_features_input_model_ControllerInterface_dispatch } JNIEXPORT jboolean JNICALL -Java_org_dolphinemu_dolphinemu_features_input_model_ControllerInterface_dispatchSensorEvent( +Java_org_dolphinemu_dolphinemu_features_input_model_ControllerInterface_dispatchSensorEventNative( JNIEnv* env, jclass, jstring j_device_qualifier, jstring j_axis_name, jfloat value) { ciface::Core::DeviceQualifier device_qualifier; From 5d7cba8cd95333cddb113956bc352cc00f474fe9 Mon Sep 17 00:00:00 2001 From: JosJuice Date: Tue, 13 May 2025 14:31:57 +0200 Subject: [PATCH 2/8] Android: Make SettingViewHolder a LifecycleOwner --- .../features/settings/ui/SettingsAdapter.kt | 20 +++++++ .../features/settings/ui/SettingsFragment.kt | 6 +- .../settings/ui/SettingsFragmentView.kt | 6 ++ .../ui/viewholder/SettingViewHolder.kt | 6 +- .../dolphinemu/utils/LifecycleViewHolder.kt | 59 +++++++++++++++++++ 5 files changed, 94 insertions(+), 3 deletions(-) create mode 100644 Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/LifecycleViewHolder.kt diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsAdapter.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsAdapter.kt index 59e76ae178..1c230f4447 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsAdapter.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsAdapter.kt @@ -16,6 +16,7 @@ import androidx.annotation.ColorInt import androidx.appcompat.app.AlertDialog import androidx.core.content.ContextCompat import androidx.fragment.app.FragmentActivity +import androidx.lifecycle.Lifecycle import androidx.recyclerview.widget.RecyclerView import com.google.android.material.color.MaterialColors import com.google.android.material.datepicker.CalendarConstraints @@ -543,6 +544,25 @@ class SettingsAdapter( } } + override fun onViewRecycled(holder: SettingViewHolder) { + super.onViewRecycled(holder) + holder.onViewRecycled() + } + + override fun onViewAttachedToWindow(holder: SettingViewHolder) { + super.onViewAttachedToWindow(holder) + holder.onViewAttachedToWindow() + } + + override fun onViewDetachedFromWindow(holder: SettingViewHolder) { + super.onViewDetachedFromWindow(holder) + holder.onViewDetachedFromWindow() + } + + fun getFragmentLifecycle(): Lifecycle { + return fragmentView.getFragmentLifecycle() + } + private fun getValueForSingleChoiceSelection(item: SingleChoiceSetting, which: Int): Int { val valuesId = item.valuesId diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsFragment.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsFragment.kt index 6a294ee139..991d3c3c2b 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsFragment.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsFragment.kt @@ -18,6 +18,7 @@ import androidx.core.view.updatePadding import androidx.fragment.app.DialogFragment import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentActivity +import androidx.lifecycle.Lifecycle import androidx.recyclerview.widget.LinearLayoutManager import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.snackbar.Snackbar @@ -25,7 +26,6 @@ import org.dolphinemu.dolphinemu.R import org.dolphinemu.dolphinemu.databinding.FragmentSettingsBinding import org.dolphinemu.dolphinemu.features.settings.model.Settings import org.dolphinemu.dolphinemu.features.settings.model.view.SettingsItem -import org.dolphinemu.dolphinemu.ui.main.MainActivity import org.dolphinemu.dolphinemu.ui.main.MainPresenter import org.dolphinemu.dolphinemu.utils.GpuDriverInstallResult import org.dolphinemu.dolphinemu.utils.SerializableHelper.serializable @@ -200,6 +200,10 @@ class SettingsFragment : Fragment(), SettingsFragmentView { .show() } + override fun getFragmentLifecycle(): Lifecycle { + return lifecycle + } + private fun askForDriverFile() { val intent = Intent(Intent.ACTION_GET_CONTENT).apply { addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsFragmentView.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsFragmentView.kt index 076beaff6f..b0ad3e28b1 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsFragmentView.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsFragmentView.kt @@ -4,6 +4,7 @@ package org.dolphinemu.dolphinemu.features.settings.ui import androidx.fragment.app.DialogFragment import androidx.fragment.app.FragmentActivity +import androidx.lifecycle.Lifecycle import org.dolphinemu.dolphinemu.features.settings.model.Settings import org.dolphinemu.dolphinemu.features.settings.model.view.SettingsItem import org.dolphinemu.dolphinemu.utils.GpuDriverInstallResult @@ -119,4 +120,9 @@ interface SettingsFragmentView { * Shows a dialog asking the user to install or uninstall a GPU driver */ fun showGpuDriverDialog() + + /** + * Returns the Lifecycle for the Fragment. + */ + fun getFragmentLifecycle(): Lifecycle } diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/viewholder/SettingViewHolder.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/viewholder/SettingViewHolder.kt index 2054b08f43..dd5d4c97f0 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/viewholder/SettingViewHolder.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/viewholder/SettingViewHolder.kt @@ -9,15 +9,17 @@ import android.view.View import android.view.View.OnLongClickListener import android.widget.TextView import android.widget.Toast -import androidx.recyclerview.widget.RecyclerView +import androidx.lifecycle.LifecycleOwner import com.google.android.material.dialog.MaterialAlertDialogBuilder import org.dolphinemu.dolphinemu.DolphinApplication import org.dolphinemu.dolphinemu.R import org.dolphinemu.dolphinemu.features.settings.model.view.SettingsItem import org.dolphinemu.dolphinemu.features.settings.ui.SettingsAdapter +import org.dolphinemu.dolphinemu.utils.LifecycleViewHolder abstract class SettingViewHolder(itemView: View, protected val adapter: SettingsAdapter) : - RecyclerView.ViewHolder(itemView), View.OnClickListener, OnLongClickListener { + LifecycleViewHolder(itemView, adapter.getFragmentLifecycle()), + LifecycleOwner, View.OnClickListener, OnLongClickListener { init { itemView.setOnClickListener(this) diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/LifecycleViewHolder.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/LifecycleViewHolder.kt new file mode 100644 index 0000000000..47696ac4bb --- /dev/null +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/LifecycleViewHolder.kt @@ -0,0 +1,59 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +package org.dolphinemu.dolphinemu.utils + +import android.view.View +import androidx.lifecycle.DefaultLifecycleObserver +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.LifecycleRegistry +import androidx.recyclerview.widget.RecyclerView + +/** + * A ViewHolder with an attached Lifecycle. + * + * @param itemView The view held by the ViewHolder. + * @param parentLifecycle If the passed-in Lifecycle changes state to DESTROYED, this ViewHolder + * will also change state to DESTROYED. This should normally be set to the + * Lifecycle of the containing Fragment or Activity to prevent instances + * of this class from leaking. + */ +abstract class LifecycleViewHolder(itemView: View, parentLifecycle: Lifecycle) : + RecyclerView.ViewHolder(itemView), LifecycleOwner { + + private val lifecycleRegistry = LifecycleRegistry(this) + + override val lifecycle: Lifecycle + get() = lifecycleRegistry + + init { + lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_CREATE) + parentLifecycle.addObserver(object : DefaultLifecycleObserver { + override fun onDestroy(owner: LifecycleOwner) { + // Make sure this ViewHolder doesn't leak if its lifecycle has observers + lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_DESTROY) + } + }) + } + + /** + * To be called from a function overriding [RecyclerView.Adapter.onViewRecycled]. + */ + fun onViewRecycled() { + lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_STOP) + } + + /** + * To be called from a function overriding [RecyclerView.Adapter.onViewAttachedToWindow]. + */ + fun onViewAttachedToWindow() { + lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_RESUME) + } + + /** + * To be called from a function overriding [RecyclerView.Adapter.onViewDetachedFromWindow]. + */ + fun onViewDetachedFromWindow() { + lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_STOP) + } +} From 4b4a775d5a1f03bd441422541bc0fa1beda92319 Mon Sep 17 00:00:00 2001 From: JosJuice Date: Tue, 13 May 2025 19:32:15 +0200 Subject: [PATCH 3/8] Android: Make AdvancedMappingControlViewHolder a LifecycleOwner --- .../input/ui/AdvancedMappingControlAdapter.kt | 25 ++++++++++++++++--- .../ui/AdvancedMappingControlViewHolder.kt | 7 ++++-- .../input/ui/AdvancedMappingDialog.kt | 5 ++-- 3 files changed, 30 insertions(+), 7 deletions(-) diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/ui/AdvancedMappingControlAdapter.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/ui/AdvancedMappingControlAdapter.kt index 1c82616726..963be36d48 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/ui/AdvancedMappingControlAdapter.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/ui/AdvancedMappingControlAdapter.kt @@ -4,12 +4,16 @@ package org.dolphinemu.dolphinemu.features.input.ui import android.view.LayoutInflater import android.view.ViewGroup +import androidx.lifecycle.Lifecycle import androidx.recyclerview.widget.RecyclerView import org.dolphinemu.dolphinemu.databinding.ListItemAdvancedMappingControlBinding import java.util.function.Consumer -class AdvancedMappingControlAdapter(private val onClickCallback: Consumer) : - RecyclerView.Adapter() { +class AdvancedMappingControlAdapter( + private val parentLifecycle: Lifecycle, + private val onClickCallback: Consumer +) : RecyclerView.Adapter() { + private var controls = emptyArray() override fun onCreateViewHolder( @@ -18,7 +22,7 @@ class AdvancedMappingControlAdapter(private val onClickCallback: Consumer -) : RecyclerView.ViewHolder(binding.root) { +) : LifecycleViewHolder(binding.root, parentLifecycle) { + private lateinit var name: String init { diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/ui/AdvancedMappingDialog.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/ui/AdvancedMappingDialog.kt index 7d1c83a217..e05de10105 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/ui/AdvancedMappingDialog.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/ui/AdvancedMappingDialog.kt @@ -38,8 +38,9 @@ class AdvancedMappingDialog( ArrayAdapter(context, android.R.layout.simple_spinner_dropdown_item, devices) binding.dropdownDevice.setAdapter(deviceAdapter) - controlAdapter = - AdvancedMappingControlAdapter { control: String -> onControlClicked(control) } + controlAdapter = AdvancedMappingControlAdapter(lifecycle) { + control: String -> onControlClicked(control) + } binding.listControl.adapter = controlAdapter binding.listControl.layoutManager = LinearLayoutManager(context) From 22fd0472ed848f4c6cff90d5c2112399478021a5 Mon Sep 17 00:00:00 2001 From: JosJuice Date: Tue, 13 May 2025 19:21:32 +0200 Subject: [PATCH 4/8] Android: Pass CoreDevice.Control to AdvancedMappingControlViewHolder --- .../input/ui/AdvancedMappingControlAdapter.kt | 5 +++-- .../input/ui/AdvancedMappingControlViewHolder.kt | 11 ++++++----- .../features/input/ui/AdvancedMappingDialog.kt | 2 +- 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/ui/AdvancedMappingControlAdapter.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/ui/AdvancedMappingControlAdapter.kt index 963be36d48..3982c62220 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/ui/AdvancedMappingControlAdapter.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/ui/AdvancedMappingControlAdapter.kt @@ -7,6 +7,7 @@ import android.view.ViewGroup import androidx.lifecycle.Lifecycle import androidx.recyclerview.widget.RecyclerView import org.dolphinemu.dolphinemu.databinding.ListItemAdvancedMappingControlBinding +import org.dolphinemu.dolphinemu.features.input.model.CoreDevice import java.util.function.Consumer class AdvancedMappingControlAdapter( @@ -14,7 +15,7 @@ class AdvancedMappingControlAdapter( private val onClickCallback: Consumer ) : RecyclerView.Adapter() { - private var controls = emptyArray() + private var controls = emptyArray() override fun onCreateViewHolder( parent: ViewGroup, @@ -30,7 +31,7 @@ class AdvancedMappingControlAdapter( override fun getItemCount(): Int = controls.size - fun setControls(controls: Array) { + fun setControls(controls: Array) { this.controls = controls notifyDataSetChanged() } diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/ui/AdvancedMappingControlViewHolder.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/ui/AdvancedMappingControlViewHolder.kt index f0bf4efe61..8c05115e85 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/ui/AdvancedMappingControlViewHolder.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/ui/AdvancedMappingControlViewHolder.kt @@ -4,6 +4,7 @@ package org.dolphinemu.dolphinemu.features.input.ui import androidx.lifecycle.Lifecycle import org.dolphinemu.dolphinemu.databinding.ListItemAdvancedMappingControlBinding +import org.dolphinemu.dolphinemu.features.input.model.CoreDevice import org.dolphinemu.dolphinemu.utils.LifecycleViewHolder import java.util.function.Consumer @@ -13,14 +14,14 @@ class AdvancedMappingControlViewHolder( onClickCallback: Consumer ) : LifecycleViewHolder(binding.root, parentLifecycle) { - private lateinit var name: String + private lateinit var control: CoreDevice.Control init { - binding.root.setOnClickListener { onClickCallback.accept(name) } + binding.root.setOnClickListener { onClickCallback.accept(control.getName()) } } - fun bind(name: String) { - this.name = name - binding.textName.text = name + fun bind(control: CoreDevice.Control) { + this.control = control + binding.textName.text = control.getName() } } diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/ui/AdvancedMappingDialog.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/ui/AdvancedMappingDialog.kt index e05de10105..570a5a4cae 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/ui/AdvancedMappingDialog.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/ui/AdvancedMappingDialog.kt @@ -73,7 +73,7 @@ class AdvancedMappingDialog( } private fun setControls(controls: Array) = - controlAdapter.setControls(controls.map { it.getName() }.toTypedArray()) + controlAdapter.setControls(controls) private fun onControlClicked(control: String) { val expression = From cf7b141eb9d2651b20515f3d0ef5280ee1e97b6c Mon Sep 17 00:00:00 2001 From: JosJuice Date: Tue, 13 May 2025 19:43:40 +0200 Subject: [PATCH 5/8] Android: Make input mapping view holders observe input state changes --- .../features/input/model/CoreDevice.kt | 2 ++ .../model/view/InputMappingControlSetting.kt | 3 +++ .../input/ui/AdvancedMappingControlAdapter.kt | 3 ++- .../ui/AdvancedMappingControlViewHolder.kt | 23 ++++++++++++++++++- .../input/ui/AdvancedMappingDialog.kt | 14 ++++++++++- .../InputMappingControlSettingViewHolder.kt | 22 ++++++++++++++++++ .../features/settings/ui/SettingsActivity.kt | 14 ++++++++++- Source/Android/jni/Input/CoreDevice.cpp | 7 ++++++ 8 files changed, 84 insertions(+), 4 deletions(-) diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/model/CoreDevice.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/model/CoreDevice.kt index 1483422c50..53f8864123 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/model/CoreDevice.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/model/CoreDevice.kt @@ -18,6 +18,8 @@ class CoreDevice private constructor(private val pointer: Long) { @Keep inner class Control private constructor(private val pointer: Long) { external fun getName(): String + + external fun getState(): Double } protected external fun finalize() diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/model/view/InputMappingControlSetting.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/model/view/InputMappingControlSetting.kt index 26a86a9411..4220e9fcab 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/model/view/InputMappingControlSetting.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/model/view/InputMappingControlSetting.kt @@ -18,6 +18,9 @@ class InputMappingControlSetting(var control: Control, val controller: EmulatedC controller.updateSingleControlReference(controlReference) } + val state: Double + get() = controlReference.getState() + fun clearValue() { value = "" } diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/ui/AdvancedMappingControlAdapter.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/ui/AdvancedMappingControlAdapter.kt index 3982c62220..8919f22658 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/ui/AdvancedMappingControlAdapter.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/ui/AdvancedMappingControlAdapter.kt @@ -12,6 +12,7 @@ import java.util.function.Consumer class AdvancedMappingControlAdapter( private val parentLifecycle: Lifecycle, + private val isInput: Boolean, private val onClickCallback: Consumer ) : RecyclerView.Adapter() { @@ -23,7 +24,7 @@ class AdvancedMappingControlAdapter( ): AdvancedMappingControlViewHolder { val inflater = LayoutInflater.from(parent.context) val binding = ListItemAdvancedMappingControlBinding.inflate(inflater) - return AdvancedMappingControlViewHolder(binding, parentLifecycle, onClickCallback) + return AdvancedMappingControlViewHolder(binding, parentLifecycle, isInput, onClickCallback) } override fun onBindViewHolder(holder: AdvancedMappingControlViewHolder, position: Int) = diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/ui/AdvancedMappingControlViewHolder.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/ui/AdvancedMappingControlViewHolder.kt index 8c05115e85..80771a6f4b 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/ui/AdvancedMappingControlViewHolder.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/ui/AdvancedMappingControlViewHolder.kt @@ -4,13 +4,16 @@ package org.dolphinemu.dolphinemu.features.input.ui import androidx.lifecycle.Lifecycle import org.dolphinemu.dolphinemu.databinding.ListItemAdvancedMappingControlBinding +import org.dolphinemu.dolphinemu.features.input.model.ControllerInterface import org.dolphinemu.dolphinemu.features.input.model.CoreDevice import org.dolphinemu.dolphinemu.utils.LifecycleViewHolder +import org.dolphinemu.dolphinemu.utils.Log import java.util.function.Consumer class AdvancedMappingControlViewHolder( private val binding: ListItemAdvancedMappingControlBinding, - parentLifecycle: Lifecycle, + private val parentLifecycle: Lifecycle, + private val isInput: Boolean, onClickCallback: Consumer ) : LifecycleViewHolder(binding.root, parentLifecycle) { @@ -18,10 +21,28 @@ class AdvancedMappingControlViewHolder( init { binding.root.setOnClickListener { onClickCallback.accept(control.getName()) } + if (isInput) { + ControllerInterface.inputStateChanged.observe(this) { + updateInputValue() + } + } } fun bind(control: CoreDevice.Control) { this.control = control binding.textName.text = control.getName() + if (isInput) { + updateInputValue() + } + } + + private fun updateInputValue() { + if (parentLifecycle.currentState == Lifecycle.State.DESTROYED) { + throw IllegalStateException("AdvancedMappingControlViewHolder leak") + } + + // TODO + Log.info("AdvancedMappingControlViewHolder: Value of " + control.getName() + " is " + + control.getState()) } } diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/ui/AdvancedMappingDialog.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/ui/AdvancedMappingDialog.kt index 570a5a4cae..2117ed3102 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/ui/AdvancedMappingDialog.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/ui/AdvancedMappingDialog.kt @@ -3,6 +3,8 @@ package org.dolphinemu.dolphinemu.features.input.ui import android.content.Context +import android.view.KeyEvent +import android.view.MotionEvent import android.view.View import android.widget.AdapterView import android.widget.AdapterView.OnItemClickListener @@ -38,7 +40,7 @@ class AdvancedMappingDialog( ArrayAdapter(context, android.R.layout.simple_spinner_dropdown_item, devices) binding.dropdownDevice.setAdapter(deviceAdapter) - controlAdapter = AdvancedMappingControlAdapter(lifecycle) { + controlAdapter = AdvancedMappingControlAdapter(lifecycle, controlReference.isInput()) { control: String -> onControlClicked(control) } binding.listControl.adapter = controlAdapter @@ -60,6 +62,16 @@ class AdvancedMappingDialog( override fun onItemClick(adapterView: AdapterView<*>?, view: View, position: Int, id: Long) = setSelectedDevice(devices[position]) + override fun dispatchKeyEvent(event: KeyEvent): Boolean { + ControllerInterface.dispatchKeyEvent(event) + return super.dispatchKeyEvent(event) + } + + override fun dispatchGenericMotionEvent(event: MotionEvent): Boolean { + ControllerInterface.dispatchGenericMotionEvent(event) + return super.dispatchGenericMotionEvent(event) + } + private fun setSelectedDevice(deviceString: String) { selectedDevice = deviceString diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/ui/viewholder/InputMappingControlSettingViewHolder.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/ui/viewholder/InputMappingControlSettingViewHolder.kt index 5bbcbf07c4..b6b30da53d 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/ui/viewholder/InputMappingControlSettingViewHolder.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/ui/viewholder/InputMappingControlSettingViewHolder.kt @@ -3,11 +3,14 @@ package org.dolphinemu.dolphinemu.features.input.ui.viewholder import android.view.View +import androidx.lifecycle.Lifecycle import org.dolphinemu.dolphinemu.databinding.ListItemMappingBinding +import org.dolphinemu.dolphinemu.features.input.model.ControllerInterface import org.dolphinemu.dolphinemu.features.input.model.view.InputMappingControlSetting import org.dolphinemu.dolphinemu.features.settings.model.view.SettingsItem import org.dolphinemu.dolphinemu.features.settings.ui.SettingsAdapter import org.dolphinemu.dolphinemu.features.settings.ui.viewholder.SettingViewHolder +import org.dolphinemu.dolphinemu.utils.Log class InputMappingControlSettingViewHolder( private val binding: ListItemMappingBinding, @@ -15,6 +18,12 @@ class InputMappingControlSettingViewHolder( ) : SettingViewHolder(binding.getRoot(), adapter) { lateinit var setting: InputMappingControlSetting + init { + ControllerInterface.inputStateChanged.observe(this) { + updateInputValue() + } + } + override val item: SettingsItem get() = setting @@ -26,6 +35,7 @@ class InputMappingControlSettingViewHolder( binding.buttonAdvancedSettings.setOnClickListener { clicked: View -> onLongClick(clicked) } setStyle(binding.textSettingName, setting) + updateInputValue() } override fun onClick(clicked: View) { @@ -52,4 +62,16 @@ class InputMappingControlSettingViewHolder( return true } + + private fun updateInputValue() { + if (adapter.getFragmentLifecycle().currentState == Lifecycle.State.DESTROYED) { + throw IllegalStateException("InputMappingControlSettingViewHolder leak") + } + + if (setting.isInput) { + // TODO + Log.info("InputMappingControlSettingViewHolder: Value of " + setting.name + " is " + + setting.state) + } + } } diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsActivity.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsActivity.kt index 63a3992487..e5db2f63d2 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsActivity.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsActivity.kt @@ -11,7 +11,9 @@ import android.content.DialogInterface import android.content.Intent import android.net.Uri import android.os.Bundle +import android.view.KeyEvent import android.view.Menu +import android.view.MotionEvent import android.view.View import android.widget.Toast import androidx.activity.enableEdgeToEdge @@ -22,11 +24,11 @@ import androidx.core.view.WindowInsetsCompat import androidx.fragment.app.DialogFragment import androidx.lifecycle.ViewModelProvider import com.google.android.material.appbar.CollapsingToolbarLayout -import com.google.android.material.color.MaterialColors import com.google.android.material.dialog.MaterialAlertDialogBuilder import org.dolphinemu.dolphinemu.NativeLibrary import org.dolphinemu.dolphinemu.R import org.dolphinemu.dolphinemu.databinding.ActivitySettingsBinding +import org.dolphinemu.dolphinemu.features.input.model.ControllerInterface import org.dolphinemu.dolphinemu.features.settings.model.Settings import org.dolphinemu.dolphinemu.features.settings.ui.SettingsFragment.Companion.newInstance import org.dolphinemu.dolphinemu.ui.main.MainPresenter @@ -197,6 +199,16 @@ class SettingsActivity : AppCompatActivity(), SettingsActivityView { } } + override fun dispatchKeyEvent(event: KeyEvent): Boolean { + ControllerInterface.dispatchKeyEvent(event) + return super.dispatchKeyEvent(event) + } + + override fun dispatchGenericMotionEvent(event: MotionEvent): Boolean { + ControllerInterface.dispatchGenericMotionEvent(event) + return super.dispatchGenericMotionEvent(event) + } + private fun canonicalizeIfPossible(uri: Uri): Uri { val canonicalizedUri = contentResolver.canonicalize(uri) return canonicalizedUri ?: uri diff --git a/Source/Android/jni/Input/CoreDevice.cpp b/Source/Android/jni/Input/CoreDevice.cpp index 4fd4758ebb..8d7984b5e3 100644 --- a/Source/Android/jni/Input/CoreDevice.cpp +++ b/Source/Android/jni/Input/CoreDevice.cpp @@ -64,6 +64,13 @@ Java_org_dolphinemu_dolphinemu_features_input_model_CoreDevice_00024Control_getN return ToJString(env, GetControlPointer(env, obj)->GetName()); } +JNIEXPORT jdouble JNICALL +Java_org_dolphinemu_dolphinemu_features_input_model_CoreDevice_00024Control_getState(JNIEnv* env, + jobject obj) +{ + return env, GetControlPointer(env, obj)->ToInput()->GetState(); +} + JNIEXPORT void JNICALL Java_org_dolphinemu_dolphinemu_features_input_model_CoreDevice_finalize(JNIEnv* env, jobject obj) { From 0dd601577d20140b78fe2fddf0382d91ec97eeff Mon Sep 17 00:00:00 2001 From: JosJuice Date: Sat, 17 May 2025 09:33:57 +0200 Subject: [PATCH 6/8] Android: Set parent when inflating RecyclerView.ViewHolders This makes sure view holders get proper widths when they use layout_width="match_parent". This becomes quite noticeable for AdvancedMappingControlAdapter in the next commit, but I'm also making the change for other adapters while I'm at it. --- .../dolphinemu/adapters/GameAdapter.kt | 3 +- .../features/cheats/ui/CheatsAdapter.kt | 6 +-- .../input/ui/AdvancedMappingControlAdapter.kt | 2 +- .../riivolution/ui/RiivolutionAdapter.kt | 2 +- .../features/settings/ui/SettingsAdapter.kt | 41 ++++++++----------- 5 files changed, 23 insertions(+), 31 deletions(-) diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/adapters/GameAdapter.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/adapters/GameAdapter.kt index c1ef9c2b8f..dbcfdd42e2 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/adapters/GameAdapter.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/adapters/GameAdapter.kt @@ -37,7 +37,8 @@ class GameAdapter : RecyclerView.Adapter(), * @return The created ViewHolder with references to all the child view's members. */ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): GameViewHolder { - val binding = CardGameBinding.inflate(LayoutInflater.from(parent.context)) + val inflater = LayoutInflater.from(parent.context) + val binding = CardGameBinding.inflate(inflater, parent, false) binding.root.apply { setOnClickListener(this@GameAdapter) setOnLongClickListener(this@GameAdapter) diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/cheats/ui/CheatsAdapter.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/cheats/ui/CheatsAdapter.kt index c75330375d..732555ea32 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/cheats/ui/CheatsAdapter.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/cheats/ui/CheatsAdapter.kt @@ -43,17 +43,17 @@ class CheatsAdapter( val inflater = LayoutInflater.from(parent.context) return when (viewType) { CheatItem.TYPE_CHEAT -> { - val listItemCheatBinding = ListItemCheatBinding.inflate(inflater) + val listItemCheatBinding = ListItemCheatBinding.inflate(inflater, parent, false) addViewListeners(listItemCheatBinding.getRoot()) CheatViewHolder(listItemCheatBinding) } CheatItem.TYPE_HEADER -> { - val listItemHeaderBinding = ListItemHeaderBinding.inflate(inflater) + val listItemHeaderBinding = ListItemHeaderBinding.inflate(inflater, parent, false) addViewListeners(listItemHeaderBinding.root) HeaderViewHolder(listItemHeaderBinding) } CheatItem.TYPE_ACTION -> { - val listItemSubmenuBinding = ListItemSubmenuBinding.inflate(inflater) + val listItemSubmenuBinding = ListItemSubmenuBinding.inflate(inflater, parent, false) addViewListeners(listItemSubmenuBinding.root) ActionViewHolder(listItemSubmenuBinding) } diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/ui/AdvancedMappingControlAdapter.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/ui/AdvancedMappingControlAdapter.kt index 8919f22658..cb20809266 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/ui/AdvancedMappingControlAdapter.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/ui/AdvancedMappingControlAdapter.kt @@ -23,7 +23,7 @@ class AdvancedMappingControlAdapter( viewType: Int ): AdvancedMappingControlViewHolder { val inflater = LayoutInflater.from(parent.context) - val binding = ListItemAdvancedMappingControlBinding.inflate(inflater) + val binding = ListItemAdvancedMappingControlBinding.inflate(inflater, parent, false) return AdvancedMappingControlViewHolder(binding, parentLifecycle, isInput, onClickCallback) } diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/riivolution/ui/RiivolutionAdapter.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/riivolution/ui/RiivolutionAdapter.kt index 9cae2f0bae..d64e10a111 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/riivolution/ui/RiivolutionAdapter.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/riivolution/ui/RiivolutionAdapter.kt @@ -32,7 +32,7 @@ class RiivolutionAdapter(private val context: Context, private val patches: Riiv override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RiivolutionViewHolder { val inflater = LayoutInflater.from(parent.context) - val binding = ListItemRiivolutionBinding.inflate(inflater) + val binding = ListItemRiivolutionBinding.inflate(inflater, parent, false) return RiivolutionViewHolder(binding.root, binding) } diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsAdapter.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsAdapter.kt index 1c230f4447..3ce937e6ab 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsAdapter.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/ui/SettingsAdapter.kt @@ -68,57 +68,48 @@ class SettingsAdapter( val inflater = LayoutInflater.from(parent.context) return when (viewType) { SettingsItem.TYPE_HEADER -> HeaderViewHolder( - ListItemHeaderBinding.inflate(inflater), + ListItemHeaderBinding.inflate(inflater, parent, false), this ) SettingsItem.TYPE_SWITCH -> SwitchSettingViewHolder( - ListItemSettingSwitchBinding.inflate(inflater), + ListItemSettingSwitchBinding.inflate(inflater, parent, false), this ) SettingsItem.TYPE_STRING_SINGLE_CHOICE, SettingsItem.TYPE_SINGLE_CHOICE_DYNAMIC_DESCRIPTIONS, SettingsItem.TYPE_SINGLE_CHOICE -> SingleChoiceViewHolder( - ListItemSettingBinding.inflate(inflater), + ListItemSettingBinding.inflate(inflater, parent, false), this ) SettingsItem.TYPE_SLIDER -> SliderViewHolder( - ListItemSettingBinding.inflate( - inflater - ), this, context + ListItemSettingBinding.inflate(inflater, parent, false), + this, + context ) SettingsItem.TYPE_SUBMENU -> SubmenuViewHolder( - ListItemSubmenuBinding.inflate( - inflater - ), this + ListItemSubmenuBinding.inflate(inflater, parent, false), + this ) SettingsItem.TYPE_INPUT_MAPPING_CONTROL -> InputMappingControlSettingViewHolder( - ListItemMappingBinding.inflate(inflater), + ListItemMappingBinding.inflate(inflater, parent, false), this ) SettingsItem.TYPE_FILE_PICKER -> FilePickerViewHolder( - ListItemSettingBinding.inflate( - inflater - ), this + ListItemSettingBinding.inflate(inflater, parent, false), + this ) SettingsItem.TYPE_RUN_RUNNABLE -> RunRunnableViewHolder( - ListItemSettingBinding.inflate( - inflater - ), this, context + ListItemSettingBinding.inflate(inflater, parent, false), + this, context ) SettingsItem.TYPE_STRING -> InputStringSettingViewHolder( - ListItemSettingBinding.inflate( - inflater - ), this + ListItemSettingBinding.inflate(inflater, parent, false), this ) SettingsItem.TYPE_HYPERLINK_HEADER -> HeaderHyperLinkViewHolder( - ListItemHeaderBinding.inflate( - inflater - ), this + ListItemHeaderBinding.inflate(inflater, parent, false), this ) SettingsItem.TYPE_DATETIME_CHOICE -> DateTimeSettingViewHolder( - ListItemSettingBinding.inflate( - inflater - ), this + ListItemSettingBinding.inflate(inflater, parent, false), this ) else -> throw IllegalArgumentException("Invalid view type: $viewType") } From 1002f296918153c55dca674b3d70a54485f00306 Mon Sep 17 00:00:00 2001 From: JosJuice Date: Wed, 14 May 2025 18:14:38 +0200 Subject: [PATCH 7/8] Android: Show input indicators in controller settings --- .../ui/AdvancedMappingControlViewHolder.kt | 29 ++++++++++++-- .../input/ui/ControlStateBarHorizontal.kt | 25 ++++++++++++ .../features/input/ui/ControlStateBarImpl.kt | 28 +++++++++++++ .../input/ui/ControlStateBarVertical.kt | 30 ++++++++++++++ .../InputMappingControlSettingViewHolder.kt | 22 +++++++++-- .../res/layout-ldrtl/list_item_mapping.xml | 15 +++++-- .../list_item_advanced_mapping_control.xml | 39 ++++++++++++++----- .../src/main/res/layout/list_item_mapping.xml | 15 +++++-- .../view_control_state_bar_horizontal.xml | 20 ++++++++++ .../view_control_state_bar_vertical.xml | 20 ++++++++++ 10 files changed, 218 insertions(+), 25 deletions(-) create mode 100644 Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/ui/ControlStateBarHorizontal.kt create mode 100644 Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/ui/ControlStateBarImpl.kt create mode 100644 Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/ui/ControlStateBarVertical.kt create mode 100644 Source/Android/app/src/main/res/layout/view_control_state_bar_horizontal.xml create mode 100644 Source/Android/app/src/main/res/layout/view_control_state_bar_vertical.xml diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/ui/AdvancedMappingControlViewHolder.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/ui/AdvancedMappingControlViewHolder.kt index 80771a6f4b..017bc1e642 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/ui/AdvancedMappingControlViewHolder.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/ui/AdvancedMappingControlViewHolder.kt @@ -2,13 +2,15 @@ package org.dolphinemu.dolphinemu.features.input.ui +import android.view.View import androidx.lifecycle.Lifecycle import org.dolphinemu.dolphinemu.databinding.ListItemAdvancedMappingControlBinding import org.dolphinemu.dolphinemu.features.input.model.ControllerInterface import org.dolphinemu.dolphinemu.features.input.model.CoreDevice import org.dolphinemu.dolphinemu.utils.LifecycleViewHolder -import org.dolphinemu.dolphinemu.utils.Log +import java.text.DecimalFormat import java.util.function.Consumer +import kotlin.math.abs class AdvancedMappingControlViewHolder( private val binding: ListItemAdvancedMappingControlBinding, @@ -19,12 +21,16 @@ class AdvancedMappingControlViewHolder( private lateinit var control: CoreDevice.Control + private var previousState = Float.POSITIVE_INFINITY + init { binding.root.setOnClickListener { onClickCallback.accept(control.getName()) } if (isInput) { ControllerInterface.inputStateChanged.observe(this) { updateInputValue() } + } else { + binding.layoutState.visibility = View.GONE } } @@ -41,8 +47,23 @@ class AdvancedMappingControlViewHolder( throw IllegalStateException("AdvancedMappingControlViewHolder leak") } - // TODO - Log.info("AdvancedMappingControlViewHolder: Value of " + control.getName() + " is " + - control.getState()) + var state = control.getState().toFloat() + if (abs(state - previousState) >= stateUpdateThreshold) { + 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 } } diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/ui/ControlStateBarHorizontal.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/ui/ControlStateBarHorizontal.kt new file mode 100644 index 0000000000..a7a02921e8 --- /dev/null +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/ui/ControlStateBarHorizontal.kt @@ -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 + } +} diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/ui/ControlStateBarImpl.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/ui/ControlStateBarImpl.kt new file mode 100644 index 0000000000..027448b0e3 --- /dev/null +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/ui/ControlStateBarImpl.kt @@ -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 + } + } +} diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/ui/ControlStateBarVertical.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/ui/ControlStateBarVertical.kt new file mode 100644 index 0000000000..cb7c37ad70 --- /dev/null +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/ui/ControlStateBarVertical.kt @@ -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 + } +} diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/ui/viewholder/InputMappingControlSettingViewHolder.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/ui/viewholder/InputMappingControlSettingViewHolder.kt index b6b30da53d..312fc9f6a5 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/ui/viewholder/InputMappingControlSettingViewHolder.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/input/ui/viewholder/InputMappingControlSettingViewHolder.kt @@ -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.ui.SettingsAdapter import org.dolphinemu.dolphinemu.features.settings.ui.viewholder.SettingViewHolder -import org.dolphinemu.dolphinemu.utils.Log +import kotlin.math.abs class InputMappingControlSettingViewHolder( private val binding: ListItemMappingBinding, adapter: SettingsAdapter ) : SettingViewHolder(binding.getRoot(), adapter) { + lateinit var setting: InputMappingControlSetting + private var previousState = Float.POSITIVE_INFINITY + init { ControllerInterface.inputStateChanged.observe(this) { updateInputValue() @@ -34,6 +37,10 @@ class InputMappingControlSettingViewHolder( binding.textSettingDescription.text = setting.value binding.buttonAdvancedSettings.setOnClickListener { clicked: View -> onLongClick(clicked) } + if (!setting.isInput) { + binding.controlStateBar.visibility = View.INVISIBLE + } + setStyle(binding.textSettingName, setting) updateInputValue() } @@ -69,9 +76,16 @@ class InputMappingControlSettingViewHolder( } if (setting.isInput) { - // TODO - Log.info("InputMappingControlSettingViewHolder: Value of " + setting.name + " is " + - setting.state) + val state = setting.state.toFloat() + if (abs(state - previousState) >= stateUpdateThreshold) { + 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 + } } diff --git a/Source/Android/app/src/main/res/layout-ldrtl/list_item_mapping.xml b/Source/Android/app/src/main/res/layout-ldrtl/list_item_mapping.xml index 94f219b4c1..e1a2118b36 100644 --- a/Source/Android/app/src/main/res/layout-ldrtl/list_item_mapping.xml +++ b/Source/Android/app/src/main/res/layout-ldrtl/list_item_mapping.xml @@ -22,7 +22,7 @@ android:layout_marginTop="@dimen/spacing_large" android:textSize="16sp" android:textAlignment="viewStart" - android:layout_toStartOf="@+id/button_advanced_settings" + android:layout_toStartOf="@id/control_state_bar" tools:text="Setting Name" /> + +