Os componentes da interface do usuário fornecem feedback ao usuário do dispositivo pela maneira como eles respondem às interações do usuário. Cada componente tem a própria maneira de responder a interações, o que ajuda o usuário a saber o que as interações dele estão fazendo. Por exemplo, se um usuário tocar em um botão na tela touchscreen de um dispositivo, ele provavelmente mudará de alguma forma, talvez adicionando uma cor de destaque. Essa mudança informa ao usuário que ele tocou no botão. Se o usuário não quiser fazer isso, ele poderá arrastar o dedo para longe do botão antes de soltar. Caso contrário, o botão será ativado.
![](https://developer.android.com/static/develop/ui/compose/images/touchinput/button-always-enabled.gif?authuser=7&hl=pt)
![](https://developer.android.com/static/develop/ui/compose/images/touchinput/button-reflects-state.gif?authuser=7&hl=pt)
A documentação de Gestos do Compose aborda como os componentes do Compose processam eventos de ponteiro de baixo nível, como movimentos e cliques do ponteiro. O Compose abstrai esses eventos de baixo nível em interações de nível mais alto. Por exemplo, uma série de eventos de ponteiro pode ser adicionada a um botão de tocar e soltar. Entender essas abstrações de nível superior pode ajudar a personalizar a resposta da IU ao usuário. Por exemplo, você pode personalizar a aparência de um componente quando o usuário interage com ele ou apenas manter um registro dessas ações. Este documento fornece as informações necessárias para modificar os elementos de IU padrão ou criar seu próprio elemento.
Interações
Em muitos casos, você não precisa saber apenas como o componente do Compose está
interpretando as interações do usuário. Por exemplo, Button
depende de
Modifier.clickable
para descobrir se o usuário clicou no botão. Se estiver adicionando um botão
típico ao seu app, você poderá definir o código onClick
do botão, e
o Modifier.clickable
executará esse código quando adequado. Isso significa que você não precisa
saber se o usuário tocou na tela ou selecionou o botão com um
teclado. O Modifier.clickable
descobre que o usuário executou um clique e responde executando o código onClick
.
No entanto, se você quiser personalizar a resposta do componente de IU para o comportamento do usuário, talvez precise saber mais do que está acontecendo nos bastidores. Esta seção fornece algumas dessas informações.
Quando um usuário interage com um componente de IU, o sistema representa o comportamento
gerando vários
eventos de
Interaction
. Por exemplo, se um usuário tocar em um botão, ele vai gerar
PressInteraction.Press
.
Se o usuário levantar o dedo dentro do botão, ele vai gerar um
PressInteraction.Release
,
informando ao botão que o clique foi concluído. Por outro lado, se o
usuário arrastar o dedo para fora do botão e levantar o dedo, o botão
vai gerar
PressInteraction.Cancel
,
para indicar que o pressionamento do botão foi cancelado, não concluído.
Essas interações são discretas. Ou seja, esses eventos de interação de baixo nível não pretendem interpretar o significado das ações do usuário ou a sequência delas. Eles também não interpretam quais ações do usuário podem ter prioridade sobre outras ações.
Essas interações geralmente vêm em pares, com um início e um fim. A segunda
interação contém uma referência à primeira. Por exemplo, se um usuário
tocar em um botão e levantar o dedo, o toque vai gerar uma interação
PressInteraction.Press
,
e a liberação vai gerar um
PressInteraction.Release
.
A Release
tem uma propriedade press
que identifica a
PressInteraction.Press
inicial.
Você pode ver as interações de um componente específico observando a
InteractionSource
. A InteractionSource
é criada com base nos fluxos
Kotlin para que você possa coletar as interações da mesma forma que
trabalha com qualquer outro fluxo. Para mais informações sobre essa decisão de design,
consulte a postagem do blog Illuminating Interactions.
Estado de interação
Para estender a funcionalidade integrada dos componentes, você também precisa
monitorar as interações por conta própria. Por exemplo, talvez você queira que um botão
mude de cor quando for pressionado. A maneira mais simples de acompanhar as interações é
observar o estado adequado da interação. InteractionSource
oferece alguns
métodos que revelam vários status de interação como estado. Por exemplo, se
você quiser ver se um botão específico foi pressionado, chame o método
InteractionSource.collectIsPressedAsState()
correspondente:
val interactionSource = remember { MutableInteractionSource() } val isPressed by interactionSource.collectIsPressedAsState() Button( onClick = { /* do something */ }, interactionSource = interactionSource ) { Text(if (isPressed) "Pressed!" else "Not pressed") }
Além de collectIsPressedAsState()
, o Compose também fornece
collectIsFocusedAsState()
, collectIsDraggedAsState()
e
collectIsHoveredAsState()
. Na verdade, esses são métodos de conveniência
baseados nas APIs InteractionSource
de nível inferior. Em alguns casos, convém
usar essas funções de nível inferior diretamente.
Por exemplo, suponha que você precise saber se um botão está sendo pressionado e
também se ele está sendo arrastado. Se você usar collectIsPressedAsState()
e collectIsDraggedAsState()
, o Compose fará muito trabalho duplicado, e
não há garantia de que você receberá todas as interações na ordem certa. Em
situações como essa, você pode trabalhar diretamente com a
InteractionSource
. Para mais informações sobre como acompanhar as interações
com InteractionSource
, consulte Trabalhar com InteractionSource
.
A seção a seguir descreve como consumir e emitir interações com InteractionSource
e MutableInteractionSource
, respectivamente.
Consumir e emitir Interaction
InteractionSource
representa um fluxo somente leitura de Interactions
. Não é
possível emitir um Interaction
para um InteractionSource
. Para emitir
Interaction
s, é necessário usar um MutableInteractionSource
, que se estende de
InteractionSource
.
Modificadores e componentes podem consumir, emitir ou consumir e emitir Interactions
.
As seções abaixo descrevem como consumir e emitir interações de
modificadores e componentes.
Exemplo de como consumir o modificador
Para um modificador que desenha uma borda para o estado em foco, você só precisa observar
Interactions
. Portanto, você pode aceitar um InteractionSource
:
fun Modifier.focusBorder(interactionSource: InteractionSource): Modifier { // ... }
Fica claro pela assinatura da função que esse modificador é um consumidor. Ele
pode consumir Interaction
s, mas não os emite.
Como produzir exemplo de modificador
Para um modificador que processa eventos de passar o cursor, como Modifier.hoverable
, é necessário emitir Interactions
e aceitar um MutableInteractionSource
como
parâmetro:
fun Modifier.hover(interactionSource: MutableInteractionSource, enabled: Boolean): Modifier { // ... }
Esse modificador é um produtor, ou seja, ele pode usar o
MutableInteractionSource
fornecido para emitir HoverInteractions
quando o cursor passar o cursor
ou o cursor aberto.
Criar componentes que consumam e produzam
Componentes de alto nível, como um Button
do Material Design, atuam como produtores e
consumidores. Eles processam eventos de entrada e foco e também mudam a aparência
em resposta a esses eventos, como mostrar uma ondulação ou animar a
elevação. Como resultado, eles expõem MutableInteractionSource
diretamente como um
parâmetro, para que você possa fornecer sua própria instância lembrada:
@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() */ }
Isso permite elevar a
MutableInteractionSource
para fora do componente e observar todos os
Interaction
s produzidos por ele. É possível usar isso para controlar a
aparência desse ou de qualquer outro componente na interface.
Se você estiver criando seus próprios componentes interativos de alto nível, recomendamos
expor MutableInteractionSource
como um parâmetro dessa maneira. Além de
seguir as práticas recomendadas de elevação de estado, isso também facilita a leitura e
o controle do estado visual de um componente, da mesma forma que qualquer outro tipo de
estado (como o ativado) pode ser lido e controlado.
O Compose segue uma abordagem arquitetônica em camadas,
para que componentes de alto nível do Material Design sejam criados com base em elementos básicos
que produzem as Interaction
s necessárias para controlar ondulações e outros
efeitos visuais. A biblioteca base oferece modificadores de interação de alto nível, como Modifier.hoverable
, Modifier.focusable
e
Modifier.draggable
.
Para criar um componente que responda a eventos de passar o cursor, basta usar
Modifier.hoverable
e transmitir um MutableInteractionSource
como parâmetro.
Sempre que o componente é passado ao passar o cursor, HoverInteraction
s são emitidos, e você pode usar
isso para mudar a aparência do 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 esse componente também seja focalizável, adicione Modifier.focusable
e transmita
o mesmo MutableInteractionSource
como parâmetro. Agora, HoverInteraction.Enter/Exit
e FocusInteraction.Focus/Unfocus
são emitidos
pelo mesmo MutableInteractionSource
, e é possível personalizar a
aparência dos dois tipos de interação no mesmo 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
é uma abstração de nível
ainda mais alto que hoverable
e focusable
. Para que um componente seja
clicável, ele implicitamente passa o cursor, e os componentes que podem ser clicados também
precisam ser focalizáveis. Você pode usar Modifier.clickable
para criar um componente que
processe interações de passar o cursor, focar e pressionar, sem precisar combinar APIs
de nível inferior. Se você também quiser tornar seu componente clicável, substitua hoverable
e focusable
por um 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!") }
Trabalhe com InteractionSource
Se você precisar de informações de baixo nível sobre interações com um componente, use
as APIs de fluxo padrão para a InteractionSource
desse componente.
Por exemplo, suponha que você queira manter uma lista das interações de pressionar e arrastar
para uma InteractionSource
. Esse código faz metade do trabalho, adicionando
os novos pressionamentos à lista conforme eles chegam:
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) } } } }
Mas, além de adicionar as novas interações, você também precisará removê-las quando elas terminarem, por exemplo, quando o usuário levantar o dedo do componente. Isso é fácil, porque as interações finais sempre carregam uma referência à interação inicial associada. O código a seguir mostra como remover as interações que terminaram:
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) } } } }
Agora, se você quiser saber se o componente está sendo pressionado ou arrastado,
basta verificar se interactions
está vazio:
val isPressedOrDragged = interactions.isNotEmpty()
Para saber qual foi a interação mais recente, consulte o último item da lista. Confira a seguir como a implementação de ondulação do Compose descobre a sobreposição de estado adequada a ser usada na interação mais recente:
val lastInteraction = when (interactions.lastOrNull()) { is DragInteraction.Start -> "Dragged" is PressInteraction.Press -> "Pressed" else -> "No state" }
Como todas as Interaction
s seguem a mesma estrutura, não há muita
diferença no código ao trabalhar com diferentes tipos de interações do usuário. O
padrão geral é o mesmo.
Os exemplos anteriores nesta seção representam o Flow
das
interações usando State
.
Isso facilita a observação dos valores atualizados,
já que a leitura do valor do estado causa recomposições automaticamente. No entanto,
a composição é um pré-frame em lote. Isso significa que, se o estado mudar e
voltar no mesmo frame, os componentes que observam o estado não
vão ver a mudança.
Isso é importante para interações, porque elas podem começar e terminar regularmente
no mesmo frame. Por exemplo, usando o exemplo anterior com Button
:
val interactionSource = remember { MutableInteractionSource() } val isPressed by interactionSource.collectIsPressedAsState() Button(onClick = { /* do something */ }, interactionSource = interactionSource) { Text(if (isPressed) "Pressed!" else "Not pressed") }
Se o pressionamento começar e terminar dentro do mesmo frame, o texto nunca vai aparecer como
"Pressed!". Na maioria dos casos, isso não é um problema. Mostrar um efeito visual por
um período tão curto resulta em oscilações e não é
muito perceptível para o usuário. Em alguns casos, como ao mostrar um efeito de ondulação ou uma
animação semelhante, é possível mostrar o efeito por um período mínimo
de tempo, em vez de parar imediatamente se o botão não estiver mais pressionado. Para
fazer isso, você pode iniciar e parar animações diretamente de dentro da lambda
de coleta, em vez de gravar em um estado. Há um exemplo desse padrão na
seção Criar um Indication
avançado com borda animada.
Exemplo: componente de build com processamento de interação personalizado
Para ver como criar componentes com uma resposta personalizada para entrada, veja um exemplo de botão modificado. Nesse caso, suponha que você queira um botão que responda aos pressionamentos mudando a aparência:
![Animação de um botão que adiciona dinamicamente um ícone de carrinho de compras quando clicado](https://developer.android.com/static/develop/ui/compose/images/handling-interactions-iconbutton.gif?authuser=7&hl=pt)
Para fazer isso, crie um elemento personalizado que pode ser composto com base em Button
e use um
parâmetro icon
adicional para desenhar o ícone, neste caso, um carrinho de compras. Você
chama collectIsPressedAsState()
para rastrear se o usuário está passando o cursor sobre o
botão. Quando estiver, você vai adicionar o ícone. O código vai ficar assim:
@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() } }
Veja como usar esse novo elemento de composição:
PressIconButton( onClick = {}, icon = { Icon(Icons.Filled.ShoppingCart, contentDescription = null) }, text = { Text("Add to cart") } )
Como esse novo PressIconButton
é criado com base no Button
do
Material já existente, ele reage às interações do usuário de todas as maneiras normais. Quando o usuário
pressiona o botão, ele muda um pouco a opacidade, como um
Button
comum do Material.
Criar e aplicar um efeito personalizado reutilizável com Indication
Nas seções anteriores, você aprendeu a mudar parte de um componente em resposta
a diferentes Interaction
s, como mostrar um ícone quando pressionado. Essa mesma
abordagem pode ser usada para mudar o valor dos parâmetros fornecidos a um
componente ou o conteúdo mostrado dentro de um componente, mas ela só é
aplicável por componente. Muitas vezes, um aplicativo ou sistema de design
terá um sistema genérico para efeitos visuais com estado, que precisa
ser aplicado a todos os componentes de maneira consistente.
Se você está criando esse tipo de sistema de design, pode ser difícil personalizar um componente e reutilizar essa personalização para outros componentes pelos seguintes motivos:
- Todos os componentes do sistema de design precisam do mesmo boilerplate
- É fácil se esquecer de aplicar esse efeito a componentes recém-criados e componentes clicáveis personalizados
- Pode ser difícil combinar o efeito personalizado com outros efeitos
Para evitar esses problemas e escalonar facilmente um componente personalizado em todo o sistema,
use Indication
.
Indication
representa um efeito visual reutilizável que pode ser aplicado a
componentes de um sistema de aplicativo ou design. O Indication
é dividido em duas
partes:
IndicationNodeFactory
: uma fábrica que cria instâncias deModifier.Node
que renderizam efeitos visuais para um componente. Para implementações mais simples que não mudam entre componentes, pode ser um singleton (objeto) e reutilizada em todo o aplicativo.Essas instâncias podem ser com estado ou sem estado. Como são criados por componente, eles podem extrair valores de um
CompositionLocal
para mudar a forma como eles aparecem ou se comportam dentro de um componente específico, como acontece com qualquer outroModifier.Node
.Modifier.indication
: um modificador que desenhaIndication
para um componente.Modifier.clickable
e outros modificadores de interação de alto nível aceitam um parâmetro de indicação diretamente. Assim, eles não só emitemInteraction
s, mas também podem desenhar efeitos visuais para osInteraction
s emitidos. Portanto, para casos simples, você pode apenas usarModifier.clickable
sem precisar deModifier.indication
.
Substituir o efeito por um Indication
Esta seção descreve como substituir um efeito de escala manual aplicado a um botão específico por uma indicação equivalente que pode ser reutilizado em vários componentes.
O código abaixo cria um botão que diminui ao ser pressionado:
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 converter o efeito de escala do snippet acima em um Indication
, siga
estas etapas:
Crie o
Modifier.Node
responsável por aplicar o efeito de escala. Quando anexado, o nó observa a origem da interação, de maneira semelhante aos exemplos anteriores. A única diferença é que ele inicia animações diretamente em vez de converter as interações recebidas em estado.O nó precisa implementar
DrawModifierNode
para substituirContentDrawScope#draw()
e renderizar um efeito de escala usando os mesmos comandos de desenho de qualquer outra API gráfica no Compose.Chamar
drawContent()
, disponível no receptorContentDrawScope
, vai renderizar o componente real a que oIndication
precisa ser aplicado. Portanto, você só precisa chamar essa função em uma transformação de escala. Garanta que suas implementações deIndication
sempre chamemdrawContent()
em algum momento. Caso contrário, o componente ao qual você está aplicando oIndication
não será renderizado.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() } } }
Crie a
IndicationNodeFactory
. A única responsabilidade dele é criar uma nova instância de nó para uma origem de interação fornecida. Como não há parâmetros para configurar a indicação, a fábrica pode ser um 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 }
O
Modifier.clickable
usaModifier.indication
internamente. Portanto, para criar um componente clicável comScaleIndication
, basta fornecer oIndication
como um 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) }
Isso também facilita a criação de componentes reutilizáveis e de alto nível usando uma
Indication
personalizada. Um botão pode ter esta aparência:@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 ) }
Você pode usar o botão da seguinte maneira:
ScaleButton(onClick = {}) { Icon(Icons.Filled.ShoppingCart, "") Spacer(Modifier.padding(10.dp)) Text(text = "Add to cart!") }
![Animação de um botão com um ícone de carrinho de compras que diminui quando pressionado](https://developer.android.com/static/develop/ui/compose/images/touchinput/button-custom-indication.gif?authuser=7&hl=pt)
Indication
personalizada.Criar um Indication
avançado com borda animada
O Indication
não se limita a efeitos de transformação, como o escalonamento de um
componente. Como IndicationNodeFactory
retorna uma Modifier.Node
, é possível desenhar
qualquer tipo de efeito acima ou abaixo do conteúdo, da mesma forma que você faria com outras APIs de desenho. Por
exemplo, é possível desenhar uma borda animada ao redor do componente e uma sobreposição
sobre ele quando ele for pressionado:
![Um botão com um efeito de arco-íris sofisticado ao ser pressionado](https://developer.android.com/static/develop/ui/compose/images/touchinput/rainbow-effect.gif?authuser=7&hl=pt)
Indication
.A implementação de Indication
é muito semelhante ao exemplo anterior.
Ela apenas cria um nó com alguns parâmetros. Como a borda animada depende
da forma e da borda do componente em que a Indication
é usada, a
implementação de Indication
também exige que a forma e a largura da borda sejam fornecidas
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 ) } }
A implementação de Modifier.Node
também é conceitualmente a mesma, mesmo que o
código de desenho seja mais complicado. Como antes, ele observa InteractionSource
quando anexado, inicia animações e implementa DrawModifierNode
para desenhar
o efeito sobre o conteúdo:
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 } }
A principal diferença é que agora há uma duração mínima para a
animação com a função animateToResting()
. Portanto, mesmo que o pressionamento seja
liberado imediatamente, a animação vai continuar. Há também o processamento
de vários pressionamentos rápidos no início de animateToPressed
. Se um pressionamento
acontecer durante uma animação de pressionamento ou em repouso, a animação anterior será
cancelada e a animação será iniciada desde o início. Para oferecer suporte a vários
efeitos simultâneos, como ondulações, em que uma nova animação de ondulação é desenhada
sobre outras ondulações, é possível rastrear as animações em uma lista, em vez de
cancelar e iniciar outras.
Recomendados para você
- Observação: o texto do link aparece quando o JavaScript está desativado.
- Entender os gestos
- Kotlin para Jetpack Compose
- Componentes e layouts do Material Design