18
18
19
19
package com.example.compose.snippets.animations.sharedelement
20
20
21
+ import androidx.activity.compose.PredictiveBackHandler
21
22
import androidx.compose.animation.AnimatedContent
22
23
import androidx.compose.animation.AnimatedVisibility
23
24
import androidx.compose.animation.AnimatedVisibilityScope
@@ -30,7 +31,9 @@ import androidx.compose.animation.SharedTransitionScope
30
31
import androidx.compose.animation.core.ArcMode
31
32
import androidx.compose.animation.core.ExperimentalAnimationSpecApi
32
33
import androidx.compose.animation.core.FastOutSlowInEasing
34
+ import androidx.compose.animation.core.SeekableTransitionState
33
35
import androidx.compose.animation.core.keyframes
36
+ import androidx.compose.animation.core.rememberTransition
34
37
import androidx.compose.animation.core.tween
35
38
import androidx.compose.animation.fadeIn
36
39
import androidx.compose.animation.fadeOut
@@ -60,12 +63,15 @@ import androidx.compose.material.icons.outlined.Create
60
63
import androidx.compose.material.icons.outlined.Favorite
61
64
import androidx.compose.material.icons.outlined.Share
62
65
import androidx.compose.material3.Icon
66
+ import androidx.compose.material3.Slider
63
67
import androidx.compose.material3.Surface
64
68
import androidx.compose.material3.Text
65
69
import androidx.compose.runtime.Composable
66
70
import androidx.compose.runtime.getValue
71
+ import androidx.compose.runtime.mutableIntStateOf
67
72
import androidx.compose.runtime.mutableStateOf
68
73
import androidx.compose.runtime.remember
74
+ import androidx.compose.runtime.rememberCoroutineScope
69
75
import androidx.compose.runtime.setValue
70
76
import androidx.compose.ui.Alignment
71
77
import androidx.compose.ui.Modifier
@@ -85,6 +91,8 @@ import androidx.navigation.navArgument
85
91
import com.example.compose.snippets.R
86
92
import com.example.compose.snippets.ui.theme.LavenderLight
87
93
import com.example.compose.snippets.ui.theme.RoseLight
94
+ import kotlin.coroutines.cancellation.CancellationException
95
+ import kotlinx.coroutines.launch
88
96
89
97
@Preview
90
98
@Composable
@@ -628,3 +636,96 @@ fun PlaceholderSizeAnimated_Demo() {
628
636
}
629
637
// [END android_compose_shared_element_placeholder_size]
630
638
}
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