Skip to content

Commit 41b6a64

Browse files
authored
Add an example of Shared elements that are seekable using SeekableTransitionState (#416)
* Added seekable predictive back (redoing commit) * Added seekable predictive back (redoing commit) * spotless * Delete SeekableSharedElement.kt * Apply Spotless * Add enableOnBackInvokedCallback * Add comments and fix custom seeking predictive back example. * Apply Spotless --------- Co-authored-by: riggaroo <[email protected]>
1 parent 6dc5a0a commit 41b6a64

File tree

3 files changed

+116
-13
lines changed

3 files changed

+116
-13
lines changed

compose/snippets/src/main/AndroidManifest.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
android:label="@string/app_name"
2828
android:roundIcon="@mipmap/ic_launcher_round"
2929
android:supportsRtl="true"
30+
android:enableOnBackInvokedCallback="true"
3031
android:theme="@style/Theme.Snippets">
3132
<!-- [START android_compose_pip_manifest_entry]-->
3233
<activity

compose/snippets/src/main/java/com/example/compose/snippets/animations/sharedelement/CustomizeSharedElementsSnippets.kt

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
package com.example.compose.snippets.animations.sharedelement
2020

21+
import androidx.activity.compose.PredictiveBackHandler
2122
import androidx.compose.animation.AnimatedContent
2223
import androidx.compose.animation.AnimatedVisibility
2324
import androidx.compose.animation.AnimatedVisibilityScope
@@ -30,7 +31,9 @@ import androidx.compose.animation.SharedTransitionScope
3031
import androidx.compose.animation.core.ArcMode
3132
import androidx.compose.animation.core.ExperimentalAnimationSpecApi
3233
import androidx.compose.animation.core.FastOutSlowInEasing
34+
import androidx.compose.animation.core.SeekableTransitionState
3335
import androidx.compose.animation.core.keyframes
36+
import androidx.compose.animation.core.rememberTransition
3437
import androidx.compose.animation.core.tween
3538
import androidx.compose.animation.fadeIn
3639
import androidx.compose.animation.fadeOut
@@ -60,12 +63,15 @@ import androidx.compose.material.icons.outlined.Create
6063
import androidx.compose.material.icons.outlined.Favorite
6164
import androidx.compose.material.icons.outlined.Share
6265
import androidx.compose.material3.Icon
66+
import androidx.compose.material3.Slider
6367
import androidx.compose.material3.Surface
6468
import androidx.compose.material3.Text
6569
import androidx.compose.runtime.Composable
6670
import androidx.compose.runtime.getValue
71+
import androidx.compose.runtime.mutableIntStateOf
6772
import androidx.compose.runtime.mutableStateOf
6873
import androidx.compose.runtime.remember
74+
import androidx.compose.runtime.rememberCoroutineScope
6975
import androidx.compose.runtime.setValue
7076
import androidx.compose.ui.Alignment
7177
import androidx.compose.ui.Modifier
@@ -85,6 +91,8 @@ import androidx.navigation.navArgument
8591
import com.example.compose.snippets.R
8692
import com.example.compose.snippets.ui.theme.LavenderLight
8793
import 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+
}

compose/snippets/src/main/java/com/example/compose/snippets/animations/sharedelement/SharedElementsWithNavigationSnippets.kt

Lines changed: 14 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,6 @@ import androidx.compose.ui.res.painterResource
4646
import androidx.compose.ui.tooling.preview.Preview
4747
import androidx.compose.ui.unit.dp
4848
import androidx.compose.ui.unit.sp
49-
import androidx.navigation.NavHostController
5049
import androidx.navigation.NavType
5150
import androidx.navigation.compose.NavHost
5251
import androidx.navigation.compose.composable
@@ -75,9 +74,9 @@ fun SharedElement_PredictiveBack() {
7574
) {
7675
composable("home") {
7776
HomeScreen(
78-
navController,
7977
this@SharedTransitionLayout,
80-
this@composable
78+
this@composable,
79+
{ navController.navigate("details/$it") }
8180
)
8281
}
8382
composable(
@@ -87,31 +86,33 @@ fun SharedElement_PredictiveBack() {
8786
val id = backStackEntry.arguments?.getInt("item")
8887
val snack = listSnacks[id!!]
8988
DetailsScreen(
90-
navController,
9189
id,
9290
snack,
9391
this@SharedTransitionLayout,
94-
this@composable
92+
this@composable,
93+
{
94+
navController.navigate("home")
95+
}
9596
)
9697
}
9798
}
9899
}
99100
}
100101

101102
@Composable
102-
private fun DetailsScreen(
103-
navController: NavHostController,
103+
fun DetailsScreen(
104104
id: Int,
105105
snack: Snack,
106106
sharedTransitionScope: SharedTransitionScope,
107-
animatedContentScope: AnimatedContentScope
107+
animatedContentScope: AnimatedContentScope,
108+
onBackPressed: () -> Unit
108109
) {
109110
with(sharedTransitionScope) {
110111
Column(
111112
Modifier
112113
.fillMaxSize()
113114
.clickable {
114-
navController.navigate("home")
115+
onBackPressed()
115116
}
116117
) {
117118
Image(
@@ -141,10 +142,10 @@ private fun DetailsScreen(
141142
}
142143

143144
@Composable
144-
private fun HomeScreen(
145-
navController: NavHostController,
145+
fun HomeScreen(
146146
sharedTransitionScope: SharedTransitionScope,
147-
animatedContentScope: AnimatedContentScope
147+
animatedContentScope: AnimatedContentScope,
148+
onItemClick: (Int) -> Unit,
148149
) {
149150
LazyColumn(
150151
modifier = Modifier
@@ -155,7 +156,7 @@ private fun HomeScreen(
155156
itemsIndexed(listSnacks) { index, item ->
156157
Row(
157158
Modifier.clickable {
158-
navController.navigate("details/$index")
159+
onItemClick(index)
159160
}
160161
) {
161162
Spacer(modifier = Modifier.width(8.dp))

0 commit comments

Comments
 (0)