이 마지막 섹션에서는 터치 입력을 기반으로 애니메이션을 실행하는 방법에 대해 알아보자. 이 시나리오에서 고려해야 할 몇 가지 고유한 사항이 있다. 첫째, 진행 중인 애니메이션이 터치 이벤트에 의해 가로챌 수 있다. 둘째, 애니메이션 값이 유일한 정보 소스가 아닐 수도 있다. 즉, 애니메이션 값을 터치 이벤트에서 오는 값과 동기화해야 할 수도 있습니다.

swipeToDismiss Modifier에서 TODO 6-1을 찾자. 여기에서 요소를 터치로 스와이프할 수 있도록 하는 Modifier로 만들려고 한다. 요소가 화면 가장자리로 플링될 때, 요소를 제거할 수 있도록 onDismissed 콜백을 호출한다.

Animateable은 지금까지 본 것중 가장 로우 레벨 API다. 제스처 시나리오에서 유용한 몇 가지 기능이 있으므로, Animateable의 인스턴스를 만들고, 스와이프할 수 있는 요소의 수평 오프셋을 나타내는 데 사용하겠다.

val offsetX = remember { Animatable(0f) } // 이 라인을 추가한다.
pointerInput {
    // 플링 애니메이션이 멈추는 포지션을 계산 하기 위해 사용됨
    val decay = splineBasedDecay<Float>(this)
    // 코루틴 스코프로 감싸 터치 이벤트 및 애니메이션을 위한 suspend function을 사용한다.
    coroutineScope {
        while (true) {
            // ...

TODO 6-2는 터치다운 이벤트를 받은 곳이다. 애니메이션이 현재 실행 중이면 가로채야 한다. 이것은 Animateable에서 stop을 호출하여 수행할 수 있다. 애니메이션이 실행되고 있지 않으면 호출이 무시된다.

// 터치다운 이벤트를 기다린다.
val pointerId = awaitPointerEventScope { awaitFirstDown().id }
offsetX.stop() // 이 라인을 추가하자
// 드래그 이벤트를 준비하고 플링시의 속도를 기록한다. 
val velocityTracker = VelocityTracker()
// 드래그 이벤트를 기다린다.
awaitPointerEventScope {

TODO 6-3에서는 지속적으로 드래그 이벤트를 수신하고 있다. 터치 이벤트의 위치를 ​​애니메이션 값과 동기화해야 하므로, 이를 위해 Animateable에서 snapTo를 사용할 수 있다.

horizontalDrag(pointerId) { change ->
    // 여기에 4줄을 추가한다.
    val horizontalDragOffset = offsetX.value + change.positionChange().x
    launch {
        offsetX.snapTo(horizontalDragOffset)
    }
    // 드래그의 속도를 기록한다.
    velocityTracker.addPosition(change.uptimeMillis, change.position)
    // 제스쳐 이벤트를 소비하고, 외부로 전달시키지 않는다.
    change.consumePositionChange()
}

TODO 6-4는 해당 요소가 방금 릴리즈 되고 플링되었던 곳이다. 요소를 원래 위치로 다시 밀어야 하는지, 아니면 밀어서 콜백을 호출해야 하는지를 결정하기 위해 플링이 멈추는 최종 위치를 계산해야 한다.

// 드래그가 끝났다. 플링의 속도를 계산하자.
val velocity = velocityTracker.calculateVelocity().x
// 이 라인을 추가한다.
val targetOffsetX = decay.calculateTargetValue(offsetX.value, velocity) 

TODO 6-5에서는 애니메이션을 시작하려고 한다. 그러나 그 전에 Animateable에 상한 및 하한 값 경계(bound)를 설정하여, 경계에 도달하는 즉시 중지되도록 한다. pointerInput Modifier를 사용하면 size 속성으로 요소의 크기에 액세스할 수 있으므로 이를 사용하여 경계를 얻는다.

offsetX.updateBounds(
    lowerBound = -size.width.toFloat(),
    upperBound = size.width.toFloat()
)

TODO 6-6은 드디어 애니메이션을 시작할 수 있는 곳이다. 먼저 앞서 계산한 플링의 멈출 위치와 요소의 크기를 비교한다. 고정되는 위치가 사이즈 이하이면 플링의 속도가 충분하지 않다는 것을 의미한다. animateTo를 사용하여 값을 0f로 되돌릴 수 있다. 그렇지 않으면, 우리는 플링 애니메이션을 시작하기 위해 animateDecay를 사용한다. 애니메이션이 완료되면(대부분 이전에 설정한 경계에 따라) 콜백을 호출할 수 있다.

launch {
    if (targetOffsetX.absoluteValue <= size.width) {
        // 속도가 충분하지 않으면 슬라이드를 돌려놓는다.
        offsetX.animateTo(targetValue = 0f, initialVelocity = velocity)
    } else {
        // 요소를 가장자리로 밀어내기에 충분한 속도
        offsetX.animateDecay(velocity, decay)
        // 요소를 스와이프하여 제거했다.
        onDismissed()
    }
}

마지막으로 TODO 6-7을 참조하자. 모든 애니메이션과 제스처가 설정되었으므로, 요소에 오프셋을 적용하는 것을 잊지 말자.

.offset { IntOffset(offsetX.value.roundToInt(), 0) }

이 섹션의 결과로, 결국 다음과 같은 코드를 나타낸다.

private fun Modifier.swipeToDismiss(
    onDismissed: () -> Unit
): Modifier = composed {
    // 이 `Animatable` 요소에 대한 수평 오프셋을 저장한다.
    val offsetX = remember { Animatable(0f) }
    pointerInput(Unit) {
        // 플링 애니메이션에서 정착될 포지션을 계산하는데 사용된다.
        val decay = splineBasedDecay<Float>(this)
        // 코루틴 스코프로 감싸 터치이벤트 및 애니메이션에 대한 suspend 함수를 사용할 수 있도록 한다.
        coroutineScope {
            while (true) {
                // 터치 다운 이벤트를 기다린다.
                val pointerId = awaitPointerEventScope { awaitFirstDown().id }
                // 진행중인 이벤트를 멈춘다.
                offsetX.stop()
                // 드래그 이벤트를 위해 준비하고 플링 속도를 기록한다.
                val velocityTracker = VelocityTracker()
                // 드래그 이벤트를 기다린다.
                awaitPointerEventScope {
                    horizontalDrag(pointerId) { change ->
                        // 오프셋 이후 포지션을 기록한다.
                        val horizontalDragOffset = offsetX.value + change.positionChange().x
                        launch {
                            // 요소가 드래그 되는 동안 `Animatable` 값을 덮어쓴다.
                            offsetX.snapTo(horizontalDragOffset)
                        }
                        // 드래그 속도를 기록한다.
                        velocityTracker.addPosition(change.uptimeMillis, change.position)
                        // 제스처 이벤트를 소비하고 외부로 전달하지 않는다.
                        change.consumePositionChange()
                    }
                }
                // 드래그가 끝났다. 플링 속도를 계산한다.
                val velocity = velocityTracker.calculateVelocity().x
                // 플링 애니메이션 이후에 해당 요소가 궁극적으로 정착될 위치를 계산한다.
                val targetOffsetX = decay.calculateTargetValue(offsetX.value, velocity)
                // 애니메이션은 경계에 도달하자마자 끝나야 한다.
                offsetX.updateBounds(
                    lowerBound = -size.width.toFloat(),
                    upperBound = size.width.toFloat()
                )
                launch {
                    if (targetOffsetX.absoluteValue <= size.width) {
                        // 속도가 충분하지 않다; 기본 위치로 다시 슬라이드해 돌려놓는다.
                        offsetX.animateTo(targetValue = 0f, initialVelocity = velocity)
                    } else {
                        // 해당 요소를 가장자리로 밀어내기에 속도가 충분하다
                        offsetX.animateDecay(velocity, decay)
                        // 해당 요소가 스와이프 되었다.
                        onDismissed()
                    }
                }
            }
        }
    }
        // 해당요소에 수평 오프셋을 적용하자.
        .offset { IntOffset(offsetX.value.roundToInt(), 0) }
}

앱을 실행하고 작업 아이템 중 하나를 살짝 밀어보자 요소가 플링 속도에 따라, 기본 위치로 다시 미끄러지고, 멀어지고, 제거되는 것을 볼 수 있다. 애니메이션이 진행되는 동안 해당 요소를 붙잡을 수도 있다.

7cdefce823f6b9bd.png
카테고리: Compose

0개의 댓글

답글 남기기

Avatar placeholder

이메일은 공개되지 않습니다. 필수 입력창은 * 로 표시되어 있습니다.