포켓몬 슬립에서 수면 계측을 시작하면 나오는 화면의 배경에서 물방울이 떠다니는 것 같은 효과를 만들어 볼 것이다.
분석을 해보자면
1. 모양은 링 모양 or 원 모양
2. 둘이 겹치면 그 부분만 진해진다 -> 투명도가 존재
3. 원 모양일 경우 크기가 커졌다 작아졌다 하는 모션 존재 (링 모양은 없음)
4. 천천히 움직여서 티가 잘 안 나지만 특정 가로길이 내에서만 움직이고 거기를 벗어나면 벽에 부딪힌 것처럼 방향이 변경된다.
var bubbleState by remember {
mutableStateOf(
BubbleState(
List(15) { index ->
val x = Random.nextInt(screenWidthPx.toInt()).toFloat()
val y = Random.nextInt(screenHeightPx.toInt()).toFloat()
val radius = with(density) {
if (index < 5) {
Random.nextInt(25, 30).dp.toPx()
} else {
Random.nextInt(10, 25).dp.toPx()
}
}
val shape: Shape = if (radius < shapeCriteria || Random.nextInt(2) == 0) {
CircleShape
} else {
RingShape(6.dp)
}
Bubble(
shape = shape,
offset = Offset(x, y),
radius = radius,
width = radius * 3,
angle = Random.nextFloat() * (PI / 2) + PI + (PI * 1 / 4)
)
}
)
)
}
초기 위치값은 랜덤을 이용해서 설정주었고
5번째까지는 링 모양으로 설정하고 링 모양은 radius의 크기가 25 이상, 그 이하면 원 모양으로 설정해주었다.
LaunchedEffect(Unit) {
while (isActive) {
awaitFrame()
bubbleState = bubbleState.copy(
bubbles = bubbleState.bubbles.map { bubble ->
bubble.update(
Size(
screenWidthPx,
screenHeightPx
)
)
bubble
}
)
delay(16L)
}
}
16L마다 위치를 업데이트해서 60FPS으로 애니메이션이 동작하도록 하였다.
fun update(
screenSize: Size,
) {
val newX = offset.x + increment * cos(angle).toFloat()
val newY = offset.y + increment * sin(angle).toFloat()
offset = when {
newX < 0 || newX > screenSize.width || newY < 0 -> {
val x = Random.nextFloat() * screenSize.width
val y = screenSize.height + radius
range = (x - width / 2)..(x + width / 2)
Offset(x, y)
}
newX !in range -> {
angle = (3 * PI / 2 - angle) + (3 * PI / 2)
Offset(
x = newX + cos(angle).toFloat(),
y = newY
)
}
else -> {
Offset(newX, newY)
}
}
scale += scaleIncrement
if (scale <= 0.6f || scale >= 1f) {
scaleIncrement = -scaleIncrement
}
}
화면을 벗어나면 다시 초기값을 설정해주고 가로길이 범위를 벗어나면 angle을 뒤집어서 반대쪽으로 움직이도록 해주었다.
for (bubble in bubbleState.bubbles) {
val offsetX = with(density) {
bubble.offset.x.toDp()
}
val offsetY = with(density) {
bubble.offset.y.toDp()
}
val radiusDp = with(density) {
bubble.radius.toDp()
}
Box(
modifier = Modifier
.size(radiusDp * 2)
.offset(
x = offsetX,
y = offsetY
)
.scale(
if (bubble.shape == CircleShape) {
bubble.scale
} else {
1f
}
)
.background(
color = Color.White.copy(alpha = 0.15f),
shape = bubble.shape
)
)
}
@Composable
fun FloatingUpEffect(
) {
val configuration = LocalConfiguration.current
val density = LocalDensity.current
val screenWidthPx = with(density) {
configuration.screenWidthDp * this.density
}
val screenHeightPx = with(density) {
configuration.screenHeightDp * this.density
}
val shapeCriteria = with(density) {
25.dp.toPx()
}
var bubbleState by remember {
mutableStateOf(
List(15) { index ->
val x = Random.nextInt(screenWidthPx.toInt()).toFloat()
val y = Random.nextInt(screenHeightPx.toInt()).toFloat()
val radius = with(density) {
if (index < 5) {
Random.nextInt(25, 30).dp.toPx()
} else {
Random.nextInt(10, 25).dp.toPx()
}
}
val shape: Shape = if (radius < shapeCriteria || Random.nextInt(2) == 0) {
CircleShape
} else {
RingShape(6.dp)
}
Bubble(
shape = shape,
offset = Offset(x, y),
radius = radius,
width = radius * 3,
angle = Random.nextFloat() * (PI / 2) + PI + (PI * 1 / 4)
)
}
)
}
LaunchedEffect(Unit) {
while (isActive) {
awaitFrame()
bubbleState = bubbleState.map { bubble ->
bubble.update(
Size(
screenWidthPx,
screenHeightPx
)
)
bubble
}
delay(16L)
}
}
Box(
modifier = Modifier
.fillMaxSize()
.background(
brush = Brush.verticalGradient(
colors = listOf(
Color(0xFF007FDE),
Color(0xFF004CC8)
)
)
)
) {
for (bubble in bubbleState) {
val offsetX = with(density) {
bubble.offset.x.toDp()
}
val offsetY = with(density) {
bubble.offset.y.toDp()
}
val radiusDp = with(density) {
bubble.radius.toDp()
}
Box(
modifier = Modifier
.size(radiusDp * 2)
.offset(
x = offsetX,
y = offsetY
)
.scale(
if (bubble.shape == CircleShape) {
bubble.scale
} else {
1f
}
)
.background(
color = Color.White.copy(alpha = 0.15f),
shape = bubble.shape
)
)
}
}
}
class Bubble(
val shape: Shape,
offset: Offset,
val radius: Float,
val width: Float,
angle: Double,
) {
var offset by mutableStateOf(offset)
private var angle by mutableDoubleStateOf(angle)
var scale by mutableFloatStateOf(Random.nextFloat() * 0.4f + 0.6f)
private var range = (offset.x - width / 2)..(offset.x + width / 2)
private val increment = radius / 25f
private var scaleIncrement = -0.001f
fun update(
screenSize: Size,
) {
val newX = offset.x + increment * cos(angle).toFloat()
val newY = offset.y + increment * sin(angle).toFloat()
offset = if (newX < 0 || newX > screenSize.width || newY < 0) {
val x = Random.nextFloat() * screenSize.width
val y = screenSize.height + radius
range = (x - width / 2)..(x + width / 2)
Offset(x, y)
} else if (newX !in range) {
angle = (3 * PI / 2 - angle) + (3 * PI / 2)
Offset(
x = newX + cos(angle).toFloat(),
y = newY
)
} else {
Offset(newX, newY)
}
scale += scaleIncrement
if (scale <= 0.6f || scale >= 1f) {
scaleIncrement = -scaleIncrement
}
}
}