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 quiso hacerlo, sabrá que debe arrastrar el dedo fuera del botón antes de soltarlo. De lo contrario, este se activará.

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 Iluminación de interacciones.
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 cómo hacer un seguimiento de las interacciones por tu cuenta con InteractionSource, consulta Trabaja con InteractionSource.
En la siguiente sección, se describe cómo consumir y emitir interacciones con InteractionSource y MutableInteractionSource, respectivamente.
Consume y emite Interaction
InteractionSource representa un flujo de solo lectura de Interactions; no es posible emitir un Interaction a un InteractionSource. Para emitir Interactions, 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 desde modificadores y componentes.
Ejemplo de consumo de modificador
Para un modificador que dibuja un borde para el estado enfocado, solo necesitas observar Interactions, por lo que puedes aceptar un InteractionSource:
fun Modifier.focusBorder(interactionSource: InteractionSource): Modifier { // ... }
En la firma de la función, se observa claramente que este modificador es un consumidor, ya que puede consumir Interactions, pero no emitirlos.
Ejemplo de modificador de producción
En el caso de un modificador que controla eventos de desplazamiento del cursor, como Modifier.hoverable, debes emitir Interactions y aceptar un MutableInteractionSource como parámetro:
fun Modifier.hover(interactionSource: MutableInteractionSource, enabled: Boolean): Modifier { // ... }
Este modificador es un productor, ya que puede usar el MutableInteractionSource proporcionado para emitir HoverInteractions cuando se coloca el cursor sobre él o se quita.
Crea componentes que consuman y produzcan
Los componentes de alto nivel, como un Button de Material, actúan como productores y consumidores. Controlan los eventos de entrada y de enfoque, y también cambian su apariencia en respuesta a estos eventos, como mostrar una ondulación o animar su elevación. Como resultado, exponen directamente MutableInteractionSource como un parámetro, de modo 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 el MutableInteractionSource fuera del componente y observar todos los Interactions que produce el componente. Puedes usarlo para controlar la apariencia de ese componente o de cualquier otro componente de tu IU.
Si compilas tus propios componentes interactivos de alto nivel, te recomendamos que expongas MutableInteractionSource como un parámetro de esta manera. Además de seguir las prácticas recomendadas de elevación del estado, esto también facilita la lectura y el control del estado visual de un componente de la misma manera en que se puede leer y controlar cualquier otro tipo de estado (como el estado habilitado).
Compose sigue un enfoque arquitectónico en capas, por lo que los componentes de Material de alto nivel se compilan sobre bloques de compilación fundamentales que producen los Interactions que necesitan para controlar las ondulaciones y otros efectos visuales. La biblioteca de Foundation proporciona modificadores de interacción de alto nivel, como Modifier.hoverable, Modifier.focusable y Modifier.draggable.
Para compilar un componente que responda a eventos de desplazamiento del mouse, puedes usar Modifier.hoverable y pasar un MutableInteractionSource como parámetro.
Cada vez que se coloca el cursor sobre el componente, este emite HoverInteractions, y puedes usar esto para cambiar la apariencia del 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 como parámetro. Ahora, tanto HoverInteraction.Enter/Exit como FocusInteraction.Focus/Unfocus se emiten a través del mismo MutableInteractionSource, y puedes personalizar la apariencia de 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 una abstracción de nivel aún más alto que hoverable y focusable. Para que un componente sea apto para hacer clic, debe ser apto para colocar el cursor sobre él de forma implícita, y los componentes en los que se puede hacer clic también deben ser aptos para recibir el enfoque. Puedes usar Modifier.clickable para crear un componente que controle las interacciones de desplazamiento, enfoque y presión, sin necesidad de combinar APIs de nivel inferior. Si también quieres que se pueda hacer clic en tu componente, puedes reemplazar hoverable y focusable por un 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 el último elemento de la lista. Por ejemplo, así es como la implementación de ondas de Compose determina la superposición de estados adecuada para usar en la interacción más reciente:
val lastInteraction = when (interactions.lastOrNull()) { is DragInteraction.Start -> "Dragged" is PressInteraction.Press -> "Pressed" else -> "No state" }
Dado que todos los Interaction siguen la misma estructura, no hay mucha diferencia en el código cuando se trabaja con diferentes tipos de interacciones del usuario: el patrón general es el mismo.
Ten en cuenta que los ejemplos anteriores de esta sección representan el Flow de las interacciones con State, lo que facilita la observación de los valores actualizados, ya que la lectura del valor de estado provocará automáticamente recomposiciones. Sin embargo, la composición se realiza en lotes antes del fotograma. Esto significa que, si el estado cambia y luego vuelve a cambiar en el mismo fotograma, los componentes que observan el estado no verán el cambio.
Esto es importante para las interacciones, ya que pueden comenzar y finalizar con regularidad dentro del mismo fotograma. Por ejemplo, con el ejemplo anterior de 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 fotograma, el texto nunca se mostrará como "Presionado". En la mayoría de los casos, esto no es un problema, ya que mostrar un efecto visual durante un período tan corto provocará parpadeos y no será muy perceptible para el usuario. En algunos casos, como cuando se muestra un efecto de onda o una animación similar, es posible que desees mostrar el efecto durante al menos un período mínimo, en lugar de detenerlo de inmediato si ya no se presiona el botón. Para ello, puedes iniciar y detener animaciones directamente desde la función lambda de recopilación, en lugar de escribir en un estado. En la sección Cómo compilar un Indication avanzado con un borde animado, se incluye un ejemplo de este patrón.
Ejemplo: Compila un componente con un controlador de interacción personalizado
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:
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 las secciones anteriores, aprendiste a cambiar parte de un componente en respuesta a diferentes Interaction, como mostrar un ícono cuando se presiona. Este mismo enfoque se puede usar para cambiar el valor de los parámetros que proporcionas a un componente o para cambiar el contenido que se muestra dentro de un componente, pero esto solo se aplica por componente. A menudo, una aplicación o un sistema de diseño tendrán un sistema genérico para los efectos visuales con estado, es decir, un efecto que se debe aplicar a todos los componentes de manera coherente.
Si estás creando este tipo de sistema de diseño, personalizar un componente y reutilizar esta personalización para otros componentes puede ser difícil por los siguientes motivos:
- Todos los componentes del sistema de diseño necesitan la misma plantilla.
- Es fácil olvidarse de aplicar este efecto a los componentes recién creados y a los componentes personalizados que se pueden hacer clic.
- Puede ser difícil combinar el efecto personalizado con otros efectos.
Para evitar estos problemas y escalar fácilmente un componente personalizado en todo tu sistema, puedes usar Indication.
Indication representa un efecto visual reutilizable que se puede aplicar a los componentes de un sistema de diseño o una aplicación. Indication se divide en dos partes:
IndicationNodeFactory: Es una fábrica que crea instancias deModifier.Nodeque renderizan efectos visuales para un componente. En el caso de las implementaciones más simples que no cambian entre los componentes, puede ser un singleton (objeto) y reutilizarse en toda la aplicación.Estas instancias pueden tener o no estado. Dado que se crean por componente, pueden recuperar valores de un
CompositionLocalpara cambiar su apariencia o comportamiento dentro de un componente en particular, como con cualquier otroModifier.Node.Modifier.indication: Es un modificador que dibujaIndicationpara un componente.Modifier.clickabley otros modificadores de interacción de alto nivel aceptan un parámetro de indicación directamente, por lo que no solo emitenInteractions, sino que también pueden dibujar efectos visuales para losInteractions que emiten. Por lo tanto, para casos simples, puedes usarModifier.clickablesin necesidad deModifier.indication.
Reemplaza el efecto con un Indication
En esta sección, se describe cómo reemplazar un efecto de escala manual aplicado a un botón específico por una indicación equivalente que se puede reutilizar en varios componentes.
El siguiente código crea un botón que se reduce cuando se 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:
Crea el
Modifier.Noderesponsable de aplicar el efecto de escala. Cuando se adjunta, el nodo observa la fuente de interacción, de manera similar a los ejemplos anteriores. La única diferencia aquí es que lanza animaciones directamente en lugar de convertir las interacciones entrantes en estados.El nodo debe implementar
DrawModifierNodepara poder anularContentDrawScope#draw()y renderizar un efecto de escala con los mismos comandos de dibujo que con cualquier otra API de gráficos en Compose.La llamada a
drawContent()disponible desde el receptorContentDrawScopedibujará el componente real al que se debe aplicarIndication, por lo que solo debes llamar a esta función dentro de una transformación de escala. Asegúrate de que tus implementaciones deIndicationsiempre llamen adrawContent()en algún momento. De lo contrario, no se dibujará el componente al que aplicas elIndication.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 una nueva instancia de nodo para una fuente de interacción proporcionada. Como no hay parámetros 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.clickableusaModifier.indicationde forma interna, por lo que, para crear un componente en el que se pueda hacer clic conScaleIndication, solo debes proporcionarIndicationcomo parámetro aclickable: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 creación de componentes reutilizables de alto nivel con un
Indicationpersonalizado. Por ejemplo, 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!") }
Indication personalizado.Crea un Indication avanzado con un borde animado
Indication no se limita solo a los efectos de transformación, como escalar un componente. Como IndicationNodeFactory devuelve un Modifier.Node, puedes dibujar cualquier tipo de efecto por encima o por debajo del contenido, como con otras APIs de dibujo. Por ejemplo, puedes dibujar un borde animado alrededor del componente y una capa superpuesta sobre él cuando se presiona:
Indication.La implementación de Indication aquí es muy similar a la del ejemplo anterior: solo crea un nodo con algunos parámetros. Dado que el borde animado depende de la forma y el borde del componente para el que se usa Indication, 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 código de dibujo es más complicado. Al igual que antes, observa InteractionSource cuando se adjunta, inicia animaciones y, luego, 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 aquí es que ahora hay una duración mínima para la animación con la función animateToResting(), por lo que, incluso si se suelta la presión de inmediato, la animación de presión continuará. También se controla la presión rápida múltiple al inicio de animateToPressed: si se produce una presión durante una presión existente o una animación de descanso, se cancela la animación anterior y se inicia la animación de presión desde el principio. Para admitir varios efectos simultáneos (como con las ondas, en las que una nueva animación de ondas se dibujará sobre otras ondas), puedes hacer un seguimiento de las animaciones en una lista, en lugar de cancelar las animaciones existentes y comenzar otras nuevas.
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