
파도의 봉우리와 골짜기의 위치를 가지고 이 위치의 y값을 변경하면서 파도가 흐르는 것처럼 보이게 할 것이다.
점들의 개수는 interval의 간격에 의해 설정해주었다.
이 점들을 간격점들이라고 부를 것이다.
progress는 봉우리가 되는 위치의 점이다.
즉, progress 의 위치가 봉우리라는 전제하에 간격점들의 y값을 x좌표 차이에 의해서 설정해줄 것이다.
progress가 오른쪽으로 이동하면서 간격점들의 y값은 변경되고 이에 따라 파도가 흐르는 것처럼 보일 것이다.
val interval = (width / 3).toInt()
val waterLevel = with(density) {
20.dp.toPx()
}
val offsets = List(interval) { index ->
val xPosition = index * interval.toFloat()
val relativeX = (xPosition - progresses) / width * 2 * Math.PI
val yValue = sin(relativeX + Math.PI / 2) * waterLevel
mutableStateOf(
Offset(
interval * index.toFloat(),
height - waterHeightPx + yValue.toFloat()
)
)
}
waterlevel은 파도의 진폭이다. 20.dp면 봉우리와 골짜기의 간격이 40.dp인 것
Canvas(
modifier = modifier
.fillMaxSize()
.background(Color.White)
) {
val path = Path().apply {
moveTo(offsets[wave][0].value.x, offsets[wave][0].value.y)
for (i in 1 until offsets[wave].size - 1) {
val x = offsets[wave][i].value.x
val y = offsets[wave][i].value.y
val prevX = offsets[wave][i - 1].value.x
val prevY = offsets[wave][i - 1].value.y
val nextX = offsets[wave][i + 1].value.x
val nextY = offsets[wave][i + 1].value.y
cubicTo(
(prevX + x) / 2,
(prevY + y) / 2,
x,
y,
(x + nextX) / 2,
(y + nextY) / 2
)
}
lineTo(width, height)
lineTo(0f, height)
}
drawPath(path = path, color = Color.Blue)
}
간격점마다 progress와의 차이점에 대해서 현재 sin 그래프 상의 y값을 계산하고 양옆 간격점 사이의 중간값을 통해 베지어곡선으로 부드러운 곡선으로 그리도록 하였다.
val progress = progressAnimate.animateFloat(
initialValue = 0f,
targetValue = width,
animationSpec = infiniteRepeatable(
animation = tween(
durationMillis = 3000,
easing = LinearEasing
),
repeatMode = RepeatMode.Restart
),
label = ""
).value
출렁이는 물결 효과를 얻었다!
이제 뒤에 여러 개의 물결을 추가해서 입체감을 주도록 하자.
val progresses = List(numberOfWaves) { waveIndex ->
progressAnimate.animateFloat(
initialValue = -waveIndex.toFloat(),
targetValue = width - waveIndex.toFloat(),
animationSpec = infiniteRepeatable(
animation = tween(
durationMillis = 3000,
easing = LinearEasing
),
repeatMode = RepeatMode.Restart
),
label = ""
).value + (width / numberOfWaves) * waveIndex
}
각 물결마다 progress를 생성하고 시작 위치에 차이를 두어서 겹치지 않도록 하였다.
val offsets = List(numberOfWaves) { wave ->
List(interval) { index ->
val xPosition = index * interval.toFloat()
val relativeX = (xPosition - progresses[wave]) / width * 2 * Math.PI
val yValue = sin(relativeX + Math.PI / 2) * waterLevel
mutableStateOf(
Offset(
interval * index.toFloat(),
height - waterHeightPx + yValue.toFloat()
)
)
}
}
offset도 마찬가지로 물결마다 생성해주었다.
이제 이 물결을 로딩화면의 프로그래스로 사용해볼 것이다.
@Composable
fun WaveEffect(
modifier: Modifier = Modifier,
waterHeight: Dp,
numberOfWaves: Int = 3,
)
var previousWaterHeight by remember { mutableStateOf(waterHeight) }
val durationMillis = height / 100
val heightDifference by remember {
derivedStateOf {
with(density) {
abs(previousWaterHeight.toPx() - waterHeight.toPx())
}
}
}
val animatedWaterHeight by animateDpAsState(
targetValue = waterHeight,
animationSpec = tween(
durationMillis = (durationMillis * heightDifference).toInt(),
easing = LinearEasing
),
label = ""
)
height가 변경될 때 애니메이션을 적용하고 변경되는 height는 외부에서 적용해주었다.
var targetHeight by remember {
mutableStateOf(startHeight)
}
LaunchedEffect(Unit) {
while (targetHeight <= heightDp) {
targetHeight += 30.dp
delay(750)
}
}
WaveEffect(
modifier = Modifier
.width(300.dp)
.aspectRatio(1f)
.clip(CircleShape),
targetHeight = targetHeight
)
@Composable
fun WaveEffect(
modifier: Modifier = Modifier,
waterHeight: Dp,
numberOfWaves: Int = 3,
) = BoxWithConstraints(modifier = modifier) {
val density = LocalDensity.current
val height = with(density) {
maxHeight.toPx()
}
val width = with(density) {
maxWidth.toPx()
}
var previousWaterHeight by remember { mutableStateOf(waterHeight) }
val durationMillis = height / 100
val heightDifference by remember {
derivedStateOf {
with(density) {
abs(previousWaterHeight.toPx() - waterHeight.toPx())
}
}
}
val animatedWaterHeight by animateDpAsState(
targetValue = waterHeight,
animationSpec = tween(
durationMillis = (durationMillis * heightDifference).toInt(),
easing = LinearEasing
), label = ""
)
val waterHeightPx = with(density) {
animatedWaterHeight.toPx()
}
val interval = (width / 3).toInt()
val waterLevel = with(density) {
20.dp.toPx()
}
val progressAnimate = rememberInfiniteTransition(label = "")
val progresses = List(numberOfWaves) { waveIndex ->
progressAnimate.animateFloat(
initialValue = -waveIndex.toFloat(),
targetValue = width - waveIndex.toFloat(),
animationSpec = infiniteRepeatable(
animation = tween(
durationMillis = 3000,
easing = LinearEasing
),
repeatMode = RepeatMode.Restart
),
label = ""
).value + (width / numberOfWaves) * waveIndex
}
val offsets =
List(numberOfWaves) { wave ->
List(interval) { index ->
val xPosition = index * interval.toFloat()
val relativeX = (xPosition - progresses[wave]) / width * 2 * Math.PI
val yValue = sin(relativeX + Math.PI / 2) * waterLevel
mutableStateOf(
Offset(
interval * index.toFloat(),
height - waterHeightPx + yValue.toFloat()
)
)
}
}
LaunchedEffect(animatedWaterHeight) {
previousWaterHeight = waterHeight
}
Canvas(
modifier = modifier
.fillMaxSize()
.background(Color.White)
) {
val paths = List(numberOfWaves) { wave ->
Path().apply {
moveTo(offsets[wave][0].value.x, offsets[wave][0].value.y)
for (i in 1 until offsets[wave].size - 1) {
val x = offsets[wave][i].value.x
val y = offsets[wave][i].value.y
val prevX = offsets[wave][i - 1].value.x
val prevY = offsets[wave][i - 1].value.y
val nextX = offsets[wave][i + 1].value.x
val nextY = offsets[wave][i + 1].value.y
cubicTo(
(prevX + x) / 2,
(prevY + y) / 2,
x,
y,
(x + nextX) / 2,
(y + nextY) / 2
)
}
lineTo(width, height)
lineTo(0f, height)
}
}
for ((index, path) in paths.withIndex()) {
drawPath(path = path, color = image1Colors[index])
}
}
}