[Android] 물결 효과 만들기

uuranus·2024년 8월 12일
0
post-thumbnail

물결 효과 공식

wave 공식

파도의 봉우리와 골짜기의 위치를 가지고 이 위치의 y값을 변경하면서 파도가 흐르는 것처럼 보이게 할 것이다.
점들의 개수는 interval의 간격에 의해 설정해주었다.
이 점들을 간격점들이라고 부를 것이다.

progress

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])
        }

    }
}
profile
Frontend Developer

0개의 댓글