Exemple d'animation avancée: gestes

Plusieurs points sont à prendre en compte lorsque nous utilisons des événements tactiles et des animations, et pas seulement des animations. Tout d'abord, nous devrons peut-être interrompre une animation en cours lorsque les événements tactiles commencent, car la priorité doit être donnée à l'interaction de l'utilisateur.

Dans l'exemple ci-dessous, nous utilisons un Animatable pour représenter la position décalée d'un composant de cercle. Les événements tactiles sont traités avec le modificateur pointerInput. Lorsque nous détectons un nouvel événement d'appui, nous appelons animateTo pour animer la valeur de décalage jusqu'à la position de l'appui. Un événement d'appui peut également se produire pendant l'animation. Dans ce cas, animateTo interrompt l'animation en cours et la lance à la nouvelle position cible, tout en maintenant la vitesse de l'animation interrompue.

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

Il est aussi courant de devoir synchroniser les valeurs d'animation avec les valeurs issues d'événements tactiles, tels qu'un déplacement. Dans l'exemple ci-dessous, l'animation "Balayer pour fermer la vue" est implémentée en tant que Modifier (au lieu d'utiliser le composable SwipeToDismiss). Le décalage horizontal de l'élément est représenté par un Animatable. Cette API présente une caractéristique utile dans les animations par gestes. Sa valeur peut être modifiée par les événements tactiles et l'animation. Lorsque nous recevons un événement tactile, nous arrêtons Animatable par la méthode stop afin d'interrompre toute animation en cours.

Lors d'un événement de déplacement, nous utilisons snapTo pour mettre à jour la valeur Animatable avec la valeur calculée à partir des événements tactiles. Pour le glissement d'un geste vif, Compose fournit VelocityTracker afin d'enregistrer les événements de déplacement et de calculer la vitesse. La vitesse peut être transmise directement à animateDecay pour cette animation de glissement. Lorsque nous voulons faire glisser à nouveau la valeur décalée vers sa position d'origine, nous spécifions la valeur décalée cible de 0f avec la méthode 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) }
}