Gelişmiş animasyon örneği: Hareketler

Dokunma etkinlikleri ve animasyonlarla çalışırken, yalnızca animasyonlarla çalışmaya kıyasla dikkate almamız gereken birkaç nokta vardır. Öncelikle, kullanıcı etkileşiminin en yüksek önceliğe sahip olması gerektiğinden, dokunma etkinlikleri başladığında devam eden bir animasyonu kesintiye uğratmamız gerekebilir.

Aşağıdaki örnekte, bir daire bileşeninin ofset konumunu temsil etmek için Animatable kullanıyoruz. Dokunma etkinlikleri pointerInput değiştiricisiyle işlenir. Yeni bir dokunma etkinliği algıladığımızda, ofset değerini dokunma konumuna animasyonla taşımak için animateTo işlevini çağırırız. Dokunma etkinliği animasyon sırasında da gerçekleşebilir. Bu durumda animateTo, devam eden animasyonu kesintiye uğratır ve kesintiye uğrayan animasyonun hızını koruyarak yeni hedef konuma doğru animasyonu başlatır.

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

Sık karşılaşılan bir diğer durum da animasyon değerlerini sürükleme gibi dokunma etkinliklerinden gelen değerlerle senkronize etmemiz gerektiğidir. Aşağıdaki örnekte, "kapatmak için kaydır" özelliğinin SwipeToDismiss composable'ı yerine Modifier olarak uygulandığını görüyoruz. Öğenin yatay ofseti Animatable olarak gösterilir. Bu API, hareket animasyonunda faydalı bir özelliğe sahiptir. Değeri, animasyonun yanı sıra dokunma etkinlikleriyle de değiştirilebilir. Dokunma etkinliği aldığımızda, devam eden animasyonların kesintiye uğraması için stop yöntemiyle Animatable öğesini durdururuz.

Sürükleme etkinliği sırasında, Animatable değerini dokunma etkinliklerinden hesaplanan değerle güncellemek için snapTo kullanırız. Kaydırma için Compose, sürükleme etkinliklerini kaydetmek ve hızı hesaplamak için VelocityTracker sağlar. Hız, kaydırma animasyonu için doğrudan animateDecay öğesine beslenebilir. Ofset değerini orijinal konumuna geri kaydırmak istediğimizde animateTo yöntemiyle 0f hedef ofset değerini belirtiriz.

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