From bd3f5d00140ebf2bb86273b6d15e4de4b8a416b3 Mon Sep 17 00:00:00 2001 From: Rebecca Franks Date: Tue, 10 Dec 2024 11:17:42 +0000 Subject: [PATCH 1/8] Added seekable predictive back (redoing commit) --- .../CustomizeSharedElementsSnippets.kt | 97 ++++++++++++++++++- .../sharedelement/SeekableSharedElement.kt | 8 ++ .../SharedElementsWithNavigationSnippets.kt | 26 ++--- 3 files changed, 118 insertions(+), 13 deletions(-) create mode 100644 compose/snippets/src/main/java/com/example/compose/snippets/animations/sharedelement/SeekableSharedElement.kt diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/animations/sharedelement/CustomizeSharedElementsSnippets.kt b/compose/snippets/src/main/java/com/example/compose/snippets/animations/sharedelement/CustomizeSharedElementsSnippets.kt index 3e60c10e..4bb9cbc9 100644 --- a/compose/snippets/src/main/java/com/example/compose/snippets/animations/sharedelement/CustomizeSharedElementsSnippets.kt +++ b/compose/snippets/src/main/java/com/example/compose/snippets/animations/sharedelement/CustomizeSharedElementsSnippets.kt @@ -14,10 +14,13 @@ * limitations under the License. */ -@file:OptIn(ExperimentalSharedTransitionApi::class) +@file:OptIn(ExperimentalSharedTransitionApi::class, ExperimentalSharedTransitionApi::class, + ExperimentalSharedTransitionApi::class +) 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 @@ -30,7 +33,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 @@ -60,12 +65,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 @@ -85,6 +93,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 kotlinx.coroutines.launch +import kotlin.coroutines.cancellation.CancellationException @Preview @Composable @@ -628,3 +638,88 @@ 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.Home) + } + val transition = rememberTransition(transitionState = seekableTransitionState) + + PredictiveBackHandler(seekableTransitionState.currentState is Screen.Details) { progress -> + try { + progress.collect { backEvent -> + // code for progress + try { + seekableTransitionState.seekTo(backEvent.progress, targetState = Screen.Home) + } catch (e: CancellationException) { + // ignore the cancellation + } + } + // code for completion + seekableTransitionState.animateTo(seekableTransitionState.targetState) + } catch (e: CancellationException) { + // code for cancellation + seekableTransitionState.animateTo(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] +} diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/animations/sharedelement/SeekableSharedElement.kt b/compose/snippets/src/main/java/com/example/compose/snippets/animations/sharedelement/SeekableSharedElement.kt new file mode 100644 index 00000000..6588b504 --- /dev/null +++ b/compose/snippets/src/main/java/com/example/compose/snippets/animations/sharedelement/SeekableSharedElement.kt @@ -0,0 +1,8 @@ +package com.example.compose.snippets.animations.sharedelement + +import androidx.compose.animation.core.SeekableTransitionState +import androidx.compose.animation.core.rememberTransition +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.tooling.preview.Preview + diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/animations/sharedelement/SharedElementsWithNavigationSnippets.kt b/compose/snippets/src/main/java/com/example/compose/snippets/animations/sharedelement/SharedElementsWithNavigationSnippets.kt index da1d178f..4ee0f09b 100644 --- a/compose/snippets/src/main/java/com/example/compose/snippets/animations/sharedelement/SharedElementsWithNavigationSnippets.kt +++ b/compose/snippets/src/main/java/com/example/compose/snippets/animations/sharedelement/SharedElementsWithNavigationSnippets.kt @@ -75,9 +75,9 @@ fun SharedElement_PredictiveBack() { ) { composable("home") { HomeScreen( - navController, this@SharedTransitionLayout, - this@composable + this@composable, + { navController.navigate("details/$it") } ) } composable( @@ -87,11 +87,13 @@ 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") + } ) } } @@ -99,19 +101,19 @@ fun SharedElement_PredictiveBack() { } @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( @@ -141,10 +143,10 @@ private fun DetailsScreen( } @Composable -private fun HomeScreen( - navController: NavHostController, +fun HomeScreen( sharedTransitionScope: SharedTransitionScope, - animatedContentScope: AnimatedContentScope + animatedContentScope: AnimatedContentScope, + onItemClick: (Int) -> Unit, ) { LazyColumn( modifier = Modifier @@ -155,7 +157,7 @@ private fun HomeScreen( itemsIndexed(listSnacks) { index, item -> Row( Modifier.clickable { - navController.navigate("details/$index") + onItemClick(index) } ) { Spacer(modifier = Modifier.width(8.dp)) From 66d7194889dde15ea2cff10cf02340cac12f6745 Mon Sep 17 00:00:00 2001 From: Rebecca Franks Date: Tue, 10 Dec 2024 11:18:25 +0000 Subject: [PATCH 2/8] Added seekable predictive back (redoing commit) --- .../sharedelement/CustomizeSharedElementsSnippets.kt | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/animations/sharedelement/CustomizeSharedElementsSnippets.kt b/compose/snippets/src/main/java/com/example/compose/snippets/animations/sharedelement/CustomizeSharedElementsSnippets.kt index 4bb9cbc9..efd3ecb2 100644 --- a/compose/snippets/src/main/java/com/example/compose/snippets/animations/sharedelement/CustomizeSharedElementsSnippets.kt +++ b/compose/snippets/src/main/java/com/example/compose/snippets/animations/sharedelement/CustomizeSharedElementsSnippets.kt @@ -14,9 +14,7 @@ * limitations under the License. */ -@file:OptIn(ExperimentalSharedTransitionApi::class, ExperimentalSharedTransitionApi::class, - ExperimentalSharedTransitionApi::class -) +@file:OptIn(ExperimentalSharedTransitionApi::class) package com.example.compose.snippets.animations.sharedelement From d779dfd91ce00e003f05a0a00ac31cfd4c647262 Mon Sep 17 00:00:00 2001 From: Rebecca Franks Date: Tue, 10 Dec 2024 11:23:46 +0000 Subject: [PATCH 3/8] spotless --- .../CustomizeSharedElementsSnippets.kt | 26 ++++++++++--------- 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/animations/sharedelement/CustomizeSharedElementsSnippets.kt b/compose/snippets/src/main/java/com/example/compose/snippets/animations/sharedelement/CustomizeSharedElementsSnippets.kt index efd3ecb2..45557e4f 100644 --- a/compose/snippets/src/main/java/com/example/compose/snippets/animations/sharedelement/CustomizeSharedElementsSnippets.kt +++ b/compose/snippets/src/main/java/com/example/compose/snippets/animations/sharedelement/CustomizeSharedElementsSnippets.kt @@ -272,10 +272,10 @@ private fun DetailsContent( // [END android_compose_shared_element_text_bounds_transform] Text( "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Curabitur sit amet lobortis velit. " + - "Lorem ipsum dolor sit amet, consectetur adipiscing elit." + - " Curabitur sagittis, lectus posuere imperdiet facilisis, nibh massa " + - "molestie est, quis dapibus orci ligula non magna. Pellentesque rhoncus " + - "hendrerit massa quis ultricies. Curabitur congue ullamcorper leo, at maximus", + "Lorem ipsum dolor sit amet, consectetur adipiscing elit." + + " Curabitur sagittis, lectus posuere imperdiet facilisis, nibh massa " + + "molestie est, quis dapibus orci ligula non magna. Pellentesque rhoncus " + + "hendrerit massa quis ultricies. Curabitur congue ullamcorper leo, at maximus", modifier = Modifier.skipToLookaheadSize() ) } @@ -332,7 +332,7 @@ private fun SharedElement_Clipping() { rememberSharedContentState(key = "title"), animatedVisibilityScope = this@AnimatedContent, - ) + ) ) } } else { @@ -370,10 +370,10 @@ private fun SharedElement_Clipping() { ) Text( "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Curabitur sit amet lobortis velit. " + - "Lorem ipsum dolor sit amet, consectetur adipiscing elit." + - " Curabitur sagittis, lectus posuere imperdiet facilisis, nibh massa " + - "molestie est, quis dapibus orci ligula non magna. Pellentesque rhoncus " + - "hendrerit massa quis ultricies. Curabitur congue ullamcorper leo, at maximus" + "Lorem ipsum dolor sit amet, consectetur adipiscing elit." + + " Curabitur sagittis, lectus posuere imperdiet facilisis, nibh massa " + + "molestie est, quis dapibus orci ligula non magna. Pellentesque rhoncus " + + "hendrerit massa quis ultricies. Curabitur congue ullamcorper leo, at maximus" ) } } @@ -673,17 +673,19 @@ fun CustomPredictiveBackHandle() { mutableIntStateOf(0) } Column { - Slider(modifier = Modifier.height(48.dp), + Slider( + modifier = Modifier.height(48.dp), value = seekableTransitionState.fraction, onValueChange = { coroutineScope.launch { - if (seekableTransitionState.currentState is Screen.Details){ + 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) { From f4c131af991af153a52f77790e66e05fb38bcf61 Mon Sep 17 00:00:00 2001 From: Rebecca Franks Date: Tue, 10 Dec 2024 11:36:02 +0000 Subject: [PATCH 4/8] Delete SeekableSharedElement.kt --- .../animations/sharedelement/SeekableSharedElement.kt | 8 -------- 1 file changed, 8 deletions(-) delete mode 100644 compose/snippets/src/main/java/com/example/compose/snippets/animations/sharedelement/SeekableSharedElement.kt diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/animations/sharedelement/SeekableSharedElement.kt b/compose/snippets/src/main/java/com/example/compose/snippets/animations/sharedelement/SeekableSharedElement.kt deleted file mode 100644 index 6588b504..00000000 --- a/compose/snippets/src/main/java/com/example/compose/snippets/animations/sharedelement/SeekableSharedElement.kt +++ /dev/null @@ -1,8 +0,0 @@ -package com.example.compose.snippets.animations.sharedelement - -import androidx.compose.animation.core.SeekableTransitionState -import androidx.compose.animation.core.rememberTransition -import androidx.compose.runtime.Composable -import androidx.compose.runtime.remember -import androidx.compose.ui.tooling.preview.Preview - From 2751e7b158ef3855fc16fe2eca72d4a4e9d496e7 Mon Sep 17 00:00:00 2001 From: riggaroo Date: Tue, 10 Dec 2024 11:37:43 +0000 Subject: [PATCH 5/8] Apply Spotless --- .../CustomizeSharedElementsSnippets.kt | 23 ++++++++++--------- .../SharedElementsWithNavigationSnippets.kt | 3 +-- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/animations/sharedelement/CustomizeSharedElementsSnippets.kt b/compose/snippets/src/main/java/com/example/compose/snippets/animations/sharedelement/CustomizeSharedElementsSnippets.kt index 45557e4f..6e47325c 100644 --- a/compose/snippets/src/main/java/com/example/compose/snippets/animations/sharedelement/CustomizeSharedElementsSnippets.kt +++ b/compose/snippets/src/main/java/com/example/compose/snippets/animations/sharedelement/CustomizeSharedElementsSnippets.kt @@ -91,8 +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 kotlinx.coroutines.launch import kotlin.coroutines.cancellation.CancellationException +import kotlinx.coroutines.launch @Preview @Composable @@ -272,10 +272,10 @@ private fun DetailsContent( // [END android_compose_shared_element_text_bounds_transform] Text( "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Curabitur sit amet lobortis velit. " + - "Lorem ipsum dolor sit amet, consectetur adipiscing elit." + - " Curabitur sagittis, lectus posuere imperdiet facilisis, nibh massa " + - "molestie est, quis dapibus orci ligula non magna. Pellentesque rhoncus " + - "hendrerit massa quis ultricies. Curabitur congue ullamcorper leo, at maximus", + "Lorem ipsum dolor sit amet, consectetur adipiscing elit." + + " Curabitur sagittis, lectus posuere imperdiet facilisis, nibh massa " + + "molestie est, quis dapibus orci ligula non magna. Pellentesque rhoncus " + + "hendrerit massa quis ultricies. Curabitur congue ullamcorper leo, at maximus", modifier = Modifier.skipToLookaheadSize() ) } @@ -332,7 +332,7 @@ private fun SharedElement_Clipping() { rememberSharedContentState(key = "title"), animatedVisibilityScope = this@AnimatedContent, - ) + ) ) } } else { @@ -370,10 +370,10 @@ private fun SharedElement_Clipping() { ) Text( "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Curabitur sit amet lobortis velit. " + - "Lorem ipsum dolor sit amet, consectetur adipiscing elit." + - " Curabitur sagittis, lectus posuere imperdiet facilisis, nibh massa " + - "molestie est, quis dapibus orci ligula non magna. Pellentesque rhoncus " + - "hendrerit massa quis ultricies. Curabitur congue ullamcorper leo, at maximus" + "Lorem ipsum dolor sit amet, consectetur adipiscing elit." + + " Curabitur sagittis, lectus posuere imperdiet facilisis, nibh massa " + + "molestie est, quis dapibus orci ligula non magna. Pellentesque rhoncus " + + "hendrerit massa quis ultricies. Curabitur congue ullamcorper leo, at maximus" ) } } @@ -685,7 +685,8 @@ fun CustomPredictiveBackHandle() { seekableTransitionState.seekTo(it, Screen.Details(lastNavigatedIndex)) } } - }) + } + ) SharedTransitionLayout(modifier = Modifier.weight(1f)) { transition.AnimatedContent { targetState -> when (targetState) { diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/animations/sharedelement/SharedElementsWithNavigationSnippets.kt b/compose/snippets/src/main/java/com/example/compose/snippets/animations/sharedelement/SharedElementsWithNavigationSnippets.kt index 4ee0f09b..cac68a58 100644 --- a/compose/snippets/src/main/java/com/example/compose/snippets/animations/sharedelement/SharedElementsWithNavigationSnippets.kt +++ b/compose/snippets/src/main/java/com/example/compose/snippets/animations/sharedelement/SharedElementsWithNavigationSnippets.kt @@ -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 @@ -77,7 +76,7 @@ fun SharedElement_PredictiveBack() { HomeScreen( this@SharedTransitionLayout, this@composable, - { navController.navigate("details/$it") } + { navController.navigate("details/$it") } ) } composable( From f4209974974cfbf4528dd8e954eaa9e49d5bcc82 Mon Sep 17 00:00:00 2001 From: Rebecca Franks Date: Tue, 10 Dec 2024 12:51:55 +0000 Subject: [PATCH 6/8] Add enableOnBackInvokedCallback --- compose/snippets/src/main/AndroidManifest.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/compose/snippets/src/main/AndroidManifest.xml b/compose/snippets/src/main/AndroidManifest.xml index c9b15e6c..4ec4d9b7 100644 --- a/compose/snippets/src/main/AndroidManifest.xml +++ b/compose/snippets/src/main/AndroidManifest.xml @@ -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"> Date: Tue, 10 Dec 2024 14:45:36 +0000 Subject: [PATCH 7/8] Add comments and fix custom seeking predictive back example. --- .../CustomizeSharedElementsSnippets.kt | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/animations/sharedelement/CustomizeSharedElementsSnippets.kt b/compose/snippets/src/main/java/com/example/compose/snippets/animations/sharedelement/CustomizeSharedElementsSnippets.kt index 6e47325c..c3668d5b 100644 --- a/compose/snippets/src/main/java/com/example/compose/snippets/animations/sharedelement/CustomizeSharedElementsSnippets.kt +++ b/compose/snippets/src/main/java/com/example/compose/snippets/animations/sharedelement/CustomizeSharedElementsSnippets.kt @@ -18,6 +18,7 @@ package com.example.compose.snippets.animations.sharedelement +import android.util.Log import androidx.activity.compose.PredictiveBackHandler import androidx.compose.animation.AnimatedContent import androidx.compose.animation.AnimatedVisibility @@ -653,19 +654,24 @@ fun CustomPredictiveBackHandle() { 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 -> - // code for progress + // For each backEvent that comes in, we manually seekTo the reported back progress try { seekableTransitionState.seekTo(backEvent.progress, targetState = Screen.Home) } catch (e: CancellationException) { - // ignore the cancellation + // seekTo may be cancelled as expected, if animateTo or subsequent seekTo calls + // before the current seekTo finishes, in this case, we ignore the cancellation. } } - // code for completion + // 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) { - // code for cancellation - seekableTransitionState.animateTo(seekableTransitionState.currentState) + // 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() From 23e1a93f5191ca977dedb3cb64b5b8958e5d5375 Mon Sep 17 00:00:00 2001 From: riggaroo Date: Tue, 10 Dec 2024 14:47:29 +0000 Subject: [PATCH 8/8] Apply Spotless --- .../animations/sharedelement/CustomizeSharedElementsSnippets.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/compose/snippets/src/main/java/com/example/compose/snippets/animations/sharedelement/CustomizeSharedElementsSnippets.kt b/compose/snippets/src/main/java/com/example/compose/snippets/animations/sharedelement/CustomizeSharedElementsSnippets.kt index c3668d5b..d599e158 100644 --- a/compose/snippets/src/main/java/com/example/compose/snippets/animations/sharedelement/CustomizeSharedElementsSnippets.kt +++ b/compose/snippets/src/main/java/com/example/compose/snippets/animations/sharedelement/CustomizeSharedElementsSnippets.kt @@ -18,7 +18,6 @@ package com.example.compose.snippets.animations.sharedelement -import android.util.Log import androidx.activity.compose.PredictiveBackHandler import androidx.compose.animation.AnimatedContent import androidx.compose.animation.AnimatedVisibility