Best Tutorial for Android Jetpack Compose

Android App Development

Stay ahead with the latest tools, trends, and best practices in Android development

Hero Animation in Jetpack Compose

Hero Animation in Jetpack Compose - Coding Bihar
Hero Animation in Jetpack Compose
In Jetpack Compose, Hero Animation (also known as Shared Element Transition) is not officially built-in like in Flutter, but you can create it manually using AnimatedContent, BoxWithConstraints, and MotionLayout or third-party libraries like Accompanist Navigation-Material.

So, what do we do?

We build it manually! ๐Ÿ’ช

In this article, you'll learn how to implement a Hero animation manually in Jetpack Compose, and make your UI feel delightful and polished. We'll walk through everything — from project setup to final animation — with clear code and plain-English explanations.

๐Ÿš€ What is Hero Animation?

Imagine you're in an e-commerce app. You tap a product from a grid and it smoothly expands into a full product detail screen. A Hero Animation, also known as a Shared Element Transition, is a technique used in UI design where an element transitions smoothly between two different screens, creating a visually connected experience for the user. The element (say, product image) is shared between the two screens and animated smoothly.

๐Ÿ› ️ Tools & Dependencies

Jetpack Compose doesn't provide shared element transitions yet, so we'll use basic animation APIs like:
  • animateDpAsState
  • animateFloatAsState
  • BoxWithConstraints
  • Navigation-Compose

Dependencies

Add this to your build.gradle file:
implementation ("androidx.navigation:navigation-compose:2.9.2")
Now, let’s build the animation!

✨ How to Create Hero Animation in Jetpack Compose

๐ŸŽจ Our Plan

We’ll build:
A List Screen showing a small red box (our “hero”).
When tapped, it navigates to a Detail Screen where the same red box expands and reveals more info — mimicking a Hero animation.

๐Ÿงฑ Step 1: Setup Navigation in Jetpack Compose

We’ll use NavHost to handle screen transitions.
@Composable
fun HeroApp() {
    val navController = rememberNavController()
    NavHost(navController = navController, startDestination = "list") {
        composable("list") { HeroListScreen(navController) }
        composable("detail") { HeroDetailScreen() }
    }
}
✅ Now we can switch between screens like a pro.

๐Ÿ–ผ️ Step 2: Hero List Screen (with the tappable item)

Here's where the animation begins — a small red box that the user taps to move to the next screen.

@Composable
fun HeroListScreen(navController: NavController) {
    Box(
        modifier = Modifier
            .fillMaxSize()
            .clickable { navController.navigate("detail") },
        contentAlignment = Alignment.Center
    ) {
        Box(
            modifier = Modifier
                .size(100.dp)
                .clip(RoundedCornerShape(20.dp))
                .background(Color.Red),
            contentAlignment = Alignment.Center
        ) {
            Text("Tap Me", color = Color.White)
        }
    }
}
๐Ÿง  Explanation:
We center a red box inside the screen. When tapped, it navigates to the "detail" screen using the navigation controller.

๐Ÿงฌ Step 3: Hero Detail Screen (Animated Version)

Now we make it feel like the red box has smoothly “grown” into its larger version.
@Composable
fun HeroDetailScreen() {
    var animateStart by remember { mutableStateOf(false) }

    val size by animateDpAsState(
        targetValue = if (animateStart) 250.dp else 100.dp,
        animationSpec = tween(600)
    )

    val corner by animateDpAsState(
        targetValue = if (animateStart) 32.dp else 20.dp,
        animationSpec = tween(600)
    )

    // Trigger animation after screen is drawn
    LaunchedEffect(Unit) {
        animateStart = true
    }

    Box(
        modifier = Modifier
            .fillMaxSize()
            .background(Color.Black),
        contentAlignment = Alignment.Center
    ) {
        Box(
            modifier = Modifier
                .size(size)
                .clip(RoundedCornerShape(corner))
                .background(Color.Red),
            contentAlignment = Alignment.Center
        ) {
            Text("Hero Detail", color = Color.White)
        }
    }
}

๐Ÿง  Explanation:

  • We use animateDpAsState to animate the box’s size and corner radius.
  • When the screen appears (LaunchedEffect(Unit)), the animation is triggered.
  • The red box expands and looks like it continued from the previous screen.
๐ŸŽ‰ This gives a smooth hero transition effect without needing shared elements!

๐ŸŒˆ Final Touch: Preview It

Make sure you call HeroApp() in your MainActivity:
class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        enableEdgeToEdge()
        setContent {
            CodingComposeTheme {
                HeroApp()

            }
        }
    }
}

OUTPUT:

๐Ÿ’ก Pro Tips

  • You can enhance this further by animating position, scale, opacity, or even using Accompanist Navigation-Animation for smoother screen transitions.
  • Wrap animations in a reusable composable for multiple shared elements.
  • ๐Ÿ‘‰ Try expanding this example — use it with images, profile cards, or product UIs. Your users will love the attention to detail.

๐Ÿงช Experimental: Shared Element Transition (Upcoming in Compose)

Jetpack Compose team is working on native Shared Element APIs, but currently you have to use workarounds.

Hero Animation from a Grid of Product Cards

✨ What We’ll Build:

  • A grid layout with multiple product cards (image + name).
  • Tap any product → Hero-like animation to detail screen.
  • Manual animation using animateDpAsState, Navigation, and shared product state.

๐Ÿงฑ Step 1: Product Data Model

data class Products(
    val id: Int,
    val name: String,
    val imageRes: Int
)

๐Ÿงฑ Step 2: Sample Product List

val sampleProducts = listOf(
    Products(1, "Red Sneakers", R.drawable.rice1),
    Products(2, "Blue T-Shirt", R.drawable.shirt1),
    Products(3, "Black Watch", R.drawable.watch1),
    Products(4, "Smartphone", R.drawable.phone11),
)

๐Ÿงฑ Step 3: Navigation Setup


@Composable
fun HeroGridApp() {
    val navController = rememberNavController()

    NavHost(navController = navController, startDestination = "list") {
        composable("list") {
            ProductGridScreen(navController)
        }
        composable(
            "detail/{productId}",
            arguments = listOf(navArgument("productId") { type = NavType.IntType })
        ) { backStackEntry ->
            val productId = backStackEntry.arguments?.getInt("productId")
            val product = sampleProducts.find { it.id == productId }
            product?.let {
                ProductGridDetailScreen(product = it)
            }
        }
    }
}

๐ŸŽจ Step 4: Grid Screen with Clickable Hero Cards


@Composable
fun ProductGridScreen(navController: NavController) {
    LazyVerticalGrid(
        columns = GridCells.Fixed(2),
        contentPadding = PaddingValues(16.dp),
        modifier = Modifier.fillMaxSize()
    ) {
        items(sampleProducts) { product ->
            Column(
                modifier = Modifier
                    .padding(8.dp)
                    .clickable {
                        navController.navigate("detail/${product.id}")
                    },
                horizontalAlignment = Alignment.CenterHorizontally
            ) {
                Image(
                    painter = painterResource(product.imageRes),
                    contentDescription = null,
                    modifier = Modifier
                        .size(120.dp)
                        .clip(RoundedCornerShape(16.dp))
                )
                Spacer(Modifier.height(8.dp))
                Text(product.name, fontSize = 14.sp, maxLines = 1)
            }
        }
    }
}

๐Ÿงฌ Step 5: Detail Screen with Animated Hero Element


@Composable
fun ProductGridDetailScreen(product: Products) {
    var animate by remember { mutableStateOf(false) }

    val size by animateDpAsState(
        targetValue = if (animate) 250.dp else 120.dp,
        animationSpec = tween(600)
    )
    val textSize by animateFloatAsState(
        targetValue = if (animate) 24f else 16f,
        animationSpec = tween(600)
    )

    LaunchedEffect(Unit) {
        animate = true
    }

    Column(
        modifier = Modifier
            .fillMaxSize()
            .background(Color.White),
        horizontalAlignment = Alignment.CenterHorizontally,
        verticalArrangement = Arrangement.Center
    ) {
        Image(
            painter = painterResource(product.imageRes),
            contentDescription = null,
            modifier = Modifier
                .size(size)
                .clip(RoundedCornerShape(24.dp))
        )
        Spacer(Modifier.height(16.dp))
        Text(product.name, fontSize = textSize.sp, fontWeight = FontWeight.Bold)
        Spacer(Modifier.height(8.dp))
        Text("Detailed description goes here.", textAlign = TextAlign.Center)
    }
}

MainActivity

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        enableEdgeToEdge()
        setContent {
            CodingComposeTheme {

                HeroGridApp()
                
            }
        }
    }
}

๐Ÿ“ฑ How it Works:

  • Tapping an image navigates to a detail screen with the same image.
  • The detail screen animates the image size and text — simulating a shared transition.
  • It gives the illusion of continuity, like the image “moved” to the next screen.

OUTPUT:



Special Message

Welcome to Coding