Skip to content

Commit

Permalink
Add an example of Shared elements that are seekable using SeekableTra…
Browse files Browse the repository at this point in the history
…nsitionState (#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 <riggaroo@users.noreply.github.com>
  • Loading branch information
riggaroo and riggaroo authored Dec 10, 2024
1 parent 6dc5a0a commit 41b6a64
Show file tree
Hide file tree
Showing 3 changed files with 116 additions and 13 deletions.
1 change: 1 addition & 0 deletions compose/snippets/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:enableOnBackInvokedCallback="true"
android:theme="@style/Theme.Snippets">
<!-- [START android_compose_pip_manifest_entry]-->
<activity
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@

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

import androidx.activity.compose.PredictiveBackHandler
import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.AnimatedVisibilityScope
Expand All @@ -30,7 +31,9 @@ import androidx.compose.animation.SharedTransitionScope
import androidx.compose.animation.core.ArcMode
import androidx.compose.animation.core.ExperimentalAnimationSpecApi
import androidx.compose.animation.core.FastOutSlowInEasing
import androidx.compose.animation.core.SeekableTransitionState
import androidx.compose.animation.core.keyframes
import androidx.compose.animation.core.rememberTransition
import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
Expand Down Expand Up @@ -60,12 +63,15 @@ import androidx.compose.material.icons.outlined.Create
import androidx.compose.material.icons.outlined.Favorite
import androidx.compose.material.icons.outlined.Share
import androidx.compose.material3.Icon
import androidx.compose.material3.Slider
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
Expand All @@ -85,6 +91,8 @@ import androidx.navigation.navArgument
import com.example.compose.snippets.R
import com.example.compose.snippets.ui.theme.LavenderLight
import com.example.compose.snippets.ui.theme.RoseLight
import kotlin.coroutines.cancellation.CancellationException
import kotlinx.coroutines.launch

@Preview
@Composable
Expand Down Expand Up @@ -628,3 +636,96 @@ fun PlaceholderSizeAnimated_Demo() {
}
// [END android_compose_shared_element_placeholder_size]
}

private sealed class Screen {
data object Home : Screen()
data class Details(val id: Int) : Screen()
}

@Preview
@Composable
fun CustomPredictiveBackHandle() {
// [START android_compose_shared_element_custom_seeking]
val seekableTransitionState = remember {
SeekableTransitionState<Screen>(Screen.Home)
}
val transition = rememberTransition(transitionState = seekableTransitionState)

PredictiveBackHandler(seekableTransitionState.currentState is Screen.Details) { progress ->
try {
// Whilst a back gesture is in progress, backEvents will be fired for each progress
// update.
progress.collect { backEvent ->
// For each backEvent that comes in, we manually seekTo the reported back progress
try {
seekableTransitionState.seekTo(backEvent.progress, targetState = Screen.Home)
} catch (e: CancellationException) {
// seekTo may be cancelled as expected, if animateTo or subsequent seekTo calls
// before the current seekTo finishes, in this case, we ignore the cancellation.
}
}
// Once collection has completed, we are either fully in the target state, or need
// to progress towards the end.
seekableTransitionState.animateTo(seekableTransitionState.targetState)
} catch (e: CancellationException) {
// When the predictive back gesture is cancelled, we snap to the end state to ensure
// it completes its seeking animation back to the currentState
seekableTransitionState.snapTo(seekableTransitionState.currentState)
}
}
val coroutineScope = rememberCoroutineScope()
var lastNavigatedIndex by remember {
mutableIntStateOf(0)
}
Column {
Slider(
modifier = Modifier.height(48.dp),
value = seekableTransitionState.fraction,
onValueChange = {
coroutineScope.launch {
if (seekableTransitionState.currentState is Screen.Details) {
seekableTransitionState.seekTo(it, Screen.Home)
} else {
// seek to the previously navigated index
seekableTransitionState.seekTo(it, Screen.Details(lastNavigatedIndex))
}
}
}
)
SharedTransitionLayout(modifier = Modifier.weight(1f)) {
transition.AnimatedContent { targetState ->
when (targetState) {
Screen.Home -> {
HomeScreen(
this@SharedTransitionLayout,
this@AnimatedContent,
onItemClick = {
coroutineScope.launch {
lastNavigatedIndex = it
seekableTransitionState.animateTo(Screen.Details(it))
}
}
)
}

is Screen.Details -> {
val snack = listSnacks[targetState.id]
DetailsScreen(
targetState.id,
snack,
this@SharedTransitionLayout,
this@AnimatedContent,
onBackPressed = {
coroutineScope.launch {
seekableTransitionState.animateTo(Screen.Home)
}
}
)
}
}
}
}
}

// [END android_compose_shared_element_custom_seeking]
}
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,6 @@ import androidx.compose.ui.res.painterResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.navigation.NavHostController
import androidx.navigation.NavType
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
Expand Down Expand Up @@ -75,9 +74,9 @@ fun SharedElement_PredictiveBack() {
) {
composable("home") {
HomeScreen(
navController,
this@SharedTransitionLayout,
this@composable
this@composable,
{ navController.navigate("details/$it") }
)
}
composable(
Expand All @@ -87,31 +86,33 @@ fun SharedElement_PredictiveBack() {
val id = backStackEntry.arguments?.getInt("item")
val snack = listSnacks[id!!]
DetailsScreen(
navController,
id,
snack,
this@SharedTransitionLayout,
this@composable
this@composable,
{
navController.navigate("home")
}
)
}
}
}
}

@Composable
private fun DetailsScreen(
navController: NavHostController,
fun DetailsScreen(
id: Int,
snack: Snack,
sharedTransitionScope: SharedTransitionScope,
animatedContentScope: AnimatedContentScope
animatedContentScope: AnimatedContentScope,
onBackPressed: () -> Unit
) {
with(sharedTransitionScope) {
Column(
Modifier
.fillMaxSize()
.clickable {
navController.navigate("home")
onBackPressed()
}
) {
Image(
Expand Down Expand Up @@ -141,10 +142,10 @@ private fun DetailsScreen(
}

@Composable
private fun HomeScreen(
navController: NavHostController,
fun HomeScreen(
sharedTransitionScope: SharedTransitionScope,
animatedContentScope: AnimatedContentScope
animatedContentScope: AnimatedContentScope,
onItemClick: (Int) -> Unit,
) {
LazyColumn(
modifier = Modifier
Expand All @@ -155,7 +156,7 @@ private fun HomeScreen(
itemsIndexed(listSnacks) { index, item ->
Row(
Modifier.clickable {
navController.navigate("details/$index")
onItemClick(index)
}
) {
Spacer(modifier = Modifier.width(8.dp))
Expand Down

0 comments on commit 41b6a64

Please sign in to comment.