[Android] 움직이는 포켓몬볼 배경화면 만들기

uuranus·2024년 9월 13일
0
post-thumbnail

기존 애니메이션 뷰 분석

포켓몬 슬립에서 수면 그래프 분석을 할 때 배경으로 몬스터볼이 대각선으로 이동하는 애니메이션이 포함되어 있다. 이 배경화면을 만들어 볼 것이다.

포켓몬 볼 그리기

path를 이용해서 그렸다.
op메서드를 이용해서 가운데 빈 부분을 만들어주었다.

private fun generatePokemonBall(
    size: Size,
): Path {
    val centerX = size.width / 2
    val centerY = size.height / 2

    val largeCircleRadius = minOf(centerX, centerY)
    val smallEmptyCircleRadius = largeCircleRadius * 0.5f
    val lineThickness = largeCircleRadius / 6

    val path = Path().apply {
        addOval(
            oval = Rect(
                center = Offset(centerX, centerY),
                radius = largeCircleRadius
            )
        )
    }


    val smallEmptyCirclePath = Path().apply {
        addOval(
            oval = Rect(
                center = Offset(centerX, centerY),
                radius = smallEmptyCircleRadius
            )
        )
    }

    path.apply {
        op(
            path1 = this,
            path2 = smallEmptyCirclePath,
            operation = PathOperation.Difference
        )
    }

    val linePath = Path().apply {
        addRect(
            rect = Rect(
                offset = Offset(0f, centerY - lineThickness / 2),
                size = Size(size.width, lineThickness)
            )
        )
    }

    path.apply {
        op(
            path1 = this,
            path2 = linePath,
            operation = PathOperation.Difference
        )
    }

    val smallCirclePath = Path().apply {
        addOval(
            oval = Rect(
                center = Offset(centerX, centerY),
                radius = smallEmptyCircleRadius - lineThickness
            )
        )
    }

    path.apply {
        op(
            path1 = this,
            path2 = smallCirclePath,
            operation = PathOperation.Union
        )
    }

    return path
}
그러면 이렇게 그려진다!

지그재그로 배치하기

var backgroundSize by remember {
    mutableStateOf(Size.Zero)
}

var rowCount = 0

val ballOffsets by remember(backgroundSize) {
    if (backgroundSize == Size.Zero) return@remember mutableStateOf(emptyList<List<Offset>>())

    val totalWidth = backgroundSize.width
    val totalHeight = backgroundSize.height

    val offsets = mutableListOf<List<Offset>>()

    var currentY = ballSize / 2

    while (currentY <= totalHeight + ballSize) {
        offsets.add(
            makeNewRowList(
                rowCount = rowCount,
                ballSize = ballSize,
                width = totalWidth,
                currentY = currentY,
                diagonalXSpace = diagonalXSpace
            )
        )
        currentY += ballSize + diagonalYSpace
        rowCount++
    }

    mutableStateOf(offsets.toList())
}


private fun makeNewRowList(
    rowCount: Int,
    ballSize: Float,
    width: Float,
    currentY: Float,
    diagonalXSpace: Float,
): List<Offset> {
    val rowOffsets = mutableListOf<Offset>()
    var currentX = if (rowCount % 2 == 0) -ballSize else 0f

    while (currentX <= width * 3 + ballSize) {
        rowOffsets.add(Offset(currentX, currentY))
        currentX += ballSize + diagonalXSpace
    }
    return rowOffsets
}

rowCount로 짝수,홀수에 맞춰서 시작 위치를 엇갈리게 시작하여 각 줄이 지그재그로 배치되도록 하였고
offset은 width의 3배 뒤까지 계산을 해 놓아서 왼쪽 위로 이동하더라도 오른쪽에 공백이 생기지 않도록 하였다.

대각선으로 애니메이션

var updatedBalls by remember(ballOffsets) {
    mutableStateOf(ballOffsets)
}

LaunchedEffect(ballOffsets) {
    if (ballOffsets.isEmpty()) return@LaunchedEffect

    while (true) {
        val newList = updatedBalls.map { listOffsets ->
            listOffsets.map { offset ->
                val newXOffset = offset.x - diagonalXSpace / animationDuration
                val newYOffset = offset.y - diagonalYSpace / animationDuration

                offset.copy(x = newXOffset, y = newYOffset)
            }
        }

        updatedBalls = if (newList.first().first().y < -ballSize) {
            val currentY = newList.last().first().y

            newList.drop(1).plusElement(
                makeNewRowList(
                    rowCount = rowCount,
                    ballSize = ballSize,
                    width = backgroundSize.width,
                    currentY = currentY + ballSize + diagonalYSpace,
                    diagonalXSpace = diagonalXSpace
                ).also {
                    rowCount++
                }
            )
        } else {
            newList
        }

        delay(16L)
    }
}

낙엽 떨어지는 효과를 만들었을 때처럼 16L마다 위치값을 계속 업데이트하여 애니메이션을 설정하였다.
화면 밖으로 나간 행은 다시 아래쪽에 새로 추가가 되어서 계속 무한히 이동하는 것처럼 보이도록 하였다.

최종 결과

profile
Frontend Developer

0개의 댓글