mirror of
https://github.com/dolphin-emu/dolphin.git
synced 2026-09-14 06:15:54 -05:00
Android/overlay-haptics: Refactor HapticProvider code to HapticEffect, add fallback primitives
This commit is contained in:
@@ -59,6 +59,7 @@ import org.dolphinemu.dolphinemu.fragments.EmulationFragment
|
||||
import org.dolphinemu.dolphinemu.fragments.MenuFragment
|
||||
import org.dolphinemu.dolphinemu.fragments.SaveLoadStateFragment
|
||||
import org.dolphinemu.dolphinemu.fragments.SaveLoadStateFragment.SaveOrLoad
|
||||
import org.dolphinemu.dolphinemu.model.HapticEffect
|
||||
import org.dolphinemu.dolphinemu.overlay.InputOverlay
|
||||
import org.dolphinemu.dolphinemu.overlay.InputOverlayPointer
|
||||
import org.dolphinemu.dolphinemu.ui.main.MainPresenter
|
||||
@@ -66,7 +67,6 @@ 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.HapticEffect
|
||||
import org.dolphinemu.dolphinemu.utils.HapticsProvider
|
||||
import org.dolphinemu.dolphinemu.utils.RateLimiter
|
||||
import org.dolphinemu.dolphinemu.utils.ThemeHelper
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
package org.dolphinemu.dolphinemu.model
|
||||
|
||||
import android.os.Build
|
||||
import android.os.VibrationEffect
|
||||
import android.os.Vibrator
|
||||
import androidx.annotation.FloatRange
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.core.view.HapticFeedbackConstantsCompat
|
||||
|
||||
/**
|
||||
* Enum to represent haptic effects.
|
||||
*
|
||||
* @param feedbackConstant One of the constants defined in [HapticFeedbackConstantsCompat].
|
||||
* @param maxTimings The maximum timing values, in milliseconds, of the timing / amplitude pairs.
|
||||
* Note that lower-end vibrators may not be able to vibrate for short (<50ms) durations.
|
||||
* @param maxAmplitudes The maximum amplitude values of the timing / amplitude pairs.
|
||||
* Amplitude values must be between 0 and 255. An amplitude value of 0 implies the motor is off.
|
||||
* @param primaryPrimitive The primary primitive ID, or null if the SDK version is too low.
|
||||
* @param fallbackPrimitive The fallback primitive ID, or null if the SDK version is too low.
|
||||
*/
|
||||
enum class HapticEffect(
|
||||
val feedbackConstant: Int,
|
||||
private val maxTimings: LongArray,
|
||||
private val maxAmplitudes: IntArray,
|
||||
val primaryPrimitive: Int?,
|
||||
val fallbackPrimitive: Int? = null
|
||||
) {
|
||||
PRESS(
|
||||
feedbackConstant = HapticFeedbackConstantsCompat.VIRTUAL_KEY,
|
||||
maxTimings = longArrayOf(0L, 100L),
|
||||
maxAmplitudes = intArrayOf(0, 255),
|
||||
primaryPrimitive = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
VibrationEffect.Composition.PRIMITIVE_QUICK_FALL
|
||||
} else {
|
||||
null
|
||||
},
|
||||
fallbackPrimitive = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
VibrationEffect.Composition.PRIMITIVE_CLICK
|
||||
} else {
|
||||
null
|
||||
}
|
||||
),
|
||||
RELEASE(
|
||||
feedbackConstant = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) {
|
||||
HapticFeedbackConstantsCompat.VIRTUAL_KEY_RELEASE
|
||||
} else {
|
||||
HapticFeedbackConstantsCompat.CONTEXT_CLICK // Better than a no-op.
|
||||
},
|
||||
maxTimings = longArrayOf(0L, 70L),
|
||||
maxAmplitudes = intArrayOf(0, 180),
|
||||
primaryPrimitive = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
VibrationEffect.Composition.PRIMITIVE_QUICK_RISE
|
||||
} else {
|
||||
null
|
||||
}
|
||||
),
|
||||
JOYSTICK(
|
||||
feedbackConstant = HapticFeedbackConstantsCompat.SEGMENT_FREQUENT_TICK,
|
||||
maxTimings = longArrayOf(0L, 50L),
|
||||
maxAmplitudes = intArrayOf(0, 128),
|
||||
primaryPrimitive = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
VibrationEffect.Composition.PRIMITIVE_LOW_TICK
|
||||
} else {
|
||||
null
|
||||
},
|
||||
fallbackPrimitive = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
VibrationEffect.Composition.PRIMITIVE_TICK
|
||||
} else {
|
||||
null
|
||||
}
|
||||
);
|
||||
|
||||
fun getMaxTimings(): LongArray = maxTimings.copyOf()
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.O)
|
||||
fun getMaxAmplitudes(): IntArray = maxAmplitudes.copyOf()
|
||||
|
||||
fun scaleTimings(@FloatRange(from = 0.0, to = 1.0) scale: Float): LongArray =
|
||||
maxTimings.map { (it.toFloat() * scale).toLong() }.toLongArray()
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.O)
|
||||
fun scaleAmplitudes(@FloatRange(from = 0.0, to = 1.0) scale: Float): IntArray =
|
||||
maxAmplitudes.map { (it.toFloat() * scale).toInt() }.toIntArray()
|
||||
|
||||
/**
|
||||
* Gets this effect's preferred primitive ID that is supported by the provided [vibrator].
|
||||
* @param vibrator A [Vibrator] instance for checking whether primitives are supported by it.
|
||||
* @return The best supported primitive ID of this effect, or null if there is none.
|
||||
*/
|
||||
@RequiresApi(Build.VERSION_CODES.R)
|
||||
fun getSupportedPrimitive(vibrator: Vibrator): Int? =
|
||||
listOf(primaryPrimitive, fallbackPrimitive).firstOrNull {
|
||||
it != null && vibrator.areAllPrimitivesSupported(it)
|
||||
}
|
||||
}
|
||||
@@ -31,7 +31,7 @@ import org.dolphinemu.dolphinemu.features.settings.model.FloatSetting
|
||||
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.getSettingForWiimoteSource
|
||||
import org.dolphinemu.dolphinemu.utils.HapticEffect
|
||||
import org.dolphinemu.dolphinemu.model.HapticEffect
|
||||
import org.dolphinemu.dolphinemu.utils.HapticsProvider
|
||||
import java.util.Arrays
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ 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
|
||||
import org.dolphinemu.dolphinemu.utils.HapticEffect
|
||||
import org.dolphinemu.dolphinemu.model.HapticEffect
|
||||
import org.dolphinemu.dolphinemu.utils.HapticsProvider
|
||||
import kotlin.math.atan2
|
||||
import kotlin.math.cos
|
||||
|
||||
@@ -5,36 +5,40 @@ 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 androidx.core.view.ViewCompat
|
||||
import org.dolphinemu.dolphinemu.features.input.model.DolphinVibratorManagerFactory
|
||||
import org.dolphinemu.dolphinemu.model.HapticEffect
|
||||
|
||||
/**
|
||||
* Provides haptic feedback to the user.
|
||||
*
|
||||
* @property vibrator The [Vibrator] instance to be used for vibration.
|
||||
* Defaults to the system default vibrator.
|
||||
* Defaults to the system's default vibrator.
|
||||
*/
|
||||
class HapticsProvider(
|
||||
private val vibrator: Vibrator =
|
||||
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
|
||||
private val supportedPrimitivesMap: Map<HapticEffect, Int?>
|
||||
private val primitiveSupport: Boolean
|
||||
|
||||
init {
|
||||
supportedPrimitivesMap = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
HapticEffect.entries.associateWith { it.getSupportedPrimitive(vibrator) }
|
||||
} else {
|
||||
HapticFeedbackConstants.VIRTUAL_KEY // Same as press feedback is better than no feedback
|
||||
HapticEffect.entries.associateWith { null }
|
||||
}
|
||||
primitiveSupport = supportedPrimitivesMap.values.all { it != null }
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 view The [View] to perform the feedback on, can be null to vibrate using the [vibrator].
|
||||
* @param view The [View] to perform the feedback on, can be null to use the [vibrator] directly.
|
||||
* @param scale The intensity scale of the feedback, will only be used if [view] is not set.
|
||||
*/
|
||||
fun provideFeedback(
|
||||
@@ -43,105 +47,23 @@ class HapticsProvider(
|
||||
@FloatRange(from = 0.0, to = 1.0) scale: Float = 0.5f
|
||||
) {
|
||||
if (view != null) {
|
||||
view.performHapticFeedback(getHapticFeedbackConstant(effect))
|
||||
} else if (primitiveSupport) {
|
||||
ViewCompat.performHapticFeedback(view, effect.feedbackConstant)
|
||||
} else if (primitiveSupport && Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
vibrator.vibrate(
|
||||
VibrationEffect
|
||||
.startComposition()
|
||||
.addPrimitive(getPrimitive(effect), scale)
|
||||
.addPrimitive(supportedPrimitivesMap[effect]!!, scale)
|
||||
.compose()
|
||||
)
|
||||
} else {
|
||||
val timings = getTimings(effect, scale)
|
||||
val scaledTimings = effect.scaleTimings(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))
|
||||
}
|
||||
vibrator.vibrate(
|
||||
VibrationEffect.createWaveform(scaledTimings, effect.scaleAmplitudes(scale), -1)
|
||||
)
|
||||
} else {
|
||||
vibrator.vibrate(timings.sum())
|
||||
vibrator.vibrate(scaledTimings.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.PRESS -> longArrayOf(0L, (100f * scale).toLong())
|
||||
HapticEffect.RELEASE -> longArrayOf(0L, (70f * scale).toLong())
|
||||
HapticEffect.JOYSTICK -> longArrayOf(0L, (50f * 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.
|
||||
// Value range is between 0 and 255 (VibrationEffect.MAX_AMPLITUDE).
|
||||
return when (effect) {
|
||||
HapticEffect.PRESS -> intArrayOf(0, (255f * scale).toInt())
|
||||
HapticEffect.RELEASE -> intArrayOf(0, (180f * scale).toInt())
|
||||
HapticEffect.JOYSTICK -> intArrayOf(0, (128f * scale).toInt())
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
HapticEffect.PRESS -> VibrationEffect.Composition.PRIMITIVE_QUICK_FALL
|
||||
HapticEffect.RELEASE -> VibrationEffect.Composition.PRIMITIVE_QUICK_RISE
|
||||
HapticEffect.JOYSTICK -> VibrationEffect.Composition.PRIMITIVE_LOW_TICK
|
||||
}
|
||||
}
|
||||
|
||||
private fun areAllPrimitivesSupported(): Boolean {
|
||||
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && vibrator.areAllPrimitivesSupported(
|
||||
*HapticEffect.entries.map { getPrimitive(it) }.toIntArray()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
enum class HapticEffect {
|
||||
PRESS,
|
||||
RELEASE,
|
||||
JOYSTICK
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user