Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion Jetsnack/app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.parcelize)
alias(libs.plugins.compose)
alias(libs.plugins.kotlin.serialization)
}

android {
Expand Down Expand Up @@ -118,8 +119,11 @@ dependencies {
implementation(libs.androidx.lifecycle.viewmodel.ktx)
implementation(libs.androidx.lifecycle.viewModelCompose)
implementation(libs.androidx.lifecycle.runtime.compose)
implementation(libs.androidx.navigation.compose)
implementation(libs.androidx.constraintlayout.compose)
implementation(libs.androidx.navigation3.ui)
implementation(libs.androidx.navigation3.runtime)
implementation(libs.androidx.lifecycle.viewmodel.navigation3)
implementation(libs.kotlinx.serialization.json)

implementation(libs.androidx.compose.runtime)
implementation(libs.androidx.compose.foundation)
Expand Down
200 changes: 98 additions & 102 deletions Jetsnack/app/src/main/java/com/example/jetsnack/ui/JetsnackApp.kt
Original file line number Diff line number Diff line change
Expand Up @@ -20,38 +20,43 @@

package com.example.jetsnack.ui

import androidx.compose.animation.AnimatedVisibilityScope
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.ExperimentalSharedTransitionApi
import androidx.compose.animation.SharedTransitionLayout
import androidx.compose.animation.SharedTransitionScope
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.slideInVertically
import androidx.compose.animation.slideOutVertically
import androidx.compose.animation.togetherWith
import androidx.compose.foundation.layout.consumeWindowInsets
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.systemBarsPadding
import androidx.compose.material3.SnackbarHost
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.compositionLocalOf
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.navigation.NavBackStackEntry
import androidx.navigation.NavType
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.currentBackStackEntryAsState
import androidx.navigation.navArgument
import androidx.navigation3.runtime.entryProvider
import androidx.navigation3.runtime.rememberNavBackStack
import androidx.navigation3.ui.NavDisplay
import com.example.jetsnack.ui.components.JetsnackScaffold
import com.example.jetsnack.ui.components.JetsnackSnackbar
import com.example.jetsnack.ui.components.rememberJetsnackScaffoldState
import com.example.jetsnack.ui.home.Feed
import com.example.jetsnack.ui.home.HomeSections
import com.example.jetsnack.ui.home.JetsnackBottomBar
import com.example.jetsnack.ui.home.addHomeGraph
import com.example.jetsnack.ui.home.composableWithCompositionLocal
import com.example.jetsnack.ui.navigation.MainDestinations
import com.example.jetsnack.ui.navigation.rememberJetsnackNavController
import com.example.jetsnack.ui.home.Profile
import com.example.jetsnack.ui.home.cart.Cart
import com.example.jetsnack.ui.home.search.Search
import com.example.jetsnack.ui.navigation.CartKey
import com.example.jetsnack.ui.navigation.FeedKey
import com.example.jetsnack.ui.navigation.ProfileKey
import com.example.jetsnack.ui.navigation.SearchKey
import com.example.jetsnack.ui.navigation.SnackDetailKey
import com.example.jetsnack.ui.navigation.addHomeSection
import com.example.jetsnack.ui.navigation.addSnackDetail
import com.example.jetsnack.ui.snackdetail.SnackDetail
import com.example.jetsnack.ui.snackdetail.nonSpatialExpressiveSpring
import com.example.jetsnack.ui.snackdetail.spatialExpressiveSpring
Expand All @@ -61,110 +66,101 @@ import com.example.jetsnack.ui.theme.JetsnackTheme
@Composable
fun JetsnackApp() {
JetsnackTheme {
val jetsnackNavController = rememberJetsnackNavController()

val backStack = rememberNavBackStack(FeedKey)
val jetsnackScaffoldState = rememberJetsnackScaffoldState()

SharedTransitionLayout {
CompositionLocalProvider(
LocalSharedTransitionScope provides this,
) {
NavHost(
navController = jetsnackNavController.navController,
startDestination = MainDestinations.HOME_ROUTE,
) {
composableWithCompositionLocal(
route = MainDestinations.HOME_ROUTE,
) { backStackEntry ->
MainContainer(
onSnackSelected = jetsnackNavController::navigateToSnackDetail,
JetsnackScaffold(
bottomBar = {
val showBottomBar = backStack.last() !is SnackDetailKey

AnimatedVisibility(visible = showBottomBar) {
Comment thread
dturner marked this conversation as resolved.
Outdated
JetsnackBottomBar(
tabs = HomeSections.entries.toTypedArray(),
currentKey = backStack.findLast { it in HomeSections.entries.map { it.route } } ?: FeedKey,
onItemClick = { navKey -> backStack.addHomeSection(navKey) },
Comment thread
dturner marked this conversation as resolved.
Outdated
modifier = Modifier
.renderInSharedTransitionScopeOverlay(
zIndexInOverlay = 1f,
)
.animateEnterExit(
enter = fadeIn(nonSpatialExpressiveSpring()) + slideInVertically(
spatialExpressiveSpring(),
) {
it
},
exit = fadeOut(nonSpatialExpressiveSpring()) + slideOutVertically(
spatialExpressiveSpring(),
) {
it
},
),
)
}
},
snackbarHost = {
SnackbarHost(
hostState = it,
modifier = Modifier.systemBarsPadding(),
snackbar = { snackbarData -> JetsnackSnackbar(snackbarData) },
)
}
},
snackBarHostState = jetsnackScaffoldState.snackBarHostState,
) { padding ->

composableWithCompositionLocal(
"${MainDestinations.SNACK_DETAIL_ROUTE}/" +
"{${MainDestinations.SNACK_ID_KEY}}" +
"?origin={${MainDestinations.ORIGIN}}",
arguments = listOf(
navArgument(MainDestinations.SNACK_ID_KEY) {
type = NavType.LongType
},
),
val modifier = Modifier
.padding(padding)
.consumeWindowInsets(padding)

) { backStackEntry ->
val arguments = requireNotNull(backStackEntry.arguments)
val snackId = arguments.getLong(MainDestinations.SNACK_ID_KEY)
val origin = arguments.getString(MainDestinations.ORIGIN)
SnackDetail(
snackId,
origin = origin ?: "",
upPress = jetsnackNavController::upPress,
)
}
}
}
}
}
}
val transitionSpec = fadeIn(nonSpatialExpressiveSpring()) togetherWith
fadeOut(nonSpatialExpressiveSpring())

@Composable
fun MainContainer(modifier: Modifier = Modifier, onSnackSelected: (Long, String, NavBackStackEntry) -> Unit) {
val jetsnackScaffoldState = rememberJetsnackScaffoldState()
val nestedNavController = rememberJetsnackNavController()
val navBackStackEntry by nestedNavController.navController.currentBackStackEntryAsState()
val currentRoute = navBackStackEntry?.destination?.route
val sharedTransitionScope = LocalSharedTransitionScope.current
?: throw IllegalStateException("No SharedElementScope found")
val animatedVisibilityScope = LocalNavAnimatedVisibilityScope.current
?: throw IllegalStateException("No SharedElementScope found")
JetsnackScaffold(
bottomBar = {
with(animatedVisibilityScope) {
with(sharedTransitionScope) {
JetsnackBottomBar(
tabs = HomeSections.entries.toTypedArray(),
currentRoute = currentRoute ?: HomeSections.FEED.route,
navigateToRoute = nestedNavController::navigateToBottomBarRoute,
modifier = Modifier
.renderInSharedTransitionScopeOverlay(
zIndexInOverlay = 1f,
)
.animateEnterExit(
enter = fadeIn(nonSpatialExpressiveSpring()) + slideInVertically(
spatialExpressiveSpring(),
) {
it
},
exit = fadeOut(nonSpatialExpressiveSpring()) + slideOutVertically(
spatialExpressiveSpring(),
) {
it
},
),
NavDisplay(
backStack = backStack,
onBack = { backStack.removeLastOrNull() },
sharedTransitionScope = this@SharedTransitionLayout,
entryProvider = entryProvider {
entry<FeedKey> {
Feed(
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Within the Feed view, we should add a check on the TopAppBar to only slide in / slide out when its going towards snackdetail and coming back from snack detail, otherwise it now performs the animation between every screen transition, due to the new single instance of a navigation scope.

onSnackClick = backStack.addSnackDetail(),
modifier = modifier
)
}
entry<CartKey> {
Cart(
onSnackClick = backStack.addSnackDetail(),
modifier = modifier
)
}
entry<SearchKey> {
Search(
onSnackClick = backStack.addSnackDetail(),
modifier = modifier
)
}
entry<ProfileKey> {
Profile(modifier)
}
entry<SnackDetailKey> { key ->
SnackDetail(
key.snackId,
origin = key.origin,
upPress = { backStack.removeLastOrNull() },
)
}
},
transitionSpec = { transitionSpec },
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure why, but comparing between develop + this branch, the transition between the Feed and Cart composables looks a bit quicker, and doesn't seem like its performing a fadeIn or fadeOut. Doesn't look bad, I'm just wondering if there was a change missed here.
Not blocking on this one :)

popTransitionSpec = { transitionSpec },
predictivePopTransitionSpec = { transitionSpec },
)
}
}
},
modifier = modifier,
snackbarHost = {
SnackbarHost(
hostState = it,
modifier = Modifier.systemBarsPadding(),
snackbar = { snackbarData -> JetsnackSnackbar(snackbarData) },
)
},
snackBarHostState = jetsnackScaffoldState.snackBarHostState,
) { padding ->
NavHost(
navController = nestedNavController.navController,
startDestination = HomeSections.FEED.route,
) {
addHomeGraph(
onSnackSelected = onSnackSelected,
modifier = Modifier
.padding(padding)
.consumeWindowInsets(padding),
)
}
}
}

val LocalNavAnimatedVisibilityScope = compositionLocalOf<AnimatedVisibilityScope?> { null }
val LocalSharedTransitionScope = compositionLocalOf<SharedTransitionScope?> { null }
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ package com.example.jetsnack.ui.components

import android.content.res.Configuration.UI_MODE_NIGHT_YES
import androidx.annotation.DrawableRes
import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.EnterExitState
import androidx.compose.animation.ExperimentalSharedTransitionApi
Expand Down Expand Up @@ -69,14 +70,14 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.navigation3.ui.LocalNavAnimatedContentScope
import coil.compose.AsyncImage
import coil.request.ImageRequest
import com.example.jetsnack.R
import com.example.jetsnack.model.CollectionType
import com.example.jetsnack.model.Snack
import com.example.jetsnack.model.SnackCollection
import com.example.jetsnack.model.snacks
import com.example.jetsnack.ui.LocalNavAnimatedVisibilityScope
import com.example.jetsnack.ui.LocalSharedTransitionScope
import com.example.jetsnack.ui.SnackSharedElementKey
import com.example.jetsnack.ui.SnackSharedElementType
Expand Down Expand Up @@ -199,8 +200,7 @@ fun SnackItem(snack: Snack, snackCollectionId: Long, onSnackClick: (Long, String
) {
val sharedTransitionScope = LocalSharedTransitionScope.current
?: throw IllegalStateException("No sharedTransitionScope found")
val animatedVisibilityScope = LocalNavAnimatedVisibilityScope.current
?: throw IllegalStateException("No animatedVisibilityScope found")
val animatedContentScope = LocalNavAnimatedContentScope.current

with(sharedTransitionScope) {
Column(
Expand All @@ -225,7 +225,7 @@ fun SnackItem(snack: Snack, snackCollectionId: Long, onSnackClick: (Long, String
type = SnackSharedElementType.Image,
),
),
animatedVisibilityScope = animatedVisibilityScope,
animatedVisibilityScope = animatedContentScope,
boundsTransform = snackDetailBoundsTransform,
),
)
Expand All @@ -244,7 +244,7 @@ fun SnackItem(snack: Snack, snackCollectionId: Long, onSnackClick: (Long, String
type = SnackSharedElementType.Title,
),
),
animatedVisibilityScope = animatedVisibilityScope,
animatedVisibilityScope = animatedContentScope,
enter = fadeIn(nonSpatialExpressiveSpring()),
exit = fadeOut(nonSpatialExpressiveSpring()),
resizeMode = SharedTransitionScope.ResizeMode.scaleToBounds(),
Expand All @@ -268,10 +268,10 @@ private fun HighlightSnackItem(
) {
val sharedTransitionScope = LocalSharedTransitionScope.current
?: throw IllegalStateException("No Scope found")
val animatedVisibilityScope = LocalNavAnimatedVisibilityScope.current
?: throw IllegalStateException("No Scope found")
val animatedContentScope = LocalNavAnimatedContentScope.current
Comment thread
dturner marked this conversation as resolved.

with(sharedTransitionScope) {
val roundedCornerAnimation by animatedVisibilityScope.transition
val roundedCornerAnimation by animatedContentScope.transition
.animateDp(label = "rounded corner") { enterExit: EnterExitState ->
when (enterExit) {
EnterExitState.PreEnter -> 0.dp
Expand All @@ -292,7 +292,7 @@ private fun HighlightSnackItem(
type = SnackSharedElementType.Bounds,
),
),
animatedVisibilityScope = animatedVisibilityScope,
animatedVisibilityScope = animatedContentScope,
boundsTransform = snackDetailBoundsTransform,
clipInOverlayDuringTransition = OverlayClip(
RoundedCornerShape(
Expand Down Expand Up @@ -339,7 +339,7 @@ private fun HighlightSnackItem(
type = SnackSharedElementType.Background,
),
),
animatedVisibilityScope = animatedVisibilityScope,
animatedVisibilityScope = animatedContentScope,
boundsTransform = snackDetailBoundsTransform,
enter = fadeIn(nonSpatialExpressiveSpring()),
exit = fadeOut(nonSpatialExpressiveSpring()),
Expand Down Expand Up @@ -374,7 +374,7 @@ private fun HighlightSnackItem(
type = SnackSharedElementType.Image,
),
),
animatedVisibilityScope = animatedVisibilityScope,
animatedVisibilityScope = animatedContentScope,
exit = fadeOut(nonSpatialExpressiveSpring()),
enter = fadeIn(nonSpatialExpressiveSpring()),
boundsTransform = snackDetailBoundsTransform,
Expand All @@ -401,7 +401,7 @@ private fun HighlightSnackItem(
type = SnackSharedElementType.Title,
),
),
animatedVisibilityScope = animatedVisibilityScope,
animatedVisibilityScope = animatedContentScope,
enter = fadeIn(nonSpatialExpressiveSpring()),
exit = fadeOut(nonSpatialExpressiveSpring()),
boundsTransform = snackDetailBoundsTransform,
Expand All @@ -425,7 +425,7 @@ private fun HighlightSnackItem(
type = SnackSharedElementType.Tagline,
),
),
animatedVisibilityScope = animatedVisibilityScope,
animatedVisibilityScope = animatedContentScope,
enter = fadeIn(nonSpatialExpressiveSpring()),
exit = fadeOut(nonSpatialExpressiveSpring()),
boundsTransform = snackDetailBoundsTransform,
Expand Down Expand Up @@ -494,10 +494,10 @@ fun SnackCardPreview() {
fun JetsnackPreviewWrapper(content: @Composable () -> Unit) {
JetsnackTheme {
SharedTransitionLayout {
AnimatedVisibility(visible = true) {
AnimatedContent(targetState = true) { _ ->
CompositionLocalProvider(
LocalSharedTransitionScope provides this@SharedTransitionLayout,
LocalNavAnimatedVisibilityScope provides this,
LocalNavAnimatedContentScope provides this,
) {
content()
}
Expand Down
Loading
Loading