Implementing a power scale in SliderHapticFeedbackProvider.
A power function is applied to vibration scaling to compensate for human
non-linear perception. The exponent of the power function is added to
SliderHapticFeedbackConfig. The new scaling is applied to vibrations
while dragging and vibrations at the bookends.
Test: atest SystemUITests:SliderHapticFeedbackProviderTest
Bug: 295932558
Flag: ACONFIG com.android.systemui.haptic_brightness_slider TEAMFOOD
Change-Id: If12c3921a7c79ec9f27f3381d3fc3372c9a33b90
diff --git a/packages/SystemUI/src/com/android/systemui/haptics/slider/SliderHapticFeedbackConfig.kt b/packages/SystemUI/src/com/android/systemui/haptics/slider/SliderHapticFeedbackConfig.kt
index 7b33e11..6cb68ba 100644
--- a/packages/SystemUI/src/com/android/systemui/haptics/slider/SliderHapticFeedbackConfig.kt
+++ b/packages/SystemUI/src/com/android/systemui/haptics/slider/SliderHapticFeedbackConfig.kt
@@ -42,4 +42,6 @@
@FloatRange(from = 0.0, to = 1.0) val upperBookendScale: Float = 1f,
/** Vibration scale at the lower bookend of the slider */
@FloatRange(from = 0.0, to = 1.0) val lowerBookendScale: Float = 0.05f,
+ /** Exponent for power function compensation */
+ @FloatRange(from = 0.0, fromInclusive = false) val exponent: Float = 1f / 0.89f,
)
diff --git a/packages/SystemUI/src/com/android/systemui/haptics/slider/SliderHapticFeedbackProvider.kt b/packages/SystemUI/src/com/android/systemui/haptics/slider/SliderHapticFeedbackProvider.kt
index f313fb3..9e6245a 100644
--- a/packages/SystemUI/src/com/android/systemui/haptics/slider/SliderHapticFeedbackProvider.kt
+++ b/packages/SystemUI/src/com/android/systemui/haptics/slider/SliderHapticFeedbackProvider.kt
@@ -21,9 +21,11 @@
import android.view.VelocityTracker
import android.view.animation.AccelerateInterpolator
import androidx.annotation.FloatRange
+import androidx.annotation.VisibleForTesting
import com.android.systemui.statusbar.VibratorHelper
import kotlin.math.abs
import kotlin.math.min
+import kotlin.math.pow
/**
* Listener of slider events that triggers haptic feedback.
@@ -63,18 +65,29 @@
* @param[absoluteVelocity] Velocity of the handle when it reached the bookend.
*/
private fun vibrateOnEdgeCollision(absoluteVelocity: Float) {
+ val powerScale = scaleOnEdgeCollision(absoluteVelocity)
+ val vibration =
+ VibrationEffect.startComposition()
+ .addPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, powerScale)
+ .compose()
+ vibratorHelper.vibrate(vibration, VIBRATION_ATTRIBUTES_PIPELINING)
+ }
+
+ /**
+ * Get the velocity-based scale at the bookends
+ *
+ * @param[absoluteVelocity] Velocity of the handle when it reached the bookend.
+ * @return The power scale for the vibration.
+ */
+ @VisibleForTesting
+ fun scaleOnEdgeCollision(absoluteVelocity: Float): Float {
val velocityInterpolated =
velocityAccelerateInterpolator.getInterpolation(
min(absoluteVelocity / config.maxVelocityToScale, 1f)
)
val bookendScaleRange = config.upperBookendScale - config.lowerBookendScale
val bookendsHitScale = bookendScaleRange * velocityInterpolated + config.lowerBookendScale
-
- val vibration =
- VibrationEffect.startComposition()
- .addPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, bookendsHitScale)
- .compose()
- vibratorHelper.vibrate(vibration, VIBRATION_ATTRIBUTES_PIPELINING)
+ return bookendsHitScale.pow(config.exponent)
}
/**
@@ -96,6 +109,31 @@
val deltaProgress = abs(normalizedSliderProgress - dragTextureLastProgress)
if (deltaProgress < config.deltaProgressForDragThreshold) return
+ val powerScale = scaleOnDragTexture(absoluteVelocity, normalizedSliderProgress)
+
+ // Trigger the vibration composition
+ val composition = VibrationEffect.startComposition()
+ repeat(config.numberOfLowTicks) {
+ composition.addPrimitive(VibrationEffect.Composition.PRIMITIVE_LOW_TICK, powerScale)
+ }
+ vibratorHelper.vibrate(composition.compose(), VIBRATION_ATTRIBUTES_PIPELINING)
+ dragTextureLastTime = currentTime
+ dragTextureLastProgress = normalizedSliderProgress
+ }
+
+ /**
+ * Get the scale of the drag texture vibration.
+ *
+ * @param[absoluteVelocity] Absolute velocity of the handle.
+ * @param[normalizedSliderProgress] Progress of the slider handled normalized to the range from
+ * 0F to 1F (inclusive).
+ * @return the scale of the vibration.
+ */
+ @VisibleForTesting
+ fun scaleOnDragTexture(
+ absoluteVelocity: Float,
+ @FloatRange(from = 0.0, to = 1.0) normalizedSliderProgress: Float
+ ): Float {
val velocityInterpolated =
velocityAccelerateInterpolator.getInterpolation(
min(absoluteVelocity / config.maxVelocityToScale, 1f)
@@ -113,15 +151,7 @@
// Total scale
val scale = positionBasedScale + velocityBasedScale
-
- // Trigger the vibration composition
- val composition = VibrationEffect.startComposition()
- repeat(config.numberOfLowTicks) {
- composition.addPrimitive(VibrationEffect.Composition.PRIMITIVE_LOW_TICK, scale)
- }
- vibratorHelper.vibrate(composition.compose(), VIBRATION_ATTRIBUTES_PIPELINING)
- dragTextureLastTime = currentTime
- dragTextureLastProgress = normalizedSliderProgress
+ return scale.pow(config.exponent)
}
override fun onHandleAcquiredByTouch() {}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/haptics/slider/SliderHapticFeedbackProviderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/haptics/slider/SliderHapticFeedbackProviderTest.kt
index 7750d25..ab6bc2c 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/haptics/slider/SliderHapticFeedbackProviderTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/haptics/slider/SliderHapticFeedbackProviderTest.kt
@@ -71,7 +71,7 @@
VibrationEffect.startComposition()
.addPrimitive(
VibrationEffect.Composition.PRIMITIVE_CLICK,
- scaleAtBookends(config.maxVelocityToScale)
+ sliderHapticFeedbackProvider.scaleOnEdgeCollision(config.maxVelocityToScale),
)
.compose()
@@ -86,7 +86,7 @@
VibrationEffect.startComposition()
.addPrimitive(
VibrationEffect.Composition.PRIMITIVE_CLICK,
- scaleAtBookends(config.maxVelocityToScale)
+ sliderHapticFeedbackProvider.scaleOnEdgeCollision(config.maxVelocityToScale)
)
.compose()
@@ -102,7 +102,7 @@
VibrationEffect.startComposition()
.addPrimitive(
VibrationEffect.Composition.PRIMITIVE_CLICK,
- scaleAtBookends(config.maxVelocityToScale)
+ sliderHapticFeedbackProvider.scaleOnEdgeCollision(config.maxVelocityToScale),
)
.compose()
@@ -117,7 +117,7 @@
VibrationEffect.startComposition()
.addPrimitive(
VibrationEffect.Composition.PRIMITIVE_CLICK,
- scaleAtBookends(config.maxVelocityToScale)
+ sliderHapticFeedbackProvider.scaleOnEdgeCollision(config.maxVelocityToScale),
)
.compose()
@@ -132,7 +132,11 @@
fun playHapticAtProgress_onQuickSuccession_playsLowTicksOnce() {
// GIVEN max velocity and slider progress
val progress = 1f
- val expectedScale = scaleAtProgressChange(config.maxVelocityToScale.toFloat(), progress)
+ val expectedScale =
+ sliderHapticFeedbackProvider.scaleOnDragTexture(
+ config.maxVelocityToScale,
+ progress,
+ )
val ticks = VibrationEffect.startComposition()
repeat(config.numberOfLowTicks) {
ticks.addPrimitive(VibrationEffect.Composition.PRIMITIVE_LOW_TICK, expectedScale)
@@ -203,7 +207,11 @@
fun playHapticAtLowerBookend_afterPlayingAtProgress_playsTwice() {
// GIVEN max velocity and slider progress
val progress = 1f
- val expectedScale = scaleAtProgressChange(config.maxVelocityToScale.toFloat(), progress)
+ val expectedScale =
+ sliderHapticFeedbackProvider.scaleOnDragTexture(
+ config.maxVelocityToScale,
+ progress,
+ )
val ticks = VibrationEffect.startComposition()
repeat(config.numberOfLowTicks) {
ticks.addPrimitive(VibrationEffect.Composition.PRIMITIVE_LOW_TICK, expectedScale)
@@ -212,7 +220,7 @@
VibrationEffect.startComposition()
.addPrimitive(
VibrationEffect.Composition.PRIMITIVE_CLICK,
- scaleAtBookends(config.maxVelocityToScale)
+ sliderHapticFeedbackProvider.scaleOnEdgeCollision(config.maxVelocityToScale),
)
.compose()
@@ -232,7 +240,11 @@
fun playHapticAtUpperBookend_afterPlayingAtProgress_playsTwice() {
// GIVEN max velocity and slider progress
val progress = 1f
- val expectedScale = scaleAtProgressChange(config.maxVelocityToScale.toFloat(), progress)
+ val expectedScale =
+ sliderHapticFeedbackProvider.scaleOnDragTexture(
+ config.maxVelocityToScale,
+ progress,
+ )
val ticks = VibrationEffect.startComposition()
repeat(config.numberOfLowTicks) {
ticks.addPrimitive(VibrationEffect.Composition.PRIMITIVE_LOW_TICK, expectedScale)
@@ -241,7 +253,7 @@
VibrationEffect.startComposition()
.addPrimitive(
VibrationEffect.Composition.PRIMITIVE_CLICK,
- scaleAtBookends(config.maxVelocityToScale)
+ sliderHapticFeedbackProvider.scaleOnEdgeCollision(config.maxVelocityToScale),
)
.compose()
@@ -289,28 +301,12 @@
assertEquals(-1f, sliderHapticFeedbackProvider.dragTextureLastProgress)
}
- private fun scaleAtBookends(velocity: Float): Float {
- val range = config.upperBookendScale - config.lowerBookendScale
- val interpolatedVelocity =
- velocityInterpolator.getInterpolation(velocity / config.maxVelocityToScale)
- return interpolatedVelocity * range + config.lowerBookendScale
- }
-
- private fun scaleAtProgressChange(velocity: Float, progress: Float): Float {
- val range = config.progressBasedDragMaxScale - config.progressBasedDragMinScale
- val interpolatedVelocity =
- velocityInterpolator.getInterpolation(velocity / config.maxVelocityToScale)
- val interpolatedProgress = progressInterpolator.getInterpolation(progress)
- val bump = interpolatedVelocity * config.additionalVelocityMaxBump
- return interpolatedProgress * range + config.progressBasedDragMinScale + bump
- }
-
private fun generateTicksComposition(velocity: Float, progress: Float): VibrationEffect {
val ticks = VibrationEffect.startComposition()
repeat(config.numberOfLowTicks) {
ticks.addPrimitive(
VibrationEffect.Composition.PRIMITIVE_LOW_TICK,
- scaleAtProgressChange(velocity, progress)
+ sliderHapticFeedbackProvider.scaleOnDragTexture(velocity, progress),
)
}
return ticks.compose()