diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/activities/EmulationActivity.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/activities/EmulationActivity.kt index 148f5d5e69..de28efe7a4 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/activities/EmulationActivity.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/activities/EmulationActivity.kt @@ -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 ) diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/model/BooleanSetting.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/model/BooleanSetting.kt index 1c0bbd586a..16b4ba2337 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/model/BooleanSetting.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/features/settings/model/BooleanSetting.kt @@ -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), diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/overlay/InputOverlay.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/overlay/InputOverlay.kt index 9b4fbc5265..bfe8e1e90b 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/overlay/InputOverlay.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/overlay/InputOverlay.kt @@ -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 } diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/overlay/InputOverlayDrawableJoystick.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/overlay/InputOverlayDrawableJoystick.kt index 49977fa27f..a3f6826295 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/overlay/InputOverlayDrawableJoystick.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/overlay/InputOverlayDrawableJoystick.kt @@ -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 } diff --git a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/HapticsProvider.kt b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/HapticsProvider.kt index be8bee9246..b9c37e1d59 100644 --- a/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/HapticsProvider.kt +++ b/Source/Android/app/src/main/java/org/dolphinemu/dolphinemu/utils/HapticsProvider.kt @@ -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 { diff --git a/Source/Android/app/src/main/res/layout/dialog_haptics_adjust.xml b/Source/Android/app/src/main/res/layout/dialog_haptics_adjust.xml index 5004124a37..06ff0fac4b 100644 --- a/Source/Android/app/src/main/res/layout/dialog_haptics_adjust.xml +++ b/Source/Android/app/src/main/res/layout/dialog_haptics_adjust.xml @@ -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" /> + + + + + + + On Press On Release Joystick + Use vibrator directly Intensity