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

Commit 93780a19 authored by Alejandro Nijamkin's avatar Alejandro Nijamkin
Browse files

[Media] Reject seek bar scrubbing if too vertical.

If a drag gesture begins on the seek bar and that drag gesture ends up
being more vertical than horizontal, don't commit the result of the
scrubbing when the drag gesture ends.

As it turns out, there's no support for this in the Slider composable
from the Material library. It just reports onValueChangeFinished
regardless of how vertical the drag was; it doesn't even have a
mechanism by which to observe or receive the actual delta.

Therefore, I went around it. I used .pointerInput which is the low-level
user input API in Compose to just watch for all gestures on the Slider.
Then, I collected and reported the drag delta to the view-model so it
has the information it needs to make its own decision when it handles the
onValueChangeFinished.

Bug: 397989775
Test: manually verified in the Compose Gallery app - starting a drag in
the bounds of the seek bar, dragging too far up or down, and releasing
reverts the position back to where it first was. Also verified that
doing the same but going more horizontally than vertically works
correctly.
Flag: EXEMPT - code not yet used in production code.

Change-Id: I5998cc8dd161769036f7e3a654394df24c9f9b2d
parent f20414a2
Loading
Loading
Loading
Loading
+49 −4
Original line number Original line Diff line number Diff line
@@ -94,6 +94,8 @@ import androidx.compose.ui.graphics.drawscope.clipRect
import androidx.compose.ui.graphics.drawscope.translate
import androidx.compose.ui.graphics.drawscope.translate
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.input.pointer.PointerEventPass
import androidx.compose.ui.input.pointer.PointerEventPass
import androidx.compose.ui.input.pointer.PointerEventType
import androidx.compose.ui.input.pointer.PointerInputChange
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.layout.Layout
import androidx.compose.ui.layout.Layout
@@ -663,11 +665,20 @@ private fun ContentScope.Navigation(
                if (isSeekBarVisible) {
                if (isSeekBarVisible) {
                    // To allow the seek bar slider to fade in and out, it's tagged as an element.
                    // To allow the seek bar slider to fade in and out, it's tagged as an element.
                    Element(key = Media.Elements.SeekBarSlider, modifier = Modifier.weight(1f)) {
                    Element(key = Media.Elements.SeekBarSlider, modifier = Modifier.weight(1f)) {
                        val sliderDragDelta = remember {
                            // Not a mutableStateOf - this is never accessed in composition and
                            // using an anonymous object avoids generics boxing of inline Offset.
                            object {
                                var value = Offset.Zero
                            }
                        }
                        Slider(
                        Slider(
                            interactionSource = interactionSource,
                            interactionSource = interactionSource,
                            value = viewModel.progress,
                            value = viewModel.progress,
                            onValueChange = { progress -> viewModel.onScrubChange(progress) },
                            onValueChange = { progress -> viewModel.onScrubChange(progress) },
                            onValueChangeFinished = { viewModel.onScrubFinished() },
                            onValueChangeFinished = {
                                viewModel.onScrubFinished(sliderDragDelta.value)
                            },
                            colors = colors,
                            colors = colors,
                            thumb = {
                            thumb = {
                                SeekBarThumb(interactionSource = interactionSource, colors = colors)
                                SeekBarThumb(interactionSource = interactionSource, colors = colors)
@@ -681,8 +692,42 @@ private fun ContentScope.Navigation(
                                )
                                )
                            },
                            },
                            modifier =
                            modifier =
                                Modifier.fillMaxWidth().clearAndSetSemantics {
                                Modifier.fillMaxWidth()
                                    .clearAndSetSemantics {
                                        contentDescription = viewModel.contentDescription
                                        contentDescription = viewModel.contentDescription
                                    }
                                    .pointerInput(Unit) {
                                        // Track and report the drag delta to the view-model so it
                                        // can
                                        // decide whether to accept the next onValueChangeFinished
                                        // or
                                        // reject it if the drag was overly vertical.
                                        awaitPointerEventScope {
                                            var down: PointerInputChange? = null
                                            while (true) {
                                                val event =
                                                    awaitPointerEvent(PointerEventPass.Initial)
                                                when (event.type) {
                                                    PointerEventType.Press -> {
                                                        // A new gesture has begun. Record the
                                                        // initial
                                                        // down input change.
                                                        down = event.changes.last()
                                                    }

                                                    PointerEventType.Move -> {
                                                        // The pointer has moved. If it's the same
                                                        // pointer as the latest down, calculate and
                                                        // report the drag delta.
                                                        val change = event.changes.last()
                                                        if (change.id == down?.id) {
                                                            sliderDragDelta.value =
                                                                change.position - down.position
                                                        }
                                                    }
                                                }
                                            }
                                        }
                                    },
                                    },
                        )
                        )
                    }
                    }
+2 −1
Original line number Original line Diff line number Diff line
@@ -17,6 +17,7 @@
package com.android.systemui.media.remedia.ui.viewmodel
package com.android.systemui.media.remedia.ui.viewmodel


import androidx.annotation.FloatRange
import androidx.annotation.FloatRange
import androidx.compose.ui.geometry.Offset


/**
/**
 * Models UI state for the navigation component of the UI (potentially containing the seek bar and
 * Models UI state for the navigation component of the UI (potentially containing the seek bar and
@@ -58,7 +59,7 @@ sealed interface MediaNavigationViewModel {
         * A callback to invoke once the user finishes "scrubbing" (e.g. stopped moving the thumb of
         * A callback to invoke once the user finishes "scrubbing" (e.g. stopped moving the thumb of
         * the seek bar). The position/progress should be committed.
         * the seek bar). The position/progress should be committed.
         */
         */
        val onScrubFinished: () -> Unit,
        val onScrubFinished: (delta: Offset) -> Unit,
        /** Accessibility string to attach to the seekbar UI element. */
        /** Accessibility string to attach to the seekbar UI element. */
        val contentDescription: String,
        val contentDescription: String,
    ) : MediaNavigationViewModel
    ) : MediaNavigationViewModel
+15 −3
Original line number Original line Diff line number Diff line
@@ -26,9 +26,9 @@ import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.compose.runtime.setValue
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.graphics.ImageBitmap
import com.android.systemui.classifier.Classifier
import com.android.systemui.classifier.Classifier
import com.android.systemui.classifier.domain.interactor.runIfNotFalseTap
import com.android.systemui.common.shared.model.ContentDescription
import com.android.systemui.common.shared.model.ContentDescription
import com.android.systemui.common.shared.model.Icon
import com.android.systemui.common.shared.model.Icon
import com.android.systemui.lifecycle.ExclusiveActivatable
import com.android.systemui.lifecycle.ExclusiveActivatable
@@ -42,6 +42,7 @@ import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import dagger.assisted.AssistedInject
import java.util.Locale
import java.util.Locale
import kotlin.math.abs
import kotlin.math.roundToLong
import kotlin.math.roundToLong
import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.Duration.Companion.milliseconds
import kotlinx.coroutines.awaitCancellation
import kotlinx.coroutines.awaitCancellation
@@ -114,8 +115,11 @@ constructor(
                                    isScrubbing = true
                                    isScrubbing = true
                                    seekProgress = progress
                                    seekProgress = progress
                                },
                                },
                                onScrubFinished = {
                                onScrubFinished = { dragDelta ->
                                    if (!falsingSystem.isFalseTouch(Classifier.MEDIA_SEEKBAR)) {
                                    if (
                                        dragDelta.isHorizontal() &&
                                            !falsingSystem.isFalseTouch(Classifier.MEDIA_SEEKBAR)
                                    ) {
                                        interactor.seek(
                                        interactor.seek(
                                            sessionKey = session.key,
                                            sessionKey = session.key,
                                            to = (seekProgress * session.durationMs).roundToLong(),
                                            to = (seekProgress * session.durationMs).roundToLong(),
@@ -346,6 +350,14 @@ constructor(
            .formatMeasures(*measures.toTypedArray())
            .formatMeasures(*measures.toTypedArray())
    }
    }


    /**
     * Returns `true` if this [Offset] is the same or larger on the horizontal axis than the
     * vertical axis.
     */
    private fun Offset.isHorizontal(): Boolean {
        return abs(x) >= abs(y)
    }

    interface FalsingSystem {
    interface FalsingSystem {
        fun runIfNotFalseTap(@FalsingManager.Penalty penalty: Int, block: () -> Unit)
        fun runIfNotFalseTap(@FalsingManager.Penalty penalty: Int, block: () -> Unit)