[Android] 당길 수 있는 시계추 효과

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

기존 애니메이션

지난 번에는 유튜브 구독설정 후 나타나는 알림설정 애니메이션을 만들었었다.

오늘은 여기에 사용자가 당겼다가 놓으면 시계추처럼 흔들리도록 인터렉션을 넣어볼 것이다.

당길 수 있도록

Box(
    modifier = modifier
        .fillMaxSize()
        .onGloballyPositioned {
            size = it.size
        }
        .pointerInput(Unit) {
            detectDragGestures(
                onDragEnd = {
                    isHanging = true
                    pendulum.startSwinging()
                },
                onDragStart = { position ->
                    isHanging = false
                    startPosition = position
                },
                onDrag = { change, _ ->
                    endPosition = change.position
                    val angle = calculateAngleDegree(startPosition, endPosition, size)
                    animation.updateInitialAngleDegree(angle)
                }
            )
        },
    contentAlignment = Alignment.Center,
) {
    pendulum.Draw(this)
}

detectDragGestures을 이용해서 드래그를 감지하고
onDragStart와 onDrag를 통해 어느 방향으로 어느 정도 드래그를 했는지를 계산하고 onDragEnd가 되면 hanging 애니메이션이 시작되도록 하였다.

각도 계산하기

private fun calculateAngleDegree(startPosition: Offset, endPosition: Offset, size: IntSize): Float {
    val centerX = size.width / 2f
    val centerY = size.height / 2f

    val startDelta = atan2(
        startPosition.y - centerY,
        startPosition.x - centerX
    )
    val endDelta = atan2(
        endPosition.y - centerY,
        endPosition.x - centerX
    )

    var angleInRadians = endDelta - startDelta

    if (angleInRadians > Math.PI.toFloat()) {
        angleInRadians -= 2 * Math.PI.toFloat()
    } else if (angleInRadians < -Math.PI.toFloat()) {
        angleInRadians += 2 * Math.PI.toFloat()
    }

    var angleInDegrees = angleInRadians * (180 / Math.PI).toFloat()

    angleInDegrees = angleInDegrees.coerceIn(-90f, 90f)

    return angleInDegrees
}

(centerX, centerY)를 기준으로 startPosition, endPosition을 통해서 어느 각도만큼 기울었는지를 계산하고 -90도 ~ 90도 범위 내로 제한하였다.

특정 위치를 중심으로 드래그

@Composable
override fun Draw(boxScope: BoxScope) {
    val iconSize = with(LocalDensity.current) {
        minOf(size.width, size.height).toDp()
    }

    val radius = with(LocalDensity.current) {
        iconSize.toPx() / 2
    }

    Box(
        modifier = Modifier
            .size(iconSize)
    ) {
        Icon(
            imageVector = Icons.Outlined.Notifications,
            contentDescription = null,
            modifier = Modifier
                .size(iconSize)
                .graphicsLayer {
                    val centerY = size.height / 2
                    val angleDegree = animation.getCurrentRotationDegree()
                    val angleRadians = angleDegree.toRadian()

                    this.rotationZ = angleDegree
                    this.translationX = -radius * sin(angleRadians)
                    this.translationY = -centerY + radius * cos(angleRadians)
                }
        )
    }
}

rotationZ는 중심좌표를 기준으로 회전하기 때문에 상단 중앙을 기준으로 돌아가도록 하고 싶으면 translationX, Y를 통해서 이동을 해주어야 한다.

이렇게 구현하면

이런 결과가 나온다!

애니메이션 적용

이제 사용자가 Drag를 끝내면 시계추처럼 흔들리는 애니메이션을 적용해보자.
여기에 원리를 작성해두었으니 애니메이션 식이 궁금한 사람은 여기가서 보고 이 글에서는 변경된 식만 작성하였다.

class PendulumAnimation(
    val initialAngleDegree: Float = 20f,
    private val dampingFactor: Float = 0.6f,
) {
    private var startAngleDegree by mutableFloatStateOf(initialAngleDegree)
    private var currentAngleDegree by mutableFloatStateOf(startAngleDegree)

    private var rotationDegree by mutableFloatStateOf(currentAngleDegree)

    @Composable
    fun Start(trigger: Boolean) {
        StartHanging(isHanging = trigger)
    }

    fun updateInitialAngleDegree(updateAngleDegree: Float) {
        this.startAngleDegree = updateAngleDegree

        this.currentAngleDegree = updateAngleDegree
        this.rotationDegree = updateAngleDegree
    }

    @Composable
    fun StartHanging(isHanging: Boolean) {
        LaunchedEffect(isHanging) {
            if (isHanging) {
                while (abs(currentAngleDegree) >= 1f) {
                    animate(
                        initialValue = currentAngleDegree,
                        targetValue = 0f,
                        animationSpec = keyframes {
                            durationMillis = 300
                            0f at 0 with LinearEasing
                            currentAngleDegree at 75 with LinearEasing
                            0f at 150 with LinearEasing
                            -currentAngleDegree at 225 with LinearEasing
                            0f at 300 with LinearEasing
                        },
                    ) { value, _ ->
                        rotationDegree = value
                    }
                    currentAngleDegree *= dampingFactor
                }

            } else {
                currentAngleDegree = startAngleDegree
            }
        }
    }

    fun getCurrentRotationDegree() = rotationDegree

}

최종 결과

깃허브 링크

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

profile
Frontend Developer

0개의 댓글