Shared Element Transition in Jetpack Compose: A Guide

Artem explains how to animate your app using **Shared Element Transition**. This feature allows UI elements to animate as they transition between app screens. To demonstrate, I’ve created a simple app with just two screens: a list on the first screen and detailed information on the second.


This content originally appeared on HackerNoon and was authored by Android Insights

\ Hello! My name is Artem, and I develop Android applications.

Recently, I started sharing my experiences on this topic.

\ This article is a text version of a video on my YouTube channel Android Insights.

\ In this article, I want to explain how to animate your app using Shared Element Transition. This feature allows UI elements to animate as they transition between app screens, making the user interface more memorable.

\ This feature was recently added, so ensure your project uses Jetpack Compose version 1.7.0-alpha07 or higher.

\ To demonstrate, I've created a simple app with just two screens: a list on the first screen and detailed information on the second.

\ Basic Application

\ \ For simplicity, I'm not using any navigation libraries. Here's the code for the root @Composable function:

@Composable
fun CatsContent(modifier: Modifier) {
   var selectedCat: Cat? by remember { mutableStateOf(null) }

   if (selectedCat != null) {
       BackHandler { selectedCat = null }

       CatDetails(
           modifier = modifier.fillMaxSize(),
           cat = selectedCat!!,
       )
   } else {
       CatsList(
           modifier = modifier.fillMaxSize(),
           onCatClicked = { cat ->
               selectedCat = cat
           }
       )
   }
}

\ When an item in the list is clicked, it is assigned to the selectedCat variable. If selectedCat is not null, the @Composable with the detailed information is displayed.

\ Here's the code for CatDetails and CatsList functions:

@Composable
fun CatDetails(modifier: Modifier, cat: Cat) {
   Column(
       modifier = modifier.verticalScroll(state = rememberScrollState()),
       verticalArrangement = Arrangement.spacedBy(8.dp),
   ) {
       Image(
           painter = painterResource(cat.iconRes),
           contentDescription = null,
       )

       Text(
           modifier = Modifier.padding(horizontal = 8.dp),
           text = stringResource(cat.textRes)
       )
   }
}

\

@Composable
fun CatsList(modifier: Modifier, onCatClicked: (Cat) -> Unit) {
   val cats = rememberCatsList()

   Box(modifier = modifier) {
       LazyColumn(verticalArrangement = Arrangement.spacedBy(8.dp)) {
           items(items = cats, key = { cat -> cat.iconRes }) { item ->
               Cat(
                   modifier = Modifier
                       .fillParentMaxWidth()
                       .clickable { onCatClicked(item) }
                       .padding(horizontal = 8.dp),
                   cat = item,
               )
           }
       }
   }
}

@Composable
fun Cat(modifier: Modifier, cat: Cat) {
   Row(
       modifier = modifier,
       horizontalArrangement = Arrangement.spacedBy(8.dp),
       verticalAlignment = Alignment.CenterVertically,
   ) {
       Image(
           modifier = Modifier.size(128.dp),
           painter = painterResource(cat.iconRes),
           contentScale = ContentScale.Crop,
           contentDescription = null,
       )

       Text(
           modifier = Modifier,
           text = stringResource(cat.textRes),
           maxLines = 3,
           overflow = TextOverflow.Ellipsis,
       )
   }
}

\

Adding Transition

Now let's add the rest of the required code in two steps

\

// OptIn is required because ExperimentalSharedTransitionApi is unstable
@OptIn(ExperimentalSharedTransitionApi::class)
@Composable
fun CatsContent(modifier: Modifier) {
   var showCatDetails by remember { mutableStateOf(false) }
   var selectedCat: Cat? by remember { mutableStateOf(null) }

   SharedTransitionLayout {
       AnimatedContent(targetState = showCatDetails) { targetState ->
           if (targetState) {
               BackHandler { showCatDetails = false }

               CatDetails(
                   modifier = modifier.fillMaxSize(),
                   cat = selectedCat!!,
                   // passing sharedTransitionScope for future purposes
                   sharedTransitionScope = this@SharedTransitionLayout,
                   // passing animatedVisibilityScope for future purposes
                   animatedVisibilityScope = this,
               )
           } else {
               CatsList(
                   modifier = modifier.fillMaxSize(),
                   // passing sharedTransitionScope for future purposes
                   sharedTransitionScope = this@SharedTransitionLayout,
                   // passing animatedVisibilityScope for future purposes
                   animatedVisibilityScope = this,
                   onCatClicked = { cat ->
                       selectedCat = cat
                       showCatDetails = true
                   }
               )
           }
       }
   }
}

\ Application behavior has already changed

\ Animated transition between screens

\ There are animations for transitioning between screens using the AnimatedContent function, but this is still not what we need.

It's time to add the rest of the code.

\

@OptIn(ExperimentalSharedTransitionApi::class)
@Composable
fun CatsList(
   modifier: Modifier,
   onCatClicked: (Cat) -> Unit,
   sharedTransitionScope: SharedTransitionScope,
   animatedVisibilityScope: AnimatedVisibilityScope,
) {
   val cats = rememberCatsList()

   Box(
       modifier = modifier,
   ) {
       LazyColumn(
           verticalArrangement = Arrangement.spacedBy(8.dp),
       ) {
           items(
               items = cats,
               key = { cat -> cat.iconRes },
               contentType = { cat -> cat::class }
           ) { item ->
               Cat(
                   modifier = Modifier
                       .fillParentMaxWidth()
                       .clickable {
                           onCatClicked(item)
                       }
                       .padding(
                           horizontal = 8.dp
                       ),
                   cat = item,
                   // passing SharedTransitionScope
                   sharedTransitionScope = sharedTransitionScope,
                   // passing AnimatedVisibilityScope
                   animatedVisibilityScope = animatedVisibilityScope,
               )
           }
       }
   }
}

@OptIn(ExperimentalSharedTransitionApi::class)
@Composable
fun Cat(
   modifier: Modifier,
   cat: Cat,
   sharedTransitionScope: SharedTransitionScope,
   animatedVisibilityScope: AnimatedVisibilityScope,
) {
   with(sharedTransitionScope) {
       Row(
           modifier = modifier,
           horizontalArrangement = Arrangement.spacedBy(8.dp),
           verticalAlignment = Alignment.CenterVertically,
       ) {
           Image(
               modifier = Modifier
                   .size(128.dp)
                   // using the new Modifier sharedElement
                   .sharedElement(
                       state = rememberSharedContentState(
                         key = cat.iconRes.toString()
                       ),
                       animatedVisibilityScope = animatedVisibilityScope,
                   ),
               painter = painterResource(cat.iconRes),
               contentScale = ContentScale.Crop,
               contentDescription = null,
           )


           Text(
               modifier = Modifier,
               text = stringResource(cat.textRes),
               maxLines = 3,
               overflow = TextOverflow.Ellipsis,
           )
       }
   }
}

\

@OptIn(ExperimentalSharedTransitionApi::class)
@Composable
fun CatDetails(
    modifier: Modifier,
    cat: Cat,
    sharedTransitionScope: SharedTransitionScope,
    animatedVisibilityScope: AnimatedVisibilityScope,
) {
    with(sharedTransitionScope) {
        Column(
            modifier = modifier.verticalScroll(
                state = rememberScrollState(),
            ),
            verticalArrangement = Arrangement.spacedBy(8.dp),
        ) {
            Image(
                // using the new Modifier sharedElement
                modifier = Modifier.sharedElement(
                    state = rememberSharedContentState(key = cat.iconRes.toString()),
                    animatedVisibilityScope = animatedVisibilityScope,
                ),
                painter = painterResource(cat.iconRes),
                contentDescription = null,
            )

            Text(
                modifier = Modifier
                    .padding(
                        horizontal = 8.dp,
                    ),
                text = stringResource(cat.textRes)
            )
        }
    }
}

\ This time everything looks as it was intended, Image moves animatedly from one screen to another

\

\ \ What changed in the code? The use of Modifier has been added:

\

Modifier.sharedElement

\ This Modifier, along with the SharedTransitionLayout, is responsible for the magic of moving shared content.

In order to be able to use it, you must be inside the SharedTransitionScope. This can be achieved in several ways:

// use the function with/run/apply etc.
with(sharedTransitionScope)

// create an extension to SharedTransitionScope
fun SharedTransitionScope.Cat

// use context receivers, in which case you can move the 
// AnimatedVisibilityScope there so as not to pass it as a parameter
context(SharedTransitionScope, AnimatedVisibilityScope)
@OptIn(ExperimentalSharedTransitionApi::class)
@Composable
fun Cat

\ This is the minimum set of parameters that need to be passed to sharedElement for it to work correctly

sharedElement(
   state = rememberSharedContentState(
     key = cat.iconRes.toString()
   ),
   animatedVisibilityScope = animatedVisibilityScope,
)

\ state - to track the state of the animation. If there are several elements on the screen that can be animated, then it is imperative to use a unique key for each of the @Composable.

animatedVisibilityScope - scope for the transition animation itself

\ You also need to use Modifier.sharedElement on the screen you are navigating to. The key on the target screen must match where the transition started, otherwise nothing will work.

\ Also, since Shared Element Transition has just appeared in Jetpack Compose, everything is in an experimental state and an annotation needs to be added

@OptIn(ExperimentalSharedTransitionApi::class)

for each function that uses this functionality. Basically, that's all you need to know to get started using Shared Element Transition. Let's take a quick recap

Conclusion

For using Shared Element Transition in your project:

  1. Ensure your Jetpack Compose version is 1.7.0-alpha08 or higher.
  2. Wrap your @Composable functions in SharedTransitionLayout.
  3. Ensure the keys passed in rememberSharedContentState are unique for each @Composable to animate and match the keys in the destination.

\ Thanks to all. I hope you found this information helpful!


This content originally appeared on HackerNoon and was authored by Android Insights


Print Share Comment Cite Upload Translate Updates
APA

Android Insights | Sciencx (2024-07-13T15:00:14+00:00) Shared Element Transition in Jetpack Compose: A Guide. Retrieved from https://www.scien.cx/2024/07/13/shared-element-transition-in-jetpack-compose-a-guide/

MLA
" » Shared Element Transition in Jetpack Compose: A Guide." Android Insights | Sciencx - Saturday July 13, 2024, https://www.scien.cx/2024/07/13/shared-element-transition-in-jetpack-compose-a-guide/
HARVARD
Android Insights | Sciencx Saturday July 13, 2024 » Shared Element Transition in Jetpack Compose: A Guide., viewed ,<https://www.scien.cx/2024/07/13/shared-element-transition-in-jetpack-compose-a-guide/>
VANCOUVER
Android Insights | Sciencx - » Shared Element Transition in Jetpack Compose: A Guide. [Internet]. [Accessed ]. Available from: https://www.scien.cx/2024/07/13/shared-element-transition-in-jetpack-compose-a-guide/
CHICAGO
" » Shared Element Transition in Jetpack Compose: A Guide." Android Insights | Sciencx - Accessed . https://www.scien.cx/2024/07/13/shared-element-transition-in-jetpack-compose-a-guide/
IEEE
" » Shared Element Transition in Jetpack Compose: A Guide." Android Insights | Sciencx [Online]. Available: https://www.scien.cx/2024/07/13/shared-element-transition-in-jetpack-compose-a-guide/. [Accessed: ]
rf:citation
» Shared Element Transition in Jetpack Compose: A Guide | Android Insights | Sciencx | https://www.scien.cx/2024/07/13/shared-element-transition-in-jetpack-compose-a-guide/ |

Please log in to upload a file.




There are no updates yet.
Click the Upload button above to add an update.

You must be logged in to translate posts. Please log in or register.