summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author Treehugger Robot <android-test-infra-autosubmit@system.gserviceaccount.com> 2025-02-28 21:14:36 -0800
committer Android (Google) Code Review <android-gerrit@google.com> 2025-02-28 21:14:36 -0800
commit033a7df1bc932bbe9ef1a03b957315d7a501b8ca (patch)
treedcd6ad3b01c9425a16898c43f1a8839c66875ea4
parentd2ad76b222241aab6c683db993f87c5c526fb15b (diff)
parentfc5a03925839c9a429ec26311e95c991c4dc98f1 (diff)
Merge "[Media] Adds building block composable elements." into main
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/remedia/shared/model/MediaSessionState.kt25
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/remedia/ui/compose/Media.kt230
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/remedia/ui/viewmodel/MediaOutputSwitcherChipViewModel.kt25
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/remedia/ui/viewmodel/MediaPlayPauseActionViewModel.kt28
-rw-r--r--packages/SystemUI/src/com/android/systemui/media/remedia/ui/viewmodel/MediaSecondaryActionViewModel.kt22
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)