Best Premium Templates For App and Software Downloading Site. Made By HIVE-Store

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