Gérer les interactions des utilisateurs

Les composants de l'interface utilisateur fournissent des informations à l'utilisateur de l'appareil en fonction de la manière dont ils répondent aux interactions de l'utilisateur. Chaque composant a sa propre façon de répondre aux interactions, ce qui permet à l'utilisateur de savoir ce que font ses interactions. Par exemple, si un utilisateur touche un bouton sur l'écran tactile d'un appareil, celui-ci est susceptible de changer d'une manière ou d'une autre, par exemple en ajoutant une couleur de surbrillance. Ce changement indique à l'utilisateur qu'il a appuyé sur le bouton. Si l'utilisateur ne voulait pas faire il saura qu'il doit faire glisser son doigt du bouton sinon le bouton s'active.

Figure 1. Les boutons apparaissent toujours activés, sans ondulation.
Figure 2. Boutons avec ondulations qui reflètent l'état activé en conséquence.

La documentation sur les gestes de Compose explique comment les composants Compose gèrent les événements de pointeur de bas niveau, tels que les mouvements de pointeur et les clics. Compose fait abstraction de ces événements de bas niveau en interactions de niveau supérieur sans la moindre configuration. Par exemple, une série d'événements de pointeur peut s'accumuler en appuyant de manière prolongée sur un bouton. Comprendre ces abstractions de niveau supérieur peut vous aider à personnaliser la manière dont votre UI répond à l'utilisateur. Par exemple, vous pouvez personnaliser l'apparence d'un composant lorsque l'utilisateur interagit avec lui ou simplement tenir à jour un journal de ces actions. Ce document fournit les informations dont vous avez besoin pour modifier les éléments d'UI standards ou concevoir les vôtres.

<ph type="x-smartling-placeholder">

Interactions

Dans de nombreux cas, vous n'avez pas besoin de savoir comment votre composant Compose interprète les interactions de l'utilisateur. Par exemple, Button s'appuie sur Modifier.clickable pour déterminer si l'utilisateur a cliqué sur le bouton. Si vous ajoutez un bouton standard à votre application, vous pouvez définir le code onClick du bouton et Modifier.clickable exécutera ce code le cas échéant. Vous n'avez donc pas besoin de savoir si l'utilisateur a appuyé sur l'écran ou a sélectionné le bouton avec un clavier, car Modifier.clickable détermine que l'utilisateur a effectué un clic et répond en exécutant votre code onClick.

Toutefois, si vous souhaitez personnaliser la réponse de votre composant d'UI au comportement des utilisateurs, vous devrez vous intéresser davantage à ce qui se passe en arrière-plan. Cette section traite de ce sujet.

Lorsqu'un utilisateur interagit avec un composant d'UI, le système représente son comportement en générant un certain nombre d'événements Interaction. Par exemple, si un utilisateur touche un bouton, celui-ci génère l'interaction PressInteraction.Press. Si l'utilisateur lève le doigt à partir du bouton, il génère l'interaction PressInteraction.Release, qui informe le bouton que le clic est terminé. En revanche, si l'utilisateur fait glisser son doigt à l'extérieur du bouton, puis le lève, le bouton génère l'interaction PressInteraction.Cancel pour indiquer que l'appui sur le bouton a été annulé.

Ces interactions sont non catégoriques. Autrement dit, ces événements d'interaction de bas niveau n'ont pas pour but d'interpréter la signification des actions de l'utilisateur ni leur ordre. Par ailleurs, ils n'interprètent pas les actions de l'utilisateur pouvant être prioritaires par rapport à d'autres actions.

Ces interactions sont généralement regroupées par paires, et présentent un début et une fin. La deuxième interaction contient une référence à la première. Par exemple, si un utilisateur appuie sur un bouton puis le relâche, cela génère les interactions PressInteraction.Press et PressInteraction.Release. Release présente une propriété press identifiant l'interaction PressInteraction.Press initiale.

Vous pouvez voir les interactions d'un composant particulier en observant son InteractionSource. InteractionSource est basé sur les flux Kotlin, ce qui vous permet de collecter les interactions à partir de celui-ci de la même manière que vous le feriez avec un autre flux. Pour en savoir plus sur cette décision de conception, consultez l'article de blog Illuminating Interactions (Interactions révélatrices).

État de l'interaction

Vous pouvez étendre les fonctionnalités intégrées de vos composants en suivant également les interactions vous-même. Par exemple, vous pouvez souhaiter qu'un bouton change de couleur lorsque l'utilisateur appuie dessus. Le moyen le plus simple de suivre les interactions consiste à observer l'état d'interaction approprié. InteractionSource propose un certain nombre de méthodes qui révèlent divers statuts d'interaction en tant qu'état. Par exemple, si vous souhaitez savoir si un utilisateur a appuyé sur un bouton, vous pouvez appeler sa méthode InteractionSource.collectIsPressedAsState() :

val interactionSource = remember { MutableInteractionSource() }
val isPressed by interactionSource.collectIsPressedAsState()

Button(
    onClick = { /* do something */ },
    interactionSource = interactionSource
) {
    Text(if (isPressed) "Pressed!" else "Not pressed")
}

Outre collectIsPressedAsState(), Compose fournit également collectIsFocusedAsState(), collectIsDraggedAsState() et collectIsHoveredAsState(). Il s'agit de méthodes pratiques, qui s'appuient sur des API InteractionSource de niveau inférieur. Dans certains cas, vous pouvez utiliser directement ces fonctions de niveau inférieur.

Par exemple, supposons que vous ayez besoin de savoir si l'utilisateur appuie sur un bouton et également s'il est déplacé. Si vous utilisez à la fois collectIsPressedAsState() et collectIsDraggedAsState(), Compose effectue beaucoup de tâches en double. Il n'est donc pas garanti que toutes les interactions soient exécutées dans le bon ordre. Pour de telles situations, vous pouvez travailler directement avec InteractionSource. Pour en savoir plus sur le suivi des interactions avec InteractionSource, consultez Utiliser InteractionSource.

La section suivante explique comment utiliser et émettre des interactions avec InteractionSource et MutableInteractionSource, respectivement.

Utiliser et émettre Interaction

InteractionSource représente un flux en lecture seule de Interactions. Il ne s'agit pas d'émettre un Interaction pour un InteractionSource. Pour émettre Interaction, vous devez utiliser un MutableInteractionSource, qui s'étend de InteractionSource

Les modificateurs et les composants peuvent utiliser, émettre ou consommer et émettre des Interactions. Les sections suivantes décrivent comment utiliser et émettre des interactions provenant à la fois des modificateurs et des composants.

Exemple d'utilisation du modificateur

Si un modificateur trace une bordure pour l'état sélectionné, il vous suffit d'observer Interactions, afin que vous puissiez accepter un InteractionSource:

fun Modifier.focusBorder(interactionSource: InteractionSource): Modifier {
    // ...
}

La signature de la fonction indique clairement que ce modificateur est un consommateur. peut consommer des Interaction, mais ne peut pas les émettre.

Exemple de génération d'un modificateur

Pour un modificateur qui gère les événements de pointage comme Modifier.hoverable, vous doivent émettre des Interactions et accepter un MutableInteractionSource en tant que à la place:

fun Modifier.hover(interactionSource: MutableInteractionSource, enabled: Boolean): Modifier {
    // ...
}

Ce modificateur est un producteur. Il peut utiliser le modèle MutableInteractionSource pour émettre HoverInteractions lorsqu'un utilisateur pointe dessus ou sans la souris.

Créer des composants qui consomment et produisent

Les composants de haut niveau, tels qu'un Button Material, agissent à la fois en tant que producteurs et les consommateurs. Ils gèrent les événements d'entrée et de sélection, et modifient leur apparence. en réponse à ces événements, par exemple en présentant une ondulation ou en animant leurs et l'élévation. Par conséquent, ils exposent directement MutableInteractionSource en tant que afin que vous puissiez fournir votre propre instance mémorisée:

@Composable
fun Button(
    onClick: () -> Unit,
    modifier: Modifier = Modifier,
    enabled: Boolean = true,

    // exposes MutableInteractionSource as a parameter
    interactionSource: MutableInteractionSource? = null,

    elevation: ButtonElevation? = ButtonDefaults.elevatedButtonElevation(),
    shape: Shape = MaterialTheme.shapes.small,
    border: BorderStroke? = null,
    colors: ButtonColors = ButtonDefaults.buttonColors(),
    contentPadding: PaddingValues = ButtonDefaults.ContentPadding,
    content: @Composable RowScope.() -> Unit
) { /* content() */ }

Cela permet de hisser MutableInteractionSource du composant et en observant toutes les Objets Interaction générés par le composant. Vous pouvez l'utiliser pour contrôler l'apparence de ce composant ou de tout autre composant dans votre interface utilisateur.

Si vous créez vos propres composants interactifs de haut niveau, nous vous recommandons que vous exposez MutableInteractionSource de cette manière en tant que paramètre. En plus en suivant les bonnes pratiques de hissage d'état, cela facilite également la lecture contrôler l'état visuel d'un composant de la même manière que toute autre (état activé, par exemple) peut être lu et contrôlé.

Compose suit une approche architecturale multicouche. Ainsi, les composants Material de haut niveau s'appuient sur les composants de base qui génèrent les Interactions nécessaires pour contrôler les ondulations et autres des effets visuels. La bibliothèque de base fournit des modificateurs d'interaction de haut niveau tels que Modifier.hoverable, Modifier.focusable et Modifier.draggable

Pour créer un composant qui répond aux événements de pointage, il vous suffit d'utiliser Modifier.hoverable et transmettez un MutableInteractionSource en tant que paramètre. Chaque fois que vous pointez sur le composant, il émet des HoverInteractions. Vous pouvez utiliser pour modifier l'apparence du composant.

// This InteractionSource will emit hover interactions
val interactionSource = remember { MutableInteractionSource() }

Box(
    Modifier
        .size(100.dp)
        .hoverable(interactionSource = interactionSource),
    contentAlignment = Alignment.Center
) {
    Text("Hello!")
}

Pour rendre ce composant sélectionnable, vous pouvez également ajouter Modifier.focusable et transmettre le même MutableInteractionSource qu'un paramètre. Les deux HoverInteraction.Enter/Exit et FocusInteraction.Focus/Unfocus sont émis. via le même MutableInteractionSource, et vous pouvez personnaliser pour les deux types d'interaction au même endroit:

// This InteractionSource will emit hover and focus interactions
val interactionSource = remember { MutableInteractionSource() }

Box(
    Modifier
        .size(100.dp)
        .hoverable(interactionSource = interactionSource)
        .focusable(interactionSource = interactionSource),
    contentAlignment = Alignment.Center
) {
    Text("Hello!")
}

Modifier.clickable est encore plus élevé de niveau d'abstraction que hoverable et focusable, pour qu'un composant soit cliquable, il l'est implicitement, et les composants sur lesquels il est possible de cliquer ne doivent également être sélectionnables. Vous pouvez utiliser Modifier.clickable pour créer un composant qui gère le survol, le focus et les interactions avec les pressions, sans avoir à combiner des éléments inférieurs au niveau des API. Si vous souhaitez également rendre votre composant cliquable, vous pouvez remplacez hoverable et focusable par clickable:

// This InteractionSource will emit hover, focus, and press interactions
val interactionSource = remember { MutableInteractionSource() }
Box(
    Modifier
        .size(100.dp)
        .clickable(
            onClick = {},
            interactionSource = interactionSource,

            // Also show a ripple effect
            indication = ripple()
        ),
    contentAlignment = Alignment.Center
) {
    Text("Hello!")
}

Travailler avec InteractionSource

Si vous avez besoin d'informations générales sur les interactions avec un composant, vous pouvez utiliser des API de flux standards pour l'InteractionSource de ce composant. Par exemple, supposons que vous souhaitiez conserver une liste des interactions d'appui et de glissement d'une InteractionSource. Ce code effectue la moitié de la tâche, en ajoutant les nouvelles pressions à mesure qu'elles se produisent :

val interactionSource = remember { MutableInteractionSource() }
val interactions = remember { mutableStateListOf<Interaction>() }

LaunchedEffect(interactionSource) {
    interactionSource.interactions.collect { interaction ->
        when (interaction) {
            is PressInteraction.Press -> {
                interactions.add(interaction)
            }
            is DragInteraction.Start -> {
                interactions.add(interaction)
            }
        }
    }
}

Toutefois, en plus d'ajouter les nouvelles interactions, vous devez également supprimer les interactions lorsqu'elles se terminent (par exemple, lorsque l'utilisateur lève le doigt du composant). Cette opération est facile à effectuer, car les interactions de fin contiennent toujours une référence à l'interaction de début associée. Ce code montre comment supprimer les interactions terminées :

val interactionSource = remember { MutableInteractionSource() }
val interactions = remember { mutableStateListOf<Interaction>() }

LaunchedEffect(interactionSource) {
    interactionSource.interactions.collect { interaction ->
        when (interaction) {
            is PressInteraction.Press -> {
                interactions.add(interaction)
            }
            is PressInteraction.Release -> {
                interactions.remove(interaction.press)
            }
            is PressInteraction.Cancel -> {
                interactions.remove(interaction.press)
            }
            is DragInteraction.Start -> {
                interactions.add(interaction)
            }
            is DragInteraction.Stop -> {
                interactions.remove(interaction.start)
            }
            is DragInteraction.Cancel -> {
                interactions.remove(interaction.start)
            }
        }
    }
}

Si vous souhaitez savoir si l'utilisateur appuie sur le composant ou le fait glisser, il vous suffit de vérifier si interactions est vide :

val isPressedOrDragged = interactions.isNotEmpty()

Si vous voulez savoir quelle a été la dernière interaction, regardez la dernière dans la liste. Par exemple, voici comment l'implémentation de l'ondulation Compose détermine la superposition d'état à utiliser pour l'interaction la plus récente:

val lastInteraction = when (interactions.lastOrNull()) {
    is DragInteraction.Start -> "Dragged"
    is PressInteraction.Press -> "Pressed"
    else -> "No state"
}

Étant donné que tous les éléments Interaction suivent la même structure, il n'y a pas beaucoup de la différence de code entre les différents types d'interactions de l'utilisateur : le schéma global est le même.

Notez que les exemples précédents de cette section représentent le Flow de des interactions à l'aide de State ce qui facilite l'observation des valeurs mises à jour, que la lecture de la valeur d'état entraînera automatiquement des recompositions. Toutefois, la composition est un pré-cadre par lots. Cela signifie que si l'état change, puis revient dans la même image, les composants qui observent l'état voir la modification.

Cela est important pour les interactions, car celles-ci peuvent commencer et se terminer régulièrement dans le même cadre. Par exemple, si l'on reprend l'exemple précédent avec Button:

val interactionSource = remember { MutableInteractionSource() }
val isPressed by interactionSource.collectIsPressedAsState()

Button(onClick = { /* do something */ }, interactionSource = interactionSource) {
    Text(if (isPressed) "Pressed!" else "Not pressed")
}

Si une pression commence et se termine dans le même cadre, le texte ne s'affichera jamais comme "Pressé !". Dans la plupart des cas, ce n'est pas un problème ; montrer un effet visuel pour une si courte durée entraînera un scintillement et ne sera pas visible par l'utilisateur. Dans certains cas, comme un effet d'ondulation ou une animation similaire, vous pouvez montrer l'effet pendant au moins un certain nombre au lieu de s'arrêter immédiatement si l'utilisateur n'appuie plus sur le bouton. À vous pouvez démarrer et arrêter directement les animations depuis la collecte lambda, au lieu d'écrire dans un état. Il y a un exemple de ce modèle dans Section Créer une Indication avancée avec bordure animée

Exemple: Composant de compilation avec gestion personnalisée des interactions

Pour voir comment créer des composants avec une réponse personnalisée à une entrée, voici un exemple de bouton modifié. Dans ce cas, supposons que vous souhaitiez qu'un bouton change d'apparence lorsque l'utilisateur appuie dessus :

Animation d&#39;un bouton qui ajoute de manière dynamique une icône de panier d&#39;épicerie lorsqu&#39;un utilisateur clique dessus
Figure 3. Bouton qui ajoute dynamiquement une icône lorsqu'un utilisateur clique dessus.

Pour ce faire, créez un composable personnalisé basé sur Button et demandez-lui d'utiliser un paramètre icon supplémentaire pour dessiner l'icône (dans ce cas, un panier). Vous appelez collectIsPressedAsState() pour savoir si l'utilisateur pointe le bouton. Le cas échéant, ajoutez l'icône. Voici ce à quoi ressemble le code :

@Composable
fun PressIconButton(
    onClick: () -> Unit,
    icon: @Composable () -> Unit,
    text: @Composable () -> Unit,
    modifier: Modifier = Modifier,
    interactionSource: MutableInteractionSource? = null
) {
    val isPressed = interactionSource?.collectIsPressedAsState()?.value ?: false

    Button(
        onClick = onClick,
        modifier = modifier,
        interactionSource = interactionSource
    ) {
        AnimatedVisibility(visible = isPressed) {
            if (isPressed) {
                Row {
                    icon()
                    Spacer(Modifier.size(ButtonDefaults.IconSpacing))
                }
            }
        }
        text()
    }
}

Et voici ce à quoi ressemble ce nouveau composable :

PressIconButton(
    onClick = {},
    icon = { Icon(Icons.Filled.ShoppingCart, contentDescription = null) },
    text = { Text("Add to cart") }
)

Comme ce nouveau PressIconButton est basé sur le Button Material existant, il réagit de manière habituelle aux interactions des utilisateurs. Lorsque l'utilisateur appuie sur le bouton, son opacité change légèrement, tout comme un Button Material ordinaire.

Créer et appliquer un effet personnalisé réutilisable avec Indication

Dans les sections précédentes, vous avez appris à modifier une partie d'un composant en réponse à différents Interaction (par exemple, afficher une icône lorsque l'utilisateur appuie dessus). Ce même peut être utilisée pour modifier la valeur des paramètres que vous fournissez ou de modifier le contenu affiché à l'intérieur d'un composant, applicable uniquement au niveau de chaque composant. Souvent, une application ou un système de conception aura un système générique pour les effets visuels avec état, un effet qui devrait appliqué à tous les composants de manière cohérente.

Si vous construisez ce type de système de conception, vous pouvez personnaliser un composant et il peut être difficile de réutiliser cette personnalisation pour d'autres composants pour les raisons suivantes:

  • Chaque composant du système de conception a besoin du même code récurrent
  • Il est facile d'oublier d'appliquer cet effet aux nouveaux composants et aux composants cliquables
  • Il peut être difficile de combiner l'effet personnalisé avec d'autres effets

Pour éviter ces problèmes et faire évoluer facilement un composant personnalisé dans votre système, vous pouvez utiliser Indication. Indication représente un effet visuel réutilisable qui peut être appliqué les composants d'une application ou d'un système de conception. Indication est divisé en deux parties:

  • IndicationNodeFactory: fabrique qui crée des instances Modifier.Node qui afficher des effets visuels pour un composant. Pour des implémentations plus simples qui ne sur plusieurs composants, il peut s'agir d'un singleton (objet) qui peut être réutilisé l'ensemble de l'application.

    Ces instances peuvent être avec ou sans état. Puisqu'ils sont créés par , ils peuvent récupérer des valeurs à partir d'un CompositionLocal pour modifier la façon dont apparaissent ou se comportent à l'intérieur d'un composant particulier, comme pour toute autre Modifier.Node

  • Modifier.indication: Modificateur qui affiche Indication pour une . Modifier.clickable et autres modificateurs d'interaction de haut niveau acceptent directement un paramètre d'indication, de sorte qu'ils émettent non seulement Interaction, mais vous pouvez également dessiner des effets visuels pour les Interaction qu'ils émettre. Dans les cas simples, vous pouvez simplement utiliser Modifier.clickable sans nécessitant Modifier.indication.

Remplacer l'effet par un Indication

Cette section explique comment remplacer un effet d'échelle manuel appliqué à une un bouton spécifique avec une indication équivalente qui peut être réutilisée sur plusieurs composants.

Le code suivant crée un bouton qui diminue vers le bas lorsque l'utilisateur appuie dessus:

val interactionSource = remember { MutableInteractionSource() }
val isPressed by interactionSource.collectIsPressedAsState()
val scale by animateFloatAsState(targetValue = if (isPressed) 0.9f else 1f, label = "scale")

Button(
    modifier = Modifier.scale(scale),
    onClick = { },
    interactionSource = interactionSource
) {
    Text(if (isPressed) "Pressed!" else "Not pressed")
}

Pour convertir l'effet d'échelle de l'extrait ci-dessus en Indication, suivez procédez comme suit:

  1. Créez le Modifier.Node responsable de l'application de l'effet d'échelle. Une fois connecté, le nœud observe la source d'interaction, de la même manière que exemples. La seule différence est qu'il lance directement les animations au lieu de convertir les interactions entrantes en états.

    Le nœud doit implémenter DrawModifierNode pour pouvoir remplacer ContentDrawScope#draw(), et afficher un effet d'échelle à l'aide du même dessin comme pour toute autre API graphique dans Compose.

    L'appel de drawContent() disponible à partir du récepteur ContentDrawScope génère le composant réel auquel l'Indication doit être appliquée. vous n'avez pas besoin d'appeler cette fonction dans une transformation d'échelle. Assurez-vous que votre Les implémentations de Indication appellent toujours drawContent() à un moment donné. Sinon, le composant auquel vous appliquez l'Indication ne sera pas dessiné.

    private class ScaleNode(private val interactionSource: InteractionSource) :
        Modifier.Node(), DrawModifierNode {
    
        var currentPressPosition: Offset = Offset.Zero
        val animatedScalePercent = Animatable(1f)
    
        private suspend fun animateToPressed(pressPosition: Offset) {
            currentPressPosition = pressPosition
            animatedScalePercent.animateTo(0.9f, spring())
        }
    
        private suspend fun animateToResting() {
            animatedScalePercent.animateTo(1f, spring())
        }
    
        override fun onAttach() {
            coroutineScope.launch {
                interactionSource.interactions.collectLatest { interaction ->
                    when (interaction) {
                        is PressInteraction.Press -> animateToPressed(interaction.pressPosition)
                        is PressInteraction.Release -> animateToResting()
                        is PressInteraction.Cancel -> animateToResting()
                    }
                }
            }
        }
    
        override fun ContentDrawScope.draw() {
            scale(
                scale = animatedScalePercent.value,
                pivot = currentPressPosition
            ) {
                this@draw.drawContent()
            }
        }
    }

  2. Créez le IndicationNodeFactory. Sa seule responsabilité est de créer nouvelle instance de nœud pour une source d'interaction fournie. Comme il n'y a pas pour configurer l'indication, la fabrique peut être un objet:

    object ScaleIndication : IndicationNodeFactory {
        override fun create(interactionSource: InteractionSource): DelegatableNode {
            return ScaleNode(interactionSource)
        }
    
        override fun equals(other: Any?): Boolean = other === ScaleIndication
        override fun hashCode() = 100
    }

  3. Modifier.clickable utilise Modifier.indication en interne. Par conséquent, pour créer une composant cliquable avec ScaleIndication, il vous suffit de fournir le Indication en tant que paramètre de clickable:

    Box(
        modifier = Modifier
            .size(100.dp)
            .clickable(
                onClick = {},
                indication = ScaleIndication,
                interactionSource = null
            )
            .background(Color.Blue),
        contentAlignment = Alignment.Center
    ) {
        Text("Hello!", color = Color.White)
    }

    Cela facilite également la création de composants de haut niveau réutilisables à l'aide d'un Indication : le bouton peut se présenter comme suit :

    @Composable
    fun ScaleButton(
        onClick: () -> Unit,
        modifier: Modifier = Modifier,
        enabled: Boolean = true,
        interactionSource: MutableInteractionSource? = null,
        shape: Shape = CircleShape,
        content: @Composable RowScope.() -> Unit
    ) {
        Row(
            modifier = modifier
                .defaultMinSize(minWidth = 76.dp, minHeight = 48.dp)
                .clickable(
                    enabled = enabled,
                    indication = ScaleIndication,
                    interactionSource = interactionSource,
                    onClick = onClick
                )
                .border(width = 2.dp, color = Color.Blue, shape = shape)
                .padding(horizontal = 16.dp, vertical = 8.dp),
            horizontalArrangement = Arrangement.Center,
            verticalAlignment = Alignment.CenterVertically,
            content = content
        )
    }

Vous pouvez ensuite utiliser le bouton comme suit:

ScaleButton(onClick = {}) {
    Icon(Icons.Filled.ShoppingCart, "")
    Spacer(Modifier.padding(10.dp))
    Text(text = "Add to cart!")
}

Animation d&#39;un bouton avec une icône de chariot de courses qui devient plus petit lorsque l&#39;utilisateur appuie dessus
Figure 4. Bouton créé avec un Indication personnalisé.

Créer une Indication avancée avec une bordure animée

Indication ne se limite pas aux effets de transformation, comme le scaling d'une . Comme IndicationNodeFactory renvoie un Modifier.Node, vous pouvez dessiner tout type d'effet au-dessus ou en dessous du contenu, comme avec les autres API de dessin. Pour Par exemple, vous pouvez dessiner une bordure animée autour du composant et une superposition sur en haut du composant lorsque vous appuyez dessus:

Bouton avec un effet arc-en-ciel fantaisie à la pression
Figure 5 : Effet de bordure animé dessiné avec Indication.

L'implémentation de Indication ici est très semblable à l'exemple précédent : un nœud avec certains paramètres est créé. Étant donné que la bordure animée dépend sur la forme et la bordure du composant pour lesquels Indication est utilisé, L'implémentation de Indication nécessite également de fournir la forme et la largeur de bordure comme paramètres:

data class NeonIndication(private val shape: Shape, private val borderWidth: Dp) : IndicationNodeFactory {

    override fun create(interactionSource: InteractionSource): DelegatableNode {
        return NeonNode(
            shape,
            // Double the border size for a stronger press effect
            borderWidth * 2,
            interactionSource
        )
    }
}

De plus, l'implémentation de Modifier.Node est conceptuellement la même, même si le dessiner du code est plus compliqué. Comme précédemment, elle observe InteractionSource. Lorsqu'il est associé, il lance des animations et implémente DrawModifierNode pour dessiner l'effet sur le contenu:

private class NeonNode(
    private val shape: Shape,
    private val borderWidth: Dp,
    private val interactionSource: InteractionSource
) : Modifier.Node(), DrawModifierNode {
    var currentPressPosition: Offset = Offset.Zero
    val animatedProgress = Animatable(0f)
    val animatedPressAlpha = Animatable(1f)

    var pressedAnimation: Job? = null
    var restingAnimation: Job? = null

    private suspend fun animateToPressed(pressPosition: Offset) {
        // Finish any existing animations, in case of a new press while we are still showing
        // an animation for a previous one
        restingAnimation?.cancel()
        pressedAnimation?.cancel()
        pressedAnimation = coroutineScope.launch {
            currentPressPosition = pressPosition
            animatedPressAlpha.snapTo(1f)
            animatedProgress.snapTo(0f)
            animatedProgress.animateTo(1f, tween(450))
        }
    }

    private fun animateToResting() {
        restingAnimation = coroutineScope.launch {
            // Wait for the existing press animation to finish if it is still ongoing
            pressedAnimation?.join()
            animatedPressAlpha.animateTo(0f, tween(250))
            animatedProgress.snapTo(0f)
        }
    }

    override fun onAttach() {
        coroutineScope.launch {
            interactionSource.interactions.collect { interaction ->
                when (interaction) {
                    is PressInteraction.Press -> animateToPressed(interaction.pressPosition)
                    is PressInteraction.Release -> animateToResting()
                    is PressInteraction.Cancel -> animateToResting()
                }
            }
        }
    }

    override fun ContentDrawScope.draw() {
        val (startPosition, endPosition) = calculateGradientStartAndEndFromPressPosition(
            currentPressPosition, size
        )
        val brush = animateBrush(
            startPosition = startPosition,
            endPosition = endPosition,
            progress = animatedProgress.value
        )
        val alpha = animatedPressAlpha.value

        drawContent()

        val outline = shape.createOutline(size, layoutDirection, this)
        // Draw overlay on top of content
        drawOutline(
            outline = outline,
            brush = brush,
            alpha = alpha * 0.1f
        )
        // Draw border on top of overlay
        drawOutline(
            outline = outline,
            brush = brush,
            alpha = alpha,
            style = Stroke(width = borderWidth.toPx())
        )
    }

    /**
     * Calculates a gradient start / end where start is the point on the bounding rectangle of
     * size [size] that intercepts with the line drawn from the center to [pressPosition],
     * and end is the intercept on the opposite end of that line.
     */
    private fun calculateGradientStartAndEndFromPressPosition(
        pressPosition: Offset,
        size: Size
    ): Pair<Offset, Offset> {
        // Convert to offset from the center
        val offset = pressPosition - size.center
        // y = mx + c, c is 0, so just test for x and y to see where the intercept is
        val gradient = offset.y / offset.x
        // We are starting from the center, so halve the width and height - convert the sign
        // to match the offset
        val width = (size.width / 2f) * sign(offset.x)
        val height = (size.height / 2f) * sign(offset.y)
        val x = height / gradient
        val y = gradient * width

        // Figure out which intercept lies within bounds
        val intercept = if (abs(y) <= abs(height)) {
            Offset(width, y)
        } else {
            Offset(x, height)
        }

        // Convert back to offsets from 0,0
        val start = intercept + size.center
        val end = Offset(size.width - start.x, size.height - start.y)
        return start to end
    }

    private fun animateBrush(
        startPosition: Offset,
        endPosition: Offset,
        progress: Float
    ): Brush {
        if (progress == 0f) return TransparentBrush

        // This is *expensive* - we are doing a lot of allocations on each animation frame. To
        // recreate a similar effect in a performant way, it would be better to create one large
        // gradient and translate it on each frame, instead of creating a whole new gradient
        // and shader. The current approach will be janky!
        val colorStops = buildList {
            when {
                progress < 1 / 6f -> {
                    val adjustedProgress = progress * 6f
                    add(0f to Blue)
                    add(adjustedProgress to Color.Transparent)
                }
                progress < 2 / 6f -> {
                    val adjustedProgress = (progress - 1 / 6f) * 6f
                    add(0f to Purple)
                    add(adjustedProgress * MaxBlueStop to Blue)
                    add(adjustedProgress to Blue)
                    add(1f to Color.Transparent)
                }
                progress < 3 / 6f -> {
                    val adjustedProgress = (progress - 2 / 6f) * 6f
                    add(0f to Pink)
                    add(adjustedProgress * MaxPurpleStop to Purple)
                    add(MaxBlueStop to Blue)
                    add(1f to Blue)
                }
                progress < 4 / 6f -> {
                    val adjustedProgress = (progress - 3 / 6f) * 6f
                    add(0f to Orange)
                    add(adjustedProgress * MaxPinkStop to Pink)
                    add(MaxPurpleStop to Purple)
                    add(MaxBlueStop to Blue)
                    add(1f to Blue)
                }
                progress < 5 / 6f -> {
                    val adjustedProgress = (progress - 4 / 6f) * 6f
                    add(0f to Yellow)
                    add(adjustedProgress * MaxOrangeStop to Orange)
                    add(MaxPinkStop to Pink)
                    add(MaxPurpleStop to Purple)
                    add(MaxBlueStop to Blue)
                    add(1f to Blue)
                }
                else -> {
                    val adjustedProgress = (progress - 5 / 6f) * 6f
                    add(0f to Yellow)
                    add(adjustedProgress * MaxYellowStop to Yellow)
                    add(MaxOrangeStop to Orange)
                    add(MaxPinkStop to Pink)
                    add(MaxPurpleStop to Purple)
                    add(MaxBlueStop to Blue)
                    add(1f to Blue)
                }
            }
        }

        return linearGradient(
            colorStops = colorStops.toTypedArray(),
            start = startPosition,
            end = endPosition
        )
    }

    companion object {
        val TransparentBrush = SolidColor(Color.Transparent)
        val Blue = Color(0xFF30C0D8)
        val Purple = Color(0xFF7848A8)
        val Pink = Color(0xFFF03078)
        val Orange = Color(0xFFF07800)
        val Yellow = Color(0xFFF0D800)
        const val MaxYellowStop = 0.16f
        const val MaxOrangeStop = 0.33f
        const val MaxPinkStop = 0.5f
        const val MaxPurpleStop = 0.67f
        const val MaxBlueStop = 0.83f
    }
}

La principale différence est qu'il existe désormais une durée minimale pour avec la fonction animateToResting(). Ainsi, même si l'utilisateur l'animation continue. Il y a aussi la gestion après plusieurs appuis rapides au début de animateToPressed, si une pression se produit pendant une animation d'appui ou de repos, l'animation précédente est est annulé et l'animation d'appui reprend depuis le début. Pour prendre en charge plusieurs des effets simultanés (comme avec des ondulations, où une nouvelle animation d'ondulations au-dessus des autres ondulations), vous pouvez suivre les animations dans une liste, annuler des animations existantes et en démarrer de nouvelles.