ตัวอย่างภาพเคลื่อนไหวขั้นสูง: ท่าทางสัมผัส

มีหลายสิ่งที่เราต้องพิจารณาเมื่อทำงานกับการโต้ตอบแบบสัมผัสและภาพเคลื่อนไหว เมื่อเทียบกับการทำงานกับภาพเคลื่อนไหวเพียงอย่างเดียว ก่อนอื่น เราอาจต้องขัดจังหวะภาพเคลื่อนไหวที่กำลังทำงานอยู่เมื่อการโต้ตอบแบบสัมผัสเริ่มต้นขึ้น เนื่องจากการโต้ตอบของผู้ใช้ควรมีลำดับความสำคัญสูงสุด

ในตัวอย่างต่อไปนี้ เราใช้ 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())

อีกรูปแบบหนึ่งที่พบบ่อยคือเราต้องซิงค์ค่าภาพเคลื่อนไหวกับค่าที่มาจากการโต้ตอบแบบสัมผัส เช่น การลาก ในตัวอย่างต่อไปนี้ เราจะเห็น "ปัดเพื่อปิด" ที่ใช้เป็น Modifier (แทนที่จะใช้ Composable SwipeToDismiss) ออฟเซ็ตแนวนอนขององค์ประกอบจะแสดงเป็น Animatable API นี้มีลักษณะเฉพาะที่เป็นประโยชน์ ในภาพเคลื่อนไหวของท่าทางสัมผัส ค่าของตัวแปรนี้สามารถเปลี่ยนแปลงได้โดยการโต้ตอบแบบสัมผัสและภาพเคลื่อนไหว เมื่อได้รับเหตุการณ์การแตะลง เราจะหยุด Animatable ด้วยเมธอด stop เพื่อให้ระบบขัดจังหวะภาพเคลื่อนไหวที่กำลังดำเนินอยู่

ในระหว่างเหตุการณ์การลาก เราใช้ snapTo เพื่ออัปเดตค่า Animatable ด้วยค่าที่คำนวณจากการโต้ตอบแบบสัมผัส สำหรับการเหวี่ยง Compose จะมี VelocityTracker เพื่อบันทึกเหตุการณ์การลากและคำนวณความเร็ว คุณสามารถป้อนความเร็วไปยัง animateDecay โดยตรงสำหรับภาพเคลื่อนไหวแบบเหวี่ยง เมื่อต้องการเลื่อนค่าออฟเซ็ตกลับไปยังตำแหน่งเดิม เราจะระบุค่าออฟเซ็ตเป้าหมายเป็น 0f ด้วยเมธอด 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) }
}