[Android] 인스타그램 좋아요 애니메이션

uuranus·2024년 8월 4일
0
post-thumbnail

기존 애니메이션 분석

먼저 기존 애니메이션을 분석해보자.

AOS 버전이랑 iOS 버전이 다르다.

AOS 버전

AOS 버전은 fill -> border, border -> fill 상관없이 하트가 약간 커졌다가 바운스되면서 다시 돌아오는 모션이 있다.

iOS 버전

border -> fill로 채워질 때는 아무런 모션이 없지만 fill -> border로 바뀔 때는 크기가 줄었다가 다시 커지는 모션이 있다.

AOS 버전

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이 시작되도록 적용했다.

iOS 버전

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

profile
Frontend Developer

0개의 댓글