[Android] 리플 효과 포인터

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

기존 애니메이션 분석

기존 애니메이션
잘 보이는지 모르겠지만 포켓몬 슬립에는 내가 터치할 때마다 저렇게 파란색으로 리플 효과가 나타난다. 이 효과를 만들어 볼 것이다.

터치 할 때마다 그리기

val ripple = remember {
    Ripple(
        radius = 100f,
        color = Color.Blue
    )
}

MyCanvas(
    modifier = modifier
        .fillMaxSize()
        .pointerInput(Unit) {
            detectTapGestures { touchPosition ->
                ripple.setCenter(touchPosition)
            }
        }
) {
    ripple.draw(it)
}

pointerInput을 통해서 touch를 할 때마다 ripple의 위치값을 변경해주었다.

Ripple은

class Ripple(
    private val radius: Float,
    private val color: Color,
) : Picture {

    private var centerOffset by mutableStateOf(Offset.Unspecified)

    fun setCenter(offset: Offset) {
        centerOffset = offset
    }
}

이렇게 생성하였고 center는 state로 생성하여서 composable이 변화를 감지하도록 하였다.

그럼 이렇게 그려진다.

애니메이션 적용하기

이제 여기에 원의 크기가 커지는 애니메이션과 마지막에 사라지는 애니메이션을 적용할 것이다.

@Composable
fun Start(trigger: Boolean) {
    UpdateRingRadiusScale(trigger)
    UpdateRingAlpha(trigger)
}

@Composable
private fun UpdateRingRadiusScale(trigger: Boolean) {
    LaunchedEffect(trigger) {
        animate(
            initialValue = 0f,
            targetValue = 1.2f,
            animationSpec = tween(
                durationMillis = durationMillis,
                easing = LinearEasing
            ),
        ) { value, _ ->
            ringRadiusScale = value
        }
    }
}

@Composable
private fun UpdateRingAlpha(trigger: Boolean) {
    LaunchedEffect(trigger) {
        animate(
            initialValue = 1f,
            targetValue = 0f,
            animationSpec = tween(
                durationMillis = durationMillis,
                easing = LinearEasing
            ),
        ) { value, _ ->
            ringAlpha = value
        }
    }
}

이렇게 터치 위치가 변경될 때마다 애니메이션이 시작되는 클래스를 생성하고

@Composable
fun RippleEffect(
    modifier: Modifier = Modifier,
    radius: Float = 100f,
) {
    var touchPos by remember { mutableStateOf(Offset.Zero) }
 	var trigger by remember { mutableStateOf(false) }

    val animation = remember(trigger) {
        RippleAnimation(durationMillis = 500)
    }

    animation.Start(trigger)

    val ripple = remember(touchPos) {
        Ripple(
            radius = radius,
            color = Color(0xFF96FDFD),
            animation = animation
        )
    }

    MyCanvas(
        modifier = modifier
            .fillMaxSize()
            .pointerInput(Unit) {
                detectTapGestures { touchPosition ->
                    touchPos = touchPosition
                    trigger = !trigger
                }
            },
        backgroundColor = Color.LightGray
    ) {
        ripple.setCenter(touchPos)
        ripple.draw(it)
    }

}

터치값이 바뀔 때마다 trigger을 변경시켜서 애니메이션이 다시 호출되도록 하였다.

그럼 이렇게 된다!

마름모 그리기

 override fun draw(drawScope: DrawScope) {
    if (centerOffset == Offset.Unspecified) return

    var currentAngle = rhombusStartAngle

    repeat(3) {
        drawRhombus(drawScope, currentAngle)
        currentAngle += PI * 2 / 3
    }

    drawRing(drawScope)
}

120도씩 돌아가면서 마름모를 그려주었다.

private fun drawRhombus(drawScope: DrawScope, angle: Double) {
    val radius = radius * animation.getCurrentRhombusRadiusScale()
    val size = Size(radius / 3f, radius / 3f) * animation.getCurrentRhombusScale()

    val path = Path()
    val centerX = centerOffset.x + radius * cos(angle).toFloat()
    val centerY = centerOffset.y - radius * sin(angle).toFloat()

    val top = Offset(
        centerX + size.height / 2f * cos(angle).toFloat(),
        centerY - size.height / 2f * sin(angle).toFloat()
    )

    val bottom = Offset(
        centerX + size.height / 2f * cos(angle + PI).toFloat(),
        centerY - size.height / 2f * sin(angle + PI).toFloat()
    )

    val perpendicularAngle = angle + PI / 2

    val start = Offset(
        centerX + size.width / 2f * cos(perpendicularAngle).toFloat(),
        centerY - size.width / 2f * sin(perpendicularAngle).toFloat()
    )

    val end = Offset(
        centerX + size.width / 2f * cos(perpendicularAngle + PI).toFloat(),
        centerY - size.width / 2f * sin(perpendicularAngle + PI).toFloat()
    )

    path.apply {
        moveTo(start.x, start.y)
        lineTo(top.x, top.y)
        lineTo(end.x, end.y)
        lineTo(bottom.x, bottom.y)
        close()
    }

    drawScope.drawPath(
        path = path,
        color = accessoryColor,
    )
}

마름모는 다음과 같이 angle과 나란하게 그려주었다.

이렇게 했더니 startAngle이 계속 0.0으로 초기화되는 문제
-> 알고 보니 setCenter가 Canvas에 그려질 때마다 호출이 되어서 setAngle도 같이 되고 있었던 것
-> touchPos 이벤트가 들어오는 자리로 setAngle을 옮겼다.

애니메이션 설정

  • 마름모의 애니메이션도 따로 설정해주었다.
  • 어느정도 커지고 나서는 다시 작아지는 모션이 있었고 원보다는 살짝 바깥쪽에 배치되어 있는 것 같아서 scale과 radiusScale 두개를 설정해주었다.

같은 곳 여러번 선택

처음에는 touchPos를 매개변수로 했더니 동일한 곳을 여러 번 누르면 다시 트리거가 되지 않아서 trigger 변수를 통해 여러번 같은 곳을 클릭해도 애니메이션이 실행되도록 하였다.

var trigger by remember { mutableStateOf(false) }

val ripple = remember {
    Ripple(
        radius = radius,
        color = Color(0xFF96FDFD),
        accessoryColor = Color(0xFFD3FFFD),
    )
}

val animation = remember(trigger) {
    RippleAnimation(durationMillis = 500)
        .also {
            ripple.setAnimation(it)
        }
}

animation.Start(trigger)

최종 결과

깃허브 링크

https://github.com/uuranus/compose-nature-effects

profile
Frontend Developer

0개의 댓글