mirror of
https://github.com/dolphin-emu/dolphin.git
synced 2026-09-29 05:40:54 -05:00
Android/overlay-haptics: Use native haptic feedback by default, add option to use vibrator directly
This commit is contained in:
@@ -715,25 +715,34 @@ class EmulationActivity : AppCompatActivity(), ThemeProvider {
|
||||
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(hapticsOnPressCheckbox, hapticsOnReleaseCheckbox, hapticsJoystickCheckbox)
|
||||
val toggleVibrationSettings = {
|
||||
checkboxes.any { it.isChecked }.let { enabled ->
|
||||
hapticsUseVibratorDirectlyName.isEnabled = enabled
|
||||
hapticsUseVibratorDirectlySwitch.isEnabled = enabled
|
||||
(hapticsUseVibratorDirectlySwitch.isChecked && enabled).let { enabled ->
|
||||
hapticsIntensityName.isEnabled = enabled
|
||||
hapticsIntensitySlider.isEnabled = enabled
|
||||
hapticsIntensityValue.isEnabled = enabled
|
||||
}
|
||||
}
|
||||
}
|
||||
hapticsOnPressCheckbox.isChecked = BooleanSetting.MAIN_OVERLAY_HAPTICS_ON_PRESS.boolean
|
||||
hapticsOnReleaseCheckbox.isChecked =
|
||||
BooleanSetting.MAIN_OVERLAY_HAPTICS_ON_RELEASE.boolean
|
||||
hapticsJoystickCheckbox.isChecked = BooleanSetting.MAIN_OVERLAY_HAPTICS_JOYSTICK.boolean
|
||||
if (checkboxes.none { it.isChecked }) {
|
||||
toggleIntensity(false)
|
||||
}
|
||||
hapticsUseVibratorDirectlySwitch.isChecked =
|
||||
BooleanSetting.MAIN_OVERLAY_HAPTICS_USE_VIBRATOR_DIRECTLY.boolean
|
||||
toggleVibrationSettings()
|
||||
checkboxes.forEach { checkbox ->
|
||||
checkbox.setOnCheckedChangeListener { _, _ ->
|
||||
toggleIntensity(checkboxes.any { it.isChecked })
|
||||
toggleVibrationSettings()
|
||||
}
|
||||
}
|
||||
hapticsUseVibratorDirectlySwitch.setOnCheckedChangeListener { _, _ ->
|
||||
toggleVibrationSettings()
|
||||
}
|
||||
hapticsIntensitySlider.apply {
|
||||
val setValueText = { value: Float ->
|
||||
hapticsIntensityValue.text =
|
||||
@@ -745,7 +754,7 @@ class EmulationActivity : AppCompatActivity(), ThemeProvider {
|
||||
value = FloatSetting.MAIN_OVERLAY_HAPTICS_SCALE.float.also { setValueText(it) }
|
||||
addOnChangeListener { _: Slider, value: Float, _: Boolean ->
|
||||
setValueText(value)
|
||||
hapticsProvider.provideFeedback(HapticEffect.JOYSTICK, value)
|
||||
hapticsProvider.provideFeedback(HapticEffect.JOYSTICK, null, value)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -761,6 +770,9 @@ class EmulationActivity : AppCompatActivity(), ThemeProvider {
|
||||
BooleanSetting.MAIN_OVERLAY_HAPTICS_JOYSTICK.setBoolean(
|
||||
settings, dialogBinding.hapticsJoystickCheckbox.isChecked
|
||||
)
|
||||
BooleanSetting.MAIN_OVERLAY_HAPTICS_USE_VIBRATOR_DIRECTLY.setBoolean(
|
||||
settings, dialogBinding.hapticsUseVibratorDirectlySwitch.isChecked
|
||||
)
|
||||
FloatSetting.MAIN_OVERLAY_HAPTICS_SCALE.setFloat(
|
||||
settings, dialogBinding.hapticsIntensitySlider.value
|
||||
)
|
||||
|
||||
@@ -689,6 +689,12 @@ enum class BooleanSetting(
|
||||
"OverlayHapticsJoystick",
|
||||
false
|
||||
),
|
||||
MAIN_OVERLAY_HAPTICS_USE_VIBRATOR_DIRECTLY(
|
||||
Settings.FILE_DOLPHIN,
|
||||
Settings.SECTION_INI_ANDROID,
|
||||
"OverlayHapticsUseVibratorDirectly",
|
||||
false
|
||||
),
|
||||
SYSCONF_SCREENSAVER(Settings.FILE_SYSCONF, "IPL", "SSV", false),
|
||||
SYSCONF_WIDESCREEN(Settings.FILE_SYSCONF, "IPL", "AR", true),
|
||||
SYSCONF_PROGRESSIVE_SCAN(Settings.FILE_SYSCONF, "IPL", "PGS", true),
|
||||
|
||||
@@ -147,6 +147,8 @@ class InputOverlay(context: Context?, attrs: AttributeSet?) : SurfaceView(contex
|
||||
val hapticsScale = FloatSetting.MAIN_OVERLAY_HAPTICS_SCALE.float
|
||||
val hapticsOnPress = BooleanSetting.MAIN_OVERLAY_HAPTICS_ON_PRESS.boolean
|
||||
val hapticsOnRelease = BooleanSetting.MAIN_OVERLAY_HAPTICS_ON_RELEASE.boolean
|
||||
val hapticsView =
|
||||
if (BooleanSetting.MAIN_OVERLAY_HAPTICS_USE_VIBRATOR_DIRECTLY.boolean) null else v
|
||||
// Tracks if any button/joystick is pressed down
|
||||
var pressed = false
|
||||
|
||||
@@ -166,6 +168,7 @@ class InputOverlay(context: Context?, attrs: AttributeSet?) : SurfaceView(contex
|
||||
if (hapticsOnRelease) {
|
||||
hapticsProvider.provideFeedback(
|
||||
HapticEffect.RELEASE,
|
||||
hapticsView,
|
||||
hapticsScale
|
||||
)
|
||||
}
|
||||
@@ -174,6 +177,7 @@ class InputOverlay(context: Context?, attrs: AttributeSet?) : SurfaceView(contex
|
||||
if (hapticsOnPress) {
|
||||
hapticsProvider.provideFeedback(
|
||||
HapticEffect.PRESS,
|
||||
hapticsView,
|
||||
hapticsScale
|
||||
)
|
||||
}
|
||||
@@ -201,6 +205,7 @@ class InputOverlay(context: Context?, attrs: AttributeSet?) : SurfaceView(contex
|
||||
if (hapticsOnRelease) {
|
||||
hapticsProvider.provideFeedback(
|
||||
HapticEffect.RELEASE,
|
||||
hapticsView,
|
||||
hapticsScale
|
||||
)
|
||||
}
|
||||
@@ -260,6 +265,7 @@ class InputOverlay(context: Context?, attrs: AttributeSet?) : SurfaceView(contex
|
||||
if (hapticsOnRelease && dpad.isPressed(i)) {
|
||||
hapticsProvider.provideFeedback(
|
||||
HapticEffect.RELEASE,
|
||||
hapticsView,
|
||||
hapticsScale
|
||||
)
|
||||
}
|
||||
@@ -272,6 +278,7 @@ class InputOverlay(context: Context?, attrs: AttributeSet?) : SurfaceView(contex
|
||||
if (hapticsOnPress && !dpad.isPressed(i)) {
|
||||
hapticsProvider.provideFeedback(
|
||||
HapticEffect.PRESS,
|
||||
hapticsView,
|
||||
hapticsScale
|
||||
)
|
||||
}
|
||||
@@ -299,6 +306,7 @@ class InputOverlay(context: Context?, attrs: AttributeSet?) : SurfaceView(contex
|
||||
if (hapticsOnRelease && dpad.isPressed(i)) {
|
||||
hapticsProvider.provideFeedback(
|
||||
HapticEffect.RELEASE,
|
||||
hapticsView,
|
||||
hapticsScale
|
||||
)
|
||||
}
|
||||
@@ -316,7 +324,7 @@ class InputOverlay(context: Context?, attrs: AttributeSet?) : SurfaceView(contex
|
||||
}
|
||||
|
||||
for (joystick in overlayJoysticks) {
|
||||
if (joystick.trackEvent(event)) {
|
||||
if (joystick.trackEvent(v, event)) {
|
||||
if (joystick.trackId != -1)
|
||||
pressed = true
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import android.graphics.Canvas
|
||||
import android.graphics.Rect
|
||||
import android.graphics.drawable.BitmapDrawable
|
||||
import android.view.MotionEvent
|
||||
import android.view.View
|
||||
import org.dolphinemu.dolphinemu.features.input.model.InputOverrider
|
||||
import org.dolphinemu.dolphinemu.features.settings.model.BooleanSetting
|
||||
import org.dolphinemu.dolphinemu.features.settings.model.FloatSetting
|
||||
@@ -103,13 +104,15 @@ class InputOverlayDrawableJoystick(
|
||||
boundsBoxBitmap.draw(canvas)
|
||||
}
|
||||
|
||||
fun trackEvent(event: MotionEvent): Boolean {
|
||||
fun trackEvent(v: View, event: MotionEvent): Boolean {
|
||||
val reCenter = BooleanSetting.MAIN_JOYSTICK_REL_CENTER.boolean
|
||||
val action = event.actionMasked
|
||||
val firstPointer = action != MotionEvent.ACTION_POINTER_DOWN &&
|
||||
action != MotionEvent.ACTION_POINTER_UP
|
||||
val pointerIndex = if (firstPointer) 0 else event.actionIndex
|
||||
val hapticsScale = FloatSetting.MAIN_OVERLAY_HAPTICS_SCALE.float
|
||||
val hapticsView =
|
||||
if (BooleanSetting.MAIN_OVERLAY_HAPTICS_USE_VIBRATOR_DIRECTLY.boolean) null else v
|
||||
var pressed = false
|
||||
|
||||
when (action) {
|
||||
@@ -123,7 +126,11 @@ class InputOverlayDrawableJoystick(
|
||||
pressed = true
|
||||
pressedState = true
|
||||
if (BooleanSetting.MAIN_OVERLAY_HAPTICS_ON_PRESS.boolean) {
|
||||
hapticsProvider.provideFeedback(HapticEffect.PRESS, hapticsScale)
|
||||
hapticsProvider.provideFeedback(
|
||||
HapticEffect.PRESS,
|
||||
hapticsView,
|
||||
hapticsScale
|
||||
)
|
||||
}
|
||||
outerBitmap.alpha = 0
|
||||
boundsBoxBitmap.alpha = opacity
|
||||
@@ -144,7 +151,11 @@ class InputOverlayDrawableJoystick(
|
||||
pressed = true
|
||||
pressedState = false
|
||||
if (BooleanSetting.MAIN_OVERLAY_HAPTICS_ON_RELEASE.boolean) {
|
||||
hapticsProvider.provideFeedback(HapticEffect.RELEASE, hapticsScale)
|
||||
hapticsProvider.provideFeedback(
|
||||
HapticEffect.RELEASE,
|
||||
hapticsView,
|
||||
hapticsScale
|
||||
)
|
||||
}
|
||||
y = 0f
|
||||
x = y
|
||||
@@ -184,7 +195,11 @@ class InputOverlayDrawableJoystick(
|
||||
val deltaX = x - hapticsPreviousX
|
||||
val deltaY = y - hapticsPreviousY
|
||||
if (deltaX.pow(2) + deltaY.pow(2) > radiusThreshold.pow(2)) {
|
||||
hapticsProvider.provideFeedback(HapticEffect.JOYSTICK, hapticsScale)
|
||||
hapticsProvider.provideFeedback(
|
||||
HapticEffect.JOYSTICK,
|
||||
hapticsView,
|
||||
hapticsScale
|
||||
)
|
||||
hapticsPreviousX = x
|
||||
hapticsPreviousY = y
|
||||
}
|
||||
|
||||
@@ -5,6 +5,8 @@ package org.dolphinemu.dolphinemu.utils
|
||||
import android.os.Build
|
||||
import android.os.VibrationEffect
|
||||
import android.os.Vibrator
|
||||
import android.view.HapticFeedbackConstants
|
||||
import android.view.View
|
||||
import androidx.annotation.FloatRange
|
||||
import androidx.annotation.RequiresApi
|
||||
import org.dolphinemu.dolphinemu.features.input.model.DolphinVibratorManagerFactory
|
||||
@@ -20,16 +22,29 @@ class HapticsProvider(
|
||||
DolphinVibratorManagerFactory.getSystemVibratorManager().getDefaultVibrator()
|
||||
) {
|
||||
private val primitiveSupport: Boolean = areAllPrimitivesSupported()
|
||||
private val releaseHapticFeedbackConstant =
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) {
|
||||
HapticFeedbackConstants.VIRTUAL_KEY_RELEASE
|
||||
} else {
|
||||
HapticFeedbackConstants.VIRTUAL_KEY // Same as press feedback is better than no feedback
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform haptic feedback by composing primitives (if supported),
|
||||
* with a fallback to a waveform or a legacy vibration.
|
||||
* Perform haptic feedback natively (if a [View] is provided),
|
||||
* or by composing primitives (if supported), falling back to a waveform or a legacy vibration.
|
||||
*
|
||||
* @param effect The [HapticEffect] of the feedback.
|
||||
* @param scale The intensity scale of the feedback.
|
||||
* @param view The [View] to perform the feedback on, can be null to vibrate using the [vibrator].
|
||||
* @param scale The intensity scale of the feedback, will only be used if [view] is not set.
|
||||
*/
|
||||
fun provideFeedback(effect: HapticEffect, @FloatRange(from = 0.0, to = 1.0) scale: Float) {
|
||||
if (primitiveSupport) {
|
||||
fun provideFeedback(
|
||||
effect: HapticEffect,
|
||||
view: View? = null,
|
||||
@FloatRange(from = 0.0, to = 1.0) scale: Float = 0.5f
|
||||
) {
|
||||
if (view != null) {
|
||||
view.performHapticFeedback(getHapticFeedbackConstant(effect))
|
||||
} else if (primitiveSupport) {
|
||||
vibrator.vibrate(
|
||||
VibrationEffect
|
||||
.startComposition()
|
||||
@@ -95,6 +110,20 @@ class HapticsProvider(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the haptic feedback constant that matches the [effect].
|
||||
*
|
||||
* @param effect The [HapticEffect] of the feedback.
|
||||
* @return The matching [HapticFeedbackConstants] constant.
|
||||
*/
|
||||
private fun getHapticFeedbackConstant(effect: HapticEffect): Int {
|
||||
return when (effect) {
|
||||
HapticEffect.PRESS -> HapticFeedbackConstants.VIRTUAL_KEY
|
||||
HapticEffect.RELEASE -> releaseHapticFeedbackConstant
|
||||
HapticEffect.JOYSTICK -> HapticFeedbackConstants.CLOCK_TICK
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.S)
|
||||
private fun getPrimitive(effect: HapticEffect): Int {
|
||||
return when (effect) {
|
||||
@@ -109,7 +138,6 @@ class HapticsProvider(
|
||||
*HapticEffect.entries.map { getPrimitive(it) }.toIntArray()
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
enum class HapticEffect {
|
||||
|
||||
@@ -29,8 +29,8 @@
|
||||
android:id="@+id/haptics_on_press_checkbox"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:minWidth="48dp"
|
||||
android:minHeight="48dp"
|
||||
android:minWidth="48dp"
|
||||
android:text="@string/haptics_on_press"
|
||||
android:textAppearance="?android:textAppearanceListItem"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
@@ -42,8 +42,8 @@
|
||||
android:id="@+id/haptics_on_release_checkbox"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:minWidth="48dp"
|
||||
android:minHeight="48dp"
|
||||
android:minWidth="48dp"
|
||||
android:text="@string/haptics_on_release"
|
||||
android:textAppearance="?android:textAppearanceListItem"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
@@ -55,8 +55,8 @@
|
||||
android:id="@+id/haptics_joystick_checkbox"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:minWidth="48dp"
|
||||
android:minHeight="48dp"
|
||||
android:minWidth="48dp"
|
||||
android:text="@string/haptics_joystick"
|
||||
android:textAppearance="?android:textAppearanceListItem"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
@@ -65,6 +65,36 @@
|
||||
app:layout_constraintTop_toTopOf="@id/haptics_on_release_checkbox" />
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:id="@+id/haptics_use_vibrator_directly"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/spacing_small"
|
||||
android:paddingHorizontal="24dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/haptics_use_vibrator_directly_name"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="@dimen/spacing_large"
|
||||
android:text="@string/haptics_use_vibrator_directly"
|
||||
android:textAlignment="viewStart"
|
||||
android:textAppearance="@style/TextAppearance.AppCompat.Medium"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/haptics_use_vibrator_directly_switch"
|
||||
app:layout_constraintEnd_toStartOf="@+id/haptics_use_vibrator_directly_switch"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="@+id/haptics_use_vibrator_directly_switch" />
|
||||
|
||||
<com.google.android.material.switchmaterial.SwitchMaterial
|
||||
android:id="@+id/haptics_use_vibrator_directly_switch"
|
||||
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_use_vibrator_directly_name"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:id="@+id/haptics_intensity"
|
||||
android:layout_width="match_parent"
|
||||
|
||||
@@ -845,6 +845,7 @@ It can efficiently compress both junk data and encrypted Wii data.
|
||||
<string name="haptics_on_press">On Press</string>
|
||||
<string name="haptics_on_release">On Release</string>
|
||||
<string name="haptics_joystick">Joystick</string>
|
||||
<string name="haptics_use_vibrator_directly">Use vibrator directly</string>
|
||||
<string name="haptics_intensity">Intensity</string>
|
||||
|
||||
<!-- Double Tap Buttons -->
|
||||
|
||||
Reference in New Issue
Block a user