Gelişmiş animasyon örneği: Hareketler

Yalnızca animasyonlarla çalışırken dikkate almamız gerekenlere kıyasla, dokunma etkinlikleri ve animasyonlarla çalışırken dikkate almamız gereken birkaç nokta vardır. Öncelikle, kullanıcı etkileşimi en yüksek önceliğe sahip olduğundan dokunma etkinlikleri başladığında devam eden bir animasyonu durdurmamız gerekebilir.

Aşağıdaki örnekte, bir daire bileşeninin kaydırma konumunu temsil etmek için Animatable kullanılmıştır. Dokunma etkinlikleri, pointerInput değiştiricisiyle işlenir. Yeni bir dokunma etkinliği algıladığımızda, animateTo işlevini çağırarak kaydırma değerini dokunma konumuna göre animasyonlandırırız. Animasyon sırasında da dokunma etkinliği gerçekleşebilir. Bu durumda animateTo, devam eden animasyonu kesintiye uğratır ve kesintiye uğrayan animasyonun hızını koruyarak animasyonu yeni hedef konumuna doğru 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 dokunma etkinliklerinden (ör. sürükleme) gelen değerlerle senkronize etmemiz gerektiğidir. Aşağıdaki örnekte, "kaydırarak kapatma" özelliğinin Modifier olarak uygulandığını görüyoruz (SwipeToDismiss composable'ı kullanılmamıştır). Öğenin yatay konumu Animatable olarak gösterilir. Bu API, hareket animasyonunda kullanışlı bir özelliğe sahiptir. Değeri, dokunma etkinliklerinin yanı sıra animasyonla da değiştirilebilir. Bir dokunma etkinliği aldığımızda, devam eden animasyonların kesilmesi için Animatable yöntemini kullanarak stop durdururuz.

Sürükleme etkinliği sırasında, dokunma etkinliklerinden hesaplanan değerle Animatable değerini güncellemek için snapTo kullanılır. Fling için Compose, sürükleme etkinliklerini kaydetmek ve hızı hesaplamak üzere VelocityTracker sağlar. Hız, aktarım animasyonu için doğrudan animateDecay'ya aktarılabilir. Kaydırma değerini orijinal konumuna geri kaydırmak istediğimizde, 0f hedef kaydırma değerini animateTo yöntemiyle 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) }
}