Google I/O 2024: Shared Element Transitions in Jetpack Compose

Google I/O 2024 introduced so many exciting new technologies, especially Gemini AI and Jetpack Compose. Though Jetpack Compose is awesome and improving rapidly to catch up with the legacy XML-based layouts (which have been out there for ages), it fell short in some areas, such as animations.

Shared Element Transitions are among the most popular APIs from the Android Animation Framework, which wasn’t available in the Jetpack Compose until now. That’s right – Google introduced shared element transitions for Jetpack Compose at their 2024 I/O event!

This long-awaited feature helps you create beautiful, fluid animations when navigating between screens in your app. Imagine a user tapping an image in a list, and it smoothly expands and animates into the detailed view. Shared element transitions in Compose provide a declarative way to achieve this effect, giving you more control over the animation process than the traditional View system. This empowers developers to design seamless user experiences that enhance their apps’ overall look and feel.

Here are some of the key capabilities of Shared Element Transitions in Jetpack Compose introduced at Google I/O 2024:

  • Declarative Animation: Shared element transitions are defined declaratively using modifiers like Modifier.sharedElement and Modifier.sharedBoundsMatchingContentSize. This animation process is much simpler compared to the imperative approach required in the View system.
  • Finer Control: Compose provides more granular control over the animation compared to traditional methods. You can define the specific element to animate, its transition bounds, and even the animation type.
  • Seamless Integration With Navigation: Shared element transitions work smoothly with Navigation Compose. When navigating between screens, you can pass the element’s key as an argument, and Compose automatically matches elements and creates the animation.

Getting Started

To take advantage of the latest APIs, make sure you’re using the latest Android Studio Jellyfish | 2023.3.1 and API Level 34.

Click the Download Materials button at the top or bottom of this tutorial. Unzip the ComposeTransitionAnimation.zip folder.

Now, launch Android Studio and open ComposeTransitionAnimation-Starter to import the starter project. The ComposeTransitionAnimation-Starter project contains the necessary boilerplates and Composables to jump straight into the animation!

ComposeTransitionAnimation-Starter resembles an e-commerce app with a basic List-Detail layout.

Build and run the app – it’ll look like this:

In this article, you’ll create a visual connection between elements on List and Detail screens using Shared Element Transition.

First, add the latest version of Compose dependencies. Open build.gradle in your app module and update:


def composeVersion = "1.7.0-beta01"

Tap Sync Now to download the dependencies.

Note: Shared element support is experimental and is in `beta`. The APIs may change in the future.

Overview of Key APIs

The latest dependencies introduced a few high-level APIs that do the heavy lifting of sharing elements between Composable layouts:

  • SharedTransitionLayout: The top-level layout required to implement shared element transitions. It provides a SharedTransitionScope. A Composable needs to be in SharedTransitionScope to use the modifiers of shared elements.
  • Modifier.sharedElement(): The modifier to flag one Composable to be matched with another Composable within the SharedTransitionScope.
  • Modifier.sharedBounds(): The modifier that tells the SharedTransitionScope to use this Composable’s bounds as the container bounds for where the transition should take place.

You’ll soon create a hero-animation using these APIs.

Implementing Shared Transition Animation

A Shared Transition Animation, or hero-animation, includes three major steps:

  1. Wrapping participating views with SharedTransitionLayout.
  2. Defining SharedTransitionScope to the source and destination views.
  3. Transition with Shared Element.

Adding SharedTransitionLayout

Open the MainActivity class. It contains ListScreen and DetailScreen, which will share elements during a transition animation. As mentioned earlier, you must wrap them with SharedTransitionLayout to make them eligible for a Shared Transition Animation.

Update the AnimatedContent block as follows:


SharedTransitionLayout {
  AnimatedContent(
    targetState = showDetails, 
    label = "shared_transition"
  ) { shouldShowDetails ->
    if (!shouldShowDetails) {
      ListScreen(
        // Existing code
        ... ... ...
      )
    } else {
      DetailScreen(
        // Existing code
        ... ... ...
       )
     }
  }
}

At this point, you may see this warning from Android Studio for using an experimental api:

To resolve this, add these imports on top of the MainActivity:


import androidx.compose.animation.ExperimentalSharedTransitionApi
import androidx.compose.animation.SharedTransitionLayout

Then add this annotation over the onCreate(savedInstanceState: Bundle?) method:


@OptIn(ExperimentalSharedTransitionApi::class)

Build and run.

Defining SharedTransitionScope

Up next, you need to define SharedTransitionScope to the views participating in the transition animation. The Composable needs to be within SharedTransitionScope to use Modifier.sharedElement() for the animation. Hence, you’ll need to pass down SharedTransitionScope from SharedTransitionLayout in MainActivity to the source and destination Composable executing the animation.

In this case, you’ll transition from the smaller Image Composable in the ListScreen (source) to the larger Composable in DetailScreen (destination).

Start with ListScreen.kt within ui package. Update the ListScreen function with these parameters:


@Composable
fun ListScreen(
  paddingValues: PaddingValues,
  items: List<Item>,
  onItemClicked: (Item) -> Unit = {},
  sharedTransitionScope: SharedTransitionScope,
  animatedVisibilityScope: AnimatedVisibilityScope,
)

Then pass the sharedTransitionScope and animatedVisibilityScope references for each ListItem:


items.forEach { item ->
  ListItem(
    item = item,
    onItemClicked = onItemClicked,
    sharedTransitionScope = sharedTransitionScope,
    animatedVisibilityScope = animatedVisibilityScope,
  )
}

Also, update th eListItem Composable method signature accordingly:


@Composable
fun ListItem(
  item: Item,
  onItemClicked: (Item) -> Unit = {},
  sharedTransitionScope: SharedTransitionScope,
  animatedVisibilityScope: AnimatedVisibilityScope,
)

You’ll see the warning for using an experimental api again from the compiler, along with the errors for the missing imports.

Fret not! Add these imports on top:


import androidx.compose.animation.AnimatedVisibilityScope
import androidx.compose.animation.ExperimentalSharedTransitionApi
import androidx.compose.animation.SharedTransitionScope

And the annotation for the ListScreen.kt file, above of the package name like this:


@file:OptIn(ExperimentalSharedTransitionApi::class)

package com.kodeco.android.composetransition.ui

That ensures you have all the necessary imports and will mute warnings for using experimental APIs for the scope of the ListScreen.kt file.

Note: Add the imports and annotation on DetailScreen.kt, too. You’ll need them shortly!

Your destination Composable is the DetailScreen method. Now add animation scopes as method parameters as follows:


@Composable
fun DetailScreen(
  item: Item, onBack: () -> Unit,
  sharedTransitionScope: SharedTransitionScope,
  animatedVisibilityScope: AnimatedVisibilityScope,
)

You’re ready to wire up ListScreen and DetailScreen to perform the transition animation.

Open MainActivity and update SharedTransitionLayout block to pass animatedVisibilityScope and sharedTransitionScope to its descendants:


SharedTransitionLayout {
  AnimatedContent(
    targetState = showDetails, 
    label = "shared_transition"
  ) { shouldShowDetails ->
    if (!shouldShowDetails) {
      ListScreen(
        paddingValues = paddingValues,
        items = items.value,
        onItemClicked = { item ->
          detailItem = item
          showDetails = !showDetails
        },
        animatedVisibilityScope = this@AnimatedContent,
        sharedTransitionScope = this@SharedTransitionLayout,
      )
    } else {
      DetailScreen(
        item = detailItem,
        onBack = { showDetails = !showDetails },
        animatedVisibilityScope = this@AnimatedContent,
        sharedTransitionScope = this@SharedTransitionLayout,
      )
    }
  }
}

Build and run again to ensure you resolved all compilation errors, but don’t expect the animation to happen yet!