Ví dụ về ảnh động nâng cao: Cử chỉ

Có một số điều mà chúng ta phải cân nhắc khi thao tác với cả với các sự kiện chạm và ảnh động, so với khi chúng ta chỉ thao tác với các ảnh động. Trước hết, chúng ta có thể cần làm gián đoạn ảnh động đang diễn ra khi các sự kiện chạm bắt đầu do tương tác của người dùng sẽ có mức độ ưu tiên cao nhất.

Trong ví dụ bên dưới, chúng tôi sử dụng Animatable để thể hiện cho vị trí chênh lệch của thành phần vòng kết nối. Các sự kiện nhấn được xử lý bằng công cụ sửa đổi pointerInput. Khi phát hiện một sự kiện nhấn mới, chúng ta gọi lệnh animateTo để tạo ảnh động cho giá trị chênh lệch vào vị trí nhấn. Một sự kiện chạm cũng có thể xảy ra trong quá trình tạo ảnh động và trong trường hợp đó, animateTo làm gián đoạn ảnh động đang diễn ra và chạy ảnh động đó đến vị trí mục tiêu mới trong khi vẫn duy trì tốc độ của ảnh động bị gián đoạn.

@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())

Một mẫu thường gặp khác là chúng ta cần đồng bộ hoá các giá trị ảnh động với các giá trị hình thành từ các sự kiện chạm, chẳng hạn như kéo. Trong ví dụ bên dưới, chúng ta thấy tính năng "vuốt để loại bỏ" được triển khai dưới dạng Modifier (thay vì sử dụng thành phần kết hợp SwipeToDismiss). Độ chênh lệch chiều ngang của thành phần được biểu thị dưới dạng Animatable. API này có một đặc điểm hữu ích trong ảnh động cử chỉ. Sự kiện nhấn cũng như nội dung ảnh động có thể thay đổi giá trị của API. Khi nhận được một sự kiện nhấn, chúng ta sẽ dừng Animatable bằng phương thức stop để có thể chặn mọi ảnh động đang phát.

Trong một sự kiện kéo, chúng ta sử dụng snapTo để cập nhật giá trị Animatable bằng giá trị được tính từ các sự kiện nhấn. Để vuốt nhanh, công cụ Compose cung cấp VelocityTracker để ghi lại các sự kiện kéo và tính tốc độ. Vận tốc có thể được cung cấp trực tiếp cho animateDecay để tạo ảnh động vuốt nhanh. Khi muốn trượt giá trị độ dời trở về vị trí ban đầu, chúng ta chỉ định giá trị chênh lệch mục tiêu của 0f bằng phương thức animateTo.

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) }
}