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

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

ในตัวอย่างด้านล่าง เราใช้ 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 Composable) ออฟเซ็ตแนวนอนขององค์ประกอบจะแสดงเป็น 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) }
}