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:
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:
-
Declarative Animation: Shared element transitions are defined declaratively using modifiers like
Modifier.sharedElement
andModifier.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.
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 inSharedTransitionScope
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:
- Wrapping participating views with
SharedTransitionLayout
. - Defining
SharedTransitionScope
to the source and destination views. - 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!