본문 바로가기
프로젝트/Roulette Wheel View

[Roulette] 룰렛을 그리고 회전시키기 (3) - 회전 결과 리턴하기

by JhDroid 2021. 3. 1.
728x90

 

 

JhDroid/android-roulette-wheel-view

Android draw roulette view. Contribute to JhDroid/android-roulette-wheel-view development by creating an account on GitHub.

github.com

 

애니메이션 시작/종료 여부 확인

1. Listener 생성

  • 일단 애니메이션의 시작/종료를 알릴 수 있는 Listener를 생성합니다.
    • AnimationListener을 사용하지 않고 Listener를 만드는 이유는 단순히 시작/종료 타이밍을 알려는 것이 아닌 애니메이션이 종료되면 결과를 리턴 받기 위함입니다.
package com.jhdroid.roulette

interface RotateListener {
    fun onRotateStart()
    fun onRotateEnd(result: String)
}
  • 코드 설명
    • onRotateStart() : 애니메이션의 시작을 알립니다. 매개변수가 필요 없습니다.
    • onRotateEnd(String) : 애니메이션의 종료를 알립니다. 결과를 전달해주기 때문에 매개변수가 필요합니다.

 

2. 회전 함수 수정

  • 이전 글에서 생성한 룰렛 회전 함수를 수정해서 사용자도 RotateListener을 통해 회전의 시작/종료를 알 수 있도록 코드를 수정합니다.
fun rotateRoulette(toDegrees: Float, duration: Long, rotateListener: RotateListener?) {
    val animListener = object : Animation.AnimationListener {
        override fun onAnimationRepeat(animation: Animation?) {}

        override fun onAnimationStart(animation: Animation?) {
            rotateListener?.onRotateStart()
        }

        override fun onAnimationEnd(animation: Animation?) {
            rotateListener?.onRotateEnd("<결과>")
        }
    }

    val rotateAnim = RotateAnimation(
            0f, toDegrees,
            Animation.RELATIVE_TO_SELF, 0.5f,
            Animation.RELATIVE_TO_SELF, 0.5f
    )
    rotateAnim.duration = duration
    rotateAnim.fillAfter = true
    rotateAnim.setAnimationListener(animListener)

    startAnimation(rotateAnim)
}
  • 코드 설명
    • animListener
      • 애니메이션의 시작, 종료, 재시작을 알 수 있도록 해주는 리스너입니다.
      • 이 리스너에서 시작/종료 여부를 받아 RotateListener를 통해 사용자에게도 전달해줍니다.
    • rotateAnim
      • 이전 글에서 작성한 코드와 동일하고 AnimationListener를 설정해주는 코드만 추가되었습니다.

 

3. 테스트 코드 수정

  • MainActivity에서 테스트할 코드를 작성해줍니다.
    • 기존 레이아웃에 결과를 보여줄 TextView를 추가해줍니다.(코드 생략)
    • MainActivity에 테스트 코드를 추가합니다.
fun rotateRoulette() {
    val rotateListener = object: RotateListener {
        override fun onRotateStart() {
            binding.rotateResultTv.text = "Result : "
        }

        override fun onRotateEnd(result: String) {
            binding.rotateResultTv.text = "Result : $result"
        }
    }

    val toDegrees = (2000..10000).random().toFloat()
    binding.roulette.rotateRoulette(toDegrees, 3000, rotateListener)
}
  • 코드 설명
    • rotateListener
      • onRotateStart() : 애니메이션이 시작되면 TextView의 내용을 초기화해줍니다.
      • onRotateEnd(String) : 애니메이션이 종료되면 결과를 전달받아 출력해줍니다.

실행 결과

  • 실행 결과를 보면 회전이 종료되고 임의로 설정한 "<결과>"를 출력하는 것을 확인할 수 있다.

 

회전 결과 계산해서 실제 결과 리턴하기

1. 회전 결과 계산

  • 회전 결과 계산식은 구글링 결과 원하는 식을 찾을 수 없어서 직접 삽질하면서 만들었습니다.
    • 때문에 계산식이 이상할 수 있지만 결과는 잘 나옵니다(?)
  • 룰렛은 동일한 회전 각도로 회전을 하면 룰렛 크기와 상관없이 항상 룰렛의 일정 각도가 12시 방향에 오게 됩니다.
  • 제가 생각한 방법은 12시에 오는 각도를 구하고 이 각도에 해당하는 룰렛 칸의 데이터를 리턴해주는 방법입니다.

 

룰렛이 항상 361도를 회전할 때의 결과 구하는 방법

  1. 회전 각도를 361도로 설정하면 룰렛 사이즈에 상관없이 항상 269도가 12시 방향에 오게 됨(결과 각도)
    • 안드로이드에서 룰렛을 그릴 때는 12시 방향에 270도가 오게 됨(1번 글 참고)
    • 360도는 제자리이기 때문에 회전 각도에서 360도를 나눈 후 나머지를 가지고 계산(나머지 각도)
      • 361도를 360으로 나눈 후 나머지는 1
    • 나머지 각도가 270보다 작으면 (270 - 나머지 각도)로 결과 각도를 구함
    • 나머지 각도가 270보다 크면 (360 - 나머지 각도 + 270)으로 결과 각도를 구함
    • 361도의 나머지 각도는 1로 270보다 작기 때문에 270 - 1 = 269 결과 각도가 됨
  2. 룰렛 사이즈에 따라 한 칸의 각도가 달라지게 됨 -> (360 /  룰렛 사이즈) = 룰렛 한 칸의 각도
    • 룰렛 사이즈가 4일 때 룰렛 한 칸의 각도는 90도
    • 1번 칸은 0~90, 2번 칸은 90~180, 3번 칸은 180~270, 4번 칸은 270~360
  3. 룰렛 사이즈만큼 for문을 돌려(1~룰렛 사이즈) 결과 각도에 해당하는 칸의 데이터를 리턴해줌
    • 결과 각도(269도)가 한 칸의 각도보다 작으면 해당 인덱스의 데이터를 리턴
    • 이때 비교하는 각도는 한 칸의 각도 중 큰 값(90, 180, 270, 360)
      • for문을 돌리면 순서대로
      • 269 < 90
        • 269 < 180
          • 269 < 270 -> 3번째 칸의 데이터 리턴
private fun getRouletteRotateResult(degrees: Float): String {
    val moveDegrees = degrees % 360
    val resultAngle = if (moveDegrees > 270) 360 - moveDegrees + 270 else 270 - moveDegrees
    for (i in 1..rouletteSize) {
        if (resultAngle < (360 / rouletteSize) * i) {
            if (i - 1 >= rouletteDataList.size) {
                return "empty"
            }

            return rouletteDataList[i - 1]
        }
    }

   return ""
}
  • for문을 돌릴 때는 1부터 돌리기 때문에 인덱스를 구할 때는 i - 1을 해줘야 합니다.
  • 해당 인덱스보다 룰렛 데이터 리스트 사이즈가 더 크다면 리스트의 해당 인덱스는 비어있다는 뜻으로 "empty"를 리턴해줍니다.

 

2. 결과 리턴 코드 적용

  • 이제 이 결과 리턴 함수를 회전 함수에 적용해서 실제 결과를 리턴하는 코드를 작성해보겠습니다.
fun rotateRoulette(toDegrees: Float, duration: Long, rotateListener: RotateListener?) {
    val animListener = object : Animation.AnimationListener {
        override fun onAnimationRepeat(animation: Animation?) {}

        override fun onAnimationStart(animation: Animation?) {
            rotateListener?.onRotateStart()
        }

        override fun onAnimationEnd(animation: Animation?) {
            rotateListener?.onRotateEnd(getRouletteRotateResult(toDegrees))
        }
    }

    val rotateAnim = RotateAnimation(
            0f, toDegrees,
            Animation.RELATIVE_TO_SELF, 0.5f,
            Animation.RELATIVE_TO_SELF, 0.5f
    )
    rotateAnim.duration = duration
    rotateAnim.fillAfter = true
    rotateAnim.setAnimationListener(animListener)

    startAnimation(rotateAnim)
}
  • Listener를 적용한 코드에서 onRotateEnd() 코드 부분에 결과 리턴 함수 적용만 했습니다.
    • 회전 각도를 받아 결과 리턴 함수에 전달해서 결과를 계산하고 애니메이션이 종료되면 그 결과를 그대로 리턴해줍니다.

 

3. 테스트

  • 아직 Roulette 클래스에 getter & setter를 만들지 않아(이걸 먼저 작성했어야 했는데...) 일단 Rolette 클래스에서 임의로 데이터를 설정해주고 테스트했습니다.
class Roulette @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {

    private val strokePaint = Paint()
    private val fillPaint = Paint()
    private val textPaint = Paint()

    private var rouletteSize = 8
    private var rouletteDataList = listOf("JhDroid", "Android", "Blog", "IT", "Developer", "Kotlin", "Java", "Happy")
    ...
}

 

결과.gif

다음 글은 사용자가 사용하기 편하도록 getter &  setter 추가와 typedArray 적용 방법입니다.

 

* 글에 틀린 부분이 있으면 댓글 부탁드립니다 :D

728x90