Add Animatable Material Components in Android Jetpack Compose.
Create jetpack compose animation painless.
What you can create from Material 3 components right now;
- Spacer Animation
- Text Animation
- Box Animation
- Card Animation
- Icon Animation
- LazyRow Animation
- and combinations
| Phone Number | Card Dealer |
|---|---|
![]() | ![]() |
States
//Create components state val animatableCardState = rememberAnimatableCardState( initialSize = DpSize(80.dp, 80.dp), targetSize = DpSize(Dp.Infinity, 120.dp), toTargetSizeAnimationSpec = tween(500, 500), // specify delay(500) for target initialShape = RoundedCornerShape(32.dp), targetShape = RoundedCornerShape(0.dp), toTargetShapeAnimationSpec = tween(500, 500), initialOffset = DpOffset(0.dp, 0.dp), targetOffset = DpOffset(0.dp, - Dp.Infinity), toInitialOffsetAnimationSpec = tween(500, 500), ) val animatableIconState = rememberAnimatableIconState( initialSize = DpSize(40.dp, 40.dp), targetSize = DpSize(80.dp, 80.dp), toTargetSizeAnimationSpec = tween(500,500), initialOffset = DpOffset(0.dp, 0.dp), targetOffset = DpOffset((-50).dp, 0.dp), toTargetOffsetAnimationSpec = tween(500, 500) ) val animatableTextState = rememberAnimatableTextState( initialFontSize = 0.sp, targetFontSize = 26.sp, toTargetFontSizeAnimationSpec = tween(500, 500), initialOffset = DpOffset(0.dp, 0.dp), targetOffset = DpOffset((-25).dp, 0.dp), toTargetOffsetAnimationSpec = tween(500, 500) ) // Create shared state val sharedAnimatableState = rememberSharedAnimatableState( listOf( animatableCardState, animatableIconState, // default index = 0 animatableIconState.copy( // create state with copy func. for same params index = 1, // specify index for same components initialSize = DpSize(0.dp, 0.dp), targetSize = DpSize(36.dp, 36.dp), targetOffset = DpOffset(40.dp, 0.dp), ), animatableTextState, // default index = 0 animatableTextState.copy( index = 1, // specify index for same components targetFontSize = 12.sp ) ) )Components
AnimatableCard( onClick = { sharedAnimatableState.animate() }, state = sharedAnimatableState // pass shared state ) { Row( modifier = Modifier.fillMaxSize(), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.Center ) { AnimatableIcon( imageVector = Icons.Default.Person, contentDescription = null, state = sharedAnimatableState // pass shared state ) Column { AnimatableText( text = "Emir Demirli", state = sharedAnimatableState // pass shared state ) AnimatableText( text = "+90 0535 508 55 52", state = sharedAnimatableState, // pass shared state stateIndex = 1 // specify index for same components ) } AnimatableIcon( imageVector = Icons.Default.Phone, contentDescription = null, state = sharedAnimatableState, // pass shared state stateIndex = 1 // specify index for same components ) } }States
val cards by remember { mutableStateOf(listOf("A","K","Q","J","10","9","8","7","6","5","4","3","2")) } var deck by remember { mutableStateOf(cards + cards + cards + cards) } val animatableCardState = rememberAnimatableCardState( initialSize = DpSize(64.dp, 64.dp), targetSize = DpSize(64.dp, 64.dp), initialOffset = DpOffset(0.dp, 120.dp), targetOffset = DpOffset(-Dp.Infinity, -Dp.Infinity) ) val animatableTextState = rememberAnimatableTextState( initialFontSize = 0.sp, targetFontSize = 24.sp ) val cardStates = mutableListOf<AnimatableState>() val textStates = mutableListOf<AnimatableState>() deck.indices.forEach { cardStates.add( animatableCardState.copy( index = it, toTargetOffsetAnimationSpec = tween(400, (it * 400)), targetOffset = DpOffset(if(it % 2 == 0) (-100).dp else 100.dp, (-150).dp) ) ) textStates.add( animatableTextState.copy( index = it, toTargetFontSizeAnimationSpec = tween(400, (it * 400)) ) ) } val sharedAnimatableState = rememberSharedAnimatableState(cardStates + textStates)Components
Box( modifier = Modifier .fillMaxSize() .clickable { deck = deck.shuffled() sharedAnimatableState.animate() }, contentAlignment = Alignment.Center ) { deck.indices.forEach { AnimatableCard( onClick = {}, state = sharedAnimatableState, stateIndex = it, fixedShape = RoundedCornerShape(16.dp) ) { Box(Modifier.fillMaxSize(), Alignment.Center) { AnimatableText( text = deck[it], state = sharedAnimatableState, stateIndex = it ) } } } }| Insta Story | Info Card |
|---|---|
![]() | ![]() |
States
val lazyListState = rememberLazyListState() val scope = rememberCoroutineScope() var selectedIndex by remember { mutableStateOf(0) } val stories by remember { mutableStateOf(Story.stories) } val animatableCardState = rememberAnimatableCardState( initialSize = DpSize(width = 70.dp, height = 70.dp), targetSize = DpSize(width = Dp.Infinity, height = Dp.Infinity), initialShape = CircleShape, targetShape = RoundedCornerShape(0.dp), initialPadding = PaddingValues(4.dp, 8.dp), targetPadding = PaddingValues(0.dp), initialBorder = BorderStroke(2.dp, Brush.verticalGradient(listOf(Color.Red, Color.Yellow))), targetBorder = BorderStroke(0.dp, Color.Unspecified) ) val cardStates = mutableListOf<AnimatableState>() stories.indices.forEach { index -> cardStates.add( animatableCardState.copy( index = index, onAnimation = { when(it) { AnimationState.INITIAL -> {} AnimationState.INITIAL_TO_TARGET -> { scope.launch { delay(150) lazyListState.animateScrollToItem(selectedIndex) } } AnimationState.TARGET -> {} AnimationState.TARGET_TO_INITIAL -> {} } }, toTargetAnimationSpec = tween(250) ) ) } val sharedAnimatableState = rememberSharedAnimatableState(cardStates)Components
Box( modifier = Modifier.fillMaxSize(), ) { LazyRow( state = lazyListState ) { items(stories.size) { index -> AnimatableCard( modifier = Modifier .size(100.dp), onClick = { selectedIndex = index cardStates[index].animate() }, state = sharedAnimatableState, stateIndex = index ) { AsyncImage( model = stories[index].url, contentDescription = null, contentScale = ContentScale.Crop, modifier = Modifier.fillMaxSize() ) } } } }Data
data class Story( val url: String ) { companion object { val stories = listOf( // ) } }States
val lazyListState = rememberLazyListState() val snapperFlingBehavior = rememberSnapperFlingBehavior( lazyListState = lazyListState, snapOffsetForItem = SnapOffsets.Start, ) val scope = rememberCoroutineScope() var selectedIndex by remember { mutableStateOf(0) } val animatableCardState = rememberAnimatableCardState( initialSize = DpSize(width = 340.dp, height = 180.dp), targetSize = DpSize(width = Dp.Infinity, height = 340.dp), initialShape = RoundedCornerShape(32.dp), targetShape = RoundedCornerShape(0.dp, 0.dp, 32.dp, 32.dp), toTargetShapeAnimationSpec = tween(750), initialPadding = PaddingValues(horizontal = 8.dp), targetPadding = PaddingValues(0.dp), onAnimation = { when(it) { AnimationState.INITIAL -> {} AnimationState.INITIAL_TO_TARGET -> { scope.launch { delay(500) lazyListState.animateScrollToItem(selectedIndex) } } AnimationState.TARGET -> {} AnimationState.TARGET_TO_INITIAL -> {} } } ) val animatableBoxState = rememberAnimatableBoxState( initialAlignment = Alignment.Center, targetAlignment = Alignment.TopCenter ) val animatableTextState = rememberAnimatableTextState( initialFontSize = 0.sp, targetFontSize = 12.sp, initialOffset = DpOffset(x = 0.dp, y = 300.dp), targetOffset = DpOffset(x = 0.dp, y = 0.dp), toTargetAnimationSpec = tween(250) ) val animatableSpacerState = rememberAnimatableSpacerState( initialSize = DpSize(width = 0.dp, height = 0.dp), targetSize = DpSize(width = 0.dp, height = 16.dp) ) val infoCards by remember { mutableStateOf(InfoCard.infoCards) } val cardStates = mutableListOf<AnimatableState>() val boxStates = mutableListOf<AnimatableState>() val textStates = mutableListOf<AnimatableState>() val spacerStates = mutableListOf<AnimatableState>() infoCards.indices.forEach { index -> cardStates.add( animatableCardState.copy( index = index ) ) boxStates.add( animatableBoxState.copy( index = index ) ) textStates.add( animatableTextState.copy( index = index ) ) if(index == 0) { spacerStates.add( animatableSpacerState.copy( index = index, initialSize = DpSize(width = 0.dp, height = 300.dp), targetSize = DpSize(width = 0.dp, height = 0.dp) ) ) } spacerStates.add( animatableSpacerState.copy( index = index + 1, ) ) } val sharedAnimatableState = rememberSharedAnimatableState( animatableStates = cardStates + boxStates + textStates + spacerStates )Components
Column( modifier = Modifier.fillMaxSize(), ) { AnimatableSpacer( state = sharedAnimatableState ) LazyRow( verticalAlignment = Alignment.CenterVertically, state = lazyListState, flingBehavior = snapperFlingBehavior ) { items(infoCards.size) { index -> AnimatableCard( onClick = { selectedIndex = index sharedAnimatableState.animate() }, state = sharedAnimatableState, stateIndex = index, colors = CardDefaults.cardColors( containerColor = Color(0xFFE9E7FE) ) ) { Row( modifier = Modifier.fillMaxSize(), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.SpaceBetween ) { AnimatableBox( modifier = Modifier .weight(1f) .fillMaxHeight() .padding(16.dp), stateIndex = index, state = sharedAnimatableState ) { LazyColumn(horizontalAlignment = Alignment.CenterHorizontally) { item { Text( text = infoCards[index].title, fontSize = 22.sp, fontWeight = FontWeight.Bold ) Text( modifier = Modifier.align(Alignment.CenterStart), text = "MGS 1", fontSize = 12.sp, color = Color.Gray ) AnimatableSpacer( stateIndex = index + 1, state = sharedAnimatableState ) AnimatableText( text = infoCards[index].info, stateIndex = index, state = sharedAnimatableState, fontWeight = FontWeight.Bold ) } } } AsyncImage( modifier = Modifier .weight(1f) .padding(8.dp) .clip(RoundedCornerShape(32.dp)), model = infoCards[index].imageUrl, contentDescription = null, contentScale = ContentScale.Crop ) } } } } }Data
data class InfoCard( val imageUrl: String, val title: String, val info: String ){ companion object { val infoCards = listOf( // ) } }You can learn to use it step by step, you need to use state and components together.
State
// Simply create state and pass it to AnimatableText val state = rememberAnimatableTextState( initialFontSize = 12.sp, targetFontSize = 60.sp )Component
Column( modifier = Modifier .fillMaxSize() .clickable { state.animate() // animate }, verticalArrangement = Arrangement.Center, horizontalAlignment = Alignment.CenterHorizontally ) { AnimatableText( text = "Animatable", state = state // pass state ) AnimatableText( text = "Compose", state = state // pass state ) }State
// Simply create box state and pass it to AnimatableBox val state = rememberAnimatableBoxState( initialSize = DpSize(60.dp, 60.dp), // set initial size targetSize = DpSize(Dp.Infinity, 120.dp), // set target size initialOffset = DpOffset(x = 0.dp, y = 0.dp), // set initial offset targetOffset = DpOffset(x = 0.dp, y = - Dp.Infinity) // set target offset // Dp.Infinity will take the maximum value according to the screen size, // ps: Dp.Infinity for offset needs centered component and sizes, however you may not use it if you want initialAlignment = Alignment.Center, // set initial alignment targetAlignment = Alignment.TopStart // set target alignment )Component
AnimatableBox( modifier = Modifier .border(1.dp, Color.Red) .clickable { state.animate() }, state = state ) { Icon( modifier = Modifier.padding(8.dp), imageVector = Icons.Default.Add, contentDescription = null ) }State
// Simply create card state and pass it to AnimatableCard val animatableCardState = rememberAnimatableCardState( initialSize = DpSize(width = 70.dp, height = 70.dp), targetSize = DpSize(width = 200.dp, height = 70.dp), initialShape = CircleShape, targetShape = RoundedCornerShape(0.dp, 0.dp, 24.dp, 0.dp), initialOffset = DpOffset(x = 0.dp, y = 0.dp), targetOffset = DpOffset(x = - Dp.Infinity, y = - Dp.Infinity) )Component
Box( modifier = Modifier .fillMaxSize() .clickable { animatableCardState.animateToInitial() // animate to initial }, contentAlignment = Alignment.Center ) { AnimatableCard( modifier = Modifier.size(100.dp), onClick = { animatableCardState.animateToTarget() // animate to target }, state = animatableCardState ) {} }States
// Simply create card state and text state val animatableCardState = rememberAnimatableCardState( initialSize = DpSize(width = 50.dp, height = 25.dp), targetSize = DpSize(width = 300.dp, height = 150.dp), initialShape = CircleShape, targetShape = RoundedCornerShape(16.dp) ) val animatableTextState = rememberAnimatableTextState( initialFontSize = 4.sp, targetFontSize = 36.sp ) // Merge the states you created into sharedState and pass it to AnimatableCard and AnimatableText val sharedAnimatableState = rememberSharedAnimatableState( animatableStates = listOf( animatableCardState, animatableTextState ), toTargetAnimationSpec = infiniteRepeatable(tween(1000), RepeatMode.Reverse) //specify shared animation spec )Components
Box( modifier = Modifier .fillMaxSize() .clickable { sharedAnimatableState.animate() }, contentAlignment = Alignment.Center ) { AnimatableCard( modifier = Modifier.size(100.dp), state = sharedAnimatableState // pass shared state ) { Box(Modifier.fillMaxSize(), Alignment.Center) { AnimatableText( text = "Animatable", state = sharedAnimatableState // pass shared state ) } } }- Open the file
settings.gradle(it looks like that)
dependencyResolutionManagement { repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) repositories { google() mavenCentral() // add jitpack here ππ½ maven { url 'https://jitpack.io' } ... } } ...- Sync the project
- Add dependencies
dependencies { implementation 'com.github.commandiron:AnimatableCompose:1.0.5' }- SharedAnimationSpec βοΈ







