[Android] 카카오톡 눈 내리는 효과 만들기

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

기존 애니메이션 분석

눈 내리는 효과처럼 하늘에서 뭔가 천천히 떨어지는 효과를 만들고자 하였다.

그 예시로 포켓몬 슬립에서 빛이 떨어지는 애니메이션 효과가 있는데 이를 분석해서 구현해보기로 하였다.

포슬립 화면

분석결과
1. 빛들의 위치는 랜덤하게 설정
2. 떨어지는 속도는 동일
3. 떨어지는 방향은 각가 다르지만 하나의 빛은 동일한 방향으로 계속 떨어진다.

빛 모양 구현하기

radial gradient를 구현하고 그 위에 solid 원을 그려서 구현하였다.

Canvas(
    modifier = Modifier
        .fillMaxSize()
        .background(blueBackground)
) {
    lights.forEach { light ->
        drawCircle(
            brush = Brush.radialGradient(
                colors = listOf(yellow, Color.Transparent),
                center = light.offset,
                radius = 8.dp.toPx()
            ),
            center = light.offset,
        )
        drawCircle(
            color = yellow,
            center = light.offset,
            radius = 5.dp.toPx()
        )
    }
}

랜덤하게 시작 위치,떨어지는 각도 설정

var lights by remember {
    mutableStateOf(
        List(20) {
            val x = Random.nextInt(screenWidthPx.toInt()).toFloat()
            val y = Random.nextInt(screenHeightPx.toInt() / 2).toFloat()
            Light(
                Offset(x, -y),
                Random.nextFloat() * (PI / 2).toFloat() + (PI / 4).toFloat()
            )
        }
    )
}

위치 업데이트

val amplitude = 6f

LaunchedEffect(Unit) {
    while (true) {
        lights = lights.map { light ->
            val angle = light.angle

            val horizontalOffset = amplitude * cos(angle)
            val verticalOffset = amplitude * sin(angle)

            val newX = light.offset.x + horizontalOffset
            val newY = light.offset.y + verticalOffset

            if (newX <= 0 || newY >= screenHeightPx) {
                val x = Random.nextInt(screenWidthPx.toInt()).toFloat()
                val y = Random.nextInt(screenHeightPx.toInt() / 2).toFloat()

                light.copy(offset = Offset(x, -y))
            } else {
                light.copy(offset = Offset(newX, newY))
            }
        }
        kotlinx.coroutines.delay(100L)
    }
} 

설정된 angle을 통해서 cos, sin을 통해서 다음 위치를 설정해준다.
화면 밖으로 나가면 다시 시작점을 설정해준다.
계산속도에 비해 UI를 그리는 속도가 느려서 죽어버리는 걸 방지하기 위해 delay를 주었다.

최종 결과

포슬립 화면은 스크린 샷이다.
움직이는 건 내가 만든 효과이고 움직이지 않는 건 스크린샷이다.
구분이 잘 안 가도록 잘 구현한 것 같다 ^_^

카카오톡 눈 내리기 효과로도 사용할 수 있다.

전체 코드

@Composable
fun WindBlownEffect() {

    val configuration = LocalConfiguration.current
    val density = LocalDensity.current

    val screenWidthPx = with(density) {
        configuration.screenWidthDp * this.density
    }

    val screenHeightPx = with(density) {
        configuration.screenHeightDp * this.density
    }

    var lights by remember {
        mutableStateOf(
            List(20) {
                val x = Random.nextInt(screenWidthPx.toInt()).toFloat()
                val y = Random.nextInt(screenHeightPx.toInt() / 2).toFloat()
                Light(
                    Offset(x, -y),
                    Random.nextFloat() * (PI / 2).toFloat() + (PI / 4).toFloat()
                )
            }
        )
    }

    val amplitude = 6f

    LaunchedEffect(Unit) {
        while (true) {

            lights = lights.map { light ->
                val angle = light.angle

                val horizontalOffset = amplitude * cos(angle)
                val verticalOffset = amplitude * sin(angle)

                val newX = light.offset.x + horizontalOffset
                val newY = light.offset.y + verticalOffset

                if (newX <= 0 || newY >= screenHeightPx) {
                    val x = Random.nextInt(screenWidthPx.toInt()).toFloat()
                    val y = Random.nextInt(screenHeightPx.toInt() / 2).toFloat()

                    light.copy(offset = Offset(x, -y))
                } else {
                    light.copy(offset = Offset(newX, newY))
                }
            }
            kotlinx.coroutines.delay(100L)
        }
    }

    val yellow = Color(0xFFFdFF99)

    Canvas(
        modifier = Modifier
            .fillMaxSize()
            .background(blueBackground)
    ) {

        lights.forEach { light ->
            drawCircle(
                brush = Brush.radialGradient(
                    colors = listOf(yellow, Color.Transparent),
                    center = light.offset,
                    radius = 8.dp.toPx()
                ),
                center = light.offset,
            )
            drawCircle(
                color = yellow,
                center = light.offset,
                radius = 5.dp.toPx()
            )
        }


    }
}

data class Light(
    val offset: Offset,
    val angle: Float,
)

깃헙 링크

profile
Frontend Developer

0개의 댓글