mirror of
https://github.com/bbara04/Purefin.git
synced 2026-07-23 19:26:50 +00:00
feat(app): implement shared transition for search functionality in HomeTopBar and HomeSearchFullScreen
This commit is contained in:
@@ -6,6 +6,8 @@ import androidx.activity.ComponentActivity
|
|||||||
import androidx.activity.compose.setContent
|
import androidx.activity.compose.setContent
|
||||||
import androidx.activity.enableEdgeToEdge
|
import androidx.activity.enableEdgeToEdge
|
||||||
import androidx.compose.animation.core.tween
|
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.fadeIn
|
||||||
import androidx.compose.animation.fadeOut
|
import androidx.compose.animation.fadeOut
|
||||||
import androidx.compose.animation.togetherWith
|
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.data.UserSessionRepository
|
||||||
import hu.bbara.purefin.navigation.LocalNavigationBackStack
|
import hu.bbara.purefin.navigation.LocalNavigationBackStack
|
||||||
import hu.bbara.purefin.navigation.LocalNavigationManager
|
import hu.bbara.purefin.navigation.LocalNavigationManager
|
||||||
|
import hu.bbara.purefin.navigation.LocalSharedTransitionScope
|
||||||
import hu.bbara.purefin.navigation.NavigationCommand
|
import hu.bbara.purefin.navigation.NavigationCommand
|
||||||
import hu.bbara.purefin.navigation.NavigationManager
|
import hu.bbara.purefin.navigation.NavigationManager
|
||||||
import hu.bbara.purefin.navigation.Route
|
import hu.bbara.purefin.navigation.Route
|
||||||
@@ -131,6 +134,7 @@ class PurefinActivity : ComponentActivity() {
|
|||||||
return (applicationInfo.flags and ApplicationInfo.FLAG_DEBUGGABLE) != 0
|
return (applicationInfo.flags and ApplicationInfo.FLAG_DEBUGGABLE) != 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalSharedTransitionApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun MainApp(
|
fun MainApp(
|
||||||
userSessionRepository: UserSessionRepository,
|
userSessionRepository: UserSessionRepository,
|
||||||
@@ -172,45 +176,48 @@ class PurefinActivity : ComponentActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
CompositionLocalProvider(
|
SharedTransitionLayout {
|
||||||
LocalNavigationManager provides navigationManager,
|
CompositionLocalProvider(
|
||||||
LocalNavigationBackStack provides backStack.toList()
|
LocalNavigationManager provides navigationManager,
|
||||||
) {
|
LocalNavigationBackStack provides backStack.toList(),
|
||||||
NavDisplay(
|
LocalSharedTransitionScope provides this
|
||||||
backStack = backStack,
|
) {
|
||||||
onBack = { navigationManager.pop() },
|
NavDisplay(
|
||||||
modifier = Modifier.fillMaxSize().background(backgroundDark),
|
backStack = backStack,
|
||||||
transitionSpec = {
|
onBack = { navigationManager.pop() },
|
||||||
fadeIn(
|
modifier = Modifier.fillMaxSize().background(backgroundDark),
|
||||||
animationSpec = tween(
|
transitionSpec = {
|
||||||
durationMillis = 140
|
fadeIn(
|
||||||
)
|
animationSpec = tween(
|
||||||
) togetherWith
|
durationMillis = 140
|
||||||
fadeOut(animationSpec = tween(durationMillis = 140))
|
)
|
||||||
},
|
) togetherWith
|
||||||
popTransitionSpec = {
|
fadeOut(animationSpec = tween(durationMillis = 140))
|
||||||
fadeIn(
|
},
|
||||||
animationSpec = tween(
|
popTransitionSpec = {
|
||||||
durationMillis = 140
|
fadeIn(
|
||||||
)
|
animationSpec = tween(
|
||||||
) togetherWith
|
durationMillis = 140
|
||||||
fadeOut(animationSpec = tween(durationMillis = 140))
|
)
|
||||||
},
|
) togetherWith
|
||||||
predictivePopTransitionSpec = { _ ->
|
fadeOut(animationSpec = tween(durationMillis = 140))
|
||||||
fadeIn(
|
},
|
||||||
animationSpec = tween(
|
predictivePopTransitionSpec = { _ ->
|
||||||
durationMillis = 140
|
fadeIn(
|
||||||
)
|
animationSpec = tween(
|
||||||
) togetherWith
|
durationMillis = 140
|
||||||
fadeOut(animationSpec = tween(durationMillis = 140))
|
)
|
||||||
},
|
) togetherWith
|
||||||
entryDecorators =
|
fadeOut(animationSpec = tween(durationMillis = 140))
|
||||||
listOf(
|
},
|
||||||
rememberSaveableStateHolderNavEntryDecorator(),
|
entryDecorators =
|
||||||
rememberViewModelStoreNavEntryDecorator(),
|
listOf(
|
||||||
),
|
rememberSaveableStateHolderNavEntryDecorator(),
|
||||||
entryProvider = appEntryProvider
|
rememberViewModelStoreNavEntryDecorator(),
|
||||||
)
|
),
|
||||||
|
entryProvider = appEntryProvider
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
LoginScreen()
|
LoginScreen()
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
package hu.bbara.purefin.navigation
|
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.ProvidableCompositionLocal
|
||||||
import androidx.compose.runtime.compositionLocalOf
|
import androidx.compose.runtime.compositionLocalOf
|
||||||
import androidx.compose.runtime.staticCompositionLocalOf
|
import androidx.compose.runtime.staticCompositionLocalOf
|
||||||
@@ -8,3 +11,11 @@ val LocalNavigationManager: ProvidableCompositionLocal<NavigationManager> =
|
|||||||
staticCompositionLocalOf { error("NavigationManager not provided") }
|
staticCompositionLocalOf { error("NavigationManager not provided") }
|
||||||
|
|
||||||
val LocalNavigationBackStack = compositionLocalOf<List<Route>> { emptyList() }
|
val LocalNavigationBackStack = compositionLocalOf<List<Route>> { emptyList() }
|
||||||
|
|
||||||
|
@OptIn(ExperimentalSharedTransitionApi::class)
|
||||||
|
val LocalSharedTransitionScope = compositionLocalOf<SharedTransitionScope?> { null }
|
||||||
|
|
||||||
|
val LocalNavSharedAnimatedVisibilityScope =
|
||||||
|
compositionLocalOf<AnimatedVisibilityScope?> { null }
|
||||||
|
|
||||||
|
const val HOME_SEARCH_SHARED_BOUNDS_KEY = "home_search_shared_bounds"
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
package hu.bbara.purefin.navigation
|
package hu.bbara.purefin.navigation
|
||||||
|
|
||||||
|
import androidx.compose.runtime.CompositionLocalProvider
|
||||||
import androidx.navigation3.runtime.EntryProviderScope
|
import androidx.navigation3.runtime.EntryProviderScope
|
||||||
|
import androidx.navigation3.ui.LocalNavAnimatedContentScope
|
||||||
import hu.bbara.purefin.ui.screen.library.LibraryScreen
|
import hu.bbara.purefin.ui.screen.library.LibraryScreen
|
||||||
import hu.bbara.purefin.ui.screen.login.LoginScreen
|
import hu.bbara.purefin.ui.screen.login.LoginScreen
|
||||||
import hu.bbara.purefin.ui.screen.AppScreen
|
import hu.bbara.purefin.ui.screen.AppScreen
|
||||||
@@ -11,10 +13,18 @@ import hu.bbara.purefin.ui.screen.series.SeriesScreen
|
|||||||
|
|
||||||
fun EntryProviderScope<Route>.appRouteEntryBuilder() {
|
fun EntryProviderScope<Route>.appRouteEntryBuilder() {
|
||||||
entry<Route.Home> {
|
entry<Route.Home> {
|
||||||
AppScreen()
|
CompositionLocalProvider(
|
||||||
|
LocalNavSharedAnimatedVisibilityScope provides LocalNavAnimatedContentScope.current
|
||||||
|
) {
|
||||||
|
AppScreen()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
entry<Route.HomeSearchRoute> {
|
entry<Route.HomeSearchRoute> {
|
||||||
HomeSearchFullScreen()
|
CompositionLocalProvider(
|
||||||
|
LocalNavSharedAnimatedVisibilityScope provides LocalNavAnimatedContentScope.current
|
||||||
|
) {
|
||||||
|
HomeSearchFullScreen()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
entry<Route.MovieRoute> {
|
entry<Route.MovieRoute> {
|
||||||
MovieScreen(movie = it.item)
|
MovieScreen(movie = it.item)
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
package hu.bbara.purefin.ui.screen.home.components
|
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.Spacer
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.shape.CircleShape
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
@@ -21,7 +25,11 @@ import androidx.compose.runtime.setValue
|
|||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.unit.dp
|
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
|
@Composable
|
||||||
fun HomeTopBar(
|
fun HomeTopBar(
|
||||||
onSearchClick: () -> Unit,
|
onSearchClick: () -> Unit,
|
||||||
@@ -30,11 +38,34 @@ fun HomeTopBar(
|
|||||||
onLogoutClick: () -> Unit,
|
onLogoutClick: () -> Unit,
|
||||||
) {
|
) {
|
||||||
val scheme = MaterialTheme.colorScheme
|
val scheme = MaterialTheme.colorScheme
|
||||||
|
val sharedTransitionScope = LocalSharedTransitionScope.current
|
||||||
|
val animatedVisibilityScope = LocalNavSharedAnimatedVisibilityScope.current
|
||||||
|
|
||||||
var isProfileMenuExpanded by remember { mutableStateOf(false) }
|
var isProfileMenuExpanded by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
DefaultTopBar()
|
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(
|
IconButton(
|
||||||
onClick = onSearchClick,
|
onClick = onSearchClick,
|
||||||
colors = IconButtonColors(
|
colors = IconButtonColors(
|
||||||
@@ -43,7 +74,7 @@ fun HomeTopBar(
|
|||||||
disabledContainerColor = scheme.surface,
|
disabledContainerColor = scheme.surface,
|
||||||
disabledContentColor = scheme.onSurface
|
disabledContentColor = scheme.onSurface
|
||||||
),
|
),
|
||||||
modifier = Modifier.size(50.dp),
|
modifier = searchButtonModifier,
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Outlined.Search,
|
imageVector = Icons.Outlined.Search,
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
package hu.bbara.purefin.ui.screen.home.components.search
|
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.BorderStroke
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
@@ -51,17 +55,41 @@ import androidx.compose.ui.unit.dp
|
|||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import hu.bbara.purefin.ui.common.image.PurefinAsyncImage
|
import hu.bbara.purefin.ui.common.image.PurefinAsyncImage
|
||||||
import hu.bbara.purefin.ui.common.image.PurefinLogo
|
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
|
import hu.bbara.purefin.ui.theme.AppTheme
|
||||||
|
|
||||||
@OptIn(ExperimentalLayoutApi::class)
|
@OptIn(ExperimentalLayoutApi::class, ExperimentalSharedTransitionApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun HomeSearchFullScreen(
|
fun HomeSearchFullScreen(
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
val scheme = MaterialTheme.colorScheme
|
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(
|
Column(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
|
.then(sharedBoundsModifier)
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.background(scheme.background)
|
.background(scheme.background)
|
||||||
.statusBarsPadding()
|
.statusBarsPadding()
|
||||||
|
|||||||
Reference in New Issue
Block a user