본 포스팅은 아래 포스팅들과 관련있습니다.


Android Property Animation 중 눈내리기 효과를 구현해보려고 했는데, 마침 오늘 첫 눈이 내렸습니다. 이번에 구현할 눈내리기 효과의 결과물은 아래와 같습니다.

이번 구현은 사실 이전의 애니메이션 효과와 크게 다르지 않습니다. 이전에 다뤘던 Translation 애니메이션의 응용 버전이랄까요? Codelab의 starShower를 조금 각색한 버전이니 Codelab을 참고하시면 큰 도움이 되실 겁니다.

먼저 크게 다음과 같은 3단계로 구현을 하게 됩니다.

  1. 하늘에서 내릴 눈 객체 생성
  2. 생성한 눈 객체를 떨어뜨리기
  3. 반복해서 여러 눈 객체를 떨어뜨리기

1. 하늘에서 내릴 눈 객체 생성

아래와 같이 별도 함수로 분리하여 눈 객체를 생성하였습니다.

private fun makeSnowObject() = AppCompatImageView(this).apply {
    setImageResource(R.drawable.ic_snow)
    scaleX = Math.random().toFloat() * 0.3f + .2f
    scaleY = scaleX
    measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED)
}

return type은 ImageView가 됩니다. scaleX에 랜덤 값을 적용하였는데, 이는 눈 객체의 크기를 랜덤하게 설정하는 것을 의미합니다. 최소 0.2배 ~ 최대 0.5배 크기로 결정됩니다.

2. 생성한 눈 객체를 떨어뜨리기

이번 스텝이 사실 애니메이션과 관련한 핵심적인 부분이라 할 수 있습니다. 우선 코드는 다음과 같습니다. 상세 설명은 주석을 참고하시기 바랍니다.

private fun snowing() {

    // 눈 객체를 생성하여 container에 추가 (container는 해당 액티비티의 전체 layout입니다.)
    val snowObj = makeSnowObject()
    container.addView(snowObj)

    // 눈 객체의 높이
    val snowHeight = snowObj.measuredHeight * snowObj.scaleY

    // 눈 객체가 하늘에서 내릴 때 랜덤한 위치에서 내리기 시작하여, 랜덤한 위치로 떨어지기 위해 시작/종료 지점의 X 좌표 계산
    val startPoint = Random.nextFloat() * container.width
    val endPoint = Random.nextFloat() * container.width

    // 눈 객체의 시작 위치와 종료 위치의 X 좌표
    val moverX = ObjectAnimator.ofFloat(snowObj, View.TRANSLATION_X, startPoint, endPoint)

    // 눈 객체의 시작 위치와 종료 위치의 Y 좌표
    // 화면의 최상단보다 좀더 위쪽에서 시작하도록 하기 위해 0이 아닌 -snowHeight로 지정
    // 화면의 하단으로 사라지게끔 하기 위해 종료 위치를 container height + snowHeight로 지정
    val moverY = ObjectAnimator.ofFloat(snowObj, View.TRANSLATION_Y, -snowHeight, (container.height + snowHeight))
    
    // 눈이 떨어지는 속도 조절
    moverY.interpolator = AccelerateInterpolator(1f)

    // 2개의 애니메이션을 하나로 묶어서 처리하기 위해 AnimatorSet 사용
    val set = AnimatorSet().apply {
        playTogether(moverX, moverY)
        duration = (Math.random() * 3000 + 3000).toLong()
        addListener(object : AnimatorListenerAdapter() {
            override fun onAnimationEnd(animation: Animator?) {
                // 애니메이션 종료 뒤 해당 눈 객체 제거
                container.removeView(snowObj)
            }
        })
    }
    set.start()
}

눈이 떨어지는 속도를 조절하기 위해 interpolator에 AccelerateInterpolator를 사용하였습니다. 애니메이션이 시간 흐름에 따라 움직임의 속도?를 제어하기 위해 interpolator를 사용할 수 있는데, 이를 테면 설정한 시간 동안 균등하게 애니메이션 효과를 적용할 수도 있지만, 처음에는 빠르게 변하다 후반에는 천천히 변하게 하거나, 처음에는 천천히 변하다 후반에는 빠르게 변하게 할 수 있습니다. AccelerateInterpolator가 초반에는 천천히 시작하다 점점 빠르게 애니메이션을 보여주는 Interpolator입니다. 즉 중력 가속도를 표현하기 위해 본 예제에서는 사용한 케이스입니다. 자세한 내용은 공식 문서를 참고하시기 바랍니다.

3. 반복해서 여러 눈 객체를 떨어뜨리기

본 스텝은 애니메이션과는 크게 관련이 없습니다. 반복적으로 눈을 내리게 하기 위해 아래와 같이 Handler를 사용하였습니다. 상세 내용은 아래 코드에 주석으로 달았습니다.

private const val SNOWING_MESSAGE_ID = 10

// Handler.Callback을 구현합니다.
class SnowingActivity : AppCompatActivity(), Handler.Callback {

    private var isSnowing: Boolean = true
    private val delayedSnowing: Handler = Handler(this)

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_snowing)

        // 액티비티 시작 시 handler에 메세지를 보냅니다.
        delayedSnowing.sendEmptyMessageDelayed(SNOWING_MESSAGE_ID, 100)
    }

    // ......

    // 핸들러에서 메세지를 수신하는 부분을 오버라이드합니다.
    override fun handleMessage(msg: Message): Boolean {
        if (msg.what == SNOWING_MESSAGE_ID && isSnowing) {
            // 눈 객체를 하나 생성하여 애니메이션 실행합니다.
            snowing()
            // 다음 눈 객체를 생성합니다.
            delayedSnowing.sendEmptyMessageDelayed(SNOWING_MESSAGE_ID, 100)
        }

        return true
    }

    override fun onDestroy() {
        super.onDestroy()
        // 액티비티 라이프사이클 종료 시 더이상 눈 객체를 생성하지 않도록 플래그 값을 변경합니다.
        isSnowing = false
        // 동일하게 핸들러에 보내진 콜백이나 메세지들을 모두 제거합니다.
        delayedSnowing.removeCallbacksAndMessages(null)
    }
}

이것으로 코드랩에 나와있는 6가지의 Property Animation에 대해 모두 알아보았습니다. 제 포스팅에 사용한 코드는 아래 위치에서 확인하실 수 있습니다.

https://github.com/yoonhok524/Android-Sandbox/tree/master/Animation


0 Comments

댓글 남기기

이메일은 공개되지 않습니다. 필수 입력창은 * 로 표시되어 있습니다