이 마지막 섹션에서는 터치 입력을 기반으로 애니메이션을 실행하는 방법에 대해 알아보자. 이 시나리오에서 고려해야 할 몇 가지 고유한 사항이 있다. 첫째, 진행 중인 애니메이션이 터치 이벤트에 의해 가로챌 수 있다. 둘째, 애니메이션 값이 유일한 정보 소스가 아닐 수도 있다. 즉, 애니메이션 값을 터치 이벤트에서 오는 값과 동기화해야 할 수도 있습니다.
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) }
}
앱을 실행하고 작업 아이템 중 하나를 살짝 밀어보자 요소가 플링 속도에 따라, 기본 위치로 다시 미끄러지고, 멀어지고, 제거되는 것을 볼 수 있다. 애니메이션이 진행되는 동안 해당 요소를 붙잡을 수도 있다.
0개의 댓글