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

Commit dfd9d303 authored by Anton Potapov's avatar Anton Potapov
Browse files

Unify slider control between Volume Panel and Volume Dialog

The new Volume Dialog and Volume Panel will both use the same Slider
control, that handles haptics, a11y and animated snapping. This change
mostly copies the VolumeSlider used in the Volume Panel and adapts it
for the Volume Dialog.

Flag: com.android.systemui.volume_redesign
Fixes: 393133850
Fixes: 395578673
Test: atest VolumeDialogScreenshotTest
Test: atest VolumePanelScreenshotTest
Test: manual on foldable
Change-Id: I31fb77d391f7da78c18cc70e7da8a87f84f6c0fc
parent 899253a8
Loading
Loading
Loading
Loading
+27 −64
Original line number Diff line number Diff line
@@ -35,9 +35,9 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon as MaterialIcon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Slider
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
@@ -73,11 +73,15 @@ import com.android.systemui.lifecycle.rememberViewModel
import com.android.systemui.res.R
import com.android.systemui.volume.haptics.ui.VolumeHapticsConfigsProvider
import com.android.systemui.volume.panel.component.volume.slider.ui.viewmodel.SliderState
import com.android.systemui.volume.ui.slider.AccessibilityParams
import com.android.systemui.volume.ui.slider.Haptics
import com.android.systemui.volume.ui.slider.Slider
import kotlin.math.round
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.map

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun VolumeSlider(
    state: SliderState,
@@ -102,17 +106,6 @@ fun VolumeSlider(
        return
    }

    val value by valueState(state)
    val interactionSource = remember { MutableInteractionSource() }
    val hapticsViewModel: SliderHapticsViewModel? =
        setUpHapticsViewModel(
            value,
            state.valueRange,
            state.hapticFilter,
            interactionSource,
            hapticsViewModelFactory,
        )

    Column(modifier = modifier.animateContentSize(), verticalArrangement = Arrangement.Top) {
        Row(
            horizontalArrangement = Arrangement.spacedBy(12.dp),
@@ -134,60 +127,30 @@ fun VolumeSlider(
            )
            button?.invoke()
        }

        Slider(
            value = value,
            value = state.value,
            valueRange = state.valueRange,
            onValueChange = { newValue ->
                hapticsViewModel?.addVelocityDataPoint(newValue)
                onValueChange(newValue)
            },
            onValueChangeFinished = {
                hapticsViewModel?.onValueChangeEnded()
                onValueChangeFinished?.invoke()
            },
            enabled = state.isEnabled,
            modifier =
                Modifier.height(40.dp)
                    .padding(top = 4.dp, bottom = 12.dp)
                    .sysuiResTag(state.label)
                    .clearAndSetSemantics {
                        if (state.isEnabled) {
                            contentDescription = state.label
                            state.a11yClickDescription?.let {
                                customActions =
                                    listOf(
                                        CustomAccessibilityAction(it) {
                                            onIconTapped()
                                            true
                                        }
                                    )
                            }

                            state.a11yStateDescription?.let { stateDescription = it }
                            progressBarRangeInfo =
                                ProgressBarRangeInfo(state.value, state.valueRange)
                        } else {
                            disabled()
                            contentDescription =
                                state.disabledMessage?.let { "${state.label}, $it" } ?: state.label
                        }
                        setProgress { targetValue ->
                            val targetDirection =
                                when {
                                    targetValue > value -> 1
                                    targetValue < value -> -1
                                    else -> 0
                                }

                            val newValue =
                                (value + targetDirection * state.a11yStep).coerceIn(
                                    state.valueRange.start,
                                    state.valueRange.endInclusive,
            onValueChanged = onValueChange,
            onValueChangeFinished = { onValueChangeFinished?.invoke() },
            isEnabled = state.isEnabled,
            stepDistance = state.a11yStep,
            accessibilityParams =
                AccessibilityParams(
                    label = state.label,
                    disabledMessage = state.disabledMessage,
                    currentStateDescription = state.a11yStateDescription,
                ),
            haptics =
                hapticsViewModelFactory?.let {
                    Haptics.Enabled(
                        hapticsViewModelFactory = it,
                        hapticFilter = state.hapticFilter,
                        orientation = Orientation.Horizontal,
                    )
                            onValueChange(newValue)
                            true
                        }
                    },
                } ?: Haptics.Disabled,
            modifier =
                Modifier.height(40.dp).padding(top = 4.dp, bottom = 12.dp).sysuiResTag(state.label),
        )
        state.disabledMessage?.let { disabledMessage ->
            AnimatedVisibility(visible = !state.isEnabled) {
@@ -348,7 +311,7 @@ private fun SliderIcon(
}

@Composable
fun setUpHapticsViewModel(
private fun setUpHapticsViewModel(
    value: Float,
    valueRange: ClosedFloatingPointRange<Float>,
    hapticFilter: SliderHapticFeedbackFilter,
+3 −0
Original line number Diff line number Diff line
@@ -4178,4 +4178,7 @@
    <string name="qs_edit_mode_reset_dialog_content">
        All Quick Settings tiles will reset to the device’s original settings
    </string>

    <!-- Template that joins disabled message with the label for the voice over. [CHAR LIMIT=NONE] -->
    <string name="volume_slider_disabled_message_template"><xliff:g example="Notification" id="stream_name">%1$s</xliff:g>, <xliff:g example="Disabled because ring is muted" id="disabled_message">%2$s</xliff:g></string>
</resources>
+43 −76
Original line number Diff line number Diff line
@@ -29,18 +29,13 @@ import androidx.compose.foundation.layout.BoxScope
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.SliderDefaults
import androidx.compose.material3.SliderState
import androidx.compose.material3.VerticalSlider
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.pointer.pointerInput
@@ -49,16 +44,17 @@ import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.android.compose.theme.PlatformTheme
import com.android.compose.ui.graphics.painter.DrawablePainter
import com.android.systemui.haptics.slider.SliderHapticFeedbackFilter
import com.android.systemui.haptics.slider.compose.ui.SliderHapticsViewModel
import com.android.systemui.lifecycle.rememberViewModel
import com.android.systemui.res.R
import com.android.systemui.volume.dialog.sliders.dagger.VolumeDialogSliderScope
import com.android.systemui.volume.dialog.sliders.ui.compose.VolumeDialogSliderTrack
import com.android.systemui.volume.dialog.sliders.ui.viewmodel.VolumeDialogOverscrollViewModel
import com.android.systemui.volume.dialog.sliders.ui.viewmodel.VolumeDialogSliderViewModel
import com.android.systemui.volume.haptics.ui.VolumeHapticsConfigsProvider
import com.android.systemui.volume.ui.slider.AccessibilityParams
import com.android.systemui.volume.ui.slider.Haptics
import com.android.systemui.volume.ui.slider.Slider
import javax.inject.Inject
import kotlin.math.round
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.currentCoroutineContext
import kotlinx.coroutines.isActive
@@ -90,7 +86,7 @@ constructor(
    }
}

@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun VolumeDialogSlider(
    viewModel: VolumeDialogSliderViewModel,
@@ -108,59 +104,8 @@ private fun VolumeDialogSlider(
        )
    val collectedSliderStateModel by viewModel.state.collectAsStateWithLifecycle(null)
    val sliderStateModel = collectedSliderStateModel ?: return

    val steps = with(sliderStateModel.valueRange) { endInclusive - start - 1 }.toInt()

    val interactionSource = remember { MutableInteractionSource() }
    val hapticsViewModel: SliderHapticsViewModel? =
        hapticsViewModelFactory?.let {
            rememberViewModel(traceName = "SliderHapticsViewModel") {
                it.create(
                    interactionSource,
                    sliderStateModel.valueRange,
                    Orientation.Vertical,
                    VolumeHapticsConfigsProvider.sliderHapticFeedbackConfig(
                        sliderStateModel.valueRange
                    ),
                    VolumeHapticsConfigsProvider.seekableSliderTrackerConfig,
                )
            }
        }

    val sliderState =
        remember(steps, sliderStateModel.valueRange) {
            SliderState(
                    value = sliderStateModel.value,
                    valueRange = sliderStateModel.valueRange,
                    steps = steps,
                )
                .also { sliderState ->
                    sliderState.onValueChangeFinished = {
                        viewModel.onSliderChangeFinished(sliderState.value)
                        hapticsViewModel?.onValueChangeEnded()
                    }
                    sliderState.onValueChange = { newValue ->
                        sliderState.value = newValue
                        hapticsViewModel?.addVelocityDataPoint(newValue)
                        overscrollViewModel.setSlider(
                            value = sliderState.value,
                            min = sliderState.valueRange.start,
                            max = sliderState.valueRange.endInclusive,
                        )
                        viewModel.setStreamVolume(newValue, true)
                    }
                }
        }

    var lastDiscreteStep by remember { mutableFloatStateOf(round(sliderStateModel.value)) }
    LaunchedEffect(sliderStateModel.value) {
        val value = sliderStateModel.value
        sliderState.value = value
        if (value != lastDiscreteStep) {
            lastDiscreteStep = value
            hapticsViewModel?.onValueChange(value)
        }
    }
    LaunchedEffect(interactionSource) {
        interactionSource.interactions.collect {
            when (it) {
@@ -171,24 +116,33 @@ private fun VolumeDialogSlider(
        }
    }

    VerticalSlider(
        state = sliderState,
        enabled = !sliderStateModel.isDisabled,
        reverseDirection = true,
    Slider(
        value = sliderStateModel.value,
        valueRange = sliderStateModel.valueRange,
        onValueChanged = { value ->
            overscrollViewModel.setSlider(
                value = value,
                min = sliderStateModel.valueRange.start,
                max = sliderStateModel.valueRange.endInclusive,
            )
            viewModel.setStreamVolume(value, true)
        },
        onValueChangeFinished = { viewModel.onSliderChangeFinished(it) },
        isEnabled = !sliderStateModel.isDisabled,
        isReverseDirection = true,
        isVertical = true,
        colors = colors,
        interactionSource = interactionSource,
        modifier =
            modifier.pointerInput(Unit) {
                coroutineScope {
                    val currentContext = currentCoroutineContext()
                    awaitPointerEventScope {
                        while (currentContext.isActive) {
                            viewModel.onTouchEvent(awaitPointerEvent())
                        }
                    }
                }
            },
        track = {
        haptics =
            hapticsViewModelFactory?.let {
                Haptics.Enabled(
                    hapticsViewModelFactory = it,
                    hapticFilter = SliderHapticFeedbackFilter(),
                    orientation = Orientation.Vertical,
                )
            } ?: Haptics.Disabled,
        stepDistance = 1f,
        track = { sliderState ->
            VolumeDialogSliderTrack(
                sliderState,
                colors = colors,
@@ -201,6 +155,19 @@ private fun VolumeDialogSlider(
                },
            )
        },
        accessibilityParams =
            AccessibilityParams(label = "", currentStateDescription = "", disabledMessage = ""),
        modifier =
            modifier.pointerInput(Unit) {
                coroutineScope {
                    val currentContext = currentCoroutineContext()
                    awaitPointerEventScope {
                        while (currentContext.isActive) {
                            viewModel.onTouchEvent(awaitPointerEvent())
                        }
                    }
                }
            },
    )
}

+2 −2
Original line number Diff line number Diff line
@@ -116,8 +116,8 @@ constructor(
        override val isEnabled: Boolean
            get() = true

        override val a11yStep: Int
            get() = 1
        override val a11yStep: Float
            get() = 1f

        override val disabledMessage: String?
            get() = null
+2 −2
Original line number Diff line number Diff line
@@ -165,7 +165,7 @@ constructor(
            label = label,
            disabledMessage = disabledMessage,
            isEnabled = isEnabled,
            a11yStep = volumeRange.step,
            a11yStep = volumeRange.step.toFloat(),
            a11yClickDescription =
                if (isAffectedByMute) {
                    context.getString(
@@ -307,7 +307,7 @@ constructor(
        override val label: String,
        override val disabledMessage: String?,
        override val isEnabled: Boolean,
        override val a11yStep: Int,
        override val a11yStep: Float,
        override val a11yClickDescription: String?,
        override val a11yStateDescription: String?,
        override val isMutable: Boolean,
Loading