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

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

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