diff --git a/media-sample/src/main/java/com/google/android/horologist/mediasample/ui/library/UampLibraryScreen.kt b/media-sample/src/main/java/com/google/android/horologist/mediasample/ui/library/UampLibraryScreen.kt index 5e52bb6536..7c01838b15 100644 --- a/media-sample/src/main/java/com/google/android/horologist/mediasample/ui/library/UampLibraryScreen.kt +++ b/media-sample/src/main/java/com/google/android/horologist/mediasample/ui/library/UampLibraryScreen.kt @@ -33,8 +33,8 @@ import androidx.wear.compose.material.Text import androidx.wear.compose.material.items import com.google.android.horologist.compose.layout.StateUtils.rememberStateWithLifecycle import com.google.android.horologist.compose.navscaffold.scrollableColumn +import com.google.android.horologist.media.ui.components.MediaChip import com.google.android.horologist.mediasample.R -import com.google.android.horologist.mediasample.ui.components.MediaChip @Composable fun UampLibraryScreen( @@ -67,7 +67,8 @@ fun UampLibraryScreen( onClick = { libraryScreenViewModel.play(it) onPlayClick() - } + }, + defaultTitle = stringResource(id = R.string.horologist_no_title), ) } } else { diff --git a/media-ui/api/current.api b/media-ui/api/current.api index fca85fee9a..9e7e0baea3 100644 --- a/media-ui/api/current.api +++ b/media-ui/api/current.api @@ -29,6 +29,11 @@ package com.google.android.horologist.media.ui.components { method @androidx.compose.runtime.Composable @com.google.android.horologist.media.ui.ExperimentalHorologistMediaUiApi public static void MediaArtwork(String? artworkUri, String? contentDescription, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.ui.graphics.painter.Painter? placeholder); } + public final class MediaChipKt { + method @androidx.compose.runtime.Composable @com.google.android.horologist.media.ui.ExperimentalHorologistMediaUiApi public static void MediaChip(com.google.android.horologist.media.ui.state.model.MediaItemUiModel mediaItem, kotlin.jvm.functions.Function0 onClick, optional androidx.compose.ui.Modifier modifier, optional String defaultTitle, optional androidx.compose.ui.graphics.painter.Painter? placeholder); + method @androidx.compose.runtime.Composable @com.google.android.horologist.media.ui.ExperimentalHorologistMediaUiApi public static void MediaChip(String title, String? artworkUri, kotlin.jvm.functions.Function0 onClick, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.ui.graphics.painter.Painter? placeholder); + } + public final class MediaControlButtonsKt { method @androidx.compose.runtime.Composable @com.google.android.horologist.media.ui.ExperimentalHorologistMediaUiApi public static void MediaControlButtons(kotlin.jvm.functions.Function0 onPlayButtonClick, kotlin.jvm.functions.Function0 onPauseButtonClick, boolean playPauseButtonEnabled, boolean playing, float percent, kotlin.jvm.functions.Function0 onSeekToPreviousButtonClick, boolean seekToPreviousButtonEnabled, kotlin.jvm.functions.Function0 onSeekToNextButtonClick, boolean seekToNextButtonEnabled, optional androidx.compose.ui.Modifier modifier, optional androidx.wear.compose.material.ButtonColors colors, optional long progressColour); method @androidx.compose.runtime.Composable @com.google.android.horologist.media.ui.ExperimentalHorologistMediaUiApi public static void MediaControlButtons(kotlin.jvm.functions.Function0 onPlayButtonClick, kotlin.jvm.functions.Function0 onPauseButtonClick, boolean playPauseButtonEnabled, boolean playing, kotlin.jvm.functions.Function0 onSeekToPreviousButtonClick, boolean seekToPreviousButtonEnabled, kotlin.jvm.functions.Function0 onSeekToNextButtonClick, boolean seekToNextButtonEnabled, optional androidx.compose.ui.Modifier modifier, optional androidx.wear.compose.material.ButtonColors colors); diff --git a/media-ui/src/androidTest/java/com/google/android/horologist/media/ui/components/MediaChipTest.kt b/media-ui/src/androidTest/java/com/google/android/horologist/media/ui/components/MediaChipTest.kt new file mode 100644 index 0000000000..e2c92be00e --- /dev/null +++ b/media-ui/src/androidTest/java/com/google/android/horologist/media/ui/components/MediaChipTest.kt @@ -0,0 +1,65 @@ +/* + * Copyright 2022 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 + * + * https://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. + */ + +@file:OptIn(ExperimentalHorologistMediaUiApi::class) + +package com.google.android.horologist.media.ui.components + +import androidx.compose.ui.test.junit4.createComposeRule +import androidx.compose.ui.test.onNodeWithText +import com.google.android.horologist.media.ui.ExperimentalHorologistMediaUiApi +import com.google.android.horologist.media.ui.state.model.MediaItemUiModel +import org.junit.Rule +import org.junit.Test + +class MediaChipTest { + + @get:Rule + val composeTestRule = createComposeRule() + + @Test + fun givenTitle_thenDisplaysTitle() { + // given + val title = "title" + + composeTestRule.setContent { + MediaChip( + mediaItem = MediaItemUiModel(id = "id", title = title), + onClick = { }, + ) + } + + // then + composeTestRule.onNodeWithText(title).assertExists() + } + + @Test + fun givenNoTitle_thenDisplaysTitle() { + // given + val defaultTitle = "defaultTitle" + + composeTestRule.setContent { + MediaChip( + mediaItem = MediaItemUiModel(id = "id"), + onClick = { }, + defaultTitle = defaultTitle + ) + } + + // then + composeTestRule.onNodeWithText(defaultTitle).assertExists() + } +} diff --git a/media-ui/src/debug/java/com/google/android/horologist/media/ui/components/MediaChipPreview.kt b/media-ui/src/debug/java/com/google/android/horologist/media/ui/components/MediaChipPreview.kt new file mode 100644 index 0000000000..4a475ba6ea --- /dev/null +++ b/media-ui/src/debug/java/com/google/android/horologist/media/ui/components/MediaChipPreview.kt @@ -0,0 +1,91 @@ +/* + * Copyright 2022 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 + * + * https://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. + */ + +@file:OptIn(ExperimentalHorologistMediaUiApi::class) + +package com.google.android.horologist.media.ui.components + +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Album +import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.vector.rememberVectorPainter +import androidx.compose.ui.tooling.preview.Preview +import com.google.android.horologist.media.ui.ExperimentalHorologistMediaUiApi +import com.google.android.horologist.media.ui.state.model.MediaItemUiModel + +@Preview( + backgroundColor = 0xff000000, + showBackground = true, +) +@Composable +fun MediaChipPreview() { + MediaChip( + mediaItem = MediaItemUiModel( + id = "id", + title = "Red Hot Chilli Peppers", + artworkUri = "artworkUri" + ), + onClick = {}, + placeholder = rememberVectorPainter(image = Icons.Default.Album) + ) +} + +@Preview( + name = "No artwork", + backgroundColor = 0xff000000, + showBackground = true, +) +@Composable +fun MediaChipPreviewNoArtwork() { + MediaChip( + mediaItem = MediaItemUiModel(id = "id", title = "Red Hot Chilli Peppers"), + onClick = {}, + placeholder = rememberVectorPainter(image = Icons.Default.Album) + ) +} + +@Preview( + name = "No title", + backgroundColor = 0xff000000, + showBackground = true, +) +@Composable +fun MediaChipPreviewNoTitle() { + MediaChip( + mediaItem = MediaItemUiModel(id = "id", artworkUri = "artworkUri"), + onClick = {}, + defaultTitle = "No title", + placeholder = rememberVectorPainter(image = Icons.Default.Album) + ) +} + +@Preview( + name = "Very long title", + backgroundColor = 0xff000000, + showBackground = true, +) +@Composable +fun MediaChipPreviewVeryLongTitle() { + MediaChip( + mediaItem = MediaItemUiModel( + id = "id", + title = "Very very very very very very very very very very very very very very very very very very very long title", + artworkUri = "artworkUri" + ), + onClick = {}, + placeholder = rememberVectorPainter(image = Icons.Default.Album) + ) +} diff --git a/media-sample/src/main/java/com/google/android/horologist/mediasample/ui/components/MediaChip.kt b/media-ui/src/main/java/com/google/android/horologist/media/ui/components/MediaChip.kt similarity index 55% rename from media-sample/src/main/java/com/google/android/horologist/mediasample/ui/components/MediaChip.kt rename to media-ui/src/main/java/com/google/android/horologist/media/ui/components/MediaChip.kt index 8040e83f98..51b8d08a65 100644 --- a/media-sample/src/main/java/com/google/android/horologist/mediasample/ui/components/MediaChip.kt +++ b/media-ui/src/main/java/com/google/android/horologist/media/ui/components/MediaChip.kt @@ -1,5 +1,5 @@ /* - * Copyright 2021 The Android Open Source Project + * Copyright 2022 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. @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.google.android.horologist.mediasample.ui.components +package com.google.android.horologist.media.ui.components import androidx.compose.foundation.layout.BoxScope import androidx.compose.foundation.layout.PaddingValues @@ -22,44 +22,66 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.size import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource +import androidx.compose.ui.graphics.painter.Painter +import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.wear.compose.material.Chip import androidx.wear.compose.material.ChipDefaults import androidx.wear.compose.material.Text -import com.google.android.horologist.media.ui.components.MediaArtwork +import com.google.android.horologist.media.ui.ExperimentalHorologistMediaUiApi import com.google.android.horologist.media.ui.state.model.MediaItemUiModel -import com.google.android.horologist.mediasample.R /** - * A rounded chip to show a single [MediaItemUiModel] with an - * optional secondary action + * A rounded chip to show a single [MediaItemUiModel]. + * + * @param mediaItem The [MediaItemUiModel] that the [title][MediaItemUiModel.title] and + * [artwork][MediaItemUiModel.artworkUri] will be used to display on the chip. + * @param onClick Will be called when the user clicks the chip. + * @param modifier The Modifier to be applied to the chip. + * @param defaultTitle A text to be used when [MediaItemUiModel.title] is null. + * @param placeholder A placeholder image to be displayed while + * [artwork][MediaItemUiModel.artworkUri] is being loaded. */ +@ExperimentalHorologistMediaUiApi @Composable -fun MediaChip( +public fun MediaChip( mediaItem: MediaItemUiModel, onClick: () -> Unit, modifier: Modifier = Modifier, + defaultTitle: String = "", + placeholder: Painter? = null, ) { val artworkUri = mediaItem.artworkUri val title = mediaItem.title - MediaChip(artworkUri = artworkUri, title = title, onClick = onClick, modifier = modifier) + MediaChip( + title = title ?: defaultTitle, + artworkUri = artworkUri, + onClick = onClick, + modifier = modifier, + placeholder = placeholder, + ) } +/** + * A rounded chip to show a single media title and its artwork. + */ +@ExperimentalHorologistMediaUiApi @Composable -fun MediaChip( +public fun MediaChip( + title: String, artworkUri: String?, - title: String?, onClick: () -> Unit, modifier: Modifier = Modifier, + placeholder: Painter? = null, ) { val appIcon: (@Composable BoxScope.() -> Unit)? = artworkUri?.let { { MediaArtwork( modifier = Modifier.size(ChipDefaults.LargeIconSize), - contentDescription = title ?: stringResource(id = R.string.horologist_no_title), - artworkUri = artworkUri + contentDescription = title, + artworkUri = artworkUri, + placeholder = placeholder, ) } } @@ -77,8 +99,9 @@ fun MediaChip( icon = appIcon, label = { Text( - text = title ?: stringResource(id = R.string.horologist_no_title), - maxLines = 2 + text = title, + maxLines = 2, + overflow = TextOverflow.Ellipsis ) } ) diff --git a/media-ui/src/test/java/com/google/android/horologist/media/ui/components/MediaChipTest.kt b/media-ui/src/test/java/com/google/android/horologist/media/ui/components/MediaChipTest.kt new file mode 100644 index 0000000000..3bc9eb7ada --- /dev/null +++ b/media-ui/src/test/java/com/google/android/horologist/media/ui/components/MediaChipTest.kt @@ -0,0 +1,88 @@ +/* + * Copyright 2022 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 + * + * https://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. + */ + +@file:OptIn(ExperimentalHorologistMediaUiApi::class) + +package com.google.android.horologist.media.ui.components + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Album +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.vector.rememberVectorPainter +import app.cash.paparazzi.Paparazzi +import com.google.android.horologist.media.ui.ExperimentalHorologistMediaUiApi +import com.google.android.horologist.paparazzi.GALAXY_WATCH4_CLASSIC_LARGE +import com.google.android.horologist.paparazzi.WearSnapshotHandler +import com.google.android.horologist.paparazzi.determineHandler +import org.junit.Rule +import org.junit.Test + +class MediaChipTest { + + @get:Rule + val paparazzi = Paparazzi( + deviceConfig = GALAXY_WATCH4_CLASSIC_LARGE, + theme = "android:ThemeOverlay.Material.Dark", + maxPercentDifference = 0.0, + snapshotHandler = WearSnapshotHandler(determineHandler(0.1)) + ) + + @Test + fun givenMediaItemWithArtwork_thenDisplaysArtwork() { + paparazzi.snapshot { + Box(modifier = Modifier.background(Color.Black), contentAlignment = Alignment.Center) { + MediaChip( + title = "Red Hot Chilli Peppers", + artworkUri = "artworkUri", + onClick = {}, + placeholder = rememberVectorPainter(image = Icons.Default.Album) + ) + } + } + } + + @Test + fun givenMediaItemWithNOArtwork_thenDoesNOTDisplayArtwork() { + paparazzi.snapshot { + Box(modifier = Modifier.background(Color.Black), contentAlignment = Alignment.Center) { + MediaChip( + title = "Red Hot Chilli Peppers", + artworkUri = null, + onClick = {}, + placeholder = rememberVectorPainter(image = Icons.Default.Album) + ) + } + } + } + + @Test + fun givenVeryLongTitle_thenEllipsizeAt2ndLine() { + paparazzi.snapshot { + Box(modifier = Modifier.background(Color.Black), contentAlignment = Alignment.Center) { + MediaChip( + title = "Very very very very very very very very very very very long title", + artworkUri = "artworkUri", + onClick = {}, + placeholder = rememberVectorPainter(image = Icons.Default.Album) + ) + } + } + } +} diff --git a/media-ui/src/test/snapshots/images/com.google.android.horologist.media.ui.components_MediaChipTest_givenMediaItemWithArtwork_thenDisplaysArtwork.png b/media-ui/src/test/snapshots/images/com.google.android.horologist.media.ui.components_MediaChipTest_givenMediaItemWithArtwork_thenDisplaysArtwork.png new file mode 100644 index 0000000000..0594fc7898 Binary files /dev/null and b/media-ui/src/test/snapshots/images/com.google.android.horologist.media.ui.components_MediaChipTest_givenMediaItemWithArtwork_thenDisplaysArtwork.png differ diff --git a/media-ui/src/test/snapshots/images/com.google.android.horologist.media.ui.components_MediaChipTest_givenMediaItemWithNOArtwork_thenDoesNOTDisplayArtwork.png b/media-ui/src/test/snapshots/images/com.google.android.horologist.media.ui.components_MediaChipTest_givenMediaItemWithNOArtwork_thenDoesNOTDisplayArtwork.png new file mode 100644 index 0000000000..ab47dcce1b Binary files /dev/null and b/media-ui/src/test/snapshots/images/com.google.android.horologist.media.ui.components_MediaChipTest_givenMediaItemWithNOArtwork_thenDoesNOTDisplayArtwork.png differ diff --git a/media-ui/src/test/snapshots/images/com.google.android.horologist.media.ui.components_MediaChipTest_givenVeryLongTitle_thenEllipsizeAt2ndLine.png b/media-ui/src/test/snapshots/images/com.google.android.horologist.media.ui.components_MediaChipTest_givenVeryLongTitle_thenEllipsizeAt2ndLine.png new file mode 100644 index 0000000000..a867ae198a Binary files /dev/null and b/media-ui/src/test/snapshots/images/com.google.android.horologist.media.ui.components_MediaChipTest_givenVeryLongTitle_thenEllipsizeAt2ndLine.png differ