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

Commit 7f30beb6 authored by Alexandr Shabalin's avatar Alexandr Shabalin Committed by Android (Google) Code Review
Browse files

Merge "[Media] Add Suggested Device Chip composable element." into main

parents 56a938f3 1950ca53
Loading
Loading
Loading
Loading
+3 −0
Original line number Diff line number Diff line
@@ -149,6 +149,9 @@ constructor(
                        )
                    }

            override val suggestedOutputDevice: MediaOutputDeviceModel?
                get() = TODO("Not yet implemented")

            override val actionButtonLayout: MediaCardActionButtonLayout
                get() =
                    dataModel.playbackStateActions?.let {
+2 −0
Original line number Diff line number Diff line
@@ -68,6 +68,8 @@ interface MediaSessionModel {

    val outputDevice: MediaOutputDeviceModel

    val suggestedOutputDevice: MediaOutputDeviceModel?

    /** How to lay out the action buttons. */
    val actionButtonLayout: MediaCardActionButtonLayout
    val playPauseAction: MediaActionModel
+113 −59
Original line number Diff line number Diff line
@@ -27,6 +27,8 @@ import androidx.compose.animation.core.RepeatMode
import androidx.compose.animation.core.animateDpAsState
import androidx.compose.animation.core.infiniteRepeatable
import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.graphics.res.animatedVectorResource
import androidx.compose.animation.graphics.res.rememberAnimatedVectorPainter
import androidx.compose.animation.graphics.vector.AnimatedImageVector
@@ -35,6 +37,7 @@ import androidx.compose.foundation.Canvas
import androidx.compose.foundation.Image
import androidx.compose.foundation.OverscrollEffect
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.gestures.Orientation
@@ -53,7 +56,7 @@ import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.pager.HorizontalPager
@@ -105,6 +108,7 @@ import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.layout.Layout
import androidx.compose.ui.layout.layout
import androidx.compose.ui.node.Ref
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.semantics.clearAndSetSemantics
import androidx.compose.ui.semantics.contentDescription
@@ -114,7 +118,6 @@ import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.unit.dp
import androidx.compose.ui.util.fastCoerceIn
import androidx.compose.ui.util.fastForEach
import androidx.compose.ui.util.fastForEachIndexed
import androidx.compose.ui.util.fastRoundToInt
import com.android.compose.PlatformButton
@@ -128,6 +131,7 @@ import com.android.compose.animation.scene.rememberMutableSceneTransitionLayoutS
import com.android.compose.animation.scene.transitions
import com.android.compose.gesture.effect.rememberOffsetOverscrollEffect
import com.android.compose.gesture.overscrollToDismiss
import com.android.compose.modifiers.thenIf
import com.android.compose.theme.LocalAndroidColorScheme
import com.android.compose.ui.graphics.painter.rememberDrawablePainter
import com.android.mechanics.spec.builder.rememberMotionBuilderContext
@@ -143,8 +147,8 @@ import com.android.systemui.media.remedia.shared.model.MediaSessionState
import com.android.systemui.media.remedia.ui.viewmodel.MediaCardGutsViewModel
import com.android.systemui.media.remedia.ui.viewmodel.MediaCardViewModel
import com.android.systemui.media.remedia.ui.viewmodel.MediaCarouselVisibility
import com.android.systemui.media.remedia.ui.viewmodel.MediaDeviceChipViewModel
import com.android.systemui.media.remedia.ui.viewmodel.MediaNavigationViewModel
import com.android.systemui.media.remedia.ui.viewmodel.MediaOutputSwitcherChipViewModel
import com.android.systemui.media.remedia.ui.viewmodel.MediaPlayPauseActionViewModel
import com.android.systemui.media.remedia.ui.viewmodel.MediaSecondaryActionViewModel
import com.android.systemui.media.remedia.ui.viewmodel.MediaSettingsButtonViewModel
@@ -464,8 +468,8 @@ private fun ContentScope.CardForegroundContent(
                horizontalArrangement = Arrangement.spacedBy(8.dp),
                modifier =
                    Modifier.align(Alignment.TopEnd)
                        // Output switcher chips must each be limited to at most 40% of the maximum
                        // width of the card.
                        // Output switcher chip must be limited to at most 40% of the maximum width
                        // of the card.
                        //
                        // This saves the maximum possible width of the card so it can be referred
                        // to by child custom layout code below.
@@ -480,36 +484,46 @@ private fun ContentScope.CardForegroundContent(
                            }
                        },
            ) {
                viewModel.outputSwitcherChips.fastForEach { chip ->
                    OutputSwitcherChip(
                        viewModel = chip,
                        colorScheme = colorScheme,
                AnimatedVisibility(
                    visible = viewModel.deviceSuggestionChip != null,
                    enter = fadeIn(),
                    exit = fadeOut(),
                ) {
                    rememberLastNonNull(viewModel.deviceSuggestionChip)?.let {
                        DeviceChip(
                            viewModel = it,
                            style =
                                DeviceChipStyle(
                                    fillColor = Color.Transparent,
                                    contentColor = colorScheme.primary,
                                    borderColor = colorScheme.primary,
                                ),
                            modifier =
                                Modifier.fractionalMaxWidth(
                                    containerMaxWidth = cardMaxWidth,
                                    fraction = 0.5f,
                                ),
                        )
                    }
                }

                DeviceChip(
                    viewModel = viewModel.outputSwitcherChip,
                    style =
                        DeviceChipStyle(
                            fillColor = colorScheme.primary,
                            contentColor = colorScheme.onPrimary,
                        ),
                    modifier =
                        Modifier
                                // Each chip must be limited to 40% of the width of the card at
                                // most.
                            // The chip must be limited to 40% of the width of the card at most.
                            //
                            // The underlying assumption is that there'll never be more than one
                            // chip with text and one more icon-only chip. Only the one with
                            // text can ever end up being too wide.
                                .layout { measurable, constraints ->
                                    val placeable =
                                        measurable.measure(
                                            constraints.copy(
                                                maxWidth =
                                                    min(
                                                        (cardMaxWidth * 0.4f).fastRoundToInt(),
                                                        constraints.maxWidth,
                                                    )
                            .fractionalMaxWidth(containerMaxWidth = cardMaxWidth, fraction = 0.4f)
                            .padding(end = 16.dp),
                )
                                        )

                                    layout(placeable.measuredWidth, placeable.measuredHeight) {
                                        placeable.place(0, 0)
                                    }
                                },
                    )
                }
            }
        }

@@ -1069,14 +1083,10 @@ private fun ContentScope.Metadata(
    }
}

/**
 * Renders a small chip showing the current output device and providing a way to switch to a
 * different output device.
 */
@Composable
private fun OutputSwitcherChip(
    viewModel: MediaOutputSwitcherChipViewModel,
    colorScheme: AnimatedColorScheme,
private fun DeviceChip(
    viewModel: MediaDeviceChipViewModel,
    style: DeviceChipStyle,
    modifier: Modifier = Modifier,
) {
    // For accessibility reasons, the touch area for the chip needs to be at least 48dp in height.
@@ -1093,39 +1103,55 @@ private fun OutputSwitcherChip(
    Box(
        modifier =
            modifier
                .height(48.dp)
                .heightIn(min = 48.dp)
                .clickable(interactionSource = clickInteractionSource, indication = null) {
                    viewModel.onClick()
                }
                .padding(top = 16.dp, end = 16.dp, bottom = 8.dp)
                .padding(top = 16.dp, bottom = 8.dp)
    ) {
        Row(
            horizontalArrangement = Arrangement.spacedBy(4.dp),
            verticalAlignment = Alignment.CenterVertically,
            modifier =
                Modifier.clip(RoundedCornerShape(12.dp))
                    .background(colorScheme.primary)
                    .background(style.fillColor)
                    .thenIf(style.borderColor != null) {
                        Modifier.border(
                            width = 1.dp,
                            color = style.borderColor!!,
                            shape = RoundedCornerShape(12.dp),
                        )
                    }
                    .indication(clickInteractionSource, ripple())
                    .padding(start = 8.dp, end = 12.dp, top = 4.dp, bottom = 4.dp),
                    .padding(horizontal = 8.dp, vertical = 4.dp),
        ) {
            if (viewModel.isConnecting) {
                CircularProgressIndicator(
                    color = style.contentColor,
                    modifier = Modifier.size(12.dp),
                    strokeWidth = 1.dp,
                )
            } else {
                Icon(
                    icon = viewModel.icon,
                tint = colorScheme.onPrimary,
                    tint = style.contentColor,
                    modifier = Modifier.size(16.dp),
                )

            viewModel.text?.let {
            }
            AnimatedVisibility(visible = viewModel.text != null) {
                rememberLastNonNull(viewModel.text)?.let {
                    Text(
                    text = viewModel.text,
                        text = it,
                        style = MaterialTheme.typography.bodySmall,
                    color = colorScheme.onPrimary,
                        color = style.contentColor,
                        maxLines = 1,
                        overflow = TextOverflow.Ellipsis,
                        modifier = Modifier.padding(horizontal = 4.dp),
                    )
                }
            }
        }
    }
}

/** Renders the primary action of media controls: the play/pause button. */
@Composable
@@ -1377,3 +1403,31 @@ private fun MediaPresentationStyle.toScene(): SceneKey {
        MediaPresentationStyle.Compact -> Media.Scenes.Compact
    }
}

/** Allows to set the maxWidth constraint as a fractional value. */
private fun Modifier.fractionalMaxWidth(containerMaxWidth: Int, fraction: Float): Modifier {
    return layout { measurable, constraints ->
        val placeable =
            measurable.measure(
                constraints.copy(
                    maxWidth =
                        min((containerMaxWidth * fraction).fastRoundToInt(), constraints.maxWidth)
                )
            )

        layout(placeable.measuredWidth, placeable.measuredHeight) { placeable.place(0, 0) }
    }
}

@Composable
fun <T> rememberLastNonNull(value: T?): T? {
    val ref = remember { Ref<T?>() }
    ref.value = value ?: ref.value
    return ref.value
}

private data class DeviceChipStyle(
    val fillColor: Color,
    val contentColor: Color,
    val borderColor: Color? = null,
)
+3 −1
Original line number Diff line number Diff line
@@ -51,7 +51,9 @@ interface MediaCardViewModel {

    val guts: MediaCardGutsViewModel

    val outputSwitcherChips: List<MediaOutputSwitcherChipViewModel>
    val outputSwitcherChip: MediaDeviceChipViewModel

    val deviceSuggestionChip: MediaDeviceChipViewModel?

    /** Simple icon-only version of the output switcher for use in compact UIs. */
    val outputSwitcherChipButton: MediaSecondaryActionViewModel.Action
+2 −1
Original line number Diff line number Diff line
@@ -18,8 +18,9 @@ package com.android.systemui.media.remedia.ui.viewmodel

import com.android.systemui.common.shared.model.Icon

data class MediaOutputSwitcherChipViewModel(
data class MediaDeviceChipViewModel(
    val icon: Icon,
    val text: String? = null,
    val isConnecting: Boolean = false,
    val onClick: () -> Unit,
)
Loading