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

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

ในตัวอย่างด้านล่าง เราใช้ 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 (แทนที่จะใช้ SwipeToDismiss ที่ประกอบได้) ออฟเซ็ตแนวนอนขององค์ประกอบจะแสดงเป็น Animatable API นี้มีลักษณะที่ใช้ประโยชน์ในการเคลื่อนไหวท่าทางได้ ค่าของ แอตทริบิวต์นี้สามารถเปลี่ยนแปลงได้โดยเหตุการณ์การแตะและภาพเคลื่อนไหว เมื่อได้รับเหตุการณ์ การแตะลง เราจะหยุด Animatable ด้วยวิธี stop เพื่อให้ การเคลื่อนไหวที่กำลังดำเนินอยู่ถูกขัดจังหวะ

ในระหว่างเหตุการณ์การลาก เราจะใช้ snapTo เพื่ออัปเดตค่า Animatable ด้วยค่าที่คำนวณจากเหตุการณ์การแตะ สำหรับ Fling, 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) }
}