Loading packages/SystemUI/src/com/android/systemui/media/remedia/domain/interactor/MediaInteractor.kt +3 −0 Original line number Diff line number Diff line Loading @@ -149,6 +149,9 @@ constructor( ) } override val suggestedOutputDevice: MediaOutputDeviceModel? get() = TODO("Not yet implemented") override val actionButtonLayout: MediaCardActionButtonLayout get() = dataModel.playbackStateActions?.let { Loading packages/SystemUI/src/com/android/systemui/media/remedia/domain/model/MediaSessionModel.kt +2 −0 Original line number Diff line number Diff line Loading @@ -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 Loading packages/SystemUI/src/com/android/systemui/media/remedia/ui/compose/Media.kt +113 −59 Original line number Diff line number Diff line Loading @@ -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 Loading @@ -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 Loading @@ -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 Loading Loading @@ -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 Loading @@ -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 Loading @@ -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 Loading @@ -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 Loading Loading @@ -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. Loading @@ -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) } }, ) } } } Loading Loading @@ -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. Loading @@ -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 Loading Loading @@ -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, ) packages/SystemUI/src/com/android/systemui/media/remedia/ui/viewmodel/MediaCardViewModel.kt +3 −1 Original line number Diff line number Diff line Loading @@ -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 Loading packages/SystemUI/src/com/android/systemui/media/remedia/ui/viewmodel/MediaOutputSwitcherChipViewModel.kt→packages/SystemUI/src/com/android/systemui/media/remedia/ui/viewmodel/MediaDeviceChipViewModel.kt +2 −1 Original line number Diff line number Diff line Loading @@ -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
packages/SystemUI/src/com/android/systemui/media/remedia/domain/interactor/MediaInteractor.kt +3 −0 Original line number Diff line number Diff line Loading @@ -149,6 +149,9 @@ constructor( ) } override val suggestedOutputDevice: MediaOutputDeviceModel? get() = TODO("Not yet implemented") override val actionButtonLayout: MediaCardActionButtonLayout get() = dataModel.playbackStateActions?.let { Loading
packages/SystemUI/src/com/android/systemui/media/remedia/domain/model/MediaSessionModel.kt +2 −0 Original line number Diff line number Diff line Loading @@ -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 Loading
packages/SystemUI/src/com/android/systemui/media/remedia/ui/compose/Media.kt +113 −59 Original line number Diff line number Diff line Loading @@ -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 Loading @@ -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 Loading @@ -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 Loading Loading @@ -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 Loading @@ -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 Loading @@ -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 Loading @@ -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 Loading Loading @@ -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. Loading @@ -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) } }, ) } } } Loading Loading @@ -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. Loading @@ -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 Loading Loading @@ -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, )
packages/SystemUI/src/com/android/systemui/media/remedia/ui/viewmodel/MediaCardViewModel.kt +3 −1 Original line number Diff line number Diff line Loading @@ -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 Loading
packages/SystemUI/src/com/android/systemui/media/remedia/ui/viewmodel/MediaOutputSwitcherChipViewModel.kt→packages/SystemUI/src/com/android/systemui/media/remedia/ui/viewmodel/MediaDeviceChipViewModel.kt +2 −1 Original line number Diff line number Diff line Loading @@ -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, )