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.
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 Interaction
s 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 HoverInteraction
s. 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 :
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 instancesModifier.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 autreModifier.Node
Modifier.indication
: Modificateur qui afficheIndication
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 seulementInteraction
, mais vous pouvez également dessiner des effets visuels pour lesInteraction
qu'ils émettre. Dans les cas simples, vous pouvez simplement utiliserModifier.clickable
sans nécessitantModifier.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:
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 remplacerContentDrawScope#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écepteurContentDrawScope
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 deIndication
appellent toujoursdrawContent()
à 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() } } }
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 }
Modifier.clickable
utiliseModifier.indication
en interne. Par conséquent, pour créer une composant cliquable avecScaleIndication
, il vous suffit de fournir leIndication
en tant que paramètre declickable
: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!") }
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:
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.
Recommandations personnalisées
- Remarque : Le texte du lien s'affiche lorsque JavaScript est désactivé
- Comprendre les gestes
- Kotlin pour Jetpack Compose
- Composants et mises en page Material