먼저 기존 애니메이션을 분석해보자.
AOS 버전이랑 iOS 버전이 다르다.
AOS 버전
AOS 버전은 fill -> border, border -> fill 상관없이 하트가 약간 커졌다가 바운스되면서 다시 돌아오는 모션이 있다.
iOS 버전
border -> fill로 채워질 때는 아무런 모션이 없지만 fill -> border로 바뀔 때는 크기가 줄었다가 다시 커지는 모션이 있다.
val scaleAnimatable = remember { Animatable(1f) }
LaunchedEffect(isLiked) {
scaleAnimatable.snapTo(0f)
scaleAnimatable.animateTo(
targetValue = 1f,
animationSpec = spring(
dampingRatio = 0.4f,
stiffness = 400f
)
)
}
border -> fill, fill -> border 동일 애니메이션이기 때문에 isLiked의 값이 바뀔 때마다 animation이 시작되도록 적용했다.
val scaleAnimatable = remember { Animatable(1f) }
LaunchedEffect(isLiked) {
if (isLiked) {
scaleAnimatable.snapTo(1f)
} else {
scaleAnimatable.animateTo(
targetValue = 0.7f,
animationSpec = tween(
durationMillis = 50,
easing = FastOutSlowInEasing
)
)
scaleAnimatable.animateTo(
targetValue = 1f,
animationSpec = spring(
dampingRatio = 1.0f,
stiffness = 8000f
)
)
}
}
border -> fill로 바뀔 때는 아무 애니메이션이 없기 때문에 그대로 두고
fill -> border로 바뀔 때는 Animatable을 이용해서 0.7f 정도로 작아진 후 바로 1f로 커지는 방식으로 애니메이션을 적용했다.
Box(
modifier = modifier,
contentAlignment = Alignment.Center
) {
Image(
painter = painterResource(id = if (isLiked) R.drawable.favorite_fill else R.drawable.favorite_outline),
contentDescription = null,
colorFilter = ColorFilter.tint(color = if (isLiked) Color(0xFFFF0A2F) else Color.Black),
modifier = Modifier
.fillMaxSize()
.scale(scale)
.pointerInput(Unit) {
detectTapGestures(onTap = {
onClick()
})
}
)
}
isLiked의 값에 따라 하트 이미지가 바뀌고 scale에 애니메이션을 적용했다.
그리고 하트를 누를 때 클릭 영역이 약간 어두워지면서 클릭되는 모션을 없애기 위해 pointerInput을 사용했다.
실제 인스타그램 피드처럼 뷰를 구현하여서 하트를 누를 때마다 좋아요 숫자와 연동되도록 구현해봤다.
FeedAOSReaction(
isLiked = isLiked
) {
isLiked = !isLiked
heartCount = if (isLiked) heartCount + 1 else heartCount - 1
}
@Composable
fun FeedAOSReaction(isLiked: Boolean, onHearClick: () -> Unit) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 24.dp, vertical = 8.dp),
verticalAlignment = Alignment.CenterVertically
) {
InstagramAndroidLikeButton(
modifier = Modifier.size(40.dp),
isLiked = isLiked
) {
onHearClick()
}
}
}
@Composable
fun InstagramAndroidLikeButton(
modifier: Modifier = Modifier,
isLiked: Boolean,
onClick: () -> Unit,
) {
}
LikeButton의 onClick 메서드를 이용해서 피드에 onClick이 호출되었을 때 현재 isLiked 상태에 따라 heartCount를 조절해주었다.
@Composable
fun InstagramiOSLikeButton(
modifier: Modifier = Modifier,
isLiked: Boolean,
onClick: () -> Unit,
) {
val scaleAnimatable = remember { Animatable(1f) }
LaunchedEffect(isLiked) {
if (isLiked) {
scaleAnimatable.snapTo(1f)
} else {
scaleAnimatable.animateTo(
targetValue = 0.7f,
animationSpec = tween(
durationMillis = 50,
easing = FastOutSlowInEasing
)
)
scaleAnimatable.animateTo(
targetValue = 1f,
animationSpec = spring(
dampingRatio = 1.0f,
stiffness = 8000f
)
)
}
}
val scale by scaleAnimatable.asState()
Box(
modifier = modifier,
contentAlignment = Alignment.Center
) {
Image(
painter = painterResource(id = if (isLiked) R.drawable.favorite_fill else R.drawable.favorite_outline),
contentDescription = null,
colorFilter = ColorFilter.tint(color = if (isLiked) Color(0xFFFF0A2F) else Color.Black),
modifier = Modifier
.fillMaxSize()
.scale(scale)
.pointerInput(Unit) {
detectTapGestures(onTap = {
onClick()
})
}
)
}
}
@Composable
fun InstagramAndroidLikeButton(
modifier: Modifier = Modifier,
isLiked: Boolean,
onClick: () -> Unit,
) {
val scaleAnimatable = remember { Animatable(1f) }
LaunchedEffect(isLiked) {
scaleAnimatable.snapTo(0f)
scaleAnimatable.animateTo(
targetValue = 1f,
animationSpec = spring(
dampingRatio = 0.4f,
stiffness = 400f,
)
)
}
val scale by scaleAnimatable.asState()
Box(
modifier = modifier,
contentAlignment = Alignment.Center
) {
Image(
painter = painterResource(id = if (isLiked) R.drawable.favorite_fill else R.drawable.favorite_outline),
contentDescription = null,
colorFilter = ColorFilter.tint(color = if (isLiked) Color(0xFFFF0A2F) else Color.Black),
modifier = Modifier
.fillMaxSize()
.scale(scale)
.pointerInput(Unit) {
detectTapGestures(onTap = {
onClick()
})
}
)
}
}
@Composable
fun InstagramiOSLikeButton(
modifier: Modifier = Modifier,
isLiked: Boolean,
onClick: () -> Unit,
) {
val scaleAnimatable = remember { Animatable(1f) }
LaunchedEffect(isLiked) {
if (isLiked) {
scaleAnimatable.snapTo(1f)
} else {
scaleAnimatable.animateTo(
targetValue = 0.7f,
animationSpec = tween(
durationMillis = 50,
easing = FastOutSlowInEasing
)
)
scaleAnimatable.animateTo(
targetValue = 1f,
animationSpec = spring(
dampingRatio = 1.0f,
stiffness = 8000f
)
)
}
}
val scale by scaleAnimatable.asState()
Box(
modifier = modifier,
contentAlignment = Alignment.Center
) {
Image(
painter = painterResource(id = if (isLiked) R.drawable.favorite_fill else R.drawable.favorite_outline),
contentDescription = null,
colorFilter = ColorFilter.tint(color = if (isLiked) Color(0xFFFF0A2F) else Color.Black),
modifier = Modifier
.fillMaxSize()
.scale(scale)
.pointerInput(Unit) {
detectTapGestures(onTap = {
onClick()
})
}
)
}
}
@Composable
fun InstagramAndroidLikeButton(
modifier: Modifier = Modifier,
isLiked: Boolean,
onClick: () -> Unit,
) {
val scaleAnimatable = remember { Animatable(1f) }
LaunchedEffect(isLiked) {
scaleAnimatable.snapTo(0f)
scaleAnimatable.animateTo(
targetValue = 1f,
animationSpec = spring(
dampingRatio = 0.4f,
stiffness = 400f,
)
)
}
val scale by scaleAnimatable.asState()
Box(
modifier = modifier,
contentAlignment = Alignment.Center
) {
Image(
painter = painterResource(id = if (isLiked) R.drawable.favorite_fill else R.drawable.favorite_outline),
contentDescription = null,
colorFilter = ColorFilter.tint(color = if (isLiked) Color(0xFFFF0A2F) else Color.Black),
modifier = Modifier
.fillMaxSize()
.scale(scale)
.pointerInput(Unit) {
detectTapGestures(onTap = {
onClick()
})
}
)
}
}
https://github.com/uuranus/compose-animations?tab=readme-ov-file