AnimatableCompose
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
How it looks
Phone Number | Card Dealer |
---|---|
Phone Number
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
)
}
}
Card Dealer
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 |
---|---|
Insta Story
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(
//
)
}
}
Info Card
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(
//
)
}
}
How to use
You can learn to use it step by step, you need to use state and components together.
AnimatableText
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
)
}
AnimatableBox
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
)
}
AnimatableCard
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
) {}
}
AnimatableCardWithText
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
)
}
}
}
Setup
- 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'
}
✔️
Todo - SharedAnimationSpec
✔️