mirror of
https://github.com/dolphin-emu/dolphin.git
synced 2025-01-08 15:20:45 +01:00
(Android) Enable haptic feedback for sliders, toggles, etc.
This commit is contained in:
parent
9b3b6bea9d
commit
4de5c51552
@ -58,6 +58,7 @@ import org.dolphinemu.dolphinemu.ui.main.ThemeProvider
|
||||
import org.dolphinemu.dolphinemu.utils.AfterDirectoryInitializationRunner
|
||||
import org.dolphinemu.dolphinemu.utils.DirectoryInitialization
|
||||
import org.dolphinemu.dolphinemu.utils.FileBrowserHelper
|
||||
import org.dolphinemu.dolphinemu.utils.HapticListener
|
||||
import org.dolphinemu.dolphinemu.utils.ThemeHelper
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
@ -702,9 +703,11 @@ class EmulationActivity : AppCompatActivity(), ThemeProvider {
|
||||
valueTo = 150f
|
||||
value = IntSetting.MAIN_CONTROL_SCALE.int.toFloat()
|
||||
stepSize = 1f
|
||||
addOnChangeListener(Slider.OnChangeListener { _: Slider?, progress: Float, _: Boolean ->
|
||||
dialogBinding.inputScaleValue.text = "${(progress.toInt() + 50)}%"
|
||||
})
|
||||
addOnChangeListener(
|
||||
HapticListener.wrapOnChangeListener({ _: Slider, progress: Float, _: Boolean ->
|
||||
dialogBinding.inputScaleValue.text = "${(progress.toInt() + 50)}%"
|
||||
}, value)
|
||||
)
|
||||
}
|
||||
inputScaleValue.text =
|
||||
"${(dialogBinding.inputScaleSlider.value.toInt() + 50)}%"
|
||||
@ -713,9 +716,11 @@ class EmulationActivity : AppCompatActivity(), ThemeProvider {
|
||||
valueTo = 100f
|
||||
value = IntSetting.MAIN_CONTROL_OPACITY.int.toFloat()
|
||||
stepSize = 1f
|
||||
addOnChangeListener(Slider.OnChangeListener { _: Slider?, progress: Float, _: Boolean ->
|
||||
inputOpacityValue.text = progress.toInt().toString() + "%"
|
||||
})
|
||||
addOnChangeListener(
|
||||
HapticListener.wrapOnChangeListener({ _: Slider, progress: Float, _: Boolean ->
|
||||
inputOpacityValue.text = progress.toInt().toString() + "%"
|
||||
}, value)
|
||||
)
|
||||
}
|
||||
inputOpacityValue.text = inputOpacitySlider.value.toInt().toString() + "%"
|
||||
}
|
||||
|
@ -23,6 +23,7 @@ import org.dolphinemu.dolphinemu.databinding.CardGameBinding
|
||||
import org.dolphinemu.dolphinemu.dialogs.GamePropertiesDialog
|
||||
import org.dolphinemu.dolphinemu.features.settings.model.BooleanSetting
|
||||
import org.dolphinemu.dolphinemu.utils.CoilUtils
|
||||
import org.dolphinemu.dolphinemu.utils.HapticListener
|
||||
import java.util.ArrayList
|
||||
|
||||
class GameAdapter : RecyclerView.Adapter<GameViewHolder>(),
|
||||
@ -39,7 +40,7 @@ class GameAdapter : RecyclerView.Adapter<GameViewHolder>(),
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): GameViewHolder {
|
||||
val binding = CardGameBinding.inflate(LayoutInflater.from(parent.context))
|
||||
binding.root.apply {
|
||||
setOnClickListener(this@GameAdapter)
|
||||
setOnClickListener(HapticListener.wrapOnClickListener(this@GameAdapter))
|
||||
setOnLongClickListener(this@GameAdapter)
|
||||
}
|
||||
|
||||
|
@ -8,6 +8,7 @@ import androidx.lifecycle.ViewModelProvider
|
||||
import org.dolphinemu.dolphinemu.databinding.ListItemCheatBinding
|
||||
import org.dolphinemu.dolphinemu.features.cheats.model.Cheat
|
||||
import org.dolphinemu.dolphinemu.features.cheats.model.CheatsViewModel
|
||||
import org.dolphinemu.dolphinemu.utils.HapticListener
|
||||
|
||||
class CheatViewHolder(private val binding: ListItemCheatBinding) :
|
||||
CheatItemViewHolder(binding.getRoot()),
|
||||
@ -25,7 +26,7 @@ class CheatViewHolder(private val binding: ListItemCheatBinding) :
|
||||
binding.textName.text = cheat.getName()
|
||||
binding.cheatSwitch.isChecked = cheat.getEnabled()
|
||||
binding.root.setOnClickListener(this)
|
||||
binding.cheatSwitch.setOnCheckedChangeListener(this)
|
||||
binding.cheatSwitch.setOnCheckedChangeListener(HapticListener.wrapOnCheckedChangeListener(this))
|
||||
}
|
||||
|
||||
override fun onClick(root: View) {
|
||||
|
@ -35,6 +35,7 @@ import org.dolphinemu.dolphinemu.features.settings.model.view.*
|
||||
import org.dolphinemu.dolphinemu.features.settings.ui.viewholder.*
|
||||
import org.dolphinemu.dolphinemu.utils.DirectoryInitialization
|
||||
import org.dolphinemu.dolphinemu.utils.FileBrowserHelper
|
||||
import org.dolphinemu.dolphinemu.utils.HapticListener
|
||||
import org.dolphinemu.dolphinemu.utils.Log
|
||||
import org.dolphinemu.dolphinemu.utils.PermissionsHandler
|
||||
import java.io.File
|
||||
@ -261,7 +262,7 @@ class SettingsAdapter(
|
||||
}
|
||||
}
|
||||
slider.value = (seekbarProgress / slider.stepSize).roundToInt() * slider.stepSize
|
||||
slider.addOnChangeListener(this)
|
||||
slider.addOnChangeListener(HapticListener.wrapOnChangeListener(this, slider.value))
|
||||
|
||||
dialog = MaterialAlertDialogBuilder(fragmentView.fragmentActivity)
|
||||
.setTitle(item.name)
|
||||
|
@ -10,6 +10,7 @@ import org.dolphinemu.dolphinemu.features.settings.model.view.SettingsItem
|
||||
import org.dolphinemu.dolphinemu.features.settings.model.view.SwitchSetting
|
||||
import org.dolphinemu.dolphinemu.features.settings.ui.SettingsAdapter
|
||||
import org.dolphinemu.dolphinemu.utils.DirectoryInitialization
|
||||
import org.dolphinemu.dolphinemu.utils.HapticListener
|
||||
import java.io.File
|
||||
import java.util.*
|
||||
|
||||
@ -49,7 +50,8 @@ class SwitchSettingViewHolder(
|
||||
binding.settingSwitch.isEnabled = iplExists || !setting.isChecked
|
||||
}
|
||||
|
||||
binding.settingSwitch.setOnCheckedChangeListener { _: CompoundButton?, isChecked: Boolean ->
|
||||
binding.settingSwitch.setOnCheckedChangeListener(
|
||||
HapticListener.wrapOnCheckedChangeListener { _: CompoundButton?, isChecked: Boolean ->
|
||||
// If a user has skip IPL disabled previously and deleted their IPL file, we need to allow
|
||||
// them to skip it or else their game will appear broken. However, once this is enabled, we
|
||||
// need to disable the option again to prevent the same issue from occurring.
|
||||
@ -60,7 +62,7 @@ class SwitchSettingViewHolder(
|
||||
adapter.onBooleanClick(setting, binding.settingSwitch.isChecked)
|
||||
|
||||
setStyle(binding.textSettingName, setting)
|
||||
}
|
||||
})
|
||||
setStyle(binding.textSettingName, setting)
|
||||
}
|
||||
|
||||
|
@ -15,6 +15,7 @@ import org.dolphinemu.dolphinemu.databinding.FragmentGridOptionsBinding
|
||||
import org.dolphinemu.dolphinemu.databinding.FragmentGridOptionsTvBinding
|
||||
import org.dolphinemu.dolphinemu.features.settings.model.NativeConfig
|
||||
import org.dolphinemu.dolphinemu.ui.main.MainView
|
||||
import org.dolphinemu.dolphinemu.utils.HapticListener
|
||||
|
||||
class GridOptionDialogFragment : BottomSheetDialogFragment() {
|
||||
|
||||
@ -69,13 +70,14 @@ class GridOptionDialogFragment : BottomSheetDialogFragment() {
|
||||
mBindingMobile.rootDownloadCovers.setOnClickListener {
|
||||
mBindingMobile.switchDownloadCovers.isChecked = !mBindingMobile.switchDownloadCovers.isChecked
|
||||
}
|
||||
mBindingMobile.switchDownloadCovers.setOnCheckedChangeListener { _: CompoundButton, _: Boolean ->
|
||||
mBindingMobile.switchDownloadCovers.setOnCheckedChangeListener(
|
||||
HapticListener.wrapOnCheckedChangeListener { _: CompoundButton, _: Boolean ->
|
||||
BooleanSetting.MAIN_USE_GAME_COVERS.setBoolean(
|
||||
NativeConfig.LAYER_BASE,
|
||||
mBindingMobile.switchDownloadCovers.isChecked
|
||||
)
|
||||
(mView as Activity).recreate()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private fun setUpTitleButtons() {
|
||||
@ -83,13 +85,14 @@ class GridOptionDialogFragment : BottomSheetDialogFragment() {
|
||||
mBindingMobile.rootShowTitles.setOnClickListener {
|
||||
mBindingMobile.switchShowTitles.isChecked = !mBindingMobile.switchShowTitles.isChecked
|
||||
}
|
||||
mBindingMobile.switchShowTitles.setOnCheckedChangeListener { _: CompoundButton, _: Boolean ->
|
||||
mBindingMobile.switchShowTitles.setOnCheckedChangeListener(
|
||||
HapticListener.wrapOnCheckedChangeListener { _: CompoundButton, _: Boolean ->
|
||||
BooleanSetting.MAIN_SHOW_GAME_TITLES.setBoolean(
|
||||
NativeConfig.LAYER_BASE,
|
||||
mBindingMobile.switchShowTitles.isChecked
|
||||
)
|
||||
mView.reloadGrid()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// TODO: Remove this when leanback is removed
|
||||
|
@ -0,0 +1,82 @@
|
||||
package org.dolphinemu.dolphinemu.utils
|
||||
|
||||
import android.view.View
|
||||
import android.widget.CompoundButton
|
||||
import androidx.core.view.HapticFeedbackConstantsCompat
|
||||
import androidx.core.view.ViewCompat
|
||||
import com.google.android.material.slider.Slider
|
||||
|
||||
/**
|
||||
* Wrapper object that enhances listeners with haptic feedback.
|
||||
*/
|
||||
object HapticListener {
|
||||
|
||||
/**
|
||||
* Wraps a [View.OnClickListener] with haptic feedback.
|
||||
*
|
||||
* @param listener The [View.OnClickListener] to be wrapped. Can be null.
|
||||
* @param feedbackConstant The haptic feedback constant to be used for haptic feedback.
|
||||
* Defaults to [HapticFeedbackConstantsCompat.CONTEXT_CLICK] if not specified.
|
||||
* @return A new listener which wraps [listener] with haptic feedback.
|
||||
*/
|
||||
fun wrapOnClickListener(
|
||||
listener: View.OnClickListener?,
|
||||
feedbackConstant: Int = HapticFeedbackConstantsCompat.CONTEXT_CLICK
|
||||
): View.OnClickListener {
|
||||
return View.OnClickListener { view: View ->
|
||||
listener?.onClick(view)
|
||||
ViewCompat.performHapticFeedback(view, feedbackConstant)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Wraps a [Slider.OnChangeListener] with haptic feedback.
|
||||
* Feedback is provided at intervals of 5% to prevent excessive vibrations.
|
||||
*
|
||||
* @param listener The [Slider.OnChangeListener] to be wrapped. Can be null.
|
||||
* @param initialValue The value used to initialize the slider.
|
||||
* @return A new listener which wraps [listener] with haptic feedback.
|
||||
*/
|
||||
fun wrapOnChangeListener(listener: Slider.OnChangeListener?, initialValue: Float): Slider.OnChangeListener {
|
||||
var previousValue = initialValue
|
||||
return Slider.OnChangeListener { slider: Slider, value: Float, fromUser: Boolean ->
|
||||
listener?.onValueChange(slider, value, fromUser)
|
||||
if (fromUser) {
|
||||
val interval = (slider.valueTo - slider.valueFrom) * 0.05f
|
||||
val previousInterval = kotlin.math.round(previousValue / interval) * interval
|
||||
val valueChange = kotlin.math.abs(value - previousInterval)
|
||||
val feedbackConstant = if (value == slider.valueFrom || value == slider.valueTo) {
|
||||
HapticFeedbackConstantsCompat.CONTEXT_CLICK
|
||||
} else if (value == previousValue || valueChange >= interval) {
|
||||
HapticFeedbackConstantsCompat.CLOCK_TICK
|
||||
} else {
|
||||
HapticFeedbackConstantsCompat.NO_HAPTICS
|
||||
}
|
||||
if (feedbackConstant != HapticFeedbackConstantsCompat.NO_HAPTICS) {
|
||||
ViewCompat.performHapticFeedback(slider, feedbackConstant)
|
||||
previousValue = value
|
||||
}
|
||||
} else {
|
||||
previousValue = value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Wraps a [CompoundButton.OnCheckedChangeListener] with haptic feedback.
|
||||
*
|
||||
* @param listener The [CompoundButton.OnCheckedChangeListener] to be wrapped. Can be null.
|
||||
* @return A new listener which wraps [listener] with haptic feedback.
|
||||
*/
|
||||
fun wrapOnCheckedChangeListener(listener: CompoundButton.OnCheckedChangeListener?): CompoundButton.OnCheckedChangeListener {
|
||||
return CompoundButton.OnCheckedChangeListener { buttonView: CompoundButton, isChecked: Boolean ->
|
||||
listener?.onCheckedChanged(buttonView, isChecked)
|
||||
/** Using old constants because [HapticFeedbackConstantsCompat.TOGGLE_ON]
|
||||
* and [HapticFeedbackConstantsCompat.TOGGLE_OFF] don't seem to work. */
|
||||
val feedbackConstant = if (buttonView.isChecked) {
|
||||
HapticFeedbackConstantsCompat.CONTEXT_CLICK
|
||||
} else HapticFeedbackConstantsCompat.CLOCK_TICK
|
||||
ViewCompat.performHapticFeedback(buttonView, feedbackConstant)
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user