Haptic feedback support for overlay controls & controller rumble
This commit is contained in:
parent
8a50676b83
commit
177c6343e3
|
@ -29,6 +29,7 @@ import com.google.android.material.slider.Slider
|
||||||
import org.dolphinemu.dolphinemu.NativeLibrary
|
import org.dolphinemu.dolphinemu.NativeLibrary
|
||||||
import org.dolphinemu.dolphinemu.R
|
import org.dolphinemu.dolphinemu.R
|
||||||
import org.dolphinemu.dolphinemu.databinding.ActivityEmulationBinding
|
import org.dolphinemu.dolphinemu.databinding.ActivityEmulationBinding
|
||||||
|
import org.dolphinemu.dolphinemu.databinding.DialogHapticsAdjustBinding
|
||||||
import org.dolphinemu.dolphinemu.databinding.DialogInputAdjustBinding
|
import org.dolphinemu.dolphinemu.databinding.DialogInputAdjustBinding
|
||||||
import org.dolphinemu.dolphinemu.databinding.DialogNfcFiguresManagerBinding
|
import org.dolphinemu.dolphinemu.databinding.DialogNfcFiguresManagerBinding
|
||||||
import org.dolphinemu.dolphinemu.features.infinitybase.InfinityConfig
|
import org.dolphinemu.dolphinemu.features.infinitybase.InfinityConfig
|
||||||
|
@ -37,7 +38,9 @@ import org.dolphinemu.dolphinemu.features.infinitybase.ui.FigureSlot
|
||||||
import org.dolphinemu.dolphinemu.features.infinitybase.ui.FigureSlotAdapter
|
import org.dolphinemu.dolphinemu.features.infinitybase.ui.FigureSlotAdapter
|
||||||
import org.dolphinemu.dolphinemu.features.input.model.ControllerInterface
|
import org.dolphinemu.dolphinemu.features.input.model.ControllerInterface
|
||||||
import org.dolphinemu.dolphinemu.features.input.model.DolphinSensorEventListener
|
import org.dolphinemu.dolphinemu.features.input.model.DolphinSensorEventListener
|
||||||
|
import org.dolphinemu.dolphinemu.features.input.model.DolphinVibratorManagerFactory
|
||||||
import org.dolphinemu.dolphinemu.features.settings.model.BooleanSetting
|
import org.dolphinemu.dolphinemu.features.settings.model.BooleanSetting
|
||||||
|
import org.dolphinemu.dolphinemu.features.settings.model.FloatSetting
|
||||||
import org.dolphinemu.dolphinemu.features.settings.model.IntSetting
|
import org.dolphinemu.dolphinemu.features.settings.model.IntSetting
|
||||||
import org.dolphinemu.dolphinemu.features.settings.model.Settings
|
import org.dolphinemu.dolphinemu.features.settings.model.Settings
|
||||||
import org.dolphinemu.dolphinemu.features.settings.model.StringSetting
|
import org.dolphinemu.dolphinemu.features.settings.model.StringSetting
|
||||||
|
@ -58,6 +61,8 @@ import org.dolphinemu.dolphinemu.ui.main.ThemeProvider
|
||||||
import org.dolphinemu.dolphinemu.utils.AfterDirectoryInitializationRunner
|
import org.dolphinemu.dolphinemu.utils.AfterDirectoryInitializationRunner
|
||||||
import org.dolphinemu.dolphinemu.utils.DirectoryInitialization
|
import org.dolphinemu.dolphinemu.utils.DirectoryInitialization
|
||||||
import org.dolphinemu.dolphinemu.utils.FileBrowserHelper
|
import org.dolphinemu.dolphinemu.utils.FileBrowserHelper
|
||||||
|
import org.dolphinemu.dolphinemu.utils.HapticEffect
|
||||||
|
import org.dolphinemu.dolphinemu.utils.HapticsProvider
|
||||||
import org.dolphinemu.dolphinemu.utils.ThemeHelper
|
import org.dolphinemu.dolphinemu.utils.ThemeHelper
|
||||||
import kotlin.math.roundToInt
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
|
@ -412,6 +417,12 @@ class EmulationActivity : AppCompatActivity(), ThemeProvider {
|
||||||
menu.findItem(R.id.menu_emulation_ir_recenter).isChecked =
|
menu.findItem(R.id.menu_emulation_ir_recenter).isChecked =
|
||||||
BooleanSetting.MAIN_IR_ALWAYS_RECENTER.boolean
|
BooleanSetting.MAIN_IR_ALWAYS_RECENTER.boolean
|
||||||
}
|
}
|
||||||
|
// Hide the haptic feedback menu item if the device has no vibrator
|
||||||
|
if (!DolphinVibratorManagerFactory.getSystemVibratorManager().getDefaultVibrator()
|
||||||
|
.hasVibrator()
|
||||||
|
) {
|
||||||
|
menu.findItem(R.id.menu_emulation_haptics).setVisible(false)
|
||||||
|
}
|
||||||
popup.setOnMenuItemClickListener { item: MenuItem -> onOptionsItemSelected(item) }
|
popup.setOnMenuItemClickListener { item: MenuItem -> onOptionsItemSelected(item) }
|
||||||
popup.show()
|
popup.show()
|
||||||
}
|
}
|
||||||
|
@ -492,6 +503,7 @@ class EmulationActivity : AppCompatActivity(), ThemeProvider {
|
||||||
MENU_ACTION_SKYLANDERS -> showSkylanderPortalSettings()
|
MENU_ACTION_SKYLANDERS -> showSkylanderPortalSettings()
|
||||||
MENU_ACTION_INFINITY_BASE -> showInfinityBaseSettings()
|
MENU_ACTION_INFINITY_BASE -> showInfinityBaseSettings()
|
||||||
MENU_ACTION_EXIT -> emulationFragment!!.stopEmulation()
|
MENU_ACTION_EXIT -> emulationFragment!!.stopEmulation()
|
||||||
|
MENU_ACTION_ADJUST_HAPTICS -> adjustHaptics()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -667,6 +679,62 @@ class EmulationActivity : AppCompatActivity(), ThemeProvider {
|
||||||
.show()
|
.show()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun adjustHaptics() {
|
||||||
|
val dialogBinding = DialogHapticsAdjustBinding.inflate(layoutInflater)
|
||||||
|
val hapticsProvider = HapticsProvider()
|
||||||
|
dialogBinding.apply {
|
||||||
|
val toggleIntensity = { isChecked: Boolean ->
|
||||||
|
hapticsIntensityName.isEnabled = isChecked
|
||||||
|
hapticsIntensitySlider.isEnabled = isChecked
|
||||||
|
hapticsIntensityValue.isEnabled = isChecked
|
||||||
|
}
|
||||||
|
val checkboxes =
|
||||||
|
listOf(hapticsPressCheckbox, hapticsReleaseCheckbox, hapticsJoystickCheckbox)
|
||||||
|
hapticsPressCheckbox.isChecked = BooleanSetting.MAIN_OVERLAY_HAPTICS_PRESS.boolean
|
||||||
|
hapticsReleaseCheckbox.isChecked = BooleanSetting.MAIN_OVERLAY_HAPTICS_RELEASE.boolean
|
||||||
|
hapticsJoystickCheckbox.isChecked = BooleanSetting.MAIN_OVERLAY_HAPTICS_JOYSTICK.boolean
|
||||||
|
if (checkboxes.none { it.isChecked }) {
|
||||||
|
toggleIntensity(false)
|
||||||
|
}
|
||||||
|
checkboxes.forEach { checkbox ->
|
||||||
|
checkbox.setOnCheckedChangeListener { _, _ ->
|
||||||
|
toggleIntensity(checkboxes.any { it.isChecked })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
hapticsIntensitySlider.apply {
|
||||||
|
val setValueText = { value: Float ->
|
||||||
|
hapticsIntensityValue.text =
|
||||||
|
getString(R.string.slider_setting_value, value * 100f, '%')
|
||||||
|
}
|
||||||
|
stepSize = 0.1f
|
||||||
|
valueFrom = 0.1f
|
||||||
|
valueTo = 1.0f
|
||||||
|
value = FloatSetting.MAIN_OVERLAY_HAPTICS_SCALE.float.also { setValueText(it) }
|
||||||
|
addOnChangeListener { _: Slider, value: Float, _: Boolean ->
|
||||||
|
setValueText(value)
|
||||||
|
hapticsProvider.provideFeedback(HapticEffect.LOW_TICK, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
MaterialAlertDialogBuilder(this)
|
||||||
|
.setView(dialogBinding.root)
|
||||||
|
.setPositiveButton(R.string.ok) { _: DialogInterface?, _: Int ->
|
||||||
|
BooleanSetting.MAIN_OVERLAY_HAPTICS_PRESS.setBoolean(
|
||||||
|
settings, dialogBinding.hapticsPressCheckbox.isChecked
|
||||||
|
)
|
||||||
|
BooleanSetting.MAIN_OVERLAY_HAPTICS_RELEASE.setBoolean(
|
||||||
|
settings, dialogBinding.hapticsReleaseCheckbox.isChecked
|
||||||
|
)
|
||||||
|
BooleanSetting.MAIN_OVERLAY_HAPTICS_JOYSTICK.setBoolean(
|
||||||
|
settings, dialogBinding.hapticsJoystickCheckbox.isChecked
|
||||||
|
)
|
||||||
|
FloatSetting.MAIN_OVERLAY_HAPTICS_SCALE.setFloat(
|
||||||
|
settings, dialogBinding.hapticsIntensitySlider.value
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
|
||||||
private fun chooseDoubleTapButton() {
|
private fun chooseDoubleTapButton() {
|
||||||
val currentValue = IntSetting.MAIN_DOUBLE_TAP_BUTTON.int
|
val currentValue = IntSetting.MAIN_DOUBLE_TAP_BUTTON.int
|
||||||
|
|
||||||
|
@ -1059,6 +1127,7 @@ class EmulationActivity : AppCompatActivity(), ThemeProvider {
|
||||||
const val MENU_ACTION_SKYLANDERS = 36
|
const val MENU_ACTION_SKYLANDERS = 36
|
||||||
const val MENU_ACTION_INFINITY_BASE = 37
|
const val MENU_ACTION_INFINITY_BASE = 37
|
||||||
const val MENU_ACTION_LATCHING_CONTROLS = 38
|
const val MENU_ACTION_LATCHING_CONTROLS = 38
|
||||||
|
const val MENU_ACTION_ADJUST_HAPTICS = 39
|
||||||
|
|
||||||
init {
|
init {
|
||||||
buttonsActionsMap.apply {
|
buttonsActionsMap.apply {
|
||||||
|
@ -1072,6 +1141,7 @@ class EmulationActivity : AppCompatActivity(), ThemeProvider {
|
||||||
append(R.id.menu_emulation_ir_recenter, MENU_SET_IR_RECENTER)
|
append(R.id.menu_emulation_ir_recenter, MENU_SET_IR_RECENTER)
|
||||||
append(R.id.menu_emulation_set_ir_mode, MENU_SET_IR_MODE)
|
append(R.id.menu_emulation_set_ir_mode, MENU_SET_IR_MODE)
|
||||||
append(R.id.menu_emulation_choose_doubletap, MENU_ACTION_CHOOSE_DOUBLETAP)
|
append(R.id.menu_emulation_choose_doubletap, MENU_ACTION_CHOOSE_DOUBLETAP)
|
||||||
|
append(R.id.menu_emulation_haptics, MENU_ACTION_ADJUST_HAPTICS)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -4,16 +4,15 @@ package org.dolphinemu.dolphinemu.features.input.model
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.hardware.input.InputManager
|
import android.hardware.input.InputManager
|
||||||
import android.os.Build
|
|
||||||
import android.os.Handler
|
import android.os.Handler
|
||||||
import android.os.VibrationEffect
|
|
||||||
import android.os.Vibrator
|
import android.os.Vibrator
|
||||||
import android.os.VibratorManager
|
|
||||||
import android.view.InputDevice
|
import android.view.InputDevice
|
||||||
import android.view.KeyEvent
|
import android.view.KeyEvent
|
||||||
import android.view.MotionEvent
|
import android.view.MotionEvent
|
||||||
import androidx.annotation.Keep
|
import androidx.annotation.Keep
|
||||||
import org.dolphinemu.dolphinemu.DolphinApplication
|
import org.dolphinemu.dolphinemu.DolphinApplication
|
||||||
|
import org.dolphinemu.dolphinemu.utils.HapticEffect
|
||||||
|
import org.dolphinemu.dolphinemu.utils.HapticsProvider
|
||||||
import org.dolphinemu.dolphinemu.utils.LooperThread
|
import org.dolphinemu.dolphinemu.utils.LooperThread
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -105,36 +104,19 @@ object ControllerInterface {
|
||||||
|
|
||||||
@Keep
|
@Keep
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
private fun getVibratorManager(device: InputDevice): DolphinVibratorManager {
|
private fun getDeviceVibratorManager(device: InputDevice): DolphinVibratorManager =
|
||||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
DolphinVibratorManagerFactory.getDeviceVibratorManager(device)
|
||||||
DolphinVibratorManagerPassthrough(device.vibratorManager)
|
|
||||||
} else {
|
|
||||||
DolphinVibratorManagerCompat(device.vibrator)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Keep
|
@Keep
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
private fun getSystemVibratorManager(): DolphinVibratorManager {
|
private fun getSystemVibratorManager(): DolphinVibratorManager =
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
DolphinVibratorManagerFactory.getSystemVibratorManager()
|
||||||
val vibratorManager = DolphinApplication.getAppContext()
|
|
||||||
.getSystemService(Context.VIBRATOR_MANAGER_SERVICE) as VibratorManager?
|
|
||||||
if (vibratorManager != null)
|
|
||||||
return DolphinVibratorManagerPassthrough(vibratorManager)
|
|
||||||
}
|
|
||||||
val vibrator = DolphinApplication.getAppContext()
|
|
||||||
.getSystemService(Context.VIBRATOR_SERVICE) as Vibrator
|
|
||||||
return DolphinVibratorManagerCompat(vibrator)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Keep
|
@Keep
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
private fun vibrate(vibrator: Vibrator) {
|
private fun vibrate(vibrator: Vibrator) {
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
// TODO: Add a slider to the Rumble options that allows adjusting the vibration intensity.
|
||||||
vibrator.vibrate(VibrationEffect.createOneShot(100, VibrationEffect.DEFAULT_AMPLITUDE))
|
HapticsProvider(vibrator).provideFeedback(HapticEffect.SPIN, 0.5f)
|
||||||
} else {
|
|
||||||
vibrator.vibrate(100)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private class InputDeviceListener : InputManager.InputDeviceListener {
|
private class InputDeviceListener : InputManager.InputDeviceListener {
|
||||||
|
|
|
@ -13,4 +13,6 @@ interface DolphinVibratorManager {
|
||||||
fun getVibrator(vibratorId: Int): Vibrator
|
fun getVibrator(vibratorId: Int): Vibrator
|
||||||
|
|
||||||
fun getVibratorIds(): IntArray
|
fun getVibratorIds(): IntArray
|
||||||
|
|
||||||
|
fun getDefaultVibrator(): Vibrator
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,4 +21,6 @@ class DolphinVibratorManagerCompat(vibrator: Vibrator) : DolphinVibratorManager
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getVibratorIds(): IntArray = vibratorIds
|
override fun getVibratorIds(): IntArray = vibratorIds
|
||||||
|
|
||||||
|
override fun getDefaultVibrator(): Vibrator = vibrator
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,33 @@
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
package org.dolphinemu.dolphinemu.features.input.model
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.os.Build
|
||||||
|
import android.os.Vibrator
|
||||||
|
import android.os.VibratorManager
|
||||||
|
import android.view.InputDevice
|
||||||
|
import org.dolphinemu.dolphinemu.DolphinApplication
|
||||||
|
|
||||||
|
object DolphinVibratorManagerFactory {
|
||||||
|
fun getDeviceVibratorManager(device: InputDevice): DolphinVibratorManager {
|
||||||
|
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||||
|
DolphinVibratorManagerPassthrough(device.vibratorManager)
|
||||||
|
} else {
|
||||||
|
DolphinVibratorManagerCompat(device.vibrator)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getSystemVibratorManager(): DolphinVibratorManager {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||||
|
val vibratorManager = DolphinApplication.getAppContext()
|
||||||
|
.getSystemService(Context.VIBRATOR_MANAGER_SERVICE) as VibratorManager?
|
||||||
|
if (vibratorManager != null) {
|
||||||
|
return DolphinVibratorManagerPassthrough(vibratorManager)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val vibrator = DolphinApplication.getAppContext()
|
||||||
|
.getSystemService(Context.VIBRATOR_SERVICE) as Vibrator
|
||||||
|
return DolphinVibratorManagerCompat(vibrator)
|
||||||
|
}
|
||||||
|
}
|
|
@ -13,4 +13,6 @@ class DolphinVibratorManagerPassthrough(private val vibratorManager: VibratorMan
|
||||||
override fun getVibrator(vibratorId: Int): Vibrator = vibratorManager.getVibrator(vibratorId)
|
override fun getVibrator(vibratorId: Int): Vibrator = vibratorManager.getVibrator(vibratorId)
|
||||||
|
|
||||||
override fun getVibratorIds(): IntArray = vibratorManager.vibratorIds
|
override fun getVibratorIds(): IntArray = vibratorManager.vibratorIds
|
||||||
|
|
||||||
|
override fun getDefaultVibrator(): Vibrator = vibratorManager.defaultVibrator
|
||||||
}
|
}
|
||||||
|
|
|
@ -646,6 +646,24 @@ enum class BooleanSetting(
|
||||||
"ButtonLatchingNunchukZ",
|
"ButtonLatchingNunchukZ",
|
||||||
false
|
false
|
||||||
),
|
),
|
||||||
|
MAIN_OVERLAY_HAPTICS_PRESS(
|
||||||
|
Settings.FILE_DOLPHIN,
|
||||||
|
Settings.SECTION_INI_ANDROID,
|
||||||
|
"OverlayHapticsPress",
|
||||||
|
false
|
||||||
|
),
|
||||||
|
MAIN_OVERLAY_HAPTICS_RELEASE(
|
||||||
|
Settings.FILE_DOLPHIN,
|
||||||
|
Settings.SECTION_INI_ANDROID,
|
||||||
|
"OverlayHapticsRelease",
|
||||||
|
false
|
||||||
|
),
|
||||||
|
MAIN_OVERLAY_HAPTICS_JOYSTICK(
|
||||||
|
Settings.FILE_DOLPHIN,
|
||||||
|
Settings.SECTION_INI_ANDROID,
|
||||||
|
"OverlayHapticsJoystick",
|
||||||
|
false
|
||||||
|
),
|
||||||
SYSCONF_SCREENSAVER(Settings.FILE_SYSCONF, "IPL", "SSV", false),
|
SYSCONF_SCREENSAVER(Settings.FILE_SYSCONF, "IPL", "SSV", false),
|
||||||
SYSCONF_WIDESCREEN(Settings.FILE_SYSCONF, "IPL", "AR", true),
|
SYSCONF_WIDESCREEN(Settings.FILE_SYSCONF, "IPL", "AR", true),
|
||||||
SYSCONF_PROGRESSIVE_SCAN(Settings.FILE_SYSCONF, "IPL", "PGS", true),
|
SYSCONF_PROGRESSIVE_SCAN(Settings.FILE_SYSCONF, "IPL", "PGS", true),
|
||||||
|
|
|
@ -8,6 +8,12 @@ enum class FloatSetting(
|
||||||
private val key: String,
|
private val key: String,
|
||||||
private val defaultValue: Float
|
private val defaultValue: Float
|
||||||
) : AbstractFloatSetting {
|
) : AbstractFloatSetting {
|
||||||
|
MAIN_OVERLAY_HAPTICS_SCALE(
|
||||||
|
Settings.FILE_DOLPHIN,
|
||||||
|
Settings.SECTION_INI_ANDROID,
|
||||||
|
"OverlayHapticsScale",
|
||||||
|
0.5f
|
||||||
|
),
|
||||||
// These entries have the same names and order as in C++, just for consistency.
|
// These entries have the same names and order as in C++, just for consistency.
|
||||||
MAIN_EMULATION_SPEED(Settings.FILE_DOLPHIN, Settings.SECTION_INI_CORE, "EmulationSpeed", 1.0f),
|
MAIN_EMULATION_SPEED(Settings.FILE_DOLPHIN, Settings.SECTION_INI_CORE, "EmulationSpeed", 1.0f),
|
||||||
MAIN_OVERCLOCK(Settings.FILE_DOLPHIN, Settings.SECTION_INI_CORE, "Overclock", 1.0f),
|
MAIN_OVERCLOCK(Settings.FILE_DOLPHIN, Settings.SECTION_INI_CORE, "Overclock", 1.0f),
|
||||||
|
|
|
@ -27,9 +27,12 @@ import org.dolphinemu.dolphinemu.features.input.model.InputOverrider
|
||||||
import org.dolphinemu.dolphinemu.features.input.model.InputOverrider.ControlId
|
import org.dolphinemu.dolphinemu.features.input.model.InputOverrider.ControlId
|
||||||
import org.dolphinemu.dolphinemu.features.input.model.controlleremu.EmulatedController
|
import org.dolphinemu.dolphinemu.features.input.model.controlleremu.EmulatedController
|
||||||
import org.dolphinemu.dolphinemu.features.settings.model.BooleanSetting
|
import org.dolphinemu.dolphinemu.features.settings.model.BooleanSetting
|
||||||
|
import org.dolphinemu.dolphinemu.features.settings.model.FloatSetting
|
||||||
import org.dolphinemu.dolphinemu.features.settings.model.IntSetting
|
import org.dolphinemu.dolphinemu.features.settings.model.IntSetting
|
||||||
import org.dolphinemu.dolphinemu.features.settings.model.IntSetting.Companion.getSettingForSIDevice
|
import org.dolphinemu.dolphinemu.features.settings.model.IntSetting.Companion.getSettingForSIDevice
|
||||||
import org.dolphinemu.dolphinemu.features.settings.model.IntSetting.Companion.getSettingForWiimoteSource
|
import org.dolphinemu.dolphinemu.features.settings.model.IntSetting.Companion.getSettingForWiimoteSource
|
||||||
|
import org.dolphinemu.dolphinemu.utils.HapticEffect
|
||||||
|
import org.dolphinemu.dolphinemu.utils.HapticsProvider
|
||||||
import java.util.Arrays
|
import java.util.Arrays
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -41,6 +44,7 @@ import java.util.Arrays
|
||||||
*/
|
*/
|
||||||
class InputOverlay(context: Context?, attrs: AttributeSet?) : SurfaceView(context, attrs),
|
class InputOverlay(context: Context?, attrs: AttributeSet?) : SurfaceView(context, attrs),
|
||||||
OnTouchListener {
|
OnTouchListener {
|
||||||
|
private val hapticsProvider: HapticsProvider = HapticsProvider()
|
||||||
private val overlayButtons: MutableSet<InputOverlayDrawableButton> = HashSet()
|
private val overlayButtons: MutableSet<InputOverlayDrawableButton> = HashSet()
|
||||||
private val overlayDpads: MutableSet<InputOverlayDrawableDpad> = HashSet()
|
private val overlayDpads: MutableSet<InputOverlayDrawableDpad> = HashSet()
|
||||||
private val overlayJoysticks: MutableSet<InputOverlayDrawableJoystick> = HashSet()
|
private val overlayJoysticks: MutableSet<InputOverlayDrawableJoystick> = HashSet()
|
||||||
|
@ -51,6 +55,7 @@ class InputOverlay(context: Context?, attrs: AttributeSet?) : SurfaceView(contex
|
||||||
private var isFirstRun = true
|
private var isFirstRun = true
|
||||||
private val gcPadRegistered = BooleanArray(4)
|
private val gcPadRegistered = BooleanArray(4)
|
||||||
private val wiimoteRegistered = BooleanArray(4)
|
private val wiimoteRegistered = BooleanArray(4)
|
||||||
|
private val dpadPreviouslyPressed = BooleanArray(4)
|
||||||
var editMode = false
|
var editMode = false
|
||||||
private var controllerType = -1
|
private var controllerType = -1
|
||||||
private var controllerIndex = 0
|
private var controllerIndex = 0
|
||||||
|
@ -140,6 +145,9 @@ class InputOverlay(context: Context?, attrs: AttributeSet?) : SurfaceView(contex
|
||||||
val firstPointer = action != MotionEvent.ACTION_POINTER_DOWN &&
|
val firstPointer = action != MotionEvent.ACTION_POINTER_DOWN &&
|
||||||
action != MotionEvent.ACTION_POINTER_UP
|
action != MotionEvent.ACTION_POINTER_UP
|
||||||
val pointerIndex = if (firstPointer) 0 else event.actionIndex
|
val pointerIndex = if (firstPointer) 0 else event.actionIndex
|
||||||
|
val hapticsScale = FloatSetting.MAIN_OVERLAY_HAPTICS_SCALE.float
|
||||||
|
val pressFeedback = BooleanSetting.MAIN_OVERLAY_HAPTICS_PRESS.boolean
|
||||||
|
val releaseFeedback = BooleanSetting.MAIN_OVERLAY_HAPTICS_RELEASE.boolean
|
||||||
// Tracks if any button/joystick is pressed down
|
// Tracks if any button/joystick is pressed down
|
||||||
var pressed = false
|
var pressed = false
|
||||||
|
|
||||||
|
@ -154,7 +162,17 @@ class InputOverlay(context: Context?, attrs: AttributeSet?) : SurfaceView(contex
|
||||||
event.getY(pointerIndex).toInt()
|
event.getY(pointerIndex).toInt()
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
button.setPressedState(if (button.latching) !button.getPressedState() else true)
|
if (button.latching && button.getPressedState()) {
|
||||||
|
button.setPressedState(false)
|
||||||
|
if (releaseFeedback) hapticsProvider.provideFeedback(
|
||||||
|
HapticEffect.QUICK_RISE, hapticsScale
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
button.setPressedState(true)
|
||||||
|
if (pressFeedback) hapticsProvider.provideFeedback(
|
||||||
|
HapticEffect.QUICK_FALL, hapticsScale
|
||||||
|
)
|
||||||
|
}
|
||||||
button.trackId = event.getPointerId(pointerIndex)
|
button.trackId = event.getPointerId(pointerIndex)
|
||||||
pressed = true
|
pressed = true
|
||||||
InputOverrider.setControlState(controllerIndex, button.control, if (button.getPressedState()) 1.0 else 0.0)
|
InputOverrider.setControlState(controllerIndex, button.control, if (button.getPressedState()) 1.0 else 0.0)
|
||||||
|
@ -173,8 +191,12 @@ class InputOverlay(context: Context?, attrs: AttributeSet?) : SurfaceView(contex
|
||||||
MotionEvent.ACTION_POINTER_UP -> {
|
MotionEvent.ACTION_POINTER_UP -> {
|
||||||
// If a pointer ends, release the button it was pressing.
|
// If a pointer ends, release the button it was pressing.
|
||||||
if (button.trackId == event.getPointerId(pointerIndex)) {
|
if (button.trackId == event.getPointerId(pointerIndex)) {
|
||||||
if (!button.latching)
|
if (!button.latching) {
|
||||||
button.setPressedState(false)
|
button.setPressedState(false)
|
||||||
|
if (releaseFeedback) hapticsProvider.provideFeedback(
|
||||||
|
HapticEffect.QUICK_RISE, hapticsScale
|
||||||
|
)
|
||||||
|
}
|
||||||
InputOverrider.setControlState(controllerIndex, button.control, if (button.getPressedState()) 1.0 else 0.0)
|
InputOverrider.setControlState(controllerIndex, button.control, if (button.getPressedState()) 1.0 else 0.0)
|
||||||
|
|
||||||
val analogControl = getAnalogControlForTrigger(button.control)
|
val analogControl = getAnalogControlForTrigger(button.control)
|
||||||
|
@ -205,6 +227,9 @@ class InputOverlay(context: Context?, attrs: AttributeSet?) : SurfaceView(contex
|
||||||
) {
|
) {
|
||||||
dpad.trackId = event.getPointerId(pointerIndex)
|
dpad.trackId = event.getPointerId(pointerIndex)
|
||||||
pressed = true
|
pressed = true
|
||||||
|
if (pressFeedback) hapticsProvider.provideFeedback(
|
||||||
|
HapticEffect.QUICK_FALL, hapticsScale
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -227,18 +252,29 @@ class InputOverlay(context: Context?, attrs: AttributeSet?) : SurfaceView(contex
|
||||||
// Release the buttons first, then press
|
// Release the buttons first, then press
|
||||||
for (i in dpadPressed.indices) {
|
for (i in dpadPressed.indices) {
|
||||||
if (!dpadPressed[i]) {
|
if (!dpadPressed[i]) {
|
||||||
|
if (dpadPreviouslyPressed[i] && releaseFeedback) {
|
||||||
|
hapticsProvider.provideFeedback(
|
||||||
|
HapticEffect.QUICK_RISE, hapticsScale
|
||||||
|
)
|
||||||
|
}
|
||||||
InputOverrider.setControlState(
|
InputOverrider.setControlState(
|
||||||
controllerIndex,
|
controllerIndex,
|
||||||
dpad.getControl(i),
|
dpad.getControl(i),
|
||||||
0.0
|
0.0
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
|
if (!dpadPreviouslyPressed[i] && pressFeedback) {
|
||||||
|
hapticsProvider.provideFeedback(
|
||||||
|
HapticEffect.QUICK_FALL, hapticsScale
|
||||||
|
)
|
||||||
|
}
|
||||||
InputOverrider.setControlState(
|
InputOverrider.setControlState(
|
||||||
controllerIndex,
|
controllerIndex,
|
||||||
dpad.getControl(i),
|
dpad.getControl(i),
|
||||||
1.0
|
1.0
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
dpadPreviouslyPressed[i] = dpadPressed[i]
|
||||||
}
|
}
|
||||||
setDpadState(
|
setDpadState(
|
||||||
dpad,
|
dpad,
|
||||||
|
@ -256,6 +292,12 @@ class InputOverlay(context: Context?, attrs: AttributeSet?) : SurfaceView(contex
|
||||||
if (dpad.trackId == event.getPointerId(pointerIndex)) {
|
if (dpad.trackId == event.getPointerId(pointerIndex)) {
|
||||||
for (i in 0 until 4) {
|
for (i in 0 until 4) {
|
||||||
dpad.setState(InputOverlayDrawableDpad.STATE_DEFAULT)
|
dpad.setState(InputOverlayDrawableDpad.STATE_DEFAULT)
|
||||||
|
if (dpadPreviouslyPressed[i]) {
|
||||||
|
dpadPreviouslyPressed[i] = false
|
||||||
|
if (releaseFeedback) hapticsProvider.provideFeedback(
|
||||||
|
HapticEffect.QUICK_RISE, hapticsScale
|
||||||
|
)
|
||||||
|
}
|
||||||
InputOverrider.setControlState(
|
InputOverrider.setControlState(
|
||||||
controllerIndex,
|
controllerIndex,
|
||||||
dpad.getControl(i),
|
dpad.getControl(i),
|
||||||
|
@ -1349,7 +1391,8 @@ class InputOverlay(context: Context?, attrs: AttributeSet?) : SurfaceView(contex
|
||||||
legacyId,
|
legacyId,
|
||||||
xControl,
|
xControl,
|
||||||
yControl,
|
yControl,
|
||||||
controllerIndex
|
controllerIndex,
|
||||||
|
hapticsProvider
|
||||||
)
|
)
|
||||||
|
|
||||||
// Need to set the image's position
|
// Need to set the image's position
|
||||||
|
|
|
@ -10,6 +10,9 @@ import android.graphics.drawable.BitmapDrawable
|
||||||
import android.view.MotionEvent
|
import android.view.MotionEvent
|
||||||
import org.dolphinemu.dolphinemu.features.input.model.InputOverrider
|
import org.dolphinemu.dolphinemu.features.input.model.InputOverrider
|
||||||
import org.dolphinemu.dolphinemu.features.settings.model.BooleanSetting
|
import org.dolphinemu.dolphinemu.features.settings.model.BooleanSetting
|
||||||
|
import org.dolphinemu.dolphinemu.features.settings.model.FloatSetting
|
||||||
|
import org.dolphinemu.dolphinemu.utils.HapticEffect
|
||||||
|
import org.dolphinemu.dolphinemu.utils.HapticsProvider
|
||||||
import kotlin.math.atan2
|
import kotlin.math.atan2
|
||||||
import kotlin.math.cos
|
import kotlin.math.cos
|
||||||
import kotlin.math.hypot
|
import kotlin.math.hypot
|
||||||
|
@ -28,6 +31,7 @@ import kotlin.math.sin
|
||||||
* @param legacyId Legacy identifier (ButtonType) for which joystick this is.
|
* @param legacyId Legacy identifier (ButtonType) for which joystick this is.
|
||||||
* @param xControl The control which the x value of the joystick will be written to.
|
* @param xControl The control which the x value of the joystick will be written to.
|
||||||
* @param yControl The control which the y value of the joystick will be written to.
|
* @param yControl The control which the y value of the joystick will be written to.
|
||||||
|
* @param hapticsProvider An instance of [HapticsProvider] for providing haptic feedback.
|
||||||
*/
|
*/
|
||||||
class InputOverlayDrawableJoystick(
|
class InputOverlayDrawableJoystick(
|
||||||
res: Resources,
|
res: Resources,
|
||||||
|
@ -39,7 +43,8 @@ class InputOverlayDrawableJoystick(
|
||||||
val legacyId: Int,
|
val legacyId: Int,
|
||||||
val xControl: Int,
|
val xControl: Int,
|
||||||
val yControl: Int,
|
val yControl: Int,
|
||||||
private val controllerIndex: Int
|
private val controllerIndex: Int,
|
||||||
|
private val hapticsProvider: HapticsProvider
|
||||||
) {
|
) {
|
||||||
var x = 0.0f
|
var x = 0.0f
|
||||||
private set
|
private set
|
||||||
|
@ -47,6 +52,11 @@ class InputOverlayDrawableJoystick(
|
||||||
private set
|
private set
|
||||||
var trackId = -1
|
var trackId = -1
|
||||||
private set
|
private set
|
||||||
|
private var angle = 0.0
|
||||||
|
private var radius = 0.0
|
||||||
|
private var gateRadius = 0.0
|
||||||
|
private var previousRadius = 0.0
|
||||||
|
private var previousAngle = 0.0
|
||||||
private var controlPositionX = 0
|
private var controlPositionX = 0
|
||||||
private var controlPositionY = 0
|
private var controlPositionY = 0
|
||||||
private var previousTouchX = 0
|
private var previousTouchX = 0
|
||||||
|
@ -100,6 +110,7 @@ class InputOverlayDrawableJoystick(
|
||||||
val firstPointer = action != MotionEvent.ACTION_POINTER_DOWN &&
|
val firstPointer = action != MotionEvent.ACTION_POINTER_DOWN &&
|
||||||
action != MotionEvent.ACTION_POINTER_UP
|
action != MotionEvent.ACTION_POINTER_UP
|
||||||
val pointerIndex = if (firstPointer) 0 else event.actionIndex
|
val pointerIndex = if (firstPointer) 0 else event.actionIndex
|
||||||
|
val hapticsScale = FloatSetting.MAIN_OVERLAY_HAPTICS_SCALE.float
|
||||||
var pressed = false
|
var pressed = false
|
||||||
|
|
||||||
when (action) {
|
when (action) {
|
||||||
|
@ -112,6 +123,9 @@ class InputOverlayDrawableJoystick(
|
||||||
) {
|
) {
|
||||||
pressed = true
|
pressed = true
|
||||||
pressedState = true
|
pressedState = true
|
||||||
|
if (BooleanSetting.MAIN_OVERLAY_HAPTICS_PRESS.boolean) {
|
||||||
|
hapticsProvider.provideFeedback(HapticEffect.QUICK_FALL, hapticsScale)
|
||||||
|
}
|
||||||
outerBitmap.alpha = 0
|
outerBitmap.alpha = 0
|
||||||
boundsBoxBitmap.alpha = opacity
|
boundsBoxBitmap.alpha = opacity
|
||||||
if (reCenter) {
|
if (reCenter) {
|
||||||
|
@ -130,6 +144,9 @@ class InputOverlayDrawableJoystick(
|
||||||
if (trackId == event.getPointerId(pointerIndex)) {
|
if (trackId == event.getPointerId(pointerIndex)) {
|
||||||
pressed = true
|
pressed = true
|
||||||
pressedState = false
|
pressedState = false
|
||||||
|
if (BooleanSetting.MAIN_OVERLAY_HAPTICS_RELEASE.boolean) {
|
||||||
|
hapticsProvider.provideFeedback(HapticEffect.QUICK_RISE, hapticsScale)
|
||||||
|
}
|
||||||
y = 0f
|
y = 0f
|
||||||
x = y
|
x = y
|
||||||
outerBitmap.alpha = opacity
|
outerBitmap.alpha = opacity
|
||||||
|
@ -139,6 +156,8 @@ class InputOverlayDrawableJoystick(
|
||||||
bounds =
|
bounds =
|
||||||
Rect(origBounds.left, origBounds.top, origBounds.right, origBounds.bottom)
|
Rect(origBounds.left, origBounds.top, origBounds.right, origBounds.bottom)
|
||||||
setInnerBounds()
|
setInnerBounds()
|
||||||
|
previousRadius = 0.0
|
||||||
|
previousAngle = 0.0
|
||||||
trackId = -1
|
trackId = -1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -161,6 +180,20 @@ class InputOverlayDrawableJoystick(
|
||||||
y = touchY / maxY
|
y = touchY / maxY
|
||||||
|
|
||||||
setInnerBounds()
|
setInnerBounds()
|
||||||
|
if (BooleanSetting.MAIN_OVERLAY_HAPTICS_JOYSTICK.boolean) {
|
||||||
|
val radiusThreshold = gateRadius * 0.33
|
||||||
|
val angularDistance = kotlin.math.abs(previousAngle - angle)
|
||||||
|
.let { kotlin.math.min(it, Math.PI + Math.PI - it) }
|
||||||
|
if (kotlin.math.abs(previousRadius - radius) > radiusThreshold
|
||||||
|
|| (radius > radiusThreshold &&
|
||||||
|
(angularDistance >= HAPTICS_MAX_ANGLE || (radius == gateRadius &&
|
||||||
|
angularDistance * hapticsScale >= HAPTICS_MIN_ANGLE)))
|
||||||
|
) {
|
||||||
|
hapticsProvider.provideFeedback(HapticEffect.LOW_TICK, hapticsScale)
|
||||||
|
previousRadius = radius
|
||||||
|
previousAngle = angle
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return pressed
|
return pressed
|
||||||
|
@ -209,12 +242,13 @@ class InputOverlayDrawableJoystick(
|
||||||
var x = x.toDouble()
|
var x = x.toDouble()
|
||||||
var y = y.toDouble()
|
var y = y.toDouble()
|
||||||
|
|
||||||
val angle = atan2(y, x) + Math.PI + Math.PI
|
angle = atan2(y, x) + Math.PI + Math.PI
|
||||||
val radius = hypot(y, x)
|
radius = hypot(y, x)
|
||||||
val maxRadius = InputOverrider.getGateRadiusAtAngle(controllerIndex, xControl, angle)
|
gateRadius = InputOverrider.getGateRadiusAtAngle(controllerIndex, xControl, angle)
|
||||||
if (radius > maxRadius) {
|
if (radius > gateRadius) {
|
||||||
x = maxRadius * cos(angle)
|
radius = gateRadius
|
||||||
y = maxRadius * sin(angle)
|
x = gateRadius * cos(angle)
|
||||||
|
y = gateRadius * sin(angle)
|
||||||
this.x = x.toFloat()
|
this.x = x.toFloat()
|
||||||
this.y = y.toFloat()
|
this.y = y.toFloat()
|
||||||
}
|
}
|
||||||
|
@ -255,4 +289,9 @@ class InputOverlayDrawableJoystick(
|
||||||
boundsBoxBitmap.alpha = value
|
boundsBoxBitmap.alpha = value
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val HAPTICS_MIN_ANGLE = Math.PI / 20.0
|
||||||
|
private const val HAPTICS_MAX_ANGLE = Math.PI / 4.0
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,126 @@
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
package org.dolphinemu.dolphinemu.utils
|
||||||
|
|
||||||
|
import android.os.Build
|
||||||
|
import android.os.VibrationEffect
|
||||||
|
import android.os.Vibrator
|
||||||
|
import androidx.annotation.FloatRange
|
||||||
|
import androidx.annotation.RequiresApi
|
||||||
|
import org.dolphinemu.dolphinemu.features.input.model.DolphinVibratorManagerFactory
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This class provides methods that facilitate performing haptic feedback.
|
||||||
|
*
|
||||||
|
* @property vibrator The [Vibrator] instance to be used for vibration.
|
||||||
|
* Defaults to the system default vibrator.
|
||||||
|
*/
|
||||||
|
class HapticsProvider(
|
||||||
|
private val vibrator: Vibrator =
|
||||||
|
DolphinVibratorManagerFactory.getSystemVibratorManager().getDefaultVibrator()
|
||||||
|
) {
|
||||||
|
private val primitiveSupport: Boolean = areAllPrimitivesSupported()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Perform haptic feedback by composing primitives (if supported),
|
||||||
|
* with a fallback to a waveform or a legacy vibration.
|
||||||
|
*
|
||||||
|
* @param effect The [HapticEffect] of the feedback.
|
||||||
|
* @param scale The intensity scale of the feedback.
|
||||||
|
*/
|
||||||
|
fun provideFeedback(effect: HapticEffect, @FloatRange(from = 0.0, to = 1.0) scale: Float) {
|
||||||
|
if (primitiveSupport) {
|
||||||
|
vibrator.vibrate(
|
||||||
|
VibrationEffect
|
||||||
|
.startComposition()
|
||||||
|
.addPrimitive(getPrimitive(effect), scale)
|
||||||
|
.compose()
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
val timings = getTimings(effect, scale)
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
|
if (vibrator.hasAmplitudeControl()) {
|
||||||
|
vibrator.vibrate(
|
||||||
|
VibrationEffect.createWaveform(
|
||||||
|
timings, getAmplitudes(effect, scale), -1
|
||||||
|
)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
vibrator.vibrate(VibrationEffect.createWaveform(timings, -1))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
vibrator.vibrate(timings.sum())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the timings for a waveform vibration based on the [effect], scaled by [scale].
|
||||||
|
*
|
||||||
|
* @param effect The [HapticEffect] of the vibration.
|
||||||
|
* @param scale The intensity scale of the vibration.
|
||||||
|
* @return The LongArray of scaled timings for the specified [effect].
|
||||||
|
*/
|
||||||
|
private fun getTimings(
|
||||||
|
effect: HapticEffect, @FloatRange(from = 0.0, to = 1.0) scale: Float
|
||||||
|
): LongArray {
|
||||||
|
// Note: It is recommended that these values differ by a ratio of 1.4 or more,
|
||||||
|
// so the difference in the duration of the vibration can be easily perceived.
|
||||||
|
// Lower-end vibrators can't vibrate at all if the duration is too short.
|
||||||
|
return when (effect) {
|
||||||
|
HapticEffect.QUICK_FALL -> longArrayOf(0L, (100f * scale).toLong())
|
||||||
|
HapticEffect.QUICK_RISE -> longArrayOf(0L, (70f * scale).toLong())
|
||||||
|
HapticEffect.LOW_TICK -> longArrayOf(0L, (50f * scale).toLong())
|
||||||
|
HapticEffect.SPIN -> LongArray(SPIN_TIMINGS.size) { (SPIN_TIMINGS[it] * scale).toLong() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the amplitudes for a waveform vibration based on the [effect], scaled by [scale].
|
||||||
|
*
|
||||||
|
* @param effect The [HapticEffect] of the vibration.
|
||||||
|
* @param scale The intensity scale of the vibration.
|
||||||
|
* @return The IntArray of scaled amplitudes for the specified [effect].
|
||||||
|
*/
|
||||||
|
@RequiresApi(Build.VERSION_CODES.O)
|
||||||
|
private fun getAmplitudes(
|
||||||
|
effect: HapticEffect, @FloatRange(from = 0.0, to = 1.0) scale: Float
|
||||||
|
): IntArray {
|
||||||
|
// Note: It is recommended that these values differ by a ratio of 1.4 or more,
|
||||||
|
// so the difference in the amplitude of the vibration can be easily perceived.
|
||||||
|
return when (effect) {
|
||||||
|
HapticEffect.QUICK_FALL -> intArrayOf(0, (180 * scale).toInt())
|
||||||
|
HapticEffect.QUICK_RISE -> intArrayOf(0, (128 * scale).toInt())
|
||||||
|
HapticEffect.LOW_TICK -> intArrayOf(0, (90 * scale).toInt())
|
||||||
|
HapticEffect.SPIN -> IntArray(SPIN_AMPLITUDES.size) { (SPIN_AMPLITUDES[it] * scale).toInt() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@RequiresApi(Build.VERSION_CODES.S)
|
||||||
|
private fun getPrimitive(effect: HapticEffect): Int {
|
||||||
|
return when (effect) {
|
||||||
|
HapticEffect.QUICK_FALL -> VibrationEffect.Composition.PRIMITIVE_QUICK_FALL
|
||||||
|
HapticEffect.QUICK_RISE -> VibrationEffect.Composition.PRIMITIVE_QUICK_RISE
|
||||||
|
HapticEffect.LOW_TICK -> VibrationEffect.Composition.PRIMITIVE_LOW_TICK
|
||||||
|
HapticEffect.SPIN -> VibrationEffect.Composition.PRIMITIVE_SPIN
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun areAllPrimitivesSupported(): Boolean {
|
||||||
|
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && vibrator.areAllPrimitivesSupported(
|
||||||
|
*HapticEffect.values().map { getPrimitive(it) }.toIntArray()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val SPIN_TIMINGS = longArrayOf(15L, 30L, 20L, 30L, 20L, 30L, 20L, 10L)
|
||||||
|
private val SPIN_AMPLITUDES = intArrayOf(0, 128, 255, 100, 200, 32, 64, 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class HapticEffect {
|
||||||
|
QUICK_FALL,
|
||||||
|
QUICK_RISE,
|
||||||
|
LOW_TICK,
|
||||||
|
SPIN
|
||||||
|
}
|
|
@ -0,0 +1,109 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.appcompat.widget.LinearLayoutCompat 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">
|
||||||
|
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
android:id="@+id/haptics_triggers"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:paddingTop="@dimen/spacing_large">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/haptics_triggers_text"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginBottom="@dimen/spacing_medlarge"
|
||||||
|
android:text="@string/haptics_triggers"
|
||||||
|
android:textAlignment="viewStart"
|
||||||
|
android:textAppearance="@style/TextAppearance.AppCompat.Title"
|
||||||
|
app:layout_constraintBottom_toTopOf="@id/haptics_release_checkbox"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
|
<com.google.android.material.checkbox.MaterialCheckBox
|
||||||
|
android:id="@+id/haptics_press_checkbox"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:minWidth="48dp"
|
||||||
|
android:minHeight="48dp"
|
||||||
|
android:text="@string/haptics_press"
|
||||||
|
android:textAppearance="?android:textAppearanceListItem"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toStartOf="@+id/haptics_release_checkbox"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="@id/haptics_release_checkbox" />
|
||||||
|
|
||||||
|
<com.google.android.material.checkbox.MaterialCheckBox
|
||||||
|
android:id="@+id/haptics_release_checkbox"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:minWidth="48dp"
|
||||||
|
android:minHeight="48dp"
|
||||||
|
android:text="@string/haptics_release"
|
||||||
|
android:textAppearance="?android:textAppearanceListItem"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toStartOf="@+id/haptics_joystick_checkbox"
|
||||||
|
app:layout_constraintStart_toEndOf="@+id/haptics_press_checkbox"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/haptics_triggers_text" />
|
||||||
|
|
||||||
|
<com.google.android.material.checkbox.MaterialCheckBox
|
||||||
|
android:id="@+id/haptics_joystick_checkbox"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:minWidth="48dp"
|
||||||
|
android:minHeight="48dp"
|
||||||
|
android:text="@string/haptics_joystick"
|
||||||
|
android:textAppearance="?android:textAppearanceListItem"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toEndOf="@+id/haptics_release_checkbox"
|
||||||
|
app:layout_constraintTop_toTopOf="@id/haptics_release_checkbox" />
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
android:id="@+id/haptics_intensity"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:layout_marginTop="@dimen/spacing_small"
|
||||||
|
android:paddingHorizontal="24dp">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/haptics_intensity_name"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@string/haptics_intensity"
|
||||||
|
android:textAlignment="viewStart"
|
||||||
|
android:textSize="16sp"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toStartOf="@+id/haptics_intensity_slider"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
|
<com.google.android.material.slider.Slider
|
||||||
|
android:id="@+id/haptics_intensity_slider"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginHorizontal="@dimen/spacing_medlarge"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toStartOf="@id/haptics_intensity_value"
|
||||||
|
app:layout_constraintStart_toEndOf="@id/haptics_intensity_name"
|
||||||
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/haptics_intensity_value"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toEndOf="@+id/haptics_intensity_slider"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
tools:text="50%" />
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|
||||||
|
</androidx.appcompat.widget.LinearLayoutCompat>
|
|
@ -27,6 +27,10 @@
|
||||||
android:id="@+id/menu_emulation_choose_controller"
|
android:id="@+id/menu_emulation_choose_controller"
|
||||||
android:title="@string/emulation_choose_controller"/>
|
android:title="@string/emulation_choose_controller"/>
|
||||||
|
|
||||||
|
<item
|
||||||
|
android:id="@+id/menu_emulation_haptics"
|
||||||
|
android:title="@string/emulation_haptics"/>
|
||||||
|
|
||||||
<item
|
<item
|
||||||
android:id="@+id/menu_emulation_reset_overlay"
|
android:id="@+id/menu_emulation_reset_overlay"
|
||||||
android:title="@string/emulation_touch_overlay_reset"/>
|
android:title="@string/emulation_touch_overlay_reset"/>
|
||||||
|
|
|
@ -29,6 +29,10 @@
|
||||||
android:id="@+id/menu_emulation_choose_controller"
|
android:id="@+id/menu_emulation_choose_controller"
|
||||||
android:title="@string/emulation_choose_controller"/>
|
android:title="@string/emulation_choose_controller"/>
|
||||||
|
|
||||||
|
<item
|
||||||
|
android:id="@+id/menu_emulation_haptics"
|
||||||
|
android:title="@string/emulation_haptics"/>
|
||||||
|
|
||||||
<item
|
<item
|
||||||
android:id="@+id/menu_emulation_ir_group"
|
android:id="@+id/menu_emulation_ir_group"
|
||||||
android:title="@string/emulation_ir_group">
|
android:title="@string/emulation_ir_group">
|
||||||
|
|
|
@ -610,6 +610,7 @@ It can efficiently compress both junk data and encrypted Wii data.
|
||||||
<string name="emulation_ir_mode">IR Mode</string>
|
<string name="emulation_ir_mode">IR Mode</string>
|
||||||
<string name="emulation_ir_sensitivity">IR Sensitivity</string>
|
<string name="emulation_ir_sensitivity">IR Sensitivity</string>
|
||||||
<string name="emulation_choose_doubletap">Double tap button</string>
|
<string name="emulation_choose_doubletap">Double tap button</string>
|
||||||
|
<string name="emulation_haptics">Touch Haptics</string>
|
||||||
|
|
||||||
<!-- GC Adapter Menu-->
|
<!-- GC Adapter Menu-->
|
||||||
<string name="gc_adapter_rumble">Enable Vibration</string>
|
<string name="gc_adapter_rumble">Enable Vibration</string>
|
||||||
|
@ -812,6 +813,13 @@ It can efficiently compress both junk data and encrypted Wii data.
|
||||||
<string name="ir_follow">Follow</string>
|
<string name="ir_follow">Follow</string>
|
||||||
<string name="ir_drag">Drag</string>
|
<string name="ir_drag">Drag</string>
|
||||||
|
|
||||||
|
<!-- Haptics -->
|
||||||
|
<string name="haptics_triggers">Feedback Triggers</string>
|
||||||
|
<string name="haptics_press">Press</string>
|
||||||
|
<string name="haptics_release">Release</string>
|
||||||
|
<string name="haptics_joystick">Joystick</string>
|
||||||
|
<string name="haptics_intensity">Intensity</string>
|
||||||
|
|
||||||
<!-- Double Tap Buttons -->
|
<!-- Double Tap Buttons -->
|
||||||
<string name="double_tap_a">Button A</string>
|
<string name="double_tap_a">Button A</string>
|
||||||
<string name="double_tap_b">Button B</string>
|
<string name="double_tap_b">Button B</string>
|
||||||
|
|
|
@ -63,7 +63,7 @@ jmethodID s_motion_event_get_source;
|
||||||
jclass s_controller_interface_class;
|
jclass s_controller_interface_class;
|
||||||
jmethodID s_controller_interface_register_input_device_listener;
|
jmethodID s_controller_interface_register_input_device_listener;
|
||||||
jmethodID s_controller_interface_unregister_input_device_listener;
|
jmethodID s_controller_interface_unregister_input_device_listener;
|
||||||
jmethodID s_controller_interface_get_vibrator_manager;
|
jmethodID s_controller_interface_get_device_vibrator_manager;
|
||||||
jmethodID s_controller_interface_get_system_vibrator_manager;
|
jmethodID s_controller_interface_get_system_vibrator_manager;
|
||||||
jmethodID s_controller_interface_vibrate;
|
jmethodID s_controller_interface_vibrate;
|
||||||
|
|
||||||
|
@ -78,6 +78,7 @@ jmethodID s_sensor_event_listener_get_negative_axes;
|
||||||
jclass s_dolphin_vibrator_manager_class;
|
jclass s_dolphin_vibrator_manager_class;
|
||||||
jmethodID s_dolphin_vibrator_manager_get_vibrator;
|
jmethodID s_dolphin_vibrator_manager_get_vibrator;
|
||||||
jmethodID s_dolphin_vibrator_manager_get_vibrator_ids;
|
jmethodID s_dolphin_vibrator_manager_get_vibrator_ids;
|
||||||
|
jmethodID s_dolphin_vibrator_manager_get_default_vibrator;
|
||||||
|
|
||||||
jintArray s_keycodes_array;
|
jintArray s_keycodes_array;
|
||||||
|
|
||||||
|
@ -746,7 +747,8 @@ private:
|
||||||
void AddMotors(JNIEnv* env, jobject input_device)
|
void AddMotors(JNIEnv* env, jobject input_device)
|
||||||
{
|
{
|
||||||
jobject vibrator_manager = env->CallStaticObjectMethod(
|
jobject vibrator_manager = env->CallStaticObjectMethod(
|
||||||
s_controller_interface_class, s_controller_interface_get_vibrator_manager, input_device);
|
s_controller_interface_class, s_controller_interface_get_device_vibrator_manager,
|
||||||
|
input_device);
|
||||||
AddMotorsFromManager(env, vibrator_manager);
|
AddMotorsFromManager(env, vibrator_manager);
|
||||||
env->DeleteLocalRef(vibrator_manager);
|
env->DeleteLocalRef(vibrator_manager);
|
||||||
}
|
}
|
||||||
|
@ -857,8 +859,8 @@ InputBackend::InputBackend(ControllerInterface* controller_interface)
|
||||||
env->GetStaticMethodID(s_controller_interface_class, "registerInputDeviceListener", "()V");
|
env->GetStaticMethodID(s_controller_interface_class, "registerInputDeviceListener", "()V");
|
||||||
s_controller_interface_unregister_input_device_listener =
|
s_controller_interface_unregister_input_device_listener =
|
||||||
env->GetStaticMethodID(s_controller_interface_class, "unregisterInputDeviceListener", "()V");
|
env->GetStaticMethodID(s_controller_interface_class, "unregisterInputDeviceListener", "()V");
|
||||||
s_controller_interface_get_vibrator_manager =
|
s_controller_interface_get_device_vibrator_manager =
|
||||||
env->GetStaticMethodID(s_controller_interface_class, "getVibratorManager",
|
env->GetStaticMethodID(s_controller_interface_class, "getDeviceVibratorManager",
|
||||||
"(Landroid/view/InputDevice;)Lorg/dolphinemu/dolphinemu/features/"
|
"(Landroid/view/InputDevice;)Lorg/dolphinemu/dolphinemu/features/"
|
||||||
"input/model/DolphinVibratorManager;");
|
"input/model/DolphinVibratorManager;");
|
||||||
s_controller_interface_get_system_vibrator_manager = env->GetStaticMethodID(
|
s_controller_interface_get_system_vibrator_manager = env->GetStaticMethodID(
|
||||||
|
@ -894,6 +896,8 @@ InputBackend::InputBackend(ControllerInterface* controller_interface)
|
||||||
env->GetMethodID(s_dolphin_vibrator_manager_class, "getVibrator", "(I)Landroid/os/Vibrator;");
|
env->GetMethodID(s_dolphin_vibrator_manager_class, "getVibrator", "(I)Landroid/os/Vibrator;");
|
||||||
s_dolphin_vibrator_manager_get_vibrator_ids =
|
s_dolphin_vibrator_manager_get_vibrator_ids =
|
||||||
env->GetMethodID(s_dolphin_vibrator_manager_class, "getVibratorIds", "()[I");
|
env->GetMethodID(s_dolphin_vibrator_manager_class, "getVibratorIds", "()[I");
|
||||||
|
s_dolphin_vibrator_manager_get_default_vibrator = env->GetMethodID(
|
||||||
|
s_dolphin_vibrator_manager_class, "getDefaultVibrator", "()Landroid/os/Vibrator;");
|
||||||
env->DeleteLocalRef(dolphin_vibrator_manager_class);
|
env->DeleteLocalRef(dolphin_vibrator_manager_class);
|
||||||
|
|
||||||
jintArray keycodes_array = CreateKeyCodesArray(env);
|
jintArray keycodes_array = CreateKeyCodesArray(env);
|
||||||
|
|
Loading…
Reference in New Issue