Skip to content

폭죽 애니메이션

HyeonSeongKang edited this page Aug 29, 2023 · 5 revisions

안녕하세요. 안드로이드 팀의 강현성 입니다.

이번 프로젝트에서 폭죽 애니메이션 구현했는데 어떻게 구현했는지에 대해서 공유하려 합니다.

🎆 폭죽 애니메이션

폭죽 애니메이션은 여러 개의 사각형들이 화면 상단의 랜덤한 위치에서 폭발시키는 효과를 구현합니다. 각 사각형은 랜덤한 방향과 속도로 움직이며, 화면 바깥으로 나가게 되면 삭제됩니다.

구현과정

1. 파티클 추가하기: 먼저 FrameLayout에 파티클(사각형)을 랜덤한 위치에 추가합니다.

repeat(numberOfExplosions) {
    // 화면 상단의 랜덤 위치에서 폭죽 시작
    val explosionCenterX = Random.nextInt(frameLayout.width).toFloat()
    val explosionCenterY = Random.nextInt(frameLayout.height / 3).toFloat()

    val particles = List(numberOfParticles) {
        val width = Random.nextInt(20) + 10
        val height = Random.nextInt(20) + 8
        val color = Color.parseColor(colors[Random.nextInt(colors.size)])

        val particle = View(frameLayout.context).apply {
            setBackgroundColor(color)
            layoutParams = FrameLayout.LayoutParams(width, height)
            x = explosionCenterX - width / 2f
            y = explosionCenterY - height / 2f
            alpha = 0.8f
        }
        frameLayout.addView(particle)
        particle
    }
}

2. 각도와 속도 결정하기:각 파티클의 이동 방향은 360도 중 랜덤 각도와 이를 기반으로 X, Y 속도를 구합니다.

x속도 = con(각도)
y속도 = sin(각도)
val angle = Math.toRadians(Random.nextInt(360).toDouble())
val speed = Random.nextInt(10) + 5  // 5~15 사이의 속도

val xVelocity = cos(angle) * speed
val yVelocity = sin(angle) * speed

3. 애니메이션으로 폭죽 효과 구현하기: 마지막으로 ValueAnimator를 사용하여 폭죽 애니메이션을 구현합니다:

  • 3.1. 파티클 움직임 설정 particles 리스트의 모든 값을 반복합니다. 각 파티클 대해서, 이전 단계에서 계산한 각도와 속도를 사용하여 움직임을 결정합니다.
particles.forEach { particle ->
    // ... [각도 및 속도 계산 코드]

  • 3.2. 애니메이터 설정
    • ofFloat(0f, 1f)는 애니메이션의 시작과 끝 값을 나타냅니다. 여기서는 0에서 1까지의 값이지만, 실제 움직임은 이 값들에 의해 직접적으로 제어되지는 않습니다.
    • duration은 애니메이션의 지속 시간을 설정하며, 여기서는 무작위로 2초에서 5초 사이의 값이 설정됩니다.
    • LinearInterpolator는 애니메이션의 속도가 꾸준히 유지되게 합니다.
val animator = ValueAnimator.ofFloat(0f, 1f).apply {
    duration = (Random.nextInt(3001) + 2000).toLong()  // 2~5초
    interpolator = LinearInterpolator()

  • 3.3. 움직임 업데이트
addUpdateListener {
     particle.x += xVelocity.toFloat()
     particle.y += yVelocity.toFloat()

     // 화면 밖으로 나가면 삭제
     if (particle.x < 0 || particle.x > frameLayout.width || particle.y < 0 || particle.y > frameLayout.height) {
        frameLayout.removeView(particle)
        this.cancel()
     }
}

  • 3.4. 애니메이션 종료 처리
animator.addListener(object : AnimatorListenerAdapter() {
   override fun onAnimationEnd(animation: Animator) {
       frameLayout.removeView(particle)
   }
})

  • 3.5. 애니메이션 시작
animator.start()

문제점

  1. 성능 저하: 너무 많은 파티클을 동시에 처리하려고 할 때, 뷰의 생성과 제거에 따른 오버헤드가 커져 앱의 전반적인 성능에 영향을 미칠 수 있습니다.
  2. 메모리 사용 증가: 각 파티클은 개별 뷰로써 메모리를 소비합니다. 따라서 너무 많은 파티클을 생성하면 앱의 메모리 사용량이 급격히 증가할 수 있습니다.
  3. UI 스레드 부하: 파티클 애니메이션은 UI 스레드에서 실행됩니다. 너무 많은 파티클이 동시에 애니메이션 되면 UI 스레드에 부하가 걸리게 되며, 이는 애니메이션의 부드러움을 해칠 수 있습니다.

해결방안

  1. 파티클 수 줄이기: 필요한 최소한의 파티클만 사용합니다. 사실 이게 가장 간단한 방법입니다.
  2. 뷰 풀 사용: 뷰 풀(View Pool)을 사용하여 이미 생성된 파티클 뷰를 재사용합니다. 파티클이 화면 밖으로 나가거나 애니메이션이 종료되면 뷰 풀로 반환하고, 새로운 파티클이 필요할 때 다시 가져와 사용합니다.(구현해보고 있습니다.)

전체코드

    private val activeAnimations = mutableListOf<Animator>()

    fun explodeView(frameLayout: FrameLayout, numberOfParticles: Int = 150, numberOfExplosions: Int = 5) {
        activeAnimations.forEach { it.cancel() }
        activeAnimations.clear()
        frameLayout.removeAllViews()

        val colors = listOf("#FF7676", "#FD3F33", "#FFB876", "#FFB801", "#76ADFF", "#357FED")

        repeat(numberOfExplosions) {
            // 화면 상단의 랜덤 위치에서 폭죽 시작
            val explosionCenterX = Random.nextInt(frameLayout.width).toFloat()
            val explosionCenterY = Random.nextInt(frameLayout.height / 3).toFloat()

            val particles = List(numberOfParticles) {
                val width = Random.nextInt(20) + 10
                val height = Random.nextInt(20) + 8
                val color = Color.parseColor(colors[Random.nextInt(colors.size)])

                val particle = View(frameLayout.context).apply {
                    setBackgroundColor(color)
                    layoutParams = FrameLayout.LayoutParams(width, height)
                    x = explosionCenterX - width / 2f
                    y = explosionCenterY - height / 2f
                    alpha = 0.8f
                }
                frameLayout.addView(particle)
                particle
            }

            particles.forEach { particle ->
                val angle = Math.toRadians(Random.nextInt(360).toDouble())
                val speed = Random.nextInt(10) + 5  // 5~15 사이의 속도

                val xVelocity = cos(angle) * speed
                val yVelocity = sin(angle) * speed

                val animator = ValueAnimator.ofFloat(0f, 1f).apply {
                    duration = (Random.nextInt(3001) + 2000).toLong()  // 2~5초
                    interpolator = LinearInterpolator()
                    addUpdateListener {
                        particle.x += xVelocity.toFloat()
                        particle.y += yVelocity.toFloat()

                        // 화면 밖으로 나가면 삭제
                        if (particle.x < 0 || particle.x > frameLayout.width || particle.y < 0 || particle.y > frameLayout.height) {
                            frameLayout.removeView(particle)
                            this.cancel()
                        }
                    }
                }

                animator.addListener(object : AnimatorListenerAdapter() {
                    override fun onAnimationEnd(animation: Animator) {
                        frameLayout.removeView(particle)
                    }
                })

                animator.start()
            }
        }
    }
Clone this wiki locally