터치 이벤트와 애니메이션을 사용할 때는 애니메이션만 사용할 때에 비해 여러 가지 사항을 고려해야 합니다. 무엇보다도 사용자 상호작용의 우선순위가 가장 높아야 하므로 터치 이벤트가 시작될 때 진행 중인 애니메이션을 중단해야 할 수도 있습니다.
아래 예에서는 Animatable
을 사용하여 원 구성요소의 오프셋 위치를 나타냅니다. 터치 이벤트는 pointerInput
수정자로 처리됩니다. 새 탭 이벤트가 감지되면 animateTo
를 호출하여 오프셋 값을 탭 위치에 애니메이션 처리합니다. 애니메이션 도중에도 탭 이벤트가 발생할 수 있으며 이 경우 animateTo
는 진행 중인 애니메이션을 중단하고 중단된 애니메이션의 속도를 유지하면서 새 타겟 위치로 애니메이션을 시작합니다.
@Composable fun Gesture() { val offset = remember { Animatable(Offset(0f, 0f), Offset.VectorConverter) } Box( modifier = Modifier .fillMaxSize() .pointerInput(Unit) { coroutineScope { while (true) { // Detect a tap event and obtain its position. awaitPointerEventScope { val position = awaitFirstDown().position launch { // Animate to the tap position. offset.animateTo(position) } } } } } ) { Circle(modifier = Modifier.offset { offset.value.toIntOffset() }) } } private fun Offset.toIntOffset() = IntOffset(x.roundToInt(), y.roundToInt())
또 하나의 빈번한 패턴은 애니메이션 값을 드래그와 같은 터치 이벤트에서 발생하는 값과 동기화해야 한다는 것입니다. 아래 예에서는 '스와이프하여 닫기'가 SwipeToDismiss
컴포저블을 사용하는 대신 Modifier
로 구현됩니다. 요소의 가로 오프셋은 Animatable
로 표시됩니다. 이 API에는 동작 애니메이션에 유용한 특성이 있습니다. 특성의 값은 애니메이션은 물론 터치 이벤트에서도 변경할 수 있습니다. 터치 다운 이벤트가 수신되면 Animatable
이 stop
메서드를 사용하여 정지되고 진행 중인 애니메이션이 중단됩니다.
드래그 이벤트 중에 snapTo
를 사용하여 Animatable
값을 터치 이벤트에서 계산된 값으로 업데이트합니다. 플링의 경우 Compose는 드래그 이벤트를 기록하고 속도를 계산할 수 있도록 VelocityTracker
를 제공합니다. 플링 애니메이션의 경우 animateDecay
에 직접 속도를 제공할 수 있습니다. 오프셋 값을 원래 위치로 되돌리려는 경우 animateTo
메서드를 사용하여 0f
의 타겟 오프셋 값을 지정합니다.
fun Modifier.swipeToDismiss( onDismissed: () -> Unit ): Modifier = composed { val offsetX = remember { Animatable(0f) } pointerInput(Unit) { // Used to calculate fling decay. val decay = splineBasedDecay<Float>(this) // Use suspend functions for touch events and the Animatable. coroutineScope { while (true) { val velocityTracker = VelocityTracker() // Stop any ongoing animation. offsetX.stop() awaitPointerEventScope { // Detect a touch down event. val pointerId = awaitFirstDown().id horizontalDrag(pointerId) { change -> // Update the animation value with touch events. launch { offsetX.snapTo( offsetX.value + change.positionChange().x ) } velocityTracker.addPosition( change.uptimeMillis, change.position ) } } // No longer receiving touch events. Prepare the animation. val velocity = velocityTracker.calculateVelocity().x val targetOffsetX = decay.calculateTargetValue( offsetX.value, velocity ) // The animation stops when it reaches the bounds. offsetX.updateBounds( lowerBound = -size.width.toFloat(), upperBound = size.width.toFloat() ) launch { if (targetOffsetX.absoluteValue <= size.width) { // Not enough velocity; Slide back. offsetX.animateTo( targetValue = 0f, initialVelocity = velocity ) } else { // The element was swiped away. offsetX.animateDecay(velocity, decay) onDismissed() } } } } } .offset { IntOffset(offsetX.value.roundToInt(), 0) } }
추천 서비스
- 참고: JavaScript가 사용 중지되어 있으면 링크 텍스트가 표시됩니다.
- 가치 기반 애니메이션
- 드래그, 스와이프, 플링하기
- 동작 이해하기