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

Commit 6045eeb4 authored by Anton Potapov's avatar Anton Potapov Committed by Android (Google) Code Review
Browse files

Merge "Rework VolumePanelRadioButtons using Layout" into main

parents 8d8f3655 97cdc0c8
Loading
Loading
Loading
Loading
+165 −86
Original line number Original line Diff line number Diff line
@@ -16,38 +16,38 @@


package com.android.systemui.volume.panel.component.selector.ui.composable
package com.android.systemui.volume.panel.component.selector.ui.composable


import androidx.compose.animation.core.animateOffsetAsState
import androidx.compose.animation.core.Animatable
import androidx.compose.foundation.Canvas
import androidx.compose.animation.core.VectorConverter
import androidx.compose.foundation.background
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.IntrinsicSize
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.CornerSize
import androidx.compose.foundation.shape.CornerSize
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.TextButton
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.CornerRadius
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.layout.Layout
import androidx.compose.ui.layout.Measurable
import androidx.compose.ui.layout.MeasurePolicy
import androidx.compose.ui.layout.MeasureResult
import androidx.compose.ui.layout.MeasureScope
import androidx.compose.ui.layout.Placeable
import androidx.compose.ui.layout.layoutId
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.util.fastFirst
import kotlinx.coroutines.launch


/**
/**
 * Radio button group for the Volume Panel. It allows selecting a single item
 * Radio button group for the Volume Panel. It allows selecting a single item
@@ -65,8 +65,8 @@ fun VolumePanelRadioButtonBar(
    spacing: Dp = VolumePanelRadioButtonBarDefaults.DefaultSpacing,
    spacing: Dp = VolumePanelRadioButtonBarDefaults.DefaultSpacing,
    labelIndicatorBackgroundSpacing: Dp =
    labelIndicatorBackgroundSpacing: Dp =
        VolumePanelRadioButtonBarDefaults.DefaultLabelIndicatorBackgroundSpacing,
        VolumePanelRadioButtonBarDefaults.DefaultLabelIndicatorBackgroundSpacing,
    indicatorCornerRadius: CornerRadius =
    indicatorCornerSize: CornerSize =
        VolumePanelRadioButtonBarDefaults.defaultIndicatorCornerRadius(),
        CornerSize(VolumePanelRadioButtonBarDefaults.DefaultIndicatorCornerRadius),
    indicatorBackgroundCornerSize: CornerSize =
    indicatorBackgroundCornerSize: CornerSize =
        CornerSize(VolumePanelRadioButtonBarDefaults.DefaultIndicatorBackgroundCornerRadius),
        CornerSize(VolumePanelRadioButtonBarDefaults.DefaultIndicatorBackgroundCornerRadius),
    colors: VolumePanelRadioButtonBarColors = VolumePanelRadioButtonBarDefaults.defaultColors(),
    colors: VolumePanelRadioButtonBarColors = VolumePanelRadioButtonBarDefaults.defaultColors(),
@@ -76,60 +76,41 @@ fun VolumePanelRadioButtonBar(
        VolumePanelRadioButtonBarScopeImpl().apply(content).apply {
        VolumePanelRadioButtonBarScopeImpl().apply(content).apply {
            require(hasSelectedItem) { "At least one item should be selected" }
            require(hasSelectedItem) { "At least one item should be selected" }
        }
        }

    val items = scope.items
    val items = scope.items


    var selectedIndex by remember { mutableIntStateOf(items.indexOfFirst { it.isSelected }) }
    val coroutineScope = rememberCoroutineScope()

    val offsetAnimatable = remember { Animatable(UNSET_OFFSET, Int.VectorConverter) }
    var size by remember { mutableStateOf(IntSize(0, 0)) }
    Layout(
    val spacingPx = with(LocalDensity.current) { spacing.toPx() }
        modifier = modifier,
    val indicatorWidth = size.width / items.size - (spacingPx * (items.size - 1) / items.size)
        content = {
    val offset by
            Spacer(
        animateOffsetAsState(
            targetValue =
                Offset(
                    selectedIndex * indicatorWidth + (spacingPx * selectedIndex),
                    0f,
                ),
            label = "VolumePanelRadioButtonOffsetAnimation",
            finishedListener = {
                for (itemIndex in items.indices) {
                    val item = items[itemIndex]
                    if (itemIndex == selectedIndex) {
                        item.onItemSelected()
                        break
                    }
                }
            }
        )

    Column(modifier = modifier) {
        Box(modifier = Modifier.height(IntrinsicSize.Max)) {
            Canvas(
                modifier =
                modifier =
                    Modifier.fillMaxSize()
                    Modifier.layoutId(RadioButtonBarComponent.ButtonsBackground)
                        .background(
                        .background(
                            colors.indicatorBackgroundColor,
                            colors.indicatorBackgroundColor,
                            RoundedCornerShape(indicatorBackgroundCornerSize),
                            RoundedCornerShape(indicatorBackgroundCornerSize),
                        )
                        )
            )
            Spacer(
                modifier =
                    Modifier.layoutId(RadioButtonBarComponent.Indicator)
                        .offset { IntOffset(offsetAnimatable.value, 0) }
                        .padding(indicatorBackgroundPadding)
                        .padding(indicatorBackgroundPadding)
                        .onGloballyPositioned { size = it.size }
                        .background(
            ) {
                            colors.indicatorColor,
                drawRoundRect(
                            RoundedCornerShape(indicatorCornerSize),
                    color = colors.indicatorColor,
                        )
                    topLeft = offset,
                    size = Size(indicatorWidth, size.height.toFloat()),
                    cornerRadius = indicatorCornerRadius,
            )
            )
            }
            Row(
            Row(
                modifier = Modifier.padding(indicatorBackgroundPadding),
                modifier =
                    Modifier.layoutId(RadioButtonBarComponent.Buttons)
                        .padding(indicatorBackgroundPadding),
                horizontalArrangement = Arrangement.spacedBy(spacing)
                horizontalArrangement = Arrangement.spacedBy(spacing)
            ) {
            ) {
                for (itemIndex in items.indices) {
                for (itemIndex in items.indices) {
                    TextButton(
                    TextButton(
                        modifier = Modifier.weight(1f),
                        modifier = Modifier.weight(1f),
                        onClick = { selectedIndex = itemIndex },
                        onClick = { items[itemIndex].onItemSelected() },
                    ) {
                    ) {
                        val item = items[itemIndex]
                        val item = items[itemIndex]
                        if (item.icon !== Empty) {
                        if (item.icon !== Empty) {
@@ -138,11 +119,10 @@ fun VolumePanelRadioButtonBar(
                    }
                    }
                }
                }
            }
            }
        }

            Row(
            Row(
                modifier =
                modifier =
                Modifier.padding(
                    Modifier.layoutId(RadioButtonBarComponent.Labels)
                        .padding(
                            start = indicatorBackgroundPadding,
                            start = indicatorBackgroundPadding,
                            top = labelIndicatorBackgroundSpacing,
                            top = labelIndicatorBackgroundSpacing,
                            end = indicatorBackgroundPadding
                            end = indicatorBackgroundPadding
@@ -152,7 +132,7 @@ fun VolumePanelRadioButtonBar(
                for (itemIndex in items.indices) {
                for (itemIndex in items.indices) {
                    TextButton(
                    TextButton(
                        modifier = Modifier.weight(1f),
                        modifier = Modifier.weight(1f),
                    onClick = { selectedIndex = itemIndex },
                        onClick = { items[itemIndex].onItemSelected() },
                    ) {
                    ) {
                        val item = items[itemIndex]
                        val item = items[itemIndex]
                        if (item.icon !== Empty) {
                        if (item.icon !== Empty) {
@@ -161,6 +141,95 @@ fun VolumePanelRadioButtonBar(
                    }
                    }
                }
                }
            }
            }
        },
        measurePolicy =
            with(LocalDensity.current) {
                val spacingPx =
                    (spacing - indicatorBackgroundPadding * 2).roundToPx().coerceAtLeast(0)

                BarMeasurePolicy(
                    buttonsCount = items.size,
                    selectedIndex = scope.selectedIndex,
                    spacingPx = spacingPx,
                ) {
                    coroutineScope.launch {
                        if (offsetAnimatable.value == UNSET_OFFSET) {
                            offsetAnimatable.snapTo(it)
                        } else {
                            offsetAnimatable.animateTo(it)
                        }
                    }
                }
            },
    )
}

private class BarMeasurePolicy(
    private val buttonsCount: Int,
    private val selectedIndex: Int,
    private val spacingPx: Int,
    private val onTargetIndicatorOffsetMeasured: (Int) -> Unit,
) : MeasurePolicy {

    override fun MeasureScope.measure(
        measurables: List<Measurable>,
        constraints: Constraints
    ): MeasureResult {
        val fillWidthConstraints = constraints.copy(minWidth = constraints.maxWidth)
        val buttonsPlaceable: Placeable =
            measurables
                .fastFirst { it.layoutId == RadioButtonBarComponent.Buttons }
                .measure(fillWidthConstraints)
        val labelsPlaceable: Placeable =
            measurables
                .fastFirst { it.layoutId == RadioButtonBarComponent.Labels }
                .measure(fillWidthConstraints)

        val buttonsBackgroundPlaceable: Placeable =
            measurables
                .fastFirst { it.layoutId == RadioButtonBarComponent.ButtonsBackground }
                .measure(
                    Constraints(
                        minWidth = buttonsPlaceable.width,
                        maxWidth = buttonsPlaceable.width,
                        minHeight = buttonsPlaceable.height,
                        maxHeight = buttonsPlaceable.height,
                    )
                )

        val totalSpacing = spacingPx * (buttonsCount - 1)
        val indicatorWidth = (buttonsBackgroundPlaceable.width - totalSpacing) / buttonsCount
        val indicatorPlaceable: Placeable =
            measurables
                .fastFirst { it.layoutId == RadioButtonBarComponent.Indicator }
                .measure(
                    Constraints(
                        minWidth = indicatorWidth,
                        maxWidth = indicatorWidth,
                        minHeight = buttonsBackgroundPlaceable.height,
                        maxHeight = buttonsBackgroundPlaceable.height,
                    )
                )

        onTargetIndicatorOffsetMeasured(
            selectedIndex * indicatorWidth + (spacingPx * selectedIndex)
        )

        return layout(constraints.maxWidth, buttonsPlaceable.height + labelsPlaceable.height) {
            buttonsBackgroundPlaceable.placeRelative(
                0,
                0,
                RadioButtonBarComponent.ButtonsBackground.zIndex,
            )
            indicatorPlaceable.placeRelative(0, 0, RadioButtonBarComponent.Indicator.zIndex)

            buttonsPlaceable.placeRelative(0, 0, RadioButtonBarComponent.Buttons.zIndex)
            labelsPlaceable.placeRelative(
                0,
                buttonsBackgroundPlaceable.height,
                RadioButtonBarComponent.Labels.zIndex,
            )
        }
    }
    }
}
}


@@ -179,12 +248,6 @@ object VolumePanelRadioButtonBarDefaults {
    val DefaultIndicatorCornerRadius = 20.dp
    val DefaultIndicatorCornerRadius = 20.dp
    val DefaultIndicatorBackgroundCornerRadius = 20.dp
    val DefaultIndicatorBackgroundCornerRadius = 20.dp


    @Composable
    fun defaultIndicatorCornerRadius(
        x: Dp = DefaultIndicatorCornerRadius,
        y: Dp = DefaultIndicatorCornerRadius,
    ): CornerRadius = with(LocalDensity.current) { CornerRadius(x.toPx(), y.toPx()) }

    /**
    /**
     * Returns the default VolumePanelRadioButtonBar colors.
     * Returns the default VolumePanelRadioButtonBar colors.
     *
     *
@@ -225,9 +288,12 @@ private val Empty: @Composable RowScope.() -> Unit = {}


private class VolumePanelRadioButtonBarScopeImpl : VolumePanelRadioButtonBarScope {
private class VolumePanelRadioButtonBarScopeImpl : VolumePanelRadioButtonBarScope {


    var hasSelectedItem: Boolean = false
    var selectedIndex: Int = UNSET_INDEX
        private set
        private set


    val hasSelectedItem: Boolean
        get() = selectedIndex != UNSET_INDEX

    private val mutableItems: MutableList<Item> = mutableListOf()
    private val mutableItems: MutableList<Item> = mutableListOf()
    val items: List<Item> = mutableItems
    val items: List<Item> = mutableItems


@@ -238,21 +304,34 @@ private class VolumePanelRadioButtonBarScopeImpl : VolumePanelRadioButtonBarScop
        label: @Composable RowScope.() -> Unit,
        label: @Composable RowScope.() -> Unit,
    ) {
    ) {
        require(!isSelected || !hasSelectedItem) { "Only one item should be selected at a time" }
        require(!isSelected || !hasSelectedItem) { "Only one item should be selected at a time" }
        hasSelectedItem = hasSelectedItem || isSelected
        if (isSelected) {
            selectedIndex = mutableItems.size
        }
        mutableItems.add(
        mutableItems.add(
            Item(
            Item(
                isSelected = isSelected,
                onItemSelected = onItemSelected,
                onItemSelected = onItemSelected,
                icon = icon,
                icon = icon,
                label = label,
                label = label,
            )
            )
        )
        )
    }
    }

    private companion object {
        const val UNSET_INDEX = -1
    }
}
}


private class Item(
private class Item(
    val isSelected: Boolean,
    val onItemSelected: () -> Unit,
    val onItemSelected: () -> Unit,
    val icon: @Composable RowScope.() -> Unit,
    val icon: @Composable RowScope.() -> Unit,
    val label: @Composable RowScope.() -> Unit,
    val label: @Composable RowScope.() -> Unit,
)
)

private const val UNSET_OFFSET = -1

private enum class RadioButtonBarComponent(val zIndex: Float) {
    ButtonsBackground(0f),
    Indicator(1f),
    Buttons(2f),
    Labels(2f),
}
+1 −1
Original line number Original line Diff line number Diff line
@@ -65,7 +65,7 @@ constructor(
            return
            return
        }
        }


        val enabledModelStates by viewModel.spatialAudioButtonByEnabled.collectAsState()
        val enabledModelStates by viewModel.spatialAudioButtons.collectAsState()
        if (enabledModelStates.isEmpty()) {
        if (enabledModelStates.isEmpty()) {
            return
            return
        }
        }
+1 −1
Original line number Original line Diff line number Diff line
@@ -56,7 +56,7 @@ constructor(
    val isAvailable: StateFlow<Boolean> =
    val isAvailable: StateFlow<Boolean> =
        availabilityCriteria.isAvailable().stateIn(scope, SharingStarted.Eagerly, true)
        availabilityCriteria.isAvailable().stateIn(scope, SharingStarted.Eagerly, true)


    val spatialAudioButtonByEnabled: StateFlow<List<SpatialAudioButtonViewModel>> =
    val spatialAudioButtons: StateFlow<List<SpatialAudioButtonViewModel>> =
        combine(interactor.isEnabled, interactor.isAvailable) { currentIsEnabled, isAvailable ->
        combine(interactor.isEnabled, interactor.isAvailable) { currentIsEnabled, isAvailable ->
                SpatialAudioEnabledModel.values
                SpatialAudioEnabledModel.values
                    .filter {
                    .filter {