diff options
| author | 2025-02-28 21:14:36 -0800 | |
|---|---|---|
| committer | 2025-02-28 21:14:36 -0800 | |
| commit | 033a7df1bc932bbe9ef1a03b957315d7a501b8ca (patch) | |
| tree | dcd6ad3b01c9425a16898c43f1a8839c66875ea4 | |
| parent | d2ad76b222241aab6c683db993f87c5c526fb15b (diff) | |
| parent | fc5a03925839c9a429ec26311e95c991c4dc98f1 (diff) | |
Merge "[Media] Adds building block composable elements." into main
5 files changed, 330 insertions, 0 deletions
diff --git a/packages/SystemUI/src/com/android/systemui/media/remedia/shared/model/MediaSessionState.kt b/packages/SystemUI/src/com/android/systemui/media/remedia/shared/model/MediaSessionState.kt new file mode 100644 index 000000000000..40d55af9ba3e --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/media/remedia/shared/model/MediaSessionState.kt @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.media.remedia.shared.model + +sealed interface MediaSessionState { + data object Playing : MediaSessionState + + data object Paused : MediaSessionState + + data object Buffering : MediaSessionState +} diff --git a/packages/SystemUI/src/com/android/systemui/media/remedia/ui/compose/Media.kt b/packages/SystemUI/src/com/android/systemui/media/remedia/ui/compose/Media.kt new file mode 100644 index 000000000000..ae276707c3db --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/media/remedia/ui/compose/Media.kt @@ -0,0 +1,230 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.media.remedia.ui.compose + +import androidx.compose.animation.Crossfade +import androidx.compose.animation.core.animateDpAsState +import androidx.compose.animation.graphics.res.animatedVectorResource +import androidx.compose.animation.graphics.res.rememberAnimatedVectorPainter +import androidx.compose.animation.graphics.vector.AnimatedImageVector +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButtonDefaults +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import com.android.compose.PlatformButton +import com.android.compose.PlatformIconButton +import com.android.compose.animation.scene.ContentScope +import com.android.compose.animation.scene.ElementKey +import com.android.compose.theme.LocalAndroidColorScheme +import com.android.systemui.common.shared.model.Icon +import com.android.systemui.common.ui.compose.Icon +import com.android.systemui.common.ui.compose.load +import com.android.systemui.media.remedia.shared.model.MediaSessionState +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 + +/** Renders the metadata labels of a track. */ +@Composable +private fun ContentScope.Metadata( + title: String, + subtitle: String, + color: Color, + modifier: Modifier = Modifier, +) { + // This element can be animated when switching between scenes inside a media card. + Element(key = Media.Elements.Metadata, modifier = modifier) { + // When the title and/or subtitle change, crossfade between the old and the new. + Crossfade(targetState = title to subtitle, label = "Labels.crossfade") { (title, subtitle) + -> + Column { + Text( + text = title, + style = MaterialTheme.typography.bodyLarge, + color = color, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + + Text( + text = subtitle, + style = MaterialTheme.typography.bodyMedium, + color = color, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + } + } + } +} + +/** + * 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, + modifier: Modifier = Modifier, +) { + PlatformButton( + onClick = viewModel.onClick, + colors = + ButtonDefaults.buttonColors( + containerColor = LocalAndroidColorScheme.current.primaryFixed + ), + contentPadding = PaddingValues(start = 8.dp, end = 12.dp, top = 4.dp, bottom = 4.dp), + modifier = modifier.height(24.dp), + ) { + Icon( + icon = viewModel.icon, + tint = LocalAndroidColorScheme.current.onPrimaryFixed, + modifier = Modifier.size(16.dp), + ) + viewModel.text?.let { + Spacer(Modifier.size(4.dp)) + Text( + text = viewModel.text, + style = MaterialTheme.typography.bodySmall, + color = LocalAndroidColorScheme.current.onPrimaryFixed, + ) + } + } +} + +/** Renders the primary action of media controls: the play/pause button. */ +@Composable +private fun ContentScope.PlayPauseAction( + viewModel: MediaPlayPauseActionViewModel, + buttonWidth: Dp, + buttonColor: Color, + iconColor: Color, + buttonCornerRadius: (isPlaying: Boolean) -> Dp, + modifier: Modifier = Modifier, +) { + val cornerRadius: Dp by + animateDpAsState( + targetValue = buttonCornerRadius(viewModel.state != MediaSessionState.Paused), + label = "PlayPauseAction.cornerRadius", + ) + // This element can be animated when switching between scenes inside a media card. + Element(key = Media.Elements.PlayPauseButton, modifier = modifier) { + PlatformButton( + onClick = viewModel.onClick, + colors = ButtonDefaults.buttonColors(containerColor = buttonColor), + shape = RoundedCornerShape(cornerRadius), + modifier = Modifier.size(width = buttonWidth, height = 48.dp), + ) { + when (viewModel.state) { + is MediaSessionState.Playing, + is MediaSessionState.Paused -> { + // TODO(b/399860531): load this expensive-to-load animated vector drawable off + // the main thread. + val iconResource = checkNotNull(viewModel.icon) + Icon( + painter = + rememberAnimatedVectorPainter( + animatedImageVector = + AnimatedImageVector.animatedVectorResource( + id = iconResource.res + ), + atEnd = viewModel.state == MediaSessionState.Playing, + ), + contentDescription = iconResource.contentDescription?.load(), + tint = iconColor, + modifier = Modifier.size(24.dp), + ) + } + is MediaSessionState.Buffering -> { + CircularProgressIndicator(color = iconColor, modifier = Modifier.size(24.dp)) + } + } + } + } +} + +/** + * Renders an icon button for an action that's not the play/pause action. + * + * If [element] is provided, the secondary action element will be able to animate when switching + * between scenes inside a media card. + */ +@Composable +private fun ContentScope.SecondaryAction( + viewModel: MediaSecondaryActionViewModel, + modifier: Modifier = Modifier, + element: ElementKey? = null, + iconColor: Color = Color.White, +) { + if (element != null) { + Element(key = element, modifier = modifier) { + SecondaryActionContent(viewModel = viewModel, iconColor = iconColor) + } + } else { + SecondaryActionContent(viewModel = viewModel, iconColor = iconColor, modifier = modifier) + } +} + +/** The content of a [SecondaryAction]. */ +@Composable +private fun SecondaryActionContent( + viewModel: MediaSecondaryActionViewModel, + iconColor: Color, + modifier: Modifier = Modifier, +) { + PlatformIconButton( + onClick = viewModel.onClick, + iconResource = (viewModel.icon as Icon.Resource).res, + contentDescription = viewModel.icon.contentDescription?.load(), + colors = IconButtonDefaults.iconButtonColors(contentColor = iconColor), + modifier = modifier.size(48.dp).padding(13.dp), + ) +} + +private object Media { + + /** + * Element keys. + * + * Composables that are wrapped in [ContentScope.Element] with one of these as their `key` + * parameter will automatically be picked up by the STL transition animation framework and will + * be animated from their bounds in the original scene to their bounds in the destination scene. + * + * In addition, tagging such elements with a key allows developers to customize the transition + * animations even further. + */ + object Elements { + val PlayPauseButton = ElementKey("play_pause") + val Metadata = ElementKey("metadata") + } +} diff --git a/packages/SystemUI/src/com/android/systemui/media/remedia/ui/viewmodel/MediaOutputSwitcherChipViewModel.kt b/packages/SystemUI/src/com/android/systemui/media/remedia/ui/viewmodel/MediaOutputSwitcherChipViewModel.kt new file mode 100644 index 000000000000..f2724da2c3af --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/media/remedia/ui/viewmodel/MediaOutputSwitcherChipViewModel.kt @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.media.remedia.ui.viewmodel + +import com.android.systemui.common.shared.model.Icon + +data class MediaOutputSwitcherChipViewModel( + val icon: Icon, + val text: String? = null, + val onClick: () -> Unit, +) diff --git a/packages/SystemUI/src/com/android/systemui/media/remedia/ui/viewmodel/MediaPlayPauseActionViewModel.kt b/packages/SystemUI/src/com/android/systemui/media/remedia/ui/viewmodel/MediaPlayPauseActionViewModel.kt new file mode 100644 index 000000000000..4cb11bc0b8d0 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/media/remedia/ui/viewmodel/MediaPlayPauseActionViewModel.kt @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.media.remedia.ui.viewmodel + +import com.android.systemui.common.shared.model.Icon +import com.android.systemui.media.remedia.shared.model.MediaSessionState + +/** Models UI state for the play/pause action button within media controls. */ +data class MediaPlayPauseActionViewModel( + val isVisible: Boolean, + val state: MediaSessionState, + val icon: Icon.Resource?, + val onClick: () -> Unit, +) diff --git a/packages/SystemUI/src/com/android/systemui/media/remedia/ui/viewmodel/MediaSecondaryActionViewModel.kt b/packages/SystemUI/src/com/android/systemui/media/remedia/ui/viewmodel/MediaSecondaryActionViewModel.kt new file mode 100644 index 000000000000..2d7765d861a7 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/media/remedia/ui/viewmodel/MediaSecondaryActionViewModel.kt @@ -0,0 +1,22 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.media.remedia.ui.viewmodel + +import com.android.systemui.common.shared.model.Icon + +/** Models UI state for a secondary action button within media controls. */ +data class MediaSecondaryActionViewModel(val icon: Icon, val onClick: () -> Unit) |