Gelişmiş animasyon örneği: Hareketler

Yalnızca animasyonlarla çalıştığımız zamana kıyasla, dokunma etkinlikleri ve animasyonlarla çalışırken dikkate almamız gereken birkaç nokta vardır. Kullanıcı etkileşiminin en yüksek önceliğe sahip olması gerektiğinden, öncelikle 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ştiriciyle işlenir. Yeni bir dokunma etkinliği algıladığımızda, ofset değerini dokunma konumuna animasyon eklemek için animateTo yöntemini çağırırız. Animasyon sırasında da bir dokunma etkinliği meydana gelebilir. Bu durumda animateTo, devam eden animasyonu kesintiye uğratır ve kesintiye uğrayan animasyonun hızını korurken animasyonu yeni hedef konumda 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 diğer bir kalıp da, animasyon değerlerini sürükleme gibi dokunma etkinliklerinden gelen değerlerle senkronize etmemizdir. Aşağıdaki örnekte, "kapatmak için hızlıca kaydırma" işleminin bir Modifier olarak uygulandığını (SwipeToDismiss composable'ı kullanmak yerine) görüyoruz. Öğenin yatay uzaklığı Animatable ile gösterilir. Bu API, hareket animasyonunda faydalı bir özelliğe sahiptir. Değeri, animasyonun yanı sıra dokunma etkinlikleri tarafından değiştirilebilir. Bir dokunma etkinliği aldığımızda, devam eden animasyona müdahale edilmesi için Animatable işlemini stop yöntemiyle durdururuz.

Bir sürükleme etkinliği sırasında Animatable değerini, dokunma etkinliklerinden hesaplanan değerle güncellemek için snapTo kullanılır. Compose, sürükleme etkinliklerini kaydetmek ve hızı hesaplamak için VelocityTracker sunar. Kısa süreli animasyon için hız doğrudan animateDecay değerine beslenebilir. Ofset değerini tekrar orijinal konumuna kaydırmak istediğimizde, animateTo yöntemi ile 0f olan 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) }
}