Los componentes de la interfaz de usuario le proporcionan comentarios al usuario del dispositivo en función de cómo responde a las interacciones. Cada componente tiene su propia forma de responder a las interacciones, lo que ayuda al usuario a saber qué hacen sus interacciones. Por ejemplo, si un usuario toca un botón en la pantalla táctil de un dispositivo, es probable que este cambie de alguna manera, quizás agregando un color de resaltado. Este cambio le informa al usuario que tocó el botón. Si el usuario no quiere hacer sabrán que deben arrastrar el dedo fuera del botón antes de suelta; de lo contrario, el botón se activará.
![](https://developer.android.google.cn/static/develop/ui/compose/images/touchinput/button-always-enabled.gif?authuser=0000&hl=es)
![](https://developer.android.google.cn/static/develop/ui/compose/images/touchinput/button-reflects-state.gif?authuser=0000&hl=es)
En la documentación de Gestos de Compose, se explica cómo los componentes de Compose controlan un evento de puntero de bajo nivel, como los movimientos y los clics del puntero. Desde el primer momento, Compose abstrae esos eventos de bajo nivel en interacciones de nivel superior; por ejemplo, una serie de eventos de puntero podría sumar una acción de presionar y soltar. Entender esas abstracciones de nivel superior puede ayudarte a personalizar la manera en que responde tu IU al usuario. Por ejemplo, es posible que quieras personalizar cómo cambia la apariencia de un componente cuando el usuario interactúa con él o quizás solo quieres mantener un registro de esas acciones del usuario. Este documento te brinda la información que necesitas para modificar los elementos estándar de la IU o diseñar los propios.
Interacciones
En muchos casos, no necesitas saber la forma exacta en la que el componente de Compose interpreta las interacciones del usuario. Por ejemplo, Button
se basa en Modifier.clickable
para determinar si el usuario hizo clic en el botón. Si agregas un botón típico a tu app, puedes definir el código onClick
de ese botón, y Modifier.clickable
ejecutará ese código cuando corresponda. Eso significa que no necesitas saber si el usuario presionó la pantalla o seleccionó el botón con un teclado. Modifier.clickable
determina que el usuario hizo un clic y responde ejecutando el código onClick
.
Sin embargo, si deseas personalizar la respuesta de tu componente de IU al comportamiento del usuario, es posible que necesites saber más acerca de lo que ocurre en niveles más profundos. En esta sección, encontrarás parte de esa información.
Cuando un usuario interactúa con un componente de la IU, el sistema representa su comportamiento mediante la generación de varios eventos Interaction
. Por ejemplo, si un usuario toca un botón, este genera PressInteraction.Press
.
Si el usuario levanta el dedo dentro del botón, se genera un PressInteraction.Release
que informa que terminó el clic. Por otro lado, si el usuario arrastra el dedo fuera del botón y luego lo levanta, el botón genera PressInteraction.Cancel
para indicar que no se completó la acción de presionar el botón.
Estas interacciones no tienen tendencias. Es decir, estos eventos de interacción de bajo nivel no pretenden interpretar el significado de las acciones del usuario ni su secuencia. Tampoco interpretan qué acciones del usuario pueden tener prioridad sobre otras.
Estas interacciones suelen venir en pares, con un inicio y un fin. La segunda interacción contiene una referencia a la primera. Por ejemplo, si un usuario toca un botón y luego levanta el dedo, el toque genera una interacción PressInteraction.Press
y levantarlo genera una PressInteraction.Release
; la Release
tiene una propiedad press
que identifica la PressInteraction.Press
inicial.
Puedes ver las interacciones de un componente en particular si observas su InteractionSource
. InteractionSource
se basa en flujos de Kotlin, por lo que puedes recopilar las interacciones a partir de allí de la misma manera en que trabajarías con cualquier otro flujo. Para obtener más información
sobre esta decisión de diseño,
consulta la entrada de blog Illuminating Interactions.
Estado de las interacciones
Se recomienda que realices un seguimiento de las interacciones tú mismo para extender la funcionalidad integrada de tus componentes. Por ejemplo, tal vez quieras que un botón cambie de color cuando se presiona. La forma más sencilla de hacer un seguimiento de las interacciones es observar el estado de interacción apropiado. InteractionSource
ofrece una serie de métodos que revelan varios estados de interacción como estados. Por ejemplo, si quieres ver si se presiona un botón en particular, puedes llamar a su método InteractionSource.collectIsPressedAsState()
:
val interactionSource = remember { MutableInteractionSource() } val isPressed by interactionSource.collectIsPressedAsState() Button( onClick = { /* do something */ }, interactionSource = interactionSource ) { Text(if (isPressed) "Pressed!" else "Not pressed") }
Además de collectIsPressedAsState()
, Compose también proporciona collectIsFocusedAsState()
, collectIsDraggedAsState()
y collectIsHoveredAsState()
. En realidad, estos son métodos prácticos compilados sobre las APIs de InteractionSource
de nivel inferior. En algunos casos, es recomendable usar esas funciones de nivel inferior directamente.
Por ejemplo, supón que necesitas saber si se presiona un botón y también si se lo está arrastrando. Si usas collectIsPressedAsState()
y collectIsDraggedAsState()
, Compose realiza gran parte del trabajo por duplicado y no hay garantía de que obtengas todas las interacciones en el orden correcto. Para este tipo de situaciones, es recomendable trabajar directamente con InteractionSource
. Para obtener más información sobre el seguimiento de las interacciones
Con InteractionSource
, consulta Cómo trabajar con InteractionSource
.
En la siguiente sección, se describe cómo consumir y emitir interacciones con
InteractionSource
y MutableInteractionSource
, respectivamente.
Consumir y emitir Interaction
InteractionSource
representa una transmisión de solo lectura de Interactions
; no es
es posible emitir un Interaction
a un InteractionSource
. Para emitir
Interaction
, debes usar un MutableInteractionSource
, que se extiende desde
InteractionSource
Los modificadores y los componentes pueden consumir, emitir o consumir y emitir Interactions
.
En las siguientes secciones, se describe cómo consumir y emitir interacciones de ambos
modificadores y componentes.
Ejemplo de modificador de consumo
Para un modificador que dibuja un borde para el estado enfocado, solo debes observar
Interactions
para que puedas aceptar una InteractionSource
:
fun Modifier.focusBorder(interactionSource: InteractionSource): Modifier { // ... }
A partir de la firma de la función, queda claro que este modificador es un consumidor:
puede consumir Interaction
, pero no emitirlos.
Ejemplo de modificador de producción
Para un modificador que controla eventos de colocar el cursor sobre un elemento, como Modifier.hoverable
, puedes
debes emitir Interactions
y aceptar un MutableInteractionSource
como
en su lugar:
fun Modifier.hover(interactionSource: MutableInteractionSource, enabled: Boolean): Modifier { // ... }
Este modificador es un productor y puede usar el
MutableInteractionSource
para emitir HoverInteractions
cuando se coloca el cursor sobre él o
sin desplazarse.
Crear componentes que consuman y produzcan
Los componentes de alto nivel, como Button
de Material, actúan como productores y
a los consumidores. Controlan los eventos de entrada y enfoque, y también cambian su apariencia.
en respuesta a estos eventos, como mostrar una onda o animar su
elevación. Como resultado, exponen MutableInteractionSource
directamente como una
para que puedas proporcionar tu propia instancia recordada:
@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() */ }
Esto permite elevar la
MutableInteractionSource
del componente y observar todos los
Interaction
que produce el componente. Puedes usar esta opción para controlar
de ese componente o de cualquier otro componente de tu IU.
Si vas a compilar tus propios componentes interactivos de alto nivel, te recomendamos
que expongas MutableInteractionSource
como parámetro de esta manera. Además
de elevación de estado, esto también facilita la lectura y
controlar el estado visual de un componente de la misma manera que cualquier otro tipo de
(como el estado habilitado) se pueden leer y controlar.
Compose sigue un enfoque arquitectónico en capas,
por lo que los componentes de Material de alto nivel se construyen sobre la base de la construcción
bloques que producen los Interaction
que necesitan para controlar las ondas y otras
efectos visuales. La biblioteca de base proporciona modificadores de interacción de alto nivel
como Modifier.hoverable
, Modifier.focusable
y
Modifier.draggable
Para compilar un componente que responda a los eventos de colocar el cursor sobre un elemento, puedes usar
Modifier.hoverable
y pasa un elemento MutableInteractionSource
como parámetro.
Cuando se coloca el cursor sobre el componente, emite HoverInteraction
s, y puedes usar
esto para cambiar la forma en que aparece el componente.
// This InteractionSource will emit hover interactions val interactionSource = remember { MutableInteractionSource() } Box( Modifier .size(100.dp) .hoverable(interactionSource = interactionSource), contentAlignment = Alignment.Center ) { Text("Hello!") }
Para que este componente también sea enfocable, puedes agregar Modifier.focusable
y pasar
el mismo MutableInteractionSource
que un parámetro. Ambos
Se emiten HoverInteraction.Enter/Exit
y FocusInteraction.Focus/Unfocus
a través de la misma MutableInteractionSource
, y puedes personalizar la
para ambos tipos de interacción en el mismo lugar:
// 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
es un valor aún más alto
nivel de abstracción que hoverable
y focusable
, para que un componente sea
se puede hacer clic en él, se puede colocar el cursor sobre él implícitamente, y los componentes en los que se puede hacer clic deben
también deben ser enfocables. Puedes usar Modifier.clickable
para crear un componente que
maneja interacciones de desplazamiento, enfoque y presión, sin necesidad de combinar partes
APIs de nivel de servicio. Si también deseas que se pueda hacer clic en tu componente, puedes
Reemplaza hoverable
y focusable
por 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!") }
Trabaja con InteractionSource
Si necesitas información detallada sobre las interacciones con un componente, puedes usar las APIs de flujo estándar para el InteractionSource
de ese componente.
Por ejemplo, supongamos que quieres mantener una lista de las interacciones de presionar y arrastrar para un InteractionSource
. Este código realiza la mitad del trabajo y agrega las presiones nuevas a la lista a medida que se reciben:
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) } } } }
Sin embargo, además de agregar las interacciones nuevas, también debes quitarlas cuando terminan (por ejemplo, cuando el usuario levanta el dedo del componente). Es fácil de hacer, ya que las interacciones de fin siempre llevan una referencia a la interacción de inicio asociada. En el siguiente código, se muestra cómo quitar las interacciones que finalizaron:
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) } } } }
Ahora, si quieres saber si el componente se está presionando o arrastrando, lo único que debes hacer es comprobar si interactions
está vacío:
val isPressedOrDragged = interactions.isNotEmpty()
Si quieres saber cuál fue la interacción más reciente, consulta la última elemento de la lista. Por ejemplo, así es como la implementación de ondas de Compose calcula cuál es la superposición de estado apropiada que debe usarse para la interacción más reciente:
val lastInteraction = when (interactions.lastOrNull()) { is DragInteraction.Start -> "Dragged" is PressInteraction.Press -> "Pressed" else -> "No state" }
Debido a que todos los Interaction
siguen la misma estructura, no hay gran parte de un
diferencia de código cuando se trabaja con diferentes tipos de interacciones de usuario: el
el patrón general es el mismo.
Ten en cuenta que los ejemplos anteriores de esta sección representan el Flow
de
Interacciones con State
lo que facilita la observación de los valores actualizados,
ya que leer el valor del estado generará recomposiciones automáticamente. Sin embargo,
la composición es un fotograma previo por lotes. Esto significa que, si el estado cambia y
luego vuelve a cambiar dentro del mismo marco, los componentes que observan el estado no
a ver el cambio.
Esto es importante para las interacciones, ya que estas pueden comenzar y finalizar regularmente
dentro del mismo marco. Por ejemplo, si usamos el ejemplo anterior con Button
:
val interactionSource = remember { MutableInteractionSource() } val isPressed by interactionSource.collectIsPressedAsState() Button(onClick = { /* do something */ }, interactionSource = interactionSource) { Text(if (isPressed) "Pressed!" else "Not pressed") }
Si una presión comienza y termina dentro del mismo marco, el texto nunca se mostrará como
"¡Presionado!". En la mayoría de los casos, no se trata de un problema: se muestra un efecto visual en
una cantidad de tiempo tan pequeña hará que parpadee, y no será muy
notorio para el usuario. Para algunos casos, como mostrar un efecto de ondas o una
animación similar, es posible que quieras
mostrar el efecto por al menos
en lugar de detenerse de inmediato si ya no se presiona el botón. Para
Para eso, pueden iniciar y detener las animaciones directamente
lambda, en lugar de escribir en un estado. Hay un ejemplo de este patrón en
La sección Cómo compilar un Indication
avanzado con borde animado.
Ejemplo: Componente de compilación con manejo de interacciones personalizadas
Para ver cómo compilar componentes con una respuesta personalizada a la entrada, a continuación se muestra un ejemplo de un botón modificado. En este caso, supongamos que quieres un botón que responda a las pulsaciones cambiando la apariencia:
![Animación de un botón que agrega de forma dinámica un ícono de carrito de compras cuando se hace clic en él](https://developer.android.google.cn/static/develop/ui/compose/images/handling-interactions-iconbutton.gif?authuser=0000&hl=es)
Para ello, compila un elemento que admite composición personalizado, basado en Button
y haz que tome un parámetro icon
adicional a fin de dibujar el ícono (en este caso, un carrito de compras). Llamas a collectIsPressedAsState()
para realizar un seguimiento a fin de que el usuario pueda colocar el cursor sobre el botón. Cuando eso sucede, agregas el ícono. A continuación, se muestra el código:
@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() } }
Así se ve cómo se usa ese nuevo elemento que admite composición:
PressIconButton( onClick = {}, icon = { Icon(Icons.Filled.ShoppingCart, contentDescription = null) }, text = { Text("Add to cart") } )
Como este PressIconButton
nuevo se compila sobre el Button
de Material existente, reacciona a las interacciones del usuario de todas las maneras habituales. Cuando el usuario presiona el botón, cambia su opacidad ligeramente, como ocurre con un Button
común de Material.
Crea y aplica un efecto personalizado reutilizable con Indication
En secciones anteriores, aprendiste a cambiar parte de un componente en respuesta
a diferentes elementos Interaction
, como mostrar un ícono cuando se presiona. Este mismo
se puede usar para cambiar el valor de los parámetros que proporciona a un
componente o cambiar el contenido que se muestra dentro de un componente, pero esto es
solo se aplica por componente. A menudo, una aplicación o un sistema de diseño
tendrá un sistema genérico para efectos visuales con estado, un efecto que debería
aplicarse a todos los componentes de forma coherente.
Si estás creando este tipo de sistema de diseño, personalizando un componente y reutilizar esta personalización para otros componentes puede ser difícil siguientes motivos:
- Cada componente del sistema de diseño necesita el mismo código estándar
- Es fácil olvidarse de aplicar este efecto a los componentes recién compilados y a componentes en los que se puede hacer clic
- Puede ser difícil combinar el efecto personalizado con otros efectos.
Para evitar estos problemas y escalar con facilidad un componente personalizado en tu sistema,
puedes usar Indication
.
Indication
representa un efecto visual reutilizable que se puede aplicar en
en una aplicación o un sistema de diseño. Indication
se divide en dos
partes:
IndicationNodeFactory
: Una fábrica que crea instancias deModifier.Node
renderizar efectos visuales de un componente. Para implementaciones más simples que no cambian entre componentes, esto puede ser un singleton (objeto) que se puede reutilizar en toda la aplicación.Estas instancias pueden ser o no. Dado que se crean por , pueden recuperar valores de un
CompositionLocal
para cambiar aparecen o se comportan dentro de un componente en particular, como cualquier otroModifier.Node
Modifier.indication
: Un modificador que dibujaIndication
para una este componente.Modifier.clickable
y otros modificadores de interacción de alto nivel aceptan un parámetro de indicación directamente, de modo que no solo emitanInteraction
, pero también puede dibujar efectos visuales para lasInteraction
que emiten. Por lo tanto, para casos simples, puedes usarModifier.clickable
sin que necesitaModifier.indication
.
Cómo reemplazar el efecto por una Indication
En esta sección, se describe cómo reemplazar un efecto de escala manual aplicado a uno un botón específico con una indicación equivalente que puede reutilizarse en varias o los componentes de la solución.
El siguiente código crea un botón que escala verticalmente cuando se lo presiona:
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") }
Para convertir el efecto de escala del fragmento anterior en un Indication
, sigue estos pasos:
estos pasos:
Crea el
Modifier.Node
responsable de aplicar el efecto de escala. Cuando se conecta, el nodo observa la fuente de interacción, similar a la anterior ejemplos. La única diferencia es que inicia animaciones directamente. en lugar de convertir las interacciones entrantes en un estado.El nodo debe implementar
DrawModifierNode
para que pueda anularlo.ContentDrawScope#draw()
y renderiza un efecto de escala con el mismo dibujo al igual que con cualquier otra API de gráficos en Compose.Las llamadas a
drawContent()
disponibles desde el receptorContentDrawScope
dibujarán el componente real al que se debe aplicarIndication
, por lo que solo debes llamar a esta función dentro de una transformación a escala. Asegúrate de que Las implementaciones deIndication
siempre llaman adrawContent()
en algún momento. De lo contrario, no se dibujará el componente al que estás aplicandoIndication
.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() } } }
Crea el
IndicationNodeFactory
. Su única responsabilidad es crear un una nueva instancia de nodo para una fuente de interacción proporcionada. Como no hay para configurar la indicación, la fábrica puede ser un objeto: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
usaModifier.indication
internamente. en el que se puede hacer clic conScaleIndication
, solo debes proporcionarIndication
como parámetro paraclickable
:Box( modifier = Modifier .size(100.dp) .clickable( onClick = {}, indication = ScaleIndication, interactionSource = null ) .background(Color.Blue), contentAlignment = Alignment.Center ) { Text("Hello!", color = Color.White) }
Esto también facilita la compilación de componentes reutilizables de alto nivel usando un
Indication
: Un botón podría verse de la siguiente manera:@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 ) }
Luego, puedes usar el botón de la siguiente manera:
ScaleButton(onClick = {}) { Icon(Icons.Filled.ShoppingCart, "") Spacer(Modifier.padding(10.dp)) Text(text = "Add to cart!") }
![Animación de un botón con el ícono de un carrito de compras que se hace más pequeño cuando se lo presiona](https://developer.android.google.cn/static/develop/ui/compose/images/touchinput/button-custom-indication.gif?authuser=0000&hl=es)
Indication
personalizado.Cómo compilar un objeto Indication
avanzado con borde animado
Indication
no se limita solo a los efectos de transformación, como el escalamiento de una
este componente. Como IndicationNodeFactory
muestra un Modifier.Node
, puedes dibujar
cualquier tipo de efecto encima o debajo del contenido, al igual que con otras APIs de dibujo Para
ejemplo, puedes dibujar un borde animado alrededor del componente y una superposición en
la parte superior del componente cuando se lo presiona:
![Un botón con un efecto de arcoíris elegante cuando se presiona](https://developer.android.google.cn/static/develop/ui/compose/images/touchinput/rainbow-effect.gif?authuser=0000&hl=es)
Indication
.La implementación de Indication
es muy similar al ejemplo anterior:
solo crea un nodo con algunos parámetros. Dado que el borde animado depende
en la forma y el borde del componente para el que se usa el Indication
, el
La implementación de Indication
también requiere que se proporcionen la forma y el ancho del borde.
como parámetros:
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 ) } }
La implementación de Modifier.Node
también es conceptualmente la misma, incluso si el
dibujar código es más complicado. Al igual que antes, observa InteractionSource
.
cuando se adjunta, inicia animaciones e implementa DrawModifierNode
para dibujar
el efecto sobre el contenido:
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 principal diferencia es que ahora hay una duración mínima para
una animación con la función animateToResting()
, de modo que incluso si la presión
inmediatamente, la animación de prensa continuará. También está la administración
para pulsaciones rápidas múltiples al inicio de animateToPressed
, si se presiona
ocurre durante una animación en reposo o de presión existente, la animación anterior se
cancelada y la animación de prensa comienza desde el principio. Para admitir varias
efectos concurrentes (como con ondas, en los que se dibujará una nueva animación de ondas)
sobre otros ondas), puedes hacer un seguimiento de las animaciones en una lista, en lugar de
cancelar las animaciones existentes e iniciar otras.
Recomendaciones para ti
- Nota: El texto del vínculo se muestra cuando JavaScript está desactivado
- Información sobre los gestos
- Kotlin para Jetpack Compose
- Componentes y diseños de Material