Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 9144c23b authored by Juan Sebastian Martinez's avatar Juan Sebastian Martinez
Browse files

Adding a SliderDragVelocityProvider for slider haptics

The provider is a small abstraction around different methods to obtain
the velocity with which a slider is being dragged. This can make the
SliderHapticFeedbackProvider usable for SeekBars as well as Slider
composables. Velocity computation can be done in any arbitrary unit
instead of only pixels. This is now reflected on the
SliderHapticFeedbackConfig docstring

Test: atest SystemUITests:SliderHapticFeedbackProviderTest
Flag: NONE minor refactor to adapt for future usages
Bug: 356389497
Change-Id: I4d31923c2cf9eecb138dac36af02d019c9a6f831
parent b13bf71c
Loading
Loading
Loading
Loading
+14 −41
Original line number Diff line number Diff line
@@ -17,13 +17,11 @@
package com.android.systemui.haptics.slider

import android.os.VibrationEffect
import android.view.VelocityTracker
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.haptics.fakeVibratorHelper
import com.android.systemui.testKosmos
import com.android.systemui.util.mockito.whenever
import com.android.systemui.util.time.fakeSystemClock
import kotlin.math.max
import kotlin.test.assertEquals
@@ -31,19 +29,17 @@ import kotlin.test.assertTrue
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mock
import org.mockito.MockitoAnnotations

@SmallTest
@RunWith(AndroidJUnit4::class)
class SliderHapticFeedbackProviderTest : SysuiTestCase() {

    @Mock private lateinit var velocityTracker: VelocityTracker

    private val kosmos = testKosmos()

    private val config = SliderHapticFeedbackConfig()

    private val dragVelocityProvider = SliderDragVelocityProvider { config.maxVelocityToScale }

    private val lowTickDuration = 12 // Mocked duration of a low tick
    private val dragTextureThresholdMillis =
        lowTickDuration * config.numberOfLowTicks + config.deltaMillisForDragInterval
@@ -52,17 +48,13 @@ class SliderHapticFeedbackProviderTest : SysuiTestCase() {

    @Before
    fun setup() {
        MockitoAnnotations.initMocks(this)
        whenever(velocityTracker.isAxisSupported(config.velocityAxis)).thenReturn(true)
        whenever(velocityTracker.getAxisVelocity(config.velocityAxis))
            .thenReturn(config.maxVelocityToScale)

        vibratorHelper.primitiveDurations[VibrationEffect.Composition.PRIMITIVE_LOW_TICK] =
            lowTickDuration
        sliderHapticFeedbackProvider =
            SliderHapticFeedbackProvider(
                vibratorHelper,
                velocityTracker,
                dragVelocityProvider,
                config,
                kosmos.fakeSystemClock,
            )
@@ -75,9 +67,7 @@ class SliderHapticFeedbackProviderTest : SysuiTestCase() {
                VibrationEffect.startComposition()
                    .addPrimitive(
                        VibrationEffect.Composition.PRIMITIVE_CLICK,
                        sliderHapticFeedbackProvider.scaleOnEdgeCollision(
                            config.maxVelocityToScale
                        ),
                        sliderHapticFeedbackProvider.scaleOnEdgeCollision(config.maxVelocityToScale),
                    )
                    .compose()

@@ -93,7 +83,7 @@ class SliderHapticFeedbackProviderTest : SysuiTestCase() {
                VibrationEffect.startComposition()
                    .addPrimitive(
                        VibrationEffect.Composition.PRIMITIVE_CLICK,
                        sliderHapticFeedbackProvider.scaleOnEdgeCollision(config.maxVelocityToScale)
                        sliderHapticFeedbackProvider.scaleOnEdgeCollision(config.maxVelocityToScale),
                    )
                    .compose()

@@ -110,9 +100,7 @@ class SliderHapticFeedbackProviderTest : SysuiTestCase() {
                VibrationEffect.startComposition()
                    .addPrimitive(
                        VibrationEffect.Composition.PRIMITIVE_CLICK,
                        sliderHapticFeedbackProvider.scaleOnEdgeCollision(
                            config.maxVelocityToScale
                        ),
                        sliderHapticFeedbackProvider.scaleOnEdgeCollision(config.maxVelocityToScale),
                    )
                    .compose()

@@ -128,9 +116,7 @@ class SliderHapticFeedbackProviderTest : SysuiTestCase() {
                VibrationEffect.startComposition()
                    .addPrimitive(
                        VibrationEffect.Composition.PRIMITIVE_CLICK,
                        sliderHapticFeedbackProvider.scaleOnEdgeCollision(
                            config.maxVelocityToScale
                        ),
                        sliderHapticFeedbackProvider.scaleOnEdgeCollision(config.maxVelocityToScale),
                    )
                    .compose()

@@ -146,10 +132,7 @@ class SliderHapticFeedbackProviderTest : SysuiTestCase() {
            // GIVEN max velocity and slider progress
            val progress = 1f
            val expectedScale =
                sliderHapticFeedbackProvider.scaleOnDragTexture(
                    config.maxVelocityToScale,
                    progress,
                )
                sliderHapticFeedbackProvider.scaleOnDragTexture(config.maxVelocityToScale, progress)
            val ticks = VibrationEffect.startComposition()
            repeat(config.numberOfLowTicks) {
                ticks.addPrimitive(VibrationEffect.Composition.PRIMITIVE_LOW_TICK, expectedScale)
@@ -222,10 +205,7 @@ class SliderHapticFeedbackProviderTest : SysuiTestCase() {
            // GIVEN max velocity and slider progress
            val progress = 1f
            val expectedScale =
                sliderHapticFeedbackProvider.scaleOnDragTexture(
                    config.maxVelocityToScale,
                    progress,
                )
                sliderHapticFeedbackProvider.scaleOnDragTexture(config.maxVelocityToScale, progress)
            val ticks = VibrationEffect.startComposition()
            repeat(config.numberOfLowTicks) {
                ticks.addPrimitive(VibrationEffect.Composition.PRIMITIVE_LOW_TICK, expectedScale)
@@ -234,9 +214,7 @@ class SliderHapticFeedbackProviderTest : SysuiTestCase() {
                VibrationEffect.startComposition()
                    .addPrimitive(
                        VibrationEffect.Composition.PRIMITIVE_CLICK,
                        sliderHapticFeedbackProvider.scaleOnEdgeCollision(
                            config.maxVelocityToScale
                        ),
                        sliderHapticFeedbackProvider.scaleOnEdgeCollision(config.maxVelocityToScale),
                    )
                    .compose()

@@ -250,7 +228,7 @@ class SliderHapticFeedbackProviderTest : SysuiTestCase() {
            // THEN there are two bookend vibrations
            assertEquals(
                /* expected= */ 2,
                vibratorHelper.timesVibratedWithEffect(bookendVibration)
                vibratorHelper.timesVibratedWithEffect(bookendVibration),
            )
        }

@@ -260,10 +238,7 @@ class SliderHapticFeedbackProviderTest : SysuiTestCase() {
            // GIVEN max velocity and slider progress
            val progress = 1f
            val expectedScale =
                sliderHapticFeedbackProvider.scaleOnDragTexture(
                    config.maxVelocityToScale,
                    progress,
                )
                sliderHapticFeedbackProvider.scaleOnDragTexture(config.maxVelocityToScale, progress)
            val ticks = VibrationEffect.startComposition()
            repeat(config.numberOfLowTicks) {
                ticks.addPrimitive(VibrationEffect.Composition.PRIMITIVE_LOW_TICK, expectedScale)
@@ -272,9 +247,7 @@ class SliderHapticFeedbackProviderTest : SysuiTestCase() {
                VibrationEffect.startComposition()
                    .addPrimitive(
                        VibrationEffect.Composition.PRIMITIVE_CLICK,
                        sliderHapticFeedbackProvider.scaleOnEdgeCollision(
                            config.maxVelocityToScale
                        ),
                        sliderHapticFeedbackProvider.scaleOnEdgeCollision(config.maxVelocityToScale),
                    )
                    .compose()

@@ -288,7 +261,7 @@ class SliderHapticFeedbackProviderTest : SysuiTestCase() {
            // THEN there are two bookend vibrations
            assertEquals(
                /* expected= */ 2,
                vibratorHelper.timesVibratedWithEffect(bookendVibration)
                vibratorHelper.timesVibratedWithEffect(bookendVibration),
            )
        }

+14 −1
Original line number Diff line number Diff line
@@ -46,12 +46,24 @@ constructor(

    private val velocityTracker = VelocityTracker.obtain()

    private val dragVelocityProvider = SliderDragVelocityProvider {
        velocityTracker.computeCurrentVelocity(
            UNITS_SECOND,
            sliderHapticFeedbackConfig.maxVelocityToScale,
        )
        if (velocityTracker.isAxisSupported(sliderHapticFeedbackConfig.velocityAxis)) {
            velocityTracker.getAxisVelocity(sliderHapticFeedbackConfig.velocityAxis)
        } else {
            0f
        }
    }

    private val sliderEventProducer = SliderStateProducer()

    private val sliderHapticFeedbackProvider =
        SliderHapticFeedbackProvider(
            vibratorHelper,
            velocityTracker,
            dragVelocityProvider,
            sliderHapticFeedbackConfig,
            systemClock,
        )
@@ -188,5 +200,6 @@ constructor(

    companion object {
        const val KEY_UP_TIMEOUT = 60L
        private const val UNITS_SECOND = 1000
    }
}
+28 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2024 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.systemui.haptics.slider

/** A provider of the velocity at which a slider is being dragged */
fun interface SliderDragVelocityProvider {

    /**
     * Get the velocity of the slider at the time this function is called.
     *
     * @return the velocity of the drag in pixels/sec
     */
    fun getTrackedVelocity(): Float
}
+1 −1
Original line number Diff line number Diff line
@@ -38,7 +38,7 @@ data class SliderHapticFeedbackConfig(
    /** Number of low ticks in a drag texture composition. This is not expected to change */
    val numberOfLowTicks: Int = 5,
    /** Maximum velocity allowed for vibration scaling. This is not expected to change. */
    val maxVelocityToScale: Float = 2000f, /* In pixels/sec */
    val maxVelocityToScale: Float = 2000f, /* In units/sec. The default units are pixels */
    /** Axis to use when computing velocity. Must be the same as the slider's axis of movement */
    val velocityAxis: Int = MotionEvent.AXIS_X,
    /** Vibration scale at the upper bookend of the slider */
+7 −16
Original line number Diff line number Diff line
@@ -38,7 +38,7 @@ import kotlin.math.pow
 */
class SliderHapticFeedbackProvider(
    private val vibratorHelper: VibratorHelper,
    private val velocityTracker: VelocityTracker,
    private val velocityProvider: SliderDragVelocityProvider,
    private val config: SliderHapticFeedbackConfig = SliderHapticFeedbackConfig(),
    private val clock: com.android.systemui.util.time.SystemClock,
) : SliderStateListener {
@@ -50,6 +50,7 @@ class SliderHapticFeedbackProvider(
    private var dragTextureLastTime = clock.elapsedRealtime()
    var dragTextureLastProgress = -1f
        private set

    private val lowTickDurationMs =
        vibratorHelper.getPrimitiveDurations(VibrationEffect.Composition.PRIMITIVE_LOW_TICK)[0]
    private var hasVibratedAtLowerBookend = false
@@ -99,7 +100,7 @@ class SliderHapticFeedbackProvider(
     */
    private fun vibrateDragTexture(
        absoluteVelocity: Float,
        @FloatRange(from = 0.0, to = 1.0) normalizedSliderProgress: Float
        @FloatRange(from = 0.0, to = 1.0) normalizedSliderProgress: Float,
    ) {
        // Check if its time to vibrate
        val currentTime = clock.elapsedRealtime()
@@ -132,7 +133,7 @@ class SliderHapticFeedbackProvider(
    @VisibleForTesting
    fun scaleOnDragTexture(
        absoluteVelocity: Float,
        @FloatRange(from = 0.0, to = 1.0) normalizedSliderProgress: Float
        @FloatRange(from = 0.0, to = 1.0) normalizedSliderProgress: Float,
    ): Float {
        val velocityInterpolated =
            velocityAccelerateInterpolator.getInterpolation(
@@ -162,33 +163,24 @@ class SliderHapticFeedbackProvider(

    override fun onLowerBookend() {
        if (!hasVibratedAtLowerBookend) {
            vibrateOnEdgeCollision(abs(getTrackedVelocity()))
            vibrateOnEdgeCollision(abs(velocityProvider.getTrackedVelocity()))
            hasVibratedAtLowerBookend = true
        }
    }

    override fun onUpperBookend() {
        if (!hasVibratedAtUpperBookend) {
            vibrateOnEdgeCollision(abs(getTrackedVelocity()))
            vibrateOnEdgeCollision(abs(velocityProvider.getTrackedVelocity()))
            hasVibratedAtUpperBookend = true
        }
    }

    override fun onProgress(@FloatRange(from = 0.0, to = 1.0) progress: Float) {
        vibrateDragTexture(abs(getTrackedVelocity()), progress)
        vibrateDragTexture(abs(velocityProvider.getTrackedVelocity()), progress)
        hasVibratedAtUpperBookend = false
        hasVibratedAtLowerBookend = false
    }

    private fun getTrackedVelocity(): Float {
        velocityTracker.computeCurrentVelocity(UNITS_SECOND, config.maxVelocityToScale)
        return if (velocityTracker.isAxisSupported(config.velocityAxis)) {
            velocityTracker.getAxisVelocity(config.velocityAxis)
        } else {
            0f
        }
    }

    override fun onProgressJump(@FloatRange(from = 0.0, to = 1.0) progress: Float) {}

    override fun onSelectAndArrow(@FloatRange(from = 0.0, to = 1.0) progress: Float) {}
@@ -199,6 +191,5 @@ class SliderHapticFeedbackProvider(
                .setUsage(VibrationAttributes.USAGE_TOUCH)
                .setFlags(VibrationAttributes.FLAG_PIPELINED_EFFECT)
                .build()
        private const val UNITS_SECOND = 1000
    }
}