From 0ac0d8a75f7bbb25b478c3fe7436657fc4f72937 Mon Sep 17 00:00:00 2001 From: yongsuk44 Date: Tue, 8 Oct 2024 21:00:11 +0900 Subject: [PATCH 1/3] [Jetcaster] Remove duplicated code in Home content layout --- .../com/example/jetcaster/ui/home/Home.kt | 150 +++--------------- 1 file changed, 18 insertions(+), 132 deletions(-) diff --git a/Jetcaster/mobile/src/main/java/com/example/jetcaster/ui/home/Home.kt b/Jetcaster/mobile/src/main/java/com/example/jetcaster/ui/home/Home.kt index 5eb20979b9..b0538c64c3 100644 --- a/Jetcaster/mobile/src/main/java/com/example/jetcaster/ui/home/Home.kt +++ b/Jetcaster/mobile/src/main/java/com/example/jetcaster/ui/home/Home.kt @@ -39,7 +39,6 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.windowInsetsPadding -import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.grid.GridCells import androidx.compose.foundation.lazy.grid.LazyVerticalGrid import androidx.compose.foundation.pager.HorizontalPager @@ -77,7 +76,6 @@ import androidx.compose.material3.adaptive.layout.PaneAdaptedValue import androidx.compose.material3.adaptive.layout.PaneScaffoldDirective import androidx.compose.material3.adaptive.layout.SupportingPaneScaffold import androidx.compose.material3.adaptive.layout.SupportingPaneScaffoldRole -import androidx.compose.material3.adaptive.layout.ThreePaneScaffoldValue import androidx.compose.material3.adaptive.navigation.ThreePaneScaffoldNavigator import androidx.compose.material3.adaptive.navigation.rememberSupportingPaneScaffoldNavigator import androidx.compose.material3.adaptive.occludingVerticalHingeBounds @@ -154,15 +152,6 @@ data class HomeState( private val HomeState.showHomeCategoryTabs: Boolean get() = featuredPodcasts.isNotEmpty() && homeCategories.isNotEmpty() -@OptIn(ExperimentalMaterial3AdaptiveApi::class) -private fun HomeState.showGrid( - scaffoldValue: ThreePaneScaffoldValue -): Boolean = windowSizeClass.windowWidthSizeClass == WindowWidthSizeClass.EXPANDED || - ( - windowSizeClass.windowWidthSizeClass == WindowWidthSizeClass.MEDIUM && - scaffoldValue[SupportingPaneScaffoldRole.Supporting] == PaneAdaptedValue.Hidden - ) - @OptIn(ExperimentalMaterial3AdaptiveApi::class) private fun ThreePaneScaffoldNavigator.isMainPaneHidden(): Boolean { return scaffoldValue[SupportingPaneScaffoldRole.Main] == PaneAdaptedValue.Hidden @@ -331,11 +320,9 @@ private fun HomeScreenReady( Surface { val podcastUri = navigator.currentDestination?.content - val showGrid = homeState.showGrid(navigator.scaffoldValue) if (podcastUri.isNullOrEmpty()) { HomeScreen( homeState = homeState, - showGrid = showGrid, modifier = Modifier.fillMaxSize() ) } else { @@ -363,7 +350,6 @@ private fun HomeScreenReady( mainPane = { HomeScreen( homeState = homeState, - showGrid = showGrid, modifier = Modifier.fillMaxSize() ) }, @@ -441,7 +427,6 @@ private fun HomeScreenBackground( @Composable private fun HomeScreen( homeState: HomeState, - showGrid: Boolean, modifier: Modifier = Modifier ) { // Effect that changes the home category selection when there are no subscribed podcasts @@ -471,7 +456,6 @@ private fun HomeScreen( // Main Content val snackBarText = stringResource(id = R.string.episode_added_to_your_queue) HomeContent( - showGrid = showGrid, showHomeCategoryTabs = homeState.showHomeCategoryTabs, featuredPodcasts = homeState.featuredPodcasts, selectedHomeCategory = homeState.selectedHomeCategory, @@ -500,7 +484,6 @@ private fun HomeScreen( @Composable private fun HomeContent( - showGrid: Boolean, showHomeCategoryTabs: Boolean, featuredPodcasts: PersistentList, selectedHomeCategory: HomeCategory, @@ -527,120 +510,24 @@ private fun HomeContent( } } - // Note: ideally, `HomeContentColumn` and `HomeContentGrid` would be the same implementation - // (i.e. a grid). However, LazyVerticalGrid does not have the concept of a sticky header. - // So we are using two different composables here depending on the provided window size class. - // See: https://issuetracker.google.com/issues/231557184 - if (showGrid) { - HomeContentGrid( - pagerState = pagerState, - showHomeCategoryTabs = showHomeCategoryTabs, - featuredPodcasts = featuredPodcasts, - selectedHomeCategory = selectedHomeCategory, - homeCategories = homeCategories, - filterableCategoriesModel = filterableCategoriesModel, - podcastCategoryFilterResult = podcastCategoryFilterResult, - library = library, - modifier = modifier, - onPodcastUnfollowed = onPodcastUnfollowed, - onHomeCategorySelected = onHomeCategorySelected, - onCategorySelected = onCategorySelected, - navigateToPodcastDetails = navigateToPodcastDetails, - navigateToPlayer = navigateToPlayer, - onTogglePodcastFollowed = onTogglePodcastFollowed, - onQueueEpisode = onQueueEpisode, - ) - } else { - HomeContentColumn( - pagerState = pagerState, - showHomeCategoryTabs = showHomeCategoryTabs, - featuredPodcasts = featuredPodcasts, - selectedHomeCategory = selectedHomeCategory, - homeCategories = homeCategories, - filterableCategoriesModel = filterableCategoriesModel, - podcastCategoryFilterResult = podcastCategoryFilterResult, - library = library, - modifier = modifier, - onPodcastUnfollowed = onPodcastUnfollowed, - onHomeCategorySelected = onHomeCategorySelected, - onCategorySelected = onCategorySelected, - navigateToPodcastDetails = navigateToPodcastDetails, - navigateToPlayer = navigateToPlayer, - onTogglePodcastFollowed = onTogglePodcastFollowed, - onQueueEpisode = onQueueEpisode, - ) - } -} - -@OptIn(ExperimentalFoundationApi::class) -@Composable -private fun HomeContentColumn( - showHomeCategoryTabs: Boolean, - pagerState: PagerState, - featuredPodcasts: PersistentList, - selectedHomeCategory: HomeCategory, - homeCategories: List, - filterableCategoriesModel: FilterableCategoriesModel, - podcastCategoryFilterResult: PodcastCategoryFilterResult, - library: LibraryInfo, - modifier: Modifier = Modifier, - onPodcastUnfollowed: (PodcastInfo) -> Unit, - onHomeCategorySelected: (HomeCategory) -> Unit, - onCategorySelected: (CategoryInfo) -> Unit, - navigateToPodcastDetails: (PodcastInfo) -> Unit, - navigateToPlayer: (EpisodeInfo) -> Unit, - onTogglePodcastFollowed: (PodcastInfo) -> Unit, - onQueueEpisode: (PlayerEpisode) -> Unit, -) { - LazyColumn( - modifier = modifier.fillMaxSize() - ) { - if (featuredPodcasts.isNotEmpty()) { - item { - FollowedPodcastItem( - pagerState = pagerState, - items = featuredPodcasts, - onPodcastUnfollowed = onPodcastUnfollowed, - navigateToPodcastDetails = navigateToPodcastDetails, - modifier = Modifier - .fillMaxWidth() - ) - } - } - - if (showHomeCategoryTabs) { - item { - HomeCategoryTabs( - categories = homeCategories, - selectedCategory = selectedHomeCategory, - showHorizontalLine = true, - onCategorySelected = onHomeCategorySelected - ) - } - } - - when (selectedHomeCategory) { - HomeCategory.Library -> { - libraryItems( - library = library, - navigateToPlayer = navigateToPlayer, - onQueueEpisode = onQueueEpisode - ) - } - - HomeCategory.Discover -> { - discoverItems( - filterableCategoriesModel = filterableCategoriesModel, - podcastCategoryFilterResult = podcastCategoryFilterResult, - navigateToPodcastDetails = navigateToPodcastDetails, - navigateToPlayer = navigateToPlayer, - onCategorySelected = onCategorySelected, - onTogglePodcastFollowed = onTogglePodcastFollowed, - onQueueEpisode = onQueueEpisode - ) - } - } - } + HomeContentGrid( + pagerState = pagerState, + showHomeCategoryTabs = showHomeCategoryTabs, + featuredPodcasts = featuredPodcasts, + selectedHomeCategory = selectedHomeCategory, + homeCategories = homeCategories, + filterableCategoriesModel = filterableCategoriesModel, + podcastCategoryFilterResult = podcastCategoryFilterResult, + library = library, + modifier = modifier, + onPodcastUnfollowed = onPodcastUnfollowed, + onHomeCategorySelected = onHomeCategorySelected, + onCategorySelected = onCategorySelected, + navigateToPodcastDetails = navigateToPodcastDetails, + navigateToPlayer = navigateToPlayer, + onTogglePodcastFollowed = onTogglePodcastFollowed, + onQueueEpisode = onQueueEpisode, + ) } @Composable @@ -947,7 +834,6 @@ private fun PreviewHome() { ) HomeScreen( homeState = homeState, - showGrid = false ) } } From 784c7f718737264d830c9a9ea623f737ec6ed81c Mon Sep 17 00:00:00 2001 From: yongsuk44 Date: Fri, 18 Oct 2024 20:44:56 +0900 Subject: [PATCH 2/3] [Jetcaster] Fixed to enable SharedElementTransition to run in the Grid list --- .../designsystem/component/PodcastImage.kt | 11 +- .../com/example/jetcaster/ui/JetcasterApp.kt | 17 +-- .../com/example/jetcaster/ui/home/Home.kt | 18 ++- .../ui/home/category/PodcastCategory.kt | 54 +------- .../jetcaster/ui/home/discover/Discover.kt | 38 ------ .../jetcaster/ui/home/library/Library.kt | 43 ++----- .../jetcaster/ui/player/PlayerScreen.kt | 40 +++--- .../jetcaster/util/SafeSharedElement.kt | 121 ++++++++++++++++++ 8 files changed, 174 insertions(+), 168 deletions(-) create mode 100644 Jetcaster/mobile/src/main/java/com/example/jetcaster/util/SafeSharedElement.kt diff --git a/Jetcaster/core/designsystem/src/main/java/com/example/jetcaster/designsystem/component/PodcastImage.kt b/Jetcaster/core/designsystem/src/main/java/com/example/jetcaster/designsystem/component/PodcastImage.kt index 72729c92cf..7662a5c666 100644 --- a/Jetcaster/core/designsystem/src/main/java/com/example/jetcaster/designsystem/component/PodcastImage.kt +++ b/Jetcaster/core/designsystem/src/main/java/com/example/jetcaster/designsystem/component/PodcastImage.kt @@ -28,7 +28,6 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Brush import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalInspectionMode @@ -46,7 +45,6 @@ fun PodcastImage( // TODO: Remove the nested component modifier when shared elements are applied to entire app imageModifier: Modifier = Modifier, contentScale: ContentScale = ContentScale.Crop, - placeholderBrush: Brush = thumbnailPlaceholderDefaultBrush(), ) { if (LocalInspectionMode.current) { Box(modifier = modifier.background(MaterialTheme.colorScheme.primary)) @@ -80,14 +78,7 @@ fun PodcastImage( .fillMaxSize() ) } - else -> { - Box( - modifier = Modifier - .background(placeholderBrush) - .fillMaxSize() - - ) - } + else -> { /* */ } } Image( diff --git a/Jetcaster/mobile/src/main/java/com/example/jetcaster/ui/JetcasterApp.kt b/Jetcaster/mobile/src/main/java/com/example/jetcaster/ui/JetcasterApp.kt index d03daf5e0d..0b4da870fe 100644 --- a/Jetcaster/mobile/src/main/java/com/example/jetcaster/ui/JetcasterApp.kt +++ b/Jetcaster/mobile/src/main/java/com/example/jetcaster/ui/JetcasterApp.kt @@ -54,16 +54,13 @@ fun JetcasterApp( startDestination = Screen.Home.route ) { composable(Screen.Home.route) { backStackEntry -> - CompositionLocalProvider( - LocalAnimatedVisibilityScope provides this - ) { - MainScreen( - windowSizeClass = adaptiveInfo.windowSizeClass, - navigateToPlayer = { episode -> - appState.navigateToPlayer(episode.uri, backStackEntry) - }, - ) - } + MainScreen( + windowSizeClass = adaptiveInfo.windowSizeClass, + navigateToPlayer = { episode -> + appState.navigateToPlayer(episode.uri, backStackEntry) + }, + animatedContentScope = this, + ) } composable(Screen.Player.route) { CompositionLocalProvider( diff --git a/Jetcaster/mobile/src/main/java/com/example/jetcaster/ui/home/Home.kt b/Jetcaster/mobile/src/main/java/com/example/jetcaster/ui/home/Home.kt index 46d48c5c30..a2b63cabb8 100644 --- a/Jetcaster/mobile/src/main/java/com/example/jetcaster/ui/home/Home.kt +++ b/Jetcaster/mobile/src/main/java/com/example/jetcaster/ui/home/Home.kt @@ -17,6 +17,7 @@ package com.example.jetcaster.ui.home import androidx.activity.compose.BackHandler +import androidx.compose.animation.AnimatedContentScope import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement @@ -78,6 +79,7 @@ import androidx.compose.material3.adaptive.navigation.rememberSupportingPaneScaf import androidx.compose.material3.adaptive.occludingVerticalHingeBounds import androidx.compose.material3.adaptive.separatingVerticalHingeBounds import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -111,6 +113,7 @@ import com.example.jetcaster.core.model.PodcastCategoryFilterResult import com.example.jetcaster.core.model.PodcastInfo import com.example.jetcaster.core.player.model.PlayerEpisode import com.example.jetcaster.designsystem.component.PodcastImage +import com.example.jetcaster.ui.LocalAnimatedVisibilityScope import com.example.jetcaster.ui.home.discover.discoverItems import com.example.jetcaster.ui.home.library.libraryItems import com.example.jetcaster.ui.podcast.PodcastDetailsScreen @@ -227,6 +230,7 @@ private fun getExcludedVerticalBounds(posture: Posture, hingePolicy: HingePolicy @Composable fun MainScreen( windowSizeClass: WindowSizeClass, + animatedContentScope: AnimatedContentScope, navigateToPlayer: (EpisodeInfo) -> Unit, viewModel: HomeViewModel = hiltViewModel() ) { @@ -240,6 +244,7 @@ fun MainScreen( windowSizeClass = windowSizeClass, navigateToPlayer = navigateToPlayer, viewModel = viewModel, + animatedContentScope = animatedContentScope ) } } @@ -288,6 +293,7 @@ fun HomeScreenErrorPreview() { private fun HomeScreenReady( uiState: HomeScreenUiState.Ready, windowSizeClass: WindowSizeClass, + animatedContentScope: AnimatedContentScope, navigateToPlayer: (EpisodeInfo) -> Unit, viewModel: HomeViewModel = hiltViewModel() ) { @@ -322,10 +328,14 @@ private fun HomeScreenReady( Surface { val podcastUri = navigator.currentDestination?.content if (podcastUri.isNullOrEmpty()) { - HomeScreen( - homeState = homeState, - modifier = Modifier.fillMaxSize() - ) + CompositionLocalProvider( + LocalAnimatedVisibilityScope provides animatedContentScope + ) { + HomeScreen( + homeState = homeState, + modifier = Modifier.fillMaxSize() + ) + } } else { SupportingPaneScaffold( value = navigator.scaffoldValue, diff --git a/Jetcaster/mobile/src/main/java/com/example/jetcaster/ui/home/category/PodcastCategory.kt b/Jetcaster/mobile/src/main/java/com/example/jetcaster/ui/home/category/PodcastCategory.kt index ab01dc9b05..0e6943ff4a 100644 --- a/Jetcaster/mobile/src/main/java/com/example/jetcaster/ui/home/category/PodcastCategory.kt +++ b/Jetcaster/mobile/src/main/java/com/example/jetcaster/ui/home/category/PodcastCategory.kt @@ -29,7 +29,6 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width -import androidx.compose.foundation.lazy.LazyListScope import androidx.compose.foundation.lazy.LazyRow import androidx.compose.foundation.lazy.grid.LazyGridScope import androidx.compose.foundation.lazy.grid.items @@ -52,56 +51,12 @@ import com.example.jetcaster.core.model.PodcastInfo import com.example.jetcaster.core.player.model.PlayerEpisode import com.example.jetcaster.designsystem.component.PodcastImage import com.example.jetcaster.designsystem.theme.Keyline1 -import com.example.jetcaster.ui.LocalAnimatedVisibilityScope -import com.example.jetcaster.ui.LocalSharedTransitionScope import com.example.jetcaster.ui.shared.EpisodeListItem import com.example.jetcaster.ui.theme.JetcasterTheme +import com.example.jetcaster.util.ShapeBasedClip import com.example.jetcaster.util.ToggleFollowPodcastIconButton import com.example.jetcaster.util.fullWidthItem - -fun LazyListScope.podcastCategory( - podcastCategoryFilterResult: PodcastCategoryFilterResult, - navigateToPodcastDetails: (PodcastInfo) -> Unit, - navigateToPlayer: (EpisodeInfo) -> Unit, - removeFromQueue: (EpisodeInfo) -> Unit, - onQueueEpisode: (PlayerEpisode) -> Unit, - onTogglePodcastFollowed: (PodcastInfo) -> Unit, -) { - item { - CategoryPodcasts( - topPodcasts = podcastCategoryFilterResult.topPodcasts, - navigateToPodcastDetails = navigateToPodcastDetails, - onTogglePodcastFollowed = onTogglePodcastFollowed, - ) - } - - val episodes = podcastCategoryFilterResult.episodes - items(episodes, key = { it.episode.uri }) { item -> - val sharedTransitionScope = LocalSharedTransitionScope.current - ?: throw IllegalStateException("No SharedElementScope found") - val animatedVisibilityScope = LocalAnimatedVisibilityScope.current - ?: throw IllegalStateException("No SharedElementScope found") - with(sharedTransitionScope) { - EpisodeListItem( - episode = item.episode, - podcast = item.podcast, - onClick = navigateToPlayer, - onQueueEpisode = onQueueEpisode, - modifier = Modifier - .fillParentMaxWidth() - .animateItem(), - imageModifier = Modifier.sharedElement( - state = rememberSharedContentState( - key = item.episode.title - ), - animatedVisibilityScope = animatedVisibilityScope, - clipInOverlayDuringTransition = OverlayClip(MaterialTheme.shapes.medium) - ), - removeFromQueue = removeFromQueue - ) - } - } -} +import com.example.jetcaster.util.safeSharedElement fun LazyGridScope.podcastCategory( podcastCategoryFilterResult: PodcastCategoryFilterResult, @@ -109,7 +64,6 @@ fun LazyGridScope.podcastCategory( navigateToPlayer: (EpisodeInfo) -> Unit, onQueueEpisode: (PlayerEpisode) -> Unit, removeFromQueue: (EpisodeInfo) -> Unit, - onTogglePodcastFollowed: (PodcastInfo) -> Unit, ) { fullWidthItem { @@ -130,6 +84,10 @@ fun LazyGridScope.podcastCategory( modifier = Modifier .fillMaxWidth() .animateItem(), + imageModifier = Modifier.safeSharedElement( + key = item.episode.title, + clipInOverlayDuringTransition = ShapeBasedClip(MaterialTheme.shapes.medium), + ), removeFromQueue = removeFromQueue, ) } diff --git a/Jetcaster/mobile/src/main/java/com/example/jetcaster/ui/home/discover/Discover.kt b/Jetcaster/mobile/src/main/java/com/example/jetcaster/ui/home/discover/Discover.kt index db0d0042b9..9a900b7a78 100644 --- a/Jetcaster/mobile/src/main/java/com/example/jetcaster/ui/home/discover/Discover.kt +++ b/Jetcaster/mobile/src/main/java/com/example/jetcaster/ui/home/discover/Discover.kt @@ -21,7 +21,6 @@ import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.lazy.LazyListScope import androidx.compose.foundation.lazy.grid.LazyGridScope import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Check @@ -51,43 +50,6 @@ import com.example.jetcaster.designsystem.theme.Keyline1 import com.example.jetcaster.ui.home.category.podcastCategory import com.example.jetcaster.util.fullWidthItem -fun LazyListScope.discoverItems( - filterableCategoriesModel: FilterableCategoriesModel, - podcastCategoryFilterResult: PodcastCategoryFilterResult, - navigateToPodcastDetails: (PodcastInfo) -> Unit, - navigateToPlayer: (EpisodeInfo) -> Unit, - onCategorySelected: (CategoryInfo) -> Unit, - onTogglePodcastFollowed: (PodcastInfo) -> Unit, - removeFromQueue: (EpisodeInfo) -> Unit, - onQueueEpisode: (PlayerEpisode) -> Unit, -) { - if (filterableCategoriesModel.isEmpty) { - // TODO: empty state - return - } - - item { - Spacer(Modifier.height(8.dp)) - - PodcastCategoryTabs( - filterableCategoriesModel = filterableCategoriesModel, - onCategorySelected = onCategorySelected, - modifier = Modifier.fillMaxWidth() - ) - - Spacer(Modifier.height(8.dp)) - } - - podcastCategory( - podcastCategoryFilterResult = podcastCategoryFilterResult, - navigateToPodcastDetails = navigateToPodcastDetails, - navigateToPlayer = navigateToPlayer, - onTogglePodcastFollowed = onTogglePodcastFollowed, - onQueueEpisode = onQueueEpisode, - removeFromQueue = removeFromQueue, - ) -} - fun LazyGridScope.discoverItems( filterableCategoriesModel: FilterableCategoriesModel, podcastCategoryFilterResult: PodcastCategoryFilterResult, diff --git a/Jetcaster/mobile/src/main/java/com/example/jetcaster/ui/home/library/Library.kt b/Jetcaster/mobile/src/main/java/com/example/jetcaster/ui/home/library/Library.kt index d982c6799d..c3dff47317 100644 --- a/Jetcaster/mobile/src/main/java/com/example/jetcaster/ui/home/library/Library.kt +++ b/Jetcaster/mobile/src/main/java/com/example/jetcaster/ui/home/library/Library.kt @@ -16,9 +16,9 @@ package com.example.jetcaster.ui.home.library +import androidx.compose.animation.ExperimentalSharedTransitionApi import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.lazy.LazyListScope import androidx.compose.foundation.lazy.grid.LazyGridScope import androidx.compose.foundation.lazy.grid.items import androidx.compose.foundation.lazy.items @@ -33,42 +33,11 @@ import com.example.jetcaster.core.model.LibraryInfo import com.example.jetcaster.core.player.model.PlayerEpisode import com.example.jetcaster.designsystem.theme.Keyline1 import com.example.jetcaster.ui.shared.EpisodeListItem +import com.example.jetcaster.util.ShapeBasedClip import com.example.jetcaster.util.fullWidthItem +import com.example.jetcaster.util.safeSharedElement -fun LazyListScope.libraryItems( - library: LibraryInfo, - navigateToPlayer: (EpisodeInfo) -> Unit, - onQueueEpisode: (PlayerEpisode) -> Unit, - removeFromQueue: (EpisodeInfo) -> Unit, -) { - item { - Text( - text = stringResource(id = R.string.latest_episodes), - modifier = Modifier.padding( - start = Keyline1, - top = 16.dp, - ), - style = MaterialTheme.typography.titleLarge, - ) - } - - items( - library, - key = { it.episode.uri } - ) { item -> - EpisodeListItem( - episode = item.episode, - podcast = item.podcast, - onClick = navigateToPlayer, - onQueueEpisode = onQueueEpisode, - modifier = Modifier - .fillParentMaxWidth() - .animateItem(), - removeFromQueue = removeFromQueue, - ) - } -} - +@OptIn(ExperimentalSharedTransitionApi::class) fun LazyGridScope.libraryItems( library: LibraryInfo, navigateToPlayer: (EpisodeInfo) -> Unit, @@ -98,6 +67,10 @@ fun LazyGridScope.libraryItems( modifier = Modifier .fillMaxWidth() .animateItem(), + imageModifier = Modifier.safeSharedElement( + key = item.episode.title, + clipInOverlayDuringTransition = ShapeBasedClip(MaterialTheme.shapes.medium), + ), removeFromQueue = removeFromQueue, ) } diff --git a/Jetcaster/mobile/src/main/java/com/example/jetcaster/ui/player/PlayerScreen.kt b/Jetcaster/mobile/src/main/java/com/example/jetcaster/ui/player/PlayerScreen.kt index c376368846..53ccf98897 100644 --- a/Jetcaster/mobile/src/main/java/com/example/jetcaster/ui/player/PlayerScreen.kt +++ b/Jetcaster/mobile/src/main/java/com/example/jetcaster/ui/player/PlayerScreen.kt @@ -100,13 +100,13 @@ import com.example.jetcaster.core.player.model.PlayerEpisode import com.example.jetcaster.designsystem.component.HtmlTextContainer import com.example.jetcaster.designsystem.component.ImageBackgroundColorScrim import com.example.jetcaster.designsystem.component.PodcastImage -import com.example.jetcaster.ui.LocalAnimatedVisibilityScope -import com.example.jetcaster.ui.LocalSharedTransitionScope import com.example.jetcaster.ui.theme.JetcasterTheme import com.example.jetcaster.ui.tooling.DevicePreviews +import com.example.jetcaster.util.ShapeBasedClip import com.example.jetcaster.util.isBookPosture import com.example.jetcaster.util.isSeparatingPosture import com.example.jetcaster.util.isTableTopPosture +import com.example.jetcaster.util.safeSharedElement import com.example.jetcaster.util.verticalGradientScrim import com.google.accompanist.adaptive.HorizontalTwoPaneStrategy import com.google.accompanist.adaptive.TwoPane @@ -355,11 +355,6 @@ private fun PlayerContentRegular( val playerEpisode = uiState.episodePlayerState val currentEpisode = playerEpisode.currentEpisode ?: return - val sharedTransitionScope = LocalSharedTransitionScope.current - ?: throw IllegalStateException("No SharedElementScope found") - val animatedVisibilityScope = LocalAnimatedVisibilityScope.current - ?: throw IllegalStateException("No SharedElementScope found") - Column( modifier = modifier .fillMaxSize() @@ -380,19 +375,11 @@ private fun PlayerContentRegular( modifier = Modifier.padding(horizontal = 8.dp) ) { Spacer(modifier = Modifier.weight(1f)) - with(sharedTransitionScope) { - PlayerImage( - podcastImageUrl = currentEpisode.podcastImageUrl, - modifier = Modifier.weight(10f), - imageModifier = Modifier.sharedElement( - state = rememberSharedContentState( - key = currentEpisode.title - ), - animatedVisibilityScope = animatedVisibilityScope, - clipInOverlayDuringTransition = OverlayClip(MaterialTheme.shapes.medium) - ), - ) - } + PlayerImage( + podcastImageUrl = currentEpisode.podcastImageUrl, + modifier = Modifier.weight(10f), + episodeTitle = currentEpisode.title + ) Spacer(modifier = Modifier.height(32.dp)) PodcastDescription(currentEpisode.title, currentEpisode.podcastName) Spacer(modifier = Modifier.height(32.dp)) @@ -449,7 +436,10 @@ private fun PlayerContentTableTopTop( .padding(32.dp), horizontalAlignment = Alignment.CenterHorizontally ) { - PlayerImage(episode.podcastImageUrl) + PlayerImage( + podcastImageUrl = episode.podcastImageUrl, + episodeTitle = episode.title, + ) } } @@ -560,6 +550,7 @@ private fun PlayerContentBookEnd( ) { PlayerImage( podcastImageUrl = episode.podcastImageUrl, + episodeTitle = episode.title, modifier = Modifier .padding(vertical = 16.dp) .weight(1f) @@ -615,8 +606,8 @@ private fun TopAppBar( @Composable private fun PlayerImage( podcastImageUrl: String, + episodeTitle: String, modifier: Modifier = Modifier, - imageModifier: Modifier = Modifier, ) { PodcastImage( podcastImageUrl = podcastImageUrl, @@ -626,7 +617,10 @@ private fun PlayerImage( .sizeIn(maxWidth = 500.dp, maxHeight = 500.dp) .aspectRatio(1f) .clip(MaterialTheme.shapes.medium), - imageModifier = imageModifier + imageModifier = Modifier.safeSharedElement( + key = episodeTitle, + clipInOverlayDuringTransition = ShapeBasedClip(MaterialTheme.shapes.medium), + ), ) } diff --git a/Jetcaster/mobile/src/main/java/com/example/jetcaster/util/SafeSharedElement.kt b/Jetcaster/mobile/src/main/java/com/example/jetcaster/util/SafeSharedElement.kt new file mode 100644 index 0000000000..3e5f699ce8 --- /dev/null +++ b/Jetcaster/mobile/src/main/java/com/example/jetcaster/util/SafeSharedElement.kt @@ -0,0 +1,121 @@ +/* + * Copyright 2024 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(ExperimentalSharedTransitionApi::class) + +package com.example.jetcaster.util + +import androidx.compose.animation.AnimatedVisibilityScope +import androidx.compose.animation.BoundsTransform +import androidx.compose.animation.ExperimentalSharedTransitionApi +import androidx.compose.animation.SharedTransitionScope +import androidx.compose.animation.SharedTransitionScope.OverlayClip +import androidx.compose.animation.SharedTransitionScope.PlaceHolderSize +import androidx.compose.animation.SharedTransitionScope.PlaceHolderSize.Companion.contentSize +import androidx.compose.animation.SharedTransitionScope.SharedContentState +import androidx.compose.animation.core.Spring.StiffnessMediumLow +import androidx.compose.animation.core.VisibilityThreshold +import androidx.compose.animation.core.spring +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Rect +import androidx.compose.ui.graphics.Path +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.graphics.addOutline +import androidx.compose.ui.unit.Density +import androidx.compose.ui.unit.LayoutDirection +import com.example.jetcaster.ui.LocalAnimatedVisibilityScope +import com.example.jetcaster.ui.LocalSharedTransitionScope + +private val DefaultSpring = spring( + stiffness = StiffnessMediumLow, + visibilityThreshold = Rect.VisibilityThreshold +) + +private val DefaultBoundsTransform = BoundsTransform { _, _ -> DefaultSpring } + +private val ParentClip: OverlayClip = + object : OverlayClip { + override fun getClipPath( + state: SharedContentState, + bounds: Rect, + layoutDirection: LayoutDirection, + density: Density + ): Path? { + return state.parentSharedContentState?.clipPathInOverlay + } + } + +class ShapeBasedClip( + val clipShape: Shape +) : OverlayClip { + private val path = Path() + + override fun getClipPath( + state: SharedContentState, + bounds: Rect, + layoutDirection: LayoutDirection, + density: Density + ): Path { + path.reset() + path.addOutline( + clipShape.createOutline( + bounds.size, + layoutDirection, + density + ) + ) + path.translate(bounds.topLeft) + return path + } +} + +/** + * This modifier performs a NoOp when sharedTransitionScope or AnimatedVisibilityScope is null. + * And by default pulls the scopes from the Composition Locals [LocalSharedTransitionScope] and [LocalNavAnimatedVisibilityScope]. + * Otherwise, it just calls [Modifier.sharedElement] with the provided parameters. + */ +@OptIn(ExperimentalSharedTransitionApi::class) +@Composable +fun Modifier.safeSharedElement( + key: Any, + // todo figure out how to allow this with null scope: + // rememberSharedContentState requires a SharedTransitionScope + // state: SharedContentState, + sharedTransitionScope: SharedTransitionScope? = LocalSharedTransitionScope.current, + animatedVisibilityScope: AnimatedVisibilityScope? = LocalAnimatedVisibilityScope.current, + boundsTransform: BoundsTransform = DefaultBoundsTransform, + placeHolderSize: PlaceHolderSize = contentSize, + renderInOverlayDuringTransition: Boolean = true, + zIndexInOverlay: Float = 0f, + clipInOverlayDuringTransition: OverlayClip = ParentClip +): Modifier { + if (sharedTransitionScope == null || animatedVisibilityScope == null) { + return this + } + with(sharedTransitionScope) { + return this@safeSharedElement then + Modifier.sharedElement( + rememberSharedContentState(key = key), + animatedVisibilityScope, + boundsTransform, + placeHolderSize, + renderInOverlayDuringTransition, + zIndexInOverlay, + clipInOverlayDuringTransition + ) + } +} From 293bc560e2d321c648b1f78c533a08b3aa6dbf91 Mon Sep 17 00:00:00 2001 From: yongsuk44 Date: Mon, 21 Oct 2024 18:30:44 +0900 Subject: [PATCH 3/3] [Jetcaster] Remove safeSharedElement todo comment --- .../main/java/com/example/jetcaster/util/SafeSharedElement.kt | 3 --- 1 file changed, 3 deletions(-) diff --git a/Jetcaster/mobile/src/main/java/com/example/jetcaster/util/SafeSharedElement.kt b/Jetcaster/mobile/src/main/java/com/example/jetcaster/util/SafeSharedElement.kt index 3e5f699ce8..40c4bb0131 100644 --- a/Jetcaster/mobile/src/main/java/com/example/jetcaster/util/SafeSharedElement.kt +++ b/Jetcaster/mobile/src/main/java/com/example/jetcaster/util/SafeSharedElement.kt @@ -92,9 +92,6 @@ class ShapeBasedClip( @Composable fun Modifier.safeSharedElement( key: Any, - // todo figure out how to allow this with null scope: - // rememberSharedContentState requires a SharedTransitionScope - // state: SharedContentState, sharedTransitionScope: SharedTransitionScope? = LocalSharedTransitionScope.current, animatedVisibilityScope: AnimatedVisibilityScope? = LocalAnimatedVisibilityScope.current, boundsTransform: BoundsTransform = DefaultBoundsTransform,