diff --git a/Jetcaster/gradle/libs.versions.toml b/Jetcaster/gradle/libs.versions.toml index 4a02af9617..c5202497df 100644 --- a/Jetcaster/gradle/libs.versions.toml +++ b/Jetcaster/gradle/libs.versions.toml @@ -24,8 +24,8 @@ androidx-test-ext-junit = "1.2.1" androidx-test-ext-truth = "1.6.0" androidx-tv-foundation = "1.0.0-alpha12" androidx-tv-material = "1.0.0" -androidx-wear-compose-material3 = "1.5.1" -androidx-wear-compose = "1.5.0" +androidx-wear-compose-material3 = "1.6.2" +androidx-wear-compose = "1.6.2" androidx-window = "1.4.0" androidxHiltNavigationCompose = "1.2.0" androix-test-uiautomator = "2.3.0" diff --git a/Jetcaster/wear/build.gradle b/Jetcaster/wear/build.gradle index 77580edf3f..2f9a23e965 100644 --- a/Jetcaster/wear/build.gradle +++ b/Jetcaster/wear/build.gradle @@ -23,14 +23,14 @@ plugins { } android { - compileSdk 35 + compileSdk 37 namespace "com.example.jetcaster" defaultConfig { applicationId "com.example.jetcaster" minSdk 26 - targetSdk 34 + targetSdk 37 versionCode 1 versionName "1.0" } @@ -108,9 +108,6 @@ dependencies { implementation(libs.androidx.media3.session) implementation libs.androidx.media3.common.ktx - // Horologist for correct Compose layout - implementation libs.horologist.compose.layout - //Horologist Media toolkit implementation libs.horologist.media.ui implementation libs.horologist.media.uimaterial3 @@ -153,7 +150,6 @@ dependencies { testImplementation libs.roborazzi testImplementation libs.roborazzi.compose testImplementation libs.roborazzi.rule - testImplementation(libs.horologist.roboscreenshots) androidTestImplementation libs.androidx.test.ext.junit androidTestImplementation libs.androidx.test.espresso.core diff --git a/Jetcaster/wear/src/main/java/com/example/jetcaster/ui/components/MediaContent.kt b/Jetcaster/wear/src/main/java/com/example/jetcaster/ui/components/MediaContent.kt index d1ecc9120c..e2a99b2a0a 100644 --- a/Jetcaster/wear/src/main/java/com/example/jetcaster/ui/components/MediaContent.kt +++ b/Jetcaster/wear/src/main/java/com/example/jetcaster/ui/components/MediaContent.kt @@ -34,6 +34,7 @@ import androidx.wear.compose.material3.AppScaffold import androidx.wear.compose.material3.ButtonDefaults import androidx.wear.compose.material3.FilledTonalButton import androidx.wear.compose.material3.ScreenScaffold +import androidx.wear.compose.material3.SurfaceTransformation import androidx.wear.compose.material3.Text import androidx.wear.compose.ui.tooling.preview.WearPreviewDevices import androidx.wear.compose.ui.tooling.preview.WearPreviewFontScales @@ -41,8 +42,6 @@ import coil.compose.AsyncImage import com.example.jetcaster.R import com.example.jetcaster.core.player.model.PlayerEpisode import com.example.jetcaster.ui.preview.WearPreviewEpisodes -import com.google.android.horologist.compose.layout.ColumnItemType -import com.google.android.horologist.compose.layout.rememberResponsiveColumnPadding import java.time.format.DateTimeFormatter import java.time.format.FormatStyle @@ -52,6 +51,7 @@ fun MediaContent( onItemClick: (PlayerEpisode) -> Unit, modifier: Modifier = Modifier, episodeArtworkPlaceholder: Painter = painterResource(id = R.drawable.music), + transformation: SurfaceTransformation? = null, ) { val mediaTitle = episode.title val duration = episode.duration @@ -98,10 +98,10 @@ fun MediaContent( ButtonDefaults.LargeIconSize, ) .clip(CircleShape), - ) }, modifier = modifier.fillMaxWidth(), + transformation = transformation, ) } @@ -114,11 +114,7 @@ fun MediaContentPreview( modifier: Modifier = Modifier, ) { AppScaffold(modifier = modifier) { - val contentPadding = rememberResponsiveColumnPadding( - first = ColumnItemType.Button, - ) - - ScreenScaffold(contentPadding = contentPadding) { + ScreenScaffold { contentPadding -> Box( modifier = Modifier .fillMaxWidth() diff --git a/Jetcaster/wear/src/main/java/com/example/jetcaster/ui/episode/EpisodeScreen.kt b/Jetcaster/wear/src/main/java/com/example/jetcaster/ui/episode/EpisodeScreen.kt index 2f9f130302..7cc2df81f1 100644 --- a/Jetcaster/wear/src/main/java/com/example/jetcaster/ui/episode/EpisodeScreen.kt +++ b/Jetcaster/wear/src/main/java/com/example/jetcaster/ui/episode/EpisodeScreen.kt @@ -37,18 +37,23 @@ import androidx.wear.compose.foundation.lazy.TransformingLazyColumnScope import androidx.wear.compose.foundation.lazy.TransformingLazyColumnState import androidx.wear.compose.foundation.lazy.items import androidx.wear.compose.foundation.lazy.rememberTransformingLazyColumnState -import androidx.wear.compose.material.ExperimentalWearMaterialApi import androidx.wear.compose.material3.AlertDialog +import androidx.wear.compose.material3.ButtonDefaults import androidx.wear.compose.material3.ButtonGroup import androidx.wear.compose.material3.FilledIconButton import androidx.wear.compose.material3.Icon import androidx.wear.compose.material3.IconButtonShapes import androidx.wear.compose.material3.ListHeader +import androidx.wear.compose.material3.ListHeaderDefaults import androidx.wear.compose.material3.LocalContentColor import androidx.wear.compose.material3.MaterialTheme import androidx.wear.compose.material3.PlaceholderState import androidx.wear.compose.material3.ScreenScaffold +import androidx.wear.compose.material3.SurfaceTransformation import androidx.wear.compose.material3.Text +import androidx.wear.compose.material3.lazy.TransformationSpec +import androidx.wear.compose.material3.lazy.rememberTransformationSpec +import androidx.wear.compose.material3.lazy.transformedHeight import androidx.wear.compose.material3.placeholder import androidx.wear.compose.material3.placeholderShimmer import androidx.wear.compose.material3.rememberPlaceholderState @@ -60,11 +65,7 @@ import com.example.jetcaster.core.player.model.toPlayerEpisode import com.example.jetcaster.designsystem.component.HtmlTextContainer import com.example.jetcaster.ui.components.MediumDateFormatter import com.example.jetcaster.ui.preview.WearPreviewEpisodes -import com.google.android.horologist.compose.layout.ColumnItemType -import com.google.android.horologist.compose.layout.ScalingLazyColumnDefaults.listTextPadding -import com.google.android.horologist.compose.layout.rememberResponsiveColumnPadding -@OptIn(ExperimentalWearMaterialApi::class) @Composable fun EpisodeScreen( onPlayButtonClick: () -> Unit, @@ -96,15 +97,9 @@ fun EpisodeScreen( onDismiss: () -> Unit, modifier: Modifier = Modifier, ) { - val contentPadding = rememberResponsiveColumnPadding( - first = ColumnItemType.ListHeader, - last = ColumnItemType.Button, - ) - val columnState = rememberTransformingLazyColumnState() ScreenScaffold( scrollState = columnState, - contentPadding = contentPadding, modifier = modifier.placeholderShimmer(placeholderState), ) { contentPadding -> when (uiState) { @@ -159,13 +154,23 @@ fun EpisodeScreenLoaded( placeholderState: PlaceholderState, modifier: Modifier = Modifier, ) { + val transformationSpec = rememberTransformationSpec() TransformingLazyColumn( modifier = modifier, state = columnState, contentPadding = contentPadding, ) { item { - ListHeader { + ListHeader( + modifier = Modifier + .fillMaxWidth() + .minimumVerticalContentPadding( + ListHeaderDefaults.minimumTopListContentPadding, + ListHeaderDefaults.minimumBottomListContentPadding, + ) + .transformedHeight(this, transformationSpec), + transformation = SurfaceTransformation(transformationSpec), + ) { Text( text = title, maxLines = 1, @@ -182,11 +187,16 @@ fun EpisodeScreenLoaded( onPlayEpisode = onPlayEpisode, onAddToQueue = onAddToQueue, placeholderState = placeholderState, + modifier = Modifier + .fillMaxWidth() + .minimumVerticalContentPadding(ButtonDefaults.minimumVerticalListContentPadding) + .transformedHeight(this, transformationSpec), ) } if (!placeholderState.isVisible) { episodeInfoContent( episode = episode, + transformationSpec = transformationSpec, ) } } @@ -205,7 +215,7 @@ fun LoadedButtonsContent( val playInteractionSource = remember { MutableInteractionSource() } val addToQueueInteractionSource = remember { MutableInteractionSource() } - ButtonGroup(modifier.fillMaxWidth().padding(bottom = 16.dp)) { + ButtonGroup(modifier.padding(bottom = 16.dp)) { FilledIconButton( onClick = { @@ -243,7 +253,7 @@ fun LoadedButtonsContent( } } -private fun TransformingLazyColumnScope.episodeInfoContent(episode: PlayerEpisode) { +private fun TransformingLazyColumnScope.episodeInfoContent(episode: PlayerEpisode, transformationSpec: TransformationSpec) { val author = episode.author val duration = episode.duration val published = episode.published @@ -256,6 +266,7 @@ private fun TransformingLazyColumnScope.episodeInfoContent(episode: PlayerEpisod maxLines = 1, overflow = TextOverflow.Ellipsis, style = MaterialTheme.typography.bodyMedium, + modifier = Modifier.transformedHeight(this, transformationSpec), ) } } @@ -279,7 +290,8 @@ private fun TransformingLazyColumnScope.episodeInfoContent(episode: PlayerEpisod overflow = TextOverflow.Ellipsis, style = MaterialTheme.typography.bodySmall, modifier = Modifier - .padding(horizontal = 8.dp), + .padding(horizontal = 8.dp) + .transformedHeight(this, transformationSpec), ) } if (summary != null) { @@ -290,7 +302,9 @@ private fun TransformingLazyColumnScope.episodeInfoContent(episode: PlayerEpisod text = it, style = MaterialTheme.typography.bodySmall, color = LocalContentColor.current, - modifier = Modifier.listTextPadding(), + modifier = Modifier + .padding(horizontal = 8.dp) + .transformedHeight(this, transformationSpec), ) } } @@ -319,11 +333,6 @@ fun EpisodeScreenLoadingPreview( @PreviewParameter(WearPreviewEpisodes::class) episode: PlayerEpisode, ) { - val contentPadding = rememberResponsiveColumnPadding( - first = ColumnItemType.ListHeader, - last = ColumnItemType.Button, - ) - val columnState = rememberTransformingLazyColumnState() EpisodeScreenLoaded( title = episode.title, @@ -332,7 +341,7 @@ fun EpisodeScreenLoadingPreview( onPlayEpisode = { }, onAddToQueue = { }, columnState = columnState, - contentPadding = contentPadding, + contentPadding = PaddingValues(), placeholderState = rememberPlaceholderState(isVisible = true), ) } @@ -345,11 +354,6 @@ fun EpisodeScreenLoadedPreview( episode: PlayerEpisode, ) { val columnState = rememberTransformingLazyColumnState() - val contentPadding = rememberResponsiveColumnPadding( - first = ColumnItemType.ListHeader, - last = ColumnItemType.Button, - ) - EpisodeScreenLoaded( title = episode.title, episode = episode, @@ -357,7 +361,7 @@ fun EpisodeScreenLoadedPreview( onPlayEpisode = { }, onAddToQueue = { }, columnState = columnState, - contentPadding = contentPadding, + contentPadding = PaddingValues(), placeholderState = rememberPlaceholderState(isVisible = false), ) } diff --git a/Jetcaster/wear/src/main/java/com/example/jetcaster/ui/latest_episodes/LatestEpisodesScreen.kt b/Jetcaster/wear/src/main/java/com/example/jetcaster/ui/latest_episodes/LatestEpisodesScreen.kt index cd7271a607..54d7089397 100644 --- a/Jetcaster/wear/src/main/java/com/example/jetcaster/ui/latest_episodes/LatestEpisodesScreen.kt +++ b/Jetcaster/wear/src/main/java/com/example/jetcaster/ui/latest_episodes/LatestEpisodesScreen.kt @@ -33,11 +33,16 @@ import androidx.wear.compose.foundation.lazy.items import androidx.wear.compose.foundation.lazy.rememberTransformingLazyColumnState import androidx.wear.compose.material3.AlertDialog import androidx.wear.compose.material3.Button +import androidx.wear.compose.material3.ButtonDefaults import androidx.wear.compose.material3.Icon import androidx.wear.compose.material3.ListHeader +import androidx.wear.compose.material3.ListHeaderDefaults import androidx.wear.compose.material3.PlaceholderState import androidx.wear.compose.material3.ScreenScaffold +import androidx.wear.compose.material3.SurfaceTransformation import androidx.wear.compose.material3.Text +import androidx.wear.compose.material3.lazy.rememberTransformationSpec +import androidx.wear.compose.material3.lazy.transformedHeight import androidx.wear.compose.material3.placeholder import androidx.wear.compose.material3.placeholderShimmer import androidx.wear.compose.material3.rememberPlaceholderState @@ -47,8 +52,6 @@ import com.example.jetcaster.R import com.example.jetcaster.core.player.model.PlayerEpisode import com.example.jetcaster.ui.components.MediaContent import com.example.jetcaster.ui.preview.WearPreviewEpisodes -import com.google.android.horologist.compose.layout.ColumnItemType -import com.google.android.horologist.compose.layout.rememberResponsiveColumnPadding @Composable fun LatestEpisodesScreen( onPlayButtonClick: () -> Unit, @@ -80,15 +83,9 @@ fun LatestEpisodeScreen( onPlayEpisode: (PlayerEpisode) -> Unit, modifier: Modifier = Modifier, ) { - val contentPadding = rememberResponsiveColumnPadding( - first = ColumnItemType.ListHeader, - last = ColumnItemType.Button, - ) - val columnState = rememberTransformingLazyColumnState() ScreenScaffold( scrollState = columnState, - contentPadding = contentPadding, modifier = modifier.placeholderShimmer(placeholderState), ) { contentPadding -> when (uiState) { @@ -135,6 +132,7 @@ fun ButtonsContent( placeholderState: PlaceholderState, modifier: Modifier = Modifier, enabled: Boolean = true, + transformation: SurfaceTransformation? = null, ) { Button( onClick = { @@ -149,6 +147,7 @@ fun ButtonsContent( ) }, modifier = modifier.fillMaxWidth().placeholder(placeholderState = placeholderState), + transformation = transformation, ) { Text(stringResource(id = R.string.button_play_content_description)) } @@ -165,13 +164,24 @@ fun LatestEpisodesScreen( placeholderState: PlaceholderState, modifier: Modifier = Modifier, ) { + val transformationSpec = rememberTransformationSpec() TransformingLazyColumn( modifier = modifier, state = scrollState, contentPadding = contentPadding, ) { item { - LatestEpisodesListHeader(placeholderState) + LatestEpisodesListHeader( + placeholderState = placeholderState, + modifier = Modifier + .fillMaxWidth() + .minimumVerticalContentPadding( + ListHeaderDefaults.minimumTopListContentPadding, + ListHeaderDefaults.minimumBottomListContentPadding, + ) + .transformedHeight(this, transformationSpec), + transformation = SurfaceTransformation(transformationSpec), + ) } item { ButtonsContent( @@ -179,6 +189,11 @@ fun LatestEpisodesScreen( onPlayButtonClick = onPlayButtonClick, onPlayEpisodes = onPlayEpisodes, placeholderState = placeholderState, + modifier = Modifier + .fillMaxWidth() + .minimumVerticalContentPadding(ButtonDefaults.minimumVerticalListContentPadding) + .transformedHeight(this, transformationSpec), + transformation = SurfaceTransformation(transformationSpec), ) } items(episodeList) { episode -> @@ -189,14 +204,26 @@ fun LatestEpisodesScreen( onPlayButtonClick() onPlayEpisode(episode) }, + modifier = Modifier + .fillMaxWidth() + .minimumVerticalContentPadding(ButtonDefaults.minimumVerticalListContentPadding) + .transformedHeight(this, transformationSpec), + transformation = SurfaceTransformation(transformationSpec), ) } } } @Composable -fun LatestEpisodesListHeader(placeholderState: PlaceholderState, modifier: Modifier = Modifier) { - ListHeader(modifier = modifier.placeholder(placeholderState)) { +fun LatestEpisodesListHeader( + placeholderState: PlaceholderState, + modifier: Modifier = Modifier, + transformation: SurfaceTransformation? = null, +) { + ListHeader( + modifier = modifier.placeholder(placeholderState), + transformation = transformation, + ) { Text( text = stringResource(id = R.string.latest_episodes), maxLines = 1, @@ -212,18 +239,13 @@ fun LatestEpisodeScreenLoadedPreview( @PreviewParameter(WearPreviewEpisodes::class) episode: PlayerEpisode, ) { - val contentPadding = rememberResponsiveColumnPadding( - first = ColumnItemType.ListHeader, - last = ColumnItemType.Button, - ) - val columnState = rememberTransformingLazyColumnState() LatestEpisodesScreen( episodeList = listOf(episode), onPlayButtonClick = { }, onPlayEpisode = { }, onPlayEpisodes = { }, - contentPadding = contentPadding, + contentPadding = PaddingValues(), scrollState = columnState, placeholderState = rememberPlaceholderState(isVisible = false), ) diff --git a/Jetcaster/wear/src/main/java/com/example/jetcaster/ui/library/LibraryScreen.kt b/Jetcaster/wear/src/main/java/com/example/jetcaster/ui/library/LibraryScreen.kt index 033cc53d8a..f4ba05758a 100644 --- a/Jetcaster/wear/src/main/java/com/example/jetcaster/ui/library/LibraryScreen.kt +++ b/Jetcaster/wear/src/main/java/com/example/jetcaster/ui/library/LibraryScreen.kt @@ -36,7 +36,6 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.PreviewParameter -import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import androidx.wear.compose.foundation.lazy.TransformingLazyColumn import androidx.wear.compose.foundation.lazy.TransformingLazyColumnState @@ -47,11 +46,13 @@ import androidx.wear.compose.material3.ButtonDefaults import androidx.wear.compose.material3.FilledTonalButton import androidx.wear.compose.material3.Icon import androidx.wear.compose.material3.ListHeader +import androidx.wear.compose.material3.ListHeaderDefaults import androidx.wear.compose.material3.MaterialTheme import androidx.wear.compose.material3.PlaceholderState import androidx.wear.compose.material3.ScreenScaffold import androidx.wear.compose.material3.SurfaceTransformation import androidx.wear.compose.material3.Text +import androidx.wear.compose.material3.TextDefaults import androidx.wear.compose.material3.lazy.rememberTransformationSpec import androidx.wear.compose.material3.lazy.transformedHeight import androidx.wear.compose.material3.placeholder @@ -65,8 +66,6 @@ import com.example.jetcaster.core.model.PodcastInfo import com.example.jetcaster.core.player.model.PlayerEpisode import com.example.jetcaster.ui.preview.WearPreviewEpisodes import com.example.jetcaster.ui.preview.WearPreviewPodcasts -import com.google.android.horologist.compose.layout.ColumnItemType -import com.google.android.horologist.compose.layout.rememberResponsiveColumnPadding @Composable fun LibraryScreen( @@ -79,15 +78,9 @@ fun LibraryScreen( val uiState by libraryScreenViewModel.uiState.collectAsState() val placeholderState = rememberPlaceholderState(isVisible = uiState is LibraryScreenUiState.Loading) - val contentPadding = rememberResponsiveColumnPadding( - first = ColumnItemType.ListHeader, - last = ColumnItemType.Button, - ) - val columnState = rememberTransformingLazyColumnState() ScreenScaffold( scrollState = columnState, - contentPadding = contentPadding, modifier = modifier.placeholderShimmer(placeholderState), ) { contentPadding -> when (val s = uiState) { @@ -132,6 +125,7 @@ fun NoSubscribedPodcastScreen( contentPadding: PaddingValues, modifier: Modifier = Modifier, ) { + val transformationSpec = rememberTransformationSpec() TransformingLazyColumn( state = columnState, contentPadding = contentPadding, @@ -140,6 +134,14 @@ fun NoSubscribedPodcastScreen( item { ListHeader( contentColor = MaterialTheme.colorScheme.onSurface, + modifier = Modifier + .fillMaxWidth() + .minimumVerticalContentPadding( + ListHeaderDefaults.minimumTopListContentPadding, + ListHeaderDefaults.minimumBottomListContentPadding, + ) + .transformedHeight(this, transformationSpec), + transformation = SurfaceTransformation(transformationSpec), ) { Text(stringResource(R.string.entity_no_featured_podcasts)) } @@ -152,6 +154,11 @@ fun NoSubscribedPodcastScreen( onClick = { onTogglePodcastFollowed(podcast.uri) }, + modifier = Modifier + .fillMaxWidth() + .minimumVerticalContentPadding(ButtonDefaults.minimumVerticalListContentPadding) + .transformedHeight(this, transformationSpec), + transformation = SurfaceTransformation(transformationSpec), ) } } else { @@ -160,6 +167,11 @@ fun NoSubscribedPodcastScreen( podcast = PodcastInfo(), podcastArtworkPlaceholder = painterResource(id = R.drawable.music), onClick = {}, + modifier = Modifier + .fillMaxWidth() + .minimumVerticalContentPadding(ButtonDefaults.minimumVerticalListContentPadding) + .transformedHeight(this, transformationSpec), + transformation = SurfaceTransformation(transformationSpec), ) } } @@ -167,7 +179,13 @@ fun NoSubscribedPodcastScreen( } @Composable -private fun PodcastContent(podcast: PodcastInfo, onClick: () -> Unit, podcastArtworkPlaceholder: Painter?, modifier: Modifier = Modifier) { +private fun PodcastContent( + podcast: PodcastInfo, + onClick: () -> Unit, + podcastArtworkPlaceholder: Painter?, + modifier: Modifier = Modifier, + transformation: SurfaceTransformation? = null, +) { val mediaTitle = podcast.title FilledTonalButton( @@ -191,10 +209,10 @@ private fun PodcastContent(podcast: PodcastInfo, onClick: () -> Unit, podcastArt ButtonDefaults.LargeIconSize, ) .clip(CircleShape), - ) }, modifier = modifier.fillMaxWidth(), + transformation = transformation, ) } @@ -209,87 +227,104 @@ fun LibraryScreen( queue: List, modifier: Modifier = Modifier, ) { - ScreenScaffold( - scrollState = columnState, + val transformationSpec = rememberTransformationSpec() + TransformingLazyColumn( + state = columnState, contentPadding = contentPadding, - modifier = modifier.placeholderShimmer(placeholderState), - ) { contentPadding -> - val transformationSpec = rememberTransformationSpec() - TransformingLazyColumn(state = columnState, contentPadding = contentPadding) { - item { - ListHeader( - modifier = Modifier - .fillMaxWidth() - .transformedHeight(this, transformationSpec) - .placeholder(placeholderState), - transformation = SurfaceTransformation(transformationSpec), - ) { - Text(stringResource(R.string.home_library)) - } + modifier = modifier, + ) { + item { + ListHeader( + modifier = Modifier + .fillMaxWidth() + .minimumVerticalContentPadding( + ListHeaderDefaults.minimumTopListContentPadding, + ListHeaderDefaults.minimumBottomListContentPadding, + ) + .transformedHeight(this, transformationSpec) + .placeholder(placeholderState), + transformation = SurfaceTransformation(transformationSpec), + ) { + Text(stringResource(R.string.home_library)) } - item { - FilledTonalButton( - label = { Text(stringResource(R.string.latest_episodes)) }, - onClick = { onLatestEpisodeClick() }, - icon = { - IconWithBackground( - R.drawable.new_releases, - stringResource(R.string.latest_episodes), - ) - }, - colors = ButtonDefaults.filledTonalButtonColors( - containerColor = MaterialTheme.colorScheme.surfaceContainer, - ), + } + item { + FilledTonalButton( + label = { Text(stringResource(R.string.latest_episodes)) }, + onClick = { onLatestEpisodeClick() }, + icon = { + IconWithBackground( + R.drawable.new_releases, + stringResource(R.string.latest_episodes), + ) + }, + colors = ButtonDefaults.filledTonalButtonColors( + containerColor = MaterialTheme.colorScheme.surfaceContainer, + ), + modifier = Modifier + .fillMaxWidth() + .minimumVerticalContentPadding(ButtonDefaults.minimumVerticalListContentPadding) + .transformedHeight(this, transformationSpec) + .placeholder(placeholderState = placeholderState), + transformation = SurfaceTransformation(transformationSpec), + ) + } + item { + FilledTonalButton( + label = { Text(stringResource(R.string.podcasts)) }, + onClick = { onYourPodcastClick() }, + icon = { + IconWithBackground(R.drawable.podcast, stringResource(R.string.podcasts)) + }, + modifier = Modifier + .fillMaxWidth() + .minimumVerticalContentPadding(ButtonDefaults.minimumVerticalListContentPadding) + .transformedHeight(this, transformationSpec) + .placeholder(placeholderState = placeholderState), + transformation = SurfaceTransformation(transformationSpec), + ) + } + item { + ListHeader( + modifier = Modifier + .fillMaxWidth() + .minimumVerticalContentPadding( + ListHeaderDefaults.minimumTopListContentPadding, + ListHeaderDefaults.minimumBottomListContentPadding, + ) + .transformedHeight(this, transformationSpec) + .placeholder(placeholderState = placeholderState), + transformation = SurfaceTransformation(transformationSpec), + ) { + Text(stringResource(R.string.queue)) + } + } + item { + if (queue.isEmpty()) { + QueueEmptyText( modifier = Modifier .fillMaxWidth() - .transformedHeight(this, transformationSpec) - .placeholder(placeholderState = placeholderState), - transformation = SurfaceTransformation(transformationSpec), + .minimumVerticalContentPadding( + TextDefaults.minimumTopListContentPadding, + TextDefaults.minimumBottomListContentPadding, + ) + .transformedHeight(this, transformationSpec), ) - } - item { + } else { FilledTonalButton( - label = { Text(stringResource(R.string.podcasts)) }, - onClick = { onYourPodcastClick() }, + label = { Text(stringResource(R.string.up_next)) }, + onClick = { onUpNextClick() }, icon = { - IconWithBackground(R.drawable.podcast, stringResource(R.string.podcasts)) + IconWithBackground(R.drawable.up_next, stringResource(R.string.up_next)) }, modifier = Modifier .fillMaxWidth() + .minimumVerticalContentPadding(ButtonDefaults.minimumVerticalListContentPadding) .transformedHeight(this, transformationSpec) .placeholder(placeholderState = placeholderState), transformation = SurfaceTransformation(transformationSpec), ) } - item { - ListHeader( - modifier = Modifier - .fillMaxWidth() - .transformedHeight(this, transformationSpec) - .placeholder(placeholderState = placeholderState), - transformation = SurfaceTransformation(transformationSpec), - ) { - Text(stringResource(R.string.queue)) - } - } - item { - if (queue.isEmpty()) { - QueueEmptyText() - } else { - FilledTonalButton( - label = { Text(stringResource(R.string.up_next)) }, - onClick = { onUpNextClick() }, - icon = { - IconWithBackground(R.drawable.up_next, stringResource(R.string.up_next)) - }, - modifier = Modifier - .fillMaxWidth() - .transformedHeight(this, transformationSpec) - .placeholder(placeholderState = placeholderState), - transformation = SurfaceTransformation(transformationSpec), - ) - } - } } } } @@ -318,7 +353,7 @@ private fun IconWithBackground(resource: Int, contentDescription: String, modifi private fun QueueEmptyText(modifier: Modifier = Modifier) { Text( text = stringResource(id = R.string.add_episode_to_queue), - modifier = modifier.padding(top = 8.dp, bottom = 8.dp), + modifier = modifier, textAlign = TextAlign.Center, style = MaterialTheme.typography.bodySmall, ) @@ -350,15 +385,11 @@ fun LibraryScreenPreview( @Composable fun PodcastContentPreview(@PreviewParameter(WearPreviewPodcasts::class) podcasts: PodcastInfo, modifier: Modifier = Modifier) { AppScaffold { - val contentPadding = rememberResponsiveColumnPadding( - first = ColumnItemType.Button, - ) - - ScreenScaffold(contentPadding = contentPadding) { + ScreenScaffold { Box( modifier = Modifier .fillMaxWidth() - .padding(contentPadding), + .padding(it), ) { PodcastContent( podcast = podcasts, diff --git a/Jetcaster/wear/src/main/java/com/example/jetcaster/ui/player/PlayerScreen.kt b/Jetcaster/wear/src/main/java/com/example/jetcaster/ui/player/PlayerScreen.kt index dfdfdbe423..45d2f0c546 100644 --- a/Jetcaster/wear/src/main/java/com/example/jetcaster/ui/player/PlayerScreen.kt +++ b/Jetcaster/wear/src/main/java/com/example/jetcaster/ui/player/PlayerScreen.kt @@ -33,6 +33,7 @@ package com.example.jetcaster.ui.player */ import android.content.Context +import androidx.annotation.OptIn import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.runtime.Composable @@ -56,10 +57,8 @@ import androidx.media3.exoplayer.source.ProgressiveMediaSource import androidx.media3.session.MediaSession import androidx.media3.ui.compose.PlayerSurface import androidx.media3.ui.compose.modifiers.resizeWithContentScale -import androidx.wear.compose.foundation.ExperimentalWearFoundationApi import androidx.wear.compose.foundation.requestFocusOnHierarchyActive import androidx.wear.compose.foundation.rotary.rotaryScrollable -import androidx.wear.compose.material.ExperimentalWearMaterialApi import androidx.wear.compose.material3.MaterialTheme import com.example.jetcaster.R import com.example.jetcaster.ui.components.SettingsButtons @@ -95,8 +94,7 @@ fun PlayerScreen( ) } -@androidx.annotation.OptIn(UnstableApi::class) -@OptIn(ExperimentalWearFoundationApi::class, ExperimentalWearMaterialApi::class) +@OptIn(UnstableApi::class) @Composable private fun PlayerScreen( playerScreenViewModel: PlayerViewModel, diff --git a/Jetcaster/wear/src/main/java/com/example/jetcaster/ui/podcast/PodcastDetailsScreen.kt b/Jetcaster/wear/src/main/java/com/example/jetcaster/ui/podcast/PodcastDetailsScreen.kt index 991e6485c7..b7e2eec3d8 100644 --- a/Jetcaster/wear/src/main/java/com/example/jetcaster/ui/podcast/PodcastDetailsScreen.kt +++ b/Jetcaster/wear/src/main/java/com/example/jetcaster/ui/podcast/PodcastDetailsScreen.kt @@ -33,11 +33,16 @@ import androidx.wear.compose.foundation.lazy.items import androidx.wear.compose.foundation.lazy.rememberTransformingLazyColumnState import androidx.wear.compose.material3.AlertDialog import androidx.wear.compose.material3.Button +import androidx.wear.compose.material3.ButtonDefaults import androidx.wear.compose.material3.Icon import androidx.wear.compose.material3.ListHeader +import androidx.wear.compose.material3.ListHeaderDefaults import androidx.wear.compose.material3.PlaceholderState import androidx.wear.compose.material3.ScreenScaffold +import androidx.wear.compose.material3.SurfaceTransformation import androidx.wear.compose.material3.Text +import androidx.wear.compose.material3.lazy.rememberTransformationSpec +import androidx.wear.compose.material3.lazy.transformedHeight import androidx.wear.compose.material3.placeholder import androidx.wear.compose.material3.placeholderShimmer import androidx.wear.compose.material3.rememberPlaceholderState @@ -48,8 +53,6 @@ import com.example.jetcaster.core.domain.testing.PreviewPodcastEpisodes import com.example.jetcaster.core.player.model.PlayerEpisode import com.example.jetcaster.ui.components.MediaContent import com.example.jetcaster.ui.preview.WearPreviewEpisodes -import com.google.android.horologist.compose.layout.ColumnItemType -import com.google.android.horologist.compose.layout.rememberResponsiveColumnPadding @Composable fun PodcastDetailsScreen( onPlayButtonClick: () -> Unit, @@ -82,19 +85,12 @@ fun PodcastDetailsScreen( onDismiss: () -> Unit, modifier: Modifier = Modifier, ) { - - val contentPadding = rememberResponsiveColumnPadding( - first = ColumnItemType.ListHeader, - last = ColumnItemType.Button, - ) - val columnState = rememberTransformingLazyColumnState() ScreenScaffold( scrollState = columnState, - contentPadding = contentPadding, modifier = modifier.placeholderShimmer(placeholderState), - ) { + ) { contentPadding -> when (uiState) { is PodcastDetailsScreenState.Loaded -> { PodcastDetailScreenLoaded( @@ -144,17 +140,27 @@ fun PodcastDetailScreenLoaded( placeholderState: PlaceholderState, modifier: Modifier = Modifier, ) { + val transformationSpec = rememberTransformationSpec() TransformingLazyColumn( modifier = modifier, state = columnState, contentPadding = contentPadding, ) { item { - ListHeader { + ListHeader( + modifier = Modifier + .fillMaxWidth() + .minimumVerticalContentPadding( + ListHeaderDefaults.minimumTopListContentPadding, + ListHeaderDefaults.minimumBottomListContentPadding, + ) + .transformedHeight(this, transformationSpec) + .placeholder(placeholderState), + transformation = SurfaceTransformation(transformationSpec), + ) { Text( text = title, maxLines = 1, overflow = TextOverflow.Ellipsis, - modifier = Modifier.placeholder(placeholderState), ) } } @@ -164,6 +170,11 @@ fun PodcastDetailScreenLoaded( onPlayButtonClick = onPlayButtonClick, onPlayEpisode = onPlayEpisode, placeholderState = placeholderState, + modifier = Modifier + .fillMaxWidth() + .minimumVerticalContentPadding(ButtonDefaults.minimumVerticalListContentPadding) + .transformedHeight(this, transformationSpec), + transformation = SurfaceTransformation(transformationSpec), ) } items(episodeList) { episode -> @@ -171,6 +182,11 @@ fun PodcastDetailScreenLoaded( episode = episode, episodeArtworkPlaceholder = painterResource(id = R.drawable.music), onItemClick = onEpisodeItemClick, + modifier = Modifier + .fillMaxWidth() + .minimumVerticalContentPadding(ButtonDefaults.minimumVerticalListContentPadding) + .transformedHeight(this, transformationSpec), + transformation = SurfaceTransformation(transformationSpec), ) } } @@ -184,8 +200,8 @@ fun ButtonsContent( placeholderState: PlaceholderState, modifier: Modifier = Modifier, enabled: Boolean = true, + transformation: SurfaceTransformation? = null, ) { - Button( onClick = { onPlayButtonClick() @@ -200,6 +216,7 @@ fun ButtonsContent( }, modifier = modifier.fillMaxWidth() .placeholder(placeholderState = placeholderState), + transformation = transformation, ) { Text(stringResource(id = R.string.button_play_content_description)) } diff --git a/Jetcaster/wear/src/main/java/com/example/jetcaster/ui/podcasts/PodcastsScreen.kt b/Jetcaster/wear/src/main/java/com/example/jetcaster/ui/podcasts/PodcastsScreen.kt index bf52a452f4..005f414ffb 100644 --- a/Jetcaster/wear/src/main/java/com/example/jetcaster/ui/podcasts/PodcastsScreen.kt +++ b/Jetcaster/wear/src/main/java/com/example/jetcaster/ui/podcasts/PodcastsScreen.kt @@ -40,9 +40,13 @@ import androidx.wear.compose.material3.AlertDialog import androidx.wear.compose.material3.ButtonDefaults import androidx.wear.compose.material3.FilledTonalButton import androidx.wear.compose.material3.ListHeader +import androidx.wear.compose.material3.ListHeaderDefaults import androidx.wear.compose.material3.PlaceholderState import androidx.wear.compose.material3.ScreenScaffold +import androidx.wear.compose.material3.SurfaceTransformation import androidx.wear.compose.material3.Text +import androidx.wear.compose.material3.lazy.rememberTransformationSpec +import androidx.wear.compose.material3.lazy.transformedHeight import androidx.wear.compose.material3.placeholder import androidx.wear.compose.material3.placeholderShimmer import androidx.wear.compose.material3.rememberPlaceholderState @@ -52,8 +56,6 @@ import coil.compose.AsyncImage import com.example.jetcaster.R import com.example.jetcaster.core.model.PodcastInfo import com.example.jetcaster.ui.preview.WearPreviewPodcasts -import com.google.android.horologist.compose.layout.ColumnItemType -import com.google.android.horologist.compose.layout.rememberResponsiveColumnPadding @Composable fun PodcastsScreen( @@ -97,17 +99,11 @@ fun PodcastsScreen( onDismiss: () -> Unit, modifier: Modifier = Modifier, ) { - val columnState = rememberTransformingLazyColumnState() - val contentPadding = rememberResponsiveColumnPadding( - first = ColumnItemType.ListHeader, - last = ColumnItemType.Button, - ) ScreenScaffold( scrollState = columnState, - contentPadding = contentPadding, modifier = modifier.placeholderShimmer(placeholderState), - ) { + ) { contentPadding -> when (podcastsScreenState) { is PodcastsScreenState.Loaded -> PodcastScreenLoaded( podcastList = podcastsScreenState.podcastList, @@ -139,16 +135,26 @@ fun PodcastScreenLoaded( placeholderState: PlaceholderState, modifier: Modifier = Modifier, ) { + val transformationSpec = rememberTransformationSpec() TransformingLazyColumn( modifier = modifier, state = columnState, contentPadding = contentPadding, ) { item { - ListHeader { + ListHeader( + modifier = Modifier + .fillMaxWidth() + .minimumVerticalContentPadding( + ListHeaderDefaults.minimumTopListContentPadding, + ListHeaderDefaults.minimumBottomListContentPadding, + ) + .transformedHeight(this, transformationSpec) + .placeholder(placeholderState), + transformation = SurfaceTransformation(transformationSpec), + ) { Text( text = stringResource(id = R.string.podcasts), - modifier = Modifier.placeholder(placeholderState), ) } } @@ -156,7 +162,11 @@ fun PodcastScreenLoaded( MediaContent( podcast = podcastList[index], onPodcastsItemClick = onPodcastsItemClick, - + modifier = Modifier + .fillMaxWidth() + .minimumVerticalContentPadding(ButtonDefaults.minimumVerticalListContentPadding) + .transformedHeight(this, transformationSpec), + transformation = SurfaceTransformation(transformationSpec), ) } } @@ -178,6 +188,7 @@ fun MediaContent( onPodcastsItemClick: (PodcastInfo) -> Unit, modifier: Modifier = Modifier, episodeArtworkPlaceholder: Painter = painterResource(id = R.drawable.music), + transformation: SurfaceTransformation? = null, ) { val mediaTitle = podcast.title val secondaryLabel = podcast.author @@ -204,11 +215,10 @@ fun MediaContent( ButtonDefaults.LargeIconSize, ) .clip(CircleShape), - ) }, modifier = modifier.fillMaxWidth(), - + transformation = transformation, ) } @@ -217,14 +227,10 @@ fun MediaContent( @Composable fun PodcastScreenLoadedPreview(@PreviewParameter(WearPreviewPodcasts::class) podcasts: PodcastInfo) { val columnState = rememberTransformingLazyColumnState() - val contentPadding = rememberResponsiveColumnPadding( - first = ColumnItemType.ListHeader, - last = ColumnItemType.Button, - ) PodcastScreenLoaded( podcastList = listOf(podcasts), onPodcastsItemClick = {}, - contentPadding = contentPadding, + contentPadding = PaddingValues(), columnState = columnState, placeholderState = rememberPlaceholderState(isVisible = false), ) diff --git a/Jetcaster/wear/src/main/java/com/example/jetcaster/ui/queue/QueueScreen.kt b/Jetcaster/wear/src/main/java/com/example/jetcaster/ui/queue/QueueScreen.kt index 7934f93619..2929b46972 100644 --- a/Jetcaster/wear/src/main/java/com/example/jetcaster/ui/queue/QueueScreen.kt +++ b/Jetcaster/wear/src/main/java/com/example/jetcaster/ui/queue/QueueScreen.kt @@ -38,15 +38,20 @@ import androidx.wear.compose.foundation.lazy.TransformingLazyColumnState import androidx.wear.compose.foundation.lazy.items import androidx.wear.compose.foundation.lazy.rememberTransformingLazyColumnState import androidx.wear.compose.material3.AlertDialog +import androidx.wear.compose.material3.ButtonDefaults import androidx.wear.compose.material3.ButtonGroup import androidx.wear.compose.material3.FilledIconButton import androidx.wear.compose.material3.Icon import androidx.wear.compose.material3.IconButtonShapes import androidx.wear.compose.material3.ListHeader +import androidx.wear.compose.material3.ListHeaderDefaults import androidx.wear.compose.material3.MaterialTheme import androidx.wear.compose.material3.PlaceholderState import androidx.wear.compose.material3.ScreenScaffold +import androidx.wear.compose.material3.SurfaceTransformation import androidx.wear.compose.material3.Text +import androidx.wear.compose.material3.lazy.rememberTransformationSpec +import androidx.wear.compose.material3.lazy.transformedHeight import androidx.wear.compose.material3.placeholder import androidx.wear.compose.material3.placeholderShimmer import androidx.wear.compose.material3.rememberPlaceholderState @@ -56,8 +61,6 @@ import com.example.jetcaster.R import com.example.jetcaster.core.player.model.PlayerEpisode import com.example.jetcaster.ui.components.MediaContent import com.example.jetcaster.ui.preview.WearPreviewEpisodes -import com.google.android.horologist.compose.layout.ColumnItemType -import com.google.android.horologist.compose.layout.rememberResponsiveColumnPadding @Composable fun QueueScreen( onPlayButtonClick: () -> Unit, @@ -92,15 +95,9 @@ fun QueueScreen( onDismiss: () -> Unit, modifier: Modifier = Modifier, ) { - val contentPadding = rememberResponsiveColumnPadding( - first = ColumnItemType.ListHeader, - last = ColumnItemType.Button, - ) - val columnState = rememberTransformingLazyColumnState() ScreenScaffold( scrollState = columnState, - contentPadding = contentPadding, modifier = modifier.placeholderShimmer(placeholderState), ) { contentPadding -> when (uiState) { @@ -141,16 +138,26 @@ fun QueueScreenLoaded( placeholderState: PlaceholderState, modifier: Modifier = Modifier, ) { + val transformationSpec = rememberTransformationSpec() TransformingLazyColumn( modifier = modifier, state = columnState, contentPadding = contentPadding, ) { item { - ListHeader { + ListHeader( + modifier = Modifier + .fillMaxWidth() + .minimumVerticalContentPadding( + ListHeaderDefaults.minimumTopListContentPadding, + ListHeaderDefaults.minimumBottomListContentPadding, + ) + .transformedHeight(this, transformationSpec) + .placeholder(placeholderState), + transformation = SurfaceTransformation(transformationSpec), + ) { Text( text = stringResource(R.string.queue), - modifier = Modifier.placeholder(placeholderState), ) } } @@ -161,6 +168,10 @@ fun QueueScreenLoaded( onPlayEpisodes = onPlayEpisodes, onDeleteQueueEpisodes = onDeleteQueueEpisodes, placeholderState = placeholderState, + modifier = Modifier + .fillMaxWidth() + .minimumVerticalContentPadding(ButtonDefaults.minimumVerticalListContentPadding) + .transformedHeight(this, transformationSpec), ) } items(episodeList) { episode -> @@ -168,6 +179,11 @@ fun QueueScreenLoaded( episode = episode, episodeArtworkPlaceholder = painterResource(id = R.drawable.music), onItemClick = onEpisodeItemClick, + modifier = Modifier + .fillMaxWidth() + .minimumVerticalContentPadding(ButtonDefaults.minimumVerticalListContentPadding) + .transformedHeight(this, transformationSpec), + transformation = SurfaceTransformation(transformationSpec), ) } } @@ -249,10 +265,6 @@ fun QueueScreenLoadedPreview( episode: PlayerEpisode, ) { val columnState = rememberTransformingLazyColumnState() - val contentPadding = rememberResponsiveColumnPadding( - first = ColumnItemType.ListHeader, - last = ColumnItemType.Button, - ) QueueScreenLoaded( episodeList = listOf(episode), onPlayButtonClick = { }, @@ -260,7 +272,7 @@ fun QueueScreenLoadedPreview( onDeleteQueueEpisodes = { }, onEpisodeItemClick = { }, columnState = columnState, - contentPadding = contentPadding, + contentPadding = PaddingValues(), placeholderState = rememberPlaceholderState(isVisible = false), ) }