
잘 보이는지 모르겠지만 포켓몬 슬립에는 내가 터치할 때마다 저렇게 파란색으로 리플 효과가 나타난다. 이 효과를 만들어 볼 것이다.
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을 옮겼다.
처음에는 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)