눈 내리는 애니메이션 구현하기

이윤설·2025년 3월 2일
0

안드로이드 연구소

목록 보기
30/33

Jetpack Compose로 눈 내리는 애니메이션 만들기


검은 배경에 하얀 눈이 자연스럽게 떨어지는 효과를 만들어보자!

기본 원리

눈 내리는 애니메이션의 핵심 원리는 다음과 같다.

  1. 여러 개의 작은 원형 입자(눈송이)를 만든다.
  2. 각 눈송이에 랜덤한 시작 위치, 크기, 속도를 부여한다.
  3. 시간이 지남에 따라 각 눈송이의 위치를 업데이트한다.
  4. 떨어지면서 좌우로 살짝 흔들리는 효과를 추가한다.

눈송이 속성 정의하기

먼저 각 눈송이의 특성을 저장할 데이터 클래스를 정의한다.

data class SnowflakeProperties(
    val initialX: Float,    // 초기 X 위치 (0~1)
    val initialY: Float,    // 초기 Y 위치 (0~1)
    val size: Float,        // 눈송이 크기
    val speed: Float,       // 낙하 속도
    val amplitude: Float,   // 좌우 흔들림 크기
    val phaseOffset: Float  // 흔들림 위상 차이
)

여기서 각 속성은 다음 역할을 합니다:

  • initialX, initialY: 화면에서의 상대적 시작 위치 (0~1 사이 값)
  • size: 눈송이의 크기
  • speed: 눈송이가 얼마나 빠르게 떨어지는지
  • amplitude: 눈송이가 좌우로 얼마나 많이 흔들리는지
  • phaseOffset: 각 눈송이가 서로 다른 흔들림 패턴을 가지게 하는 값

참고로 initialX와 initialY는 눈송이의 초기 위치를 정한다.
Random.nextFloat()는 0부터 1 사이의 무작위 값을 생성한다. 이 값은 화면 비율을 의미하는데, 예를 들어 initialX가 0.5라면 화면 가로 중앙에 위치한다는 뜻이다. 당연히 1이면 화면의 오른쪽 끝을 의미한다.

이처럼 관련된 모든 요소들을 랜덤으로 설정한다. 모든 눈송이의 낙하 속도, 시작 위치, 크기, 좌우 흔들림, 흔들림 위상 차이를 다 다르게 만들기 위함이다.

눈송이 생성하기

이제 화면에 표시할 여러 개의 눈송이를 생성하자.

val numSnowflakes = 70

val snowflakes = remember {
    List(numSnowflakes) {
        SnowflakeProperties(
            initialX = Random.nextFloat(),
            initialY = Random.nextFloat(),
            size = Random.nextFloat() * 4f + 1f,  // 1~5 크기
            speed = Random.nextFloat() * 0.006f + 0.004f,
            amplitude = Random.nextFloat() * 0.05f,
            phaseOffset = Random.nextFloat() * 6.28f // 2π
        )
    }
}

remember를 사용하여 재구성(recomposition) 시 눈송이 목록이 다시 생성되지 않도록 합니다. 각 눈송이는 랜덤한 속성을 가지므로 모두 조금씩 다르게 움직이게 됩니다.

시간에 따른 애니메이션 값 만들기

눈송이 움직임을 위한 시간 값을 애니메이션화한다.

val transition = rememberInfiniteTransition(label = "snowTransition")
val time by transition.animateFloat(
    initialValue = 0f,
    targetValue = 1f,
    animationSpec = infiniteRepeatable(
        animation = tween(10000, easing = LinearEasing),
        repeatMode = RepeatMode.Restart
    ),
    label = "snowTime"
)

이 코드는 0에서 1까지 10초 동안 변화하는 값을 만들고, 1에 도달하면 다시 0으로 돌아가 무한히 반복한다.
이 시간 값을 사용해 눈송이의 위치를 계산할 것이다.

눈송이 그리기

Canvas를 사용해 모든 눈송이를 그리자.

fun DrawScope.drawSnowflakes(snowflakes: List<SnowflakeProperties>, time: Float) {
    val width = size.width
    val height = size.height
    
    for (flake in snowflakes) {
        // 시간에 따라 y 위치 계산 (수직 이동)
        val normalizedTime = (time * 5 + flake.initialY) % 1f
        val y = normalizedTime * height
        
        // 시간과 위치에 따른 좌우 흔들림 계산
        val waveX = sin((normalizedTime * 10f) + flake.phaseOffset) * flake.amplitude
        
        // x 위치 계산
        val x = ((flake.initialX + waveX) % 1f) * width
        
        // 흰색 눈송이 그리기
        drawCircle(
            color = Color.White.copy(alpha = 0.8f),
            radius = flake.size * density,
            center = Offset(x, y)
        )
    }
}
  1. 수직 위치 계산: 시간이 지남에 따라 눈송이가 위에서 아래로 내려오도록 y좌표를 계산한다.

  2. 좌우 흔들림 계산: 사인(sin) 함수를 사용해 눈송이가 떨어지면서 좌우로 살짝 흔들리게 한다.

  3. 실제 위치 계산: 계산된 값을 실제 화면 픽셀 위치로 변환한다.

  4. 눈송이 그리기: 계산된 위치에 하얀 원을 그려서 눈송이를 표현한다.

결국 모든 눈송이는 각자 다른 속도와 흔들림으로 화면 위에서 아래로 떨어지는데,
이 과정이 계속 반복되어 눈이 내리는 것처럼 보이게 된다.

전체 구성하기

마지막으로 모든 요소를 합쳐 완성된 애니메이션을 만들면 된다.

fun WhiteSnowAnimation() {
    // (앞서 설명한 눈송이 생성 및 시간 애니메이션 코드)
    
    Box(
        modifier = Modifier
            .fillMaxSize()
            .background(Color(0xFF000000))
    ) {
        Canvas(modifier = Modifier.fillMaxSize()) {
            drawSnowflakes(snowflakes, time)
        }
    }
}

검은 배경의 전체 화면 Box 안에 Canvas를 배치하고, 그 위에 눈송이들을 그린다.

애니메이션 조정하기

애니메이션을 원하는 대로 수정할 수 있는 매개변수들은 다음과 같다.

  1. 눈송이 개수: numSnowflakes 값을 조정하여 화면에 표시할 눈송이 수를 변경할 수 있다. 많을수록 더 풍성하지만 성능에 영향을 줄 수 있다.

  2. 눈송이 크기: size = Random.nextFloat() * 4f + 1f 부분을 수정하여 눈송이의 크기 범위를 조정할 수 있다.

  3. 낙하 속도: speed 값과 time * 5 부분을 조정하여 눈이 내리는 속도를 변경할 수 있다.

  4. 흔들림 정도: amplitude 값을 조정하여 눈송이가 좌우로 얼마나 많이 흔들릴지 결정할 수 있다.

성능 최적화 팁

  1. remember 사용: 눈송이 데이터를 remember로 감싸서 리컴포지션이 발생할 때마다 다시 계산하지 않도록 한다.

  2. 적절한 눈송이 수: 디바이스 성능에 따라 눈송이 수를 조정하는 것이 좋다. 저사양 기기에서는 50개 이하가 적당하다.

코드

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            MaterialTheme {
                Surface(
                    modifier = Modifier.fillMaxSize(),
                    color = Color.Black
                ) {
                    WhiteSnowAnimation()
                }
            }
        }
    }
}

@Composable
fun WhiteSnowAnimation() {
    // 눈송이 개수
    val numSnowflakes = 70

    // 눈송이 속성 미리 계산
    val snowflakes = remember {
        List(numSnowflakes) {
            SnowflakeProperties(
                initialX = Random.nextFloat(),
                initialY = Random.nextFloat(),
                size = Random.nextFloat() * 4f + 1f,  // 1~5 크기
                speed = Random.nextFloat() * 0.006f + 0.004f,
                amplitude = Random.nextFloat() * 0.05f,
                phaseOffset = Random.nextFloat() * 6.28f // 2π
            )
        }
    }

    // 시간 애니메이션
    val transition = rememberInfiniteTransition(label = "snowTransition")
    val time by transition.animateFloat(
        initialValue = 0f,
        targetValue = 1f,
        animationSpec = infiniteRepeatable(
            animation = tween(10000, easing = LinearEasing),
            repeatMode = RepeatMode.Restart
        ),
        label = "snowTime"
    )

    Box(
        modifier = Modifier
            .fillMaxSize()
            .background(Color(0xFF000000))
    ) {
        Canvas(modifier = Modifier.fillMaxSize()) {
            drawSnowflakes(snowflakes, time)
        }
    }
}

// 눈송이 속성 클래스
data class SnowflakeProperties(
    val initialX: Float,
    val initialY: Float,
    val size: Float,
    val speed: Float,
    val amplitude: Float,
    val phaseOffset: Float
)

// 눈송이 그리기 함수
fun DrawScope.drawSnowflakes(snowflakes: List<SnowflakeProperties>, time: Float) {
    val width = size.width
    val height = size.height

    for (flake in snowflakes) {
        // 시간에 따라 y 위치 계산 (수직 이동)
        val normalizedTime = (time * 5 + flake.initialY) % 1f
        val y = normalizedTime * height

        // 시간과 위치에 따른 좌우 흔들림 계산
        val waveX = sin((normalizedTime * 10f) + flake.phaseOffset) * flake.amplitude

        // x 위치 계산
        val x = ((flake.initialX + waveX) % 1f) * width

        // 흰색 눈송이 그리기
        drawCircle(
            color = Color.White.copy(alpha = 0.8f),
            radius = flake.size * density,
            center = Offset(x, y)
        )
    }
}

애니메이션 개념 정리

  1. rememberInfiniteTransition - 무한히 반복되는 애니메이션을 만드는 도구

  2. animateFloat - 숫자 값이 시간에 따라 부드럽게 변하도록 한다.

  3. infiniteRepeatable - 애니메이션이 끝나도 계속 반복되게 한다.

  4. Canvas - 화면에 직접 그림을 그릴 수 있는 도구.

  5. sin() 함수 - 물결 모양의 주기적인 움직임을 만들어 낸다.

  6. % 1f - 값을 0부터 1 사이로 유지하여 화면 내에서 순환하게 한다.

profile
화려한 외면이 아닌 단단한 내면

0개의 댓글

관련 채용 정보