
포켓몬 슬립에서 수면 그래프 분석을 할 때 배경으로 몬스터볼이 대각선으로 이동하는 애니메이션이 포함되어 있다. 이 배경화면을 만들어 볼 것이다.
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마다 위치값을 계속 업데이트하여 애니메이션을 설정하였다.
화면 밖으로 나간 행은 다시 아래쪽에 새로 추가가 되어서 계속 무한히 이동하는 것처럼 보이도록 하였다.
