1818
1919package com.example.compose.snippets.animations.sharedelement
2020
21+ import androidx.activity.compose.PredictiveBackHandler
2122import androidx.compose.animation.AnimatedContent
2223import androidx.compose.animation.AnimatedVisibility
2324import androidx.compose.animation.AnimatedVisibilityScope
@@ -30,7 +31,9 @@ import androidx.compose.animation.SharedTransitionScope
3031import androidx.compose.animation.core.ArcMode
3132import androidx.compose.animation.core.ExperimentalAnimationSpecApi
3233import androidx.compose.animation.core.FastOutSlowInEasing
34+ import androidx.compose.animation.core.SeekableTransitionState
3335import androidx.compose.animation.core.keyframes
36+ import androidx.compose.animation.core.rememberTransition
3437import androidx.compose.animation.core.tween
3538import androidx.compose.animation.fadeIn
3639import androidx.compose.animation.fadeOut
@@ -60,12 +63,15 @@ import androidx.compose.material.icons.outlined.Create
6063import androidx.compose.material.icons.outlined.Favorite
6164import androidx.compose.material.icons.outlined.Share
6265import androidx.compose.material3.Icon
66+ import androidx.compose.material3.Slider
6367import androidx.compose.material3.Surface
6468import androidx.compose.material3.Text
6569import androidx.compose.runtime.Composable
6670import androidx.compose.runtime.getValue
71+ import androidx.compose.runtime.mutableIntStateOf
6772import androidx.compose.runtime.mutableStateOf
6873import androidx.compose.runtime.remember
74+ import androidx.compose.runtime.rememberCoroutineScope
6975import androidx.compose.runtime.setValue
7076import androidx.compose.ui.Alignment
7177import androidx.compose.ui.Modifier
@@ -85,6 +91,8 @@ import androidx.navigation.navArgument
8591import com.example.compose.snippets.R
8692import com.example.compose.snippets.ui.theme.LavenderLight
8793import com.example.compose.snippets.ui.theme.RoseLight
94+ import kotlin.coroutines.cancellation.CancellationException
95+ import kotlinx.coroutines.launch
8896
8997@Preview
9098@Composable
@@ -628,3 +636,96 @@ fun PlaceholderSizeAnimated_Demo() {
628636 }
629637// [END android_compose_shared_element_placeholder_size]
630638}
639+
640+ private sealed class Screen {
641+ data object Home : Screen ()
642+ data class Details (val id : Int ) : Screen()
643+ }
644+
645+ @Preview
646+ @Composable
647+ fun CustomPredictiveBackHandle () {
648+ // [START android_compose_shared_element_custom_seeking]
649+ val seekableTransitionState = remember {
650+ SeekableTransitionState <Screen >(Screen .Home )
651+ }
652+ val transition = rememberTransition(transitionState = seekableTransitionState)
653+
654+ PredictiveBackHandler (seekableTransitionState.currentState is Screen .Details ) { progress ->
655+ try {
656+ // Whilst a back gesture is in progress, backEvents will be fired for each progress
657+ // update.
658+ progress.collect { backEvent ->
659+ // For each backEvent that comes in, we manually seekTo the reported back progress
660+ try {
661+ seekableTransitionState.seekTo(backEvent.progress, targetState = Screen .Home )
662+ } catch (e: CancellationException ) {
663+ // seekTo may be cancelled as expected, if animateTo or subsequent seekTo calls
664+ // before the current seekTo finishes, in this case, we ignore the cancellation.
665+ }
666+ }
667+ // Once collection has completed, we are either fully in the target state, or need
668+ // to progress towards the end.
669+ seekableTransitionState.animateTo(seekableTransitionState.targetState)
670+ } catch (e: CancellationException ) {
671+ // When the predictive back gesture is cancelled, we snap to the end state to ensure
672+ // it completes its seeking animation back to the currentState
673+ seekableTransitionState.snapTo(seekableTransitionState.currentState)
674+ }
675+ }
676+ val coroutineScope = rememberCoroutineScope()
677+ var lastNavigatedIndex by remember {
678+ mutableIntStateOf(0 )
679+ }
680+ Column {
681+ Slider (
682+ modifier = Modifier .height(48 .dp),
683+ value = seekableTransitionState.fraction,
684+ onValueChange = {
685+ coroutineScope.launch {
686+ if (seekableTransitionState.currentState is Screen .Details ) {
687+ seekableTransitionState.seekTo(it, Screen .Home )
688+ } else {
689+ // seek to the previously navigated index
690+ seekableTransitionState.seekTo(it, Screen .Details (lastNavigatedIndex))
691+ }
692+ }
693+ }
694+ )
695+ SharedTransitionLayout (modifier = Modifier .weight(1f )) {
696+ transition.AnimatedContent { targetState ->
697+ when (targetState) {
698+ Screen .Home -> {
699+ HomeScreen (
700+ this @SharedTransitionLayout,
701+ this @AnimatedContent,
702+ onItemClick = {
703+ coroutineScope.launch {
704+ lastNavigatedIndex = it
705+ seekableTransitionState.animateTo(Screen .Details (it))
706+ }
707+ }
708+ )
709+ }
710+
711+ is Screen .Details -> {
712+ val snack = listSnacks[targetState.id]
713+ DetailsScreen (
714+ targetState.id,
715+ snack,
716+ this @SharedTransitionLayout,
717+ this @AnimatedContent,
718+ onBackPressed = {
719+ coroutineScope.launch {
720+ seekableTransitionState.animateTo(Screen .Home )
721+ }
722+ }
723+ )
724+ }
725+ }
726+ }
727+ }
728+ }
729+
730+ // [END android_compose_shared_element_custom_seeking]
731+ }
0 commit comments