From 64b1032b7442a172713b4c741ec7eecae854961e Mon Sep 17 00:00:00 2001 From: Barnabas Balogh Date: Sat, 2 May 2026 13:06:21 +0200 Subject: [PATCH] feat(app): implement shared transition for search functionality in HomeTopBar and HomeSearchFullScreen --- .../java/hu/bbara/purefin/PurefinActivity.kt | 85 ++++++++++--------- .../purefin/navigation/NavigationLocals.kt | 11 +++ .../purefin/navigation/RouteEntryBuilder.kt | 14 ++- .../ui/screen/home/components/HomeTopBar.kt | 35 +++++++- .../components/search/HomeSearchFullScreen.kt | 30 ++++++- 5 files changed, 131 insertions(+), 44 deletions(-) diff --git a/app/src/main/java/hu/bbara/purefin/PurefinActivity.kt b/app/src/main/java/hu/bbara/purefin/PurefinActivity.kt index 776d0a25..c750d898 100644 --- a/app/src/main/java/hu/bbara/purefin/PurefinActivity.kt +++ b/app/src/main/java/hu/bbara/purefin/PurefinActivity.kt @@ -6,6 +6,8 @@ import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge import androidx.compose.animation.core.tween +import androidx.compose.animation.ExperimentalSharedTransitionApi +import androidx.compose.animation.SharedTransitionLayout import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut import androidx.compose.animation.togetherWith @@ -41,6 +43,7 @@ import hu.bbara.purefin.jellyfin.JellyfinAuthInterceptor import hu.bbara.purefin.data.UserSessionRepository import hu.bbara.purefin.navigation.LocalNavigationBackStack import hu.bbara.purefin.navigation.LocalNavigationManager +import hu.bbara.purefin.navigation.LocalSharedTransitionScope import hu.bbara.purefin.navigation.NavigationCommand import hu.bbara.purefin.navigation.NavigationManager import hu.bbara.purefin.navigation.Route @@ -131,6 +134,7 @@ class PurefinActivity : ComponentActivity() { return (applicationInfo.flags and ApplicationInfo.FLAG_DEBUGGABLE) != 0 } + @OptIn(ExperimentalSharedTransitionApi::class) @Composable fun MainApp( userSessionRepository: UserSessionRepository, @@ -172,45 +176,48 @@ class PurefinActivity : ComponentActivity() { } } - CompositionLocalProvider( - LocalNavigationManager provides navigationManager, - LocalNavigationBackStack provides backStack.toList() - ) { - NavDisplay( - backStack = backStack, - onBack = { navigationManager.pop() }, - modifier = Modifier.fillMaxSize().background(backgroundDark), - transitionSpec = { - fadeIn( - animationSpec = tween( - durationMillis = 140 - ) - ) togetherWith - fadeOut(animationSpec = tween(durationMillis = 140)) - }, - popTransitionSpec = { - fadeIn( - animationSpec = tween( - durationMillis = 140 - ) - ) togetherWith - fadeOut(animationSpec = tween(durationMillis = 140)) - }, - predictivePopTransitionSpec = { _ -> - fadeIn( - animationSpec = tween( - durationMillis = 140 - ) - ) togetherWith - fadeOut(animationSpec = tween(durationMillis = 140)) - }, - entryDecorators = - listOf( - rememberSaveableStateHolderNavEntryDecorator(), - rememberViewModelStoreNavEntryDecorator(), - ), - entryProvider = appEntryProvider - ) + SharedTransitionLayout { + CompositionLocalProvider( + LocalNavigationManager provides navigationManager, + LocalNavigationBackStack provides backStack.toList(), + LocalSharedTransitionScope provides this + ) { + NavDisplay( + backStack = backStack, + onBack = { navigationManager.pop() }, + modifier = Modifier.fillMaxSize().background(backgroundDark), + transitionSpec = { + fadeIn( + animationSpec = tween( + durationMillis = 140 + ) + ) togetherWith + fadeOut(animationSpec = tween(durationMillis = 140)) + }, + popTransitionSpec = { + fadeIn( + animationSpec = tween( + durationMillis = 140 + ) + ) togetherWith + fadeOut(animationSpec = tween(durationMillis = 140)) + }, + predictivePopTransitionSpec = { _ -> + fadeIn( + animationSpec = tween( + durationMillis = 140 + ) + ) togetherWith + fadeOut(animationSpec = tween(durationMillis = 140)) + }, + entryDecorators = + listOf( + rememberSaveableStateHolderNavEntryDecorator(), + rememberViewModelStoreNavEntryDecorator(), + ), + entryProvider = appEntryProvider + ) + } } } else { LoginScreen() diff --git a/app/src/main/java/hu/bbara/purefin/navigation/NavigationLocals.kt b/app/src/main/java/hu/bbara/purefin/navigation/NavigationLocals.kt index 3a2723fd..5dede312 100644 --- a/app/src/main/java/hu/bbara/purefin/navigation/NavigationLocals.kt +++ b/app/src/main/java/hu/bbara/purefin/navigation/NavigationLocals.kt @@ -1,5 +1,8 @@ package hu.bbara.purefin.navigation +import androidx.compose.animation.AnimatedVisibilityScope +import androidx.compose.animation.ExperimentalSharedTransitionApi +import androidx.compose.animation.SharedTransitionScope import androidx.compose.runtime.ProvidableCompositionLocal import androidx.compose.runtime.compositionLocalOf import androidx.compose.runtime.staticCompositionLocalOf @@ -8,3 +11,11 @@ val LocalNavigationManager: ProvidableCompositionLocal = staticCompositionLocalOf { error("NavigationManager not provided") } val LocalNavigationBackStack = compositionLocalOf> { emptyList() } + +@OptIn(ExperimentalSharedTransitionApi::class) +val LocalSharedTransitionScope = compositionLocalOf { null } + +val LocalNavSharedAnimatedVisibilityScope = + compositionLocalOf { null } + +const val HOME_SEARCH_SHARED_BOUNDS_KEY = "home_search_shared_bounds" diff --git a/app/src/main/java/hu/bbara/purefin/navigation/RouteEntryBuilder.kt b/app/src/main/java/hu/bbara/purefin/navigation/RouteEntryBuilder.kt index 56dd8967..f494092d 100644 --- a/app/src/main/java/hu/bbara/purefin/navigation/RouteEntryBuilder.kt +++ b/app/src/main/java/hu/bbara/purefin/navigation/RouteEntryBuilder.kt @@ -1,6 +1,8 @@ package hu.bbara.purefin.navigation +import androidx.compose.runtime.CompositionLocalProvider import androidx.navigation3.runtime.EntryProviderScope +import androidx.navigation3.ui.LocalNavAnimatedContentScope import hu.bbara.purefin.ui.screen.library.LibraryScreen import hu.bbara.purefin.ui.screen.login.LoginScreen import hu.bbara.purefin.ui.screen.AppScreen @@ -11,10 +13,18 @@ import hu.bbara.purefin.ui.screen.series.SeriesScreen fun EntryProviderScope.appRouteEntryBuilder() { entry { - AppScreen() + CompositionLocalProvider( + LocalNavSharedAnimatedVisibilityScope provides LocalNavAnimatedContentScope.current + ) { + AppScreen() + } } entry { - HomeSearchFullScreen() + CompositionLocalProvider( + LocalNavSharedAnimatedVisibilityScope provides LocalNavAnimatedContentScope.current + ) { + HomeSearchFullScreen() + } } entry { MovieScreen(movie = it.item) diff --git a/app/src/main/java/hu/bbara/purefin/ui/screen/home/components/HomeTopBar.kt b/app/src/main/java/hu/bbara/purefin/ui/screen/home/components/HomeTopBar.kt index 855fe34c..dfa59cbe 100644 --- a/app/src/main/java/hu/bbara/purefin/ui/screen/home/components/HomeTopBar.kt +++ b/app/src/main/java/hu/bbara/purefin/ui/screen/home/components/HomeTopBar.kt @@ -1,5 +1,9 @@ package hu.bbara.purefin.ui.screen.home.components +import androidx.compose.animation.ExperimentalSharedTransitionApi +import androidx.compose.animation.SharedTransitionScope +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.CircleShape @@ -21,7 +25,11 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.unit.dp +import hu.bbara.purefin.navigation.HOME_SEARCH_SHARED_BOUNDS_KEY +import hu.bbara.purefin.navigation.LocalNavSharedAnimatedVisibilityScope +import hu.bbara.purefin.navigation.LocalSharedTransitionScope +@OptIn(ExperimentalSharedTransitionApi::class) @Composable fun HomeTopBar( onSearchClick: () -> Unit, @@ -30,11 +38,34 @@ fun HomeTopBar( onLogoutClick: () -> Unit, ) { val scheme = MaterialTheme.colorScheme + val sharedTransitionScope = LocalSharedTransitionScope.current + val animatedVisibilityScope = LocalNavSharedAnimatedVisibilityScope.current var isProfileMenuExpanded by remember { mutableStateOf(false) } DefaultTopBar() { + val searchButtonModifier = if ( + sharedTransitionScope != null && + animatedVisibilityScope != null + ) { + with(sharedTransitionScope) { + Modifier + .sharedBounds( + sharedContentState = rememberSharedContentState( + key = HOME_SEARCH_SHARED_BOUNDS_KEY + ), + animatedVisibilityScope = animatedVisibilityScope, + enter = fadeIn(), + exit = fadeOut(), + resizeMode = SharedTransitionScope.ResizeMode.scaleToBounds() + ) + .size(50.dp) + } + } else { + Modifier.size(50.dp) + } + IconButton( onClick = onSearchClick, colors = IconButtonColors( @@ -43,7 +74,7 @@ fun HomeTopBar( disabledContainerColor = scheme.surface, disabledContentColor = scheme.onSurface ), - modifier = Modifier.size(50.dp), + modifier = searchButtonModifier, ) { Icon( imageVector = Icons.Outlined.Search, @@ -97,4 +128,4 @@ fun HomeTopBar( ) } } -} \ No newline at end of file +} diff --git a/app/src/main/java/hu/bbara/purefin/ui/screen/home/components/search/HomeSearchFullScreen.kt b/app/src/main/java/hu/bbara/purefin/ui/screen/home/components/search/HomeSearchFullScreen.kt index c0a8cc65..c957ed43 100644 --- a/app/src/main/java/hu/bbara/purefin/ui/screen/home/components/search/HomeSearchFullScreen.kt +++ b/app/src/main/java/hu/bbara/purefin/ui/screen/home/components/search/HomeSearchFullScreen.kt @@ -1,5 +1,9 @@ package hu.bbara.purefin.ui.screen.home.components.search +import androidx.compose.animation.ExperimentalSharedTransitionApi +import androidx.compose.animation.SharedTransitionScope +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement @@ -51,17 +55,41 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import hu.bbara.purefin.ui.common.image.PurefinAsyncImage import hu.bbara.purefin.ui.common.image.PurefinLogo +import hu.bbara.purefin.navigation.HOME_SEARCH_SHARED_BOUNDS_KEY +import hu.bbara.purefin.navigation.LocalNavSharedAnimatedVisibilityScope +import hu.bbara.purefin.navigation.LocalSharedTransitionScope import hu.bbara.purefin.ui.theme.AppTheme -@OptIn(ExperimentalLayoutApi::class) +@OptIn(ExperimentalLayoutApi::class, ExperimentalSharedTransitionApi::class) @Composable fun HomeSearchFullScreen( modifier: Modifier = Modifier ) { val scheme = MaterialTheme.colorScheme + val sharedTransitionScope = LocalSharedTransitionScope.current + val animatedVisibilityScope = LocalNavSharedAnimatedVisibilityScope.current + val sharedBoundsModifier = if ( + sharedTransitionScope != null && + animatedVisibilityScope != null + ) { + with(sharedTransitionScope) { + Modifier.sharedBounds( + sharedContentState = rememberSharedContentState( + key = HOME_SEARCH_SHARED_BOUNDS_KEY + ), + animatedVisibilityScope = animatedVisibilityScope, + enter = fadeIn(), + exit = fadeOut(), + resizeMode = SharedTransitionScope.ResizeMode.scaleToBounds() + ) + } + } else { + Modifier + } Column( modifier = modifier + .then(sharedBoundsModifier) .fillMaxSize() .background(scheme.background) .statusBarsPadding()