I componenti dell'interfaccia utente forniscono feedback all'utente del dispositivo in base al modo in cui rispondono alle interazioni dell'utente. Ogni componente ha il proprio modo di rispondere alle interazioni, il che aiuta l'utente a capire cosa stanno facendo le sue interazioni. Ad esempio, se un utente tocca un pulsante sul touchscreen di un dispositivo, è probabile che il pulsante cambi in qualche modo, ad esempio aggiungendo un colore di evidenziazione. Questa modifica consente all'utente di sapere che ha toccato il pulsante. Se l'utente non voleva eseguire questa operazione, saprà che deve trascinare il dito lontano dal pulsante prima di rilasciarlo, altrimenti il pulsante si attiverà.
La documentazione relativa ai gesti di Compose spiega come i componenti di Compose gestiscono gli eventi di puntatore di basso livello, come i movimenti e i clic del puntatore. Per impostazione predefinita, Compose astrae questi eventi di basso livello in interazioni di livello superiore. Ad esempio, una serie di eventi di puntatore potrebbe corrispondere alla pressione e al rilascio di un pulsante. Comprendere queste astrazioni di livello superiore può aiutarti a personalizzare il modo in cui la UI risponde all'utente. Ad esempio, potresti voler personalizzare il modo in cui l'aspetto di un componente cambia quando l'utente interagisce con questo o semplicemente mantenere un log di queste azioni utente. Questo documento fornisce le informazioni necessarie per modificare gli elementi UI standard o progettarne di personalizzati.
Interazioni
In molti casi, non è necessario sapere esattamente come il componente Compose interpreta le interazioni degli utenti. Ad esempio, Button si basa su
Modifier.clickable
per capire se l'utente ha fatto clic sul pulsante. Se aggiungi un pulsante tipico alla tua app, puoi definire il codice onClick del pulsante e Modifier.clickable esegue il codice quando opportuno. Ciò significa che non devi sapere se l'utente ha toccato lo schermo o selezionato il pulsante con una tastiera; Modifier.clickable rileva che l'utente ha eseguito un clic e risponde eseguendo il tuo codice onClick.
Tuttavia, se vuoi personalizzare la risposta del componente UI al comportamento dell'utente, potresti aver bisogno di saperne di più su ciò che accade dietro le quinte. Questa sezione fornisce alcune di queste informazioni.
Quando un utente interagisce con un componente della UI, il sistema rappresenta il suo comportamento generando una serie di eventi Interaction. Ad esempio, se un utente tocca un pulsante, quest'ultimo genera
PressInteraction.Press.
Se l'utente solleva il dito all'interno del pulsante, viene generato un evento
PressInteraction.Release,
che indica al pulsante che il clic è stato completato. Se invece l'utente trascina il dito fuori dal pulsante e poi lo solleva, il pulsante genera PressInteraction.Cancel per indicare che la pressione sul pulsante è stata annullata, non completata.
Queste interazioni sono imparziali. In altre parole, questi eventi di interazione di basso livello non intendono interpretare il significato delle azioni dell'utente o la loro sequenza. Inoltre, non interpretano quali azioni dell'utente potrebbero avere la priorità su altre azioni.
Queste interazioni generalmente si presentano in coppia, con un inizio e una fine. La seconda interazione contiene un riferimento alla prima. Ad esempio, se un utente tocca un pulsante e poi solleva il dito, il tocco genera un'interazione PressInteraction.Press e il rilascio genera un'interazione PressInteraction.Release; Release ha una proprietà press che identifica l'interazione PressInteraction.Press iniziale.
Puoi visualizzare le interazioni per un determinato componente osservando il suo
InteractionSource. InteractionSource si basa sui flussi Kotlin, quindi puoi raccogliere le interazioni nello stesso modo in cui lavoreresti con qualsiasi altro flusso. Per saperne di più su questa decisione di progettazione, consulta il blog post Illuminating Interactions.
Stato dell'interazione
Potresti voler estendere la funzionalità integrata dei tuoi componenti monitorando anche le interazioni in autonomia. Ad esempio, potresti voler cambiare il colore di un pulsante quando viene premuto. Il modo più semplice per monitorare le interazioni è osservare lo stato di interazione appropriato. InteractionSource offre una serie di metodi che mostrano vari stati di interazione. Ad esempio, se vuoi vedere se viene premuto un determinato pulsante, puoi chiamare il relativo metodo InteractionSource.collectIsPressedAsState():
val interactionSource = remember { MutableInteractionSource() } val isPressed by interactionSource.collectIsPressedAsState() Button( onClick = { /* do something */ }, interactionSource = interactionSource ) { Text(if (isPressed) "Pressed!" else "Not pressed") }
Oltre a collectIsPressedAsState(), Compose fornisce anche
collectIsFocusedAsState(), collectIsDraggedAsState() e
collectIsHoveredAsState(). Questi metodi sono in realtà metodi di convenienza basati su API InteractionSource di livello inferiore. In alcuni casi, potresti voler utilizzare direttamente queste funzioni di livello inferiore.
Ad esempio, supponiamo che tu debba sapere se un pulsante viene premuto e
anche se viene trascinato. Se utilizzi sia collectIsPressedAsState()
sia collectIsDraggedAsState(), Compose esegue molte operazioni duplicate e
non è garantito che tutte le interazioni vengano eseguite nell'ordine corretto. In situazioni come questa, potresti voler collaborare direttamente con InteractionSource. Per saperne di più sul monitoraggio delle interazioni con InteractionSource, consulta Utilizza InteractionSource.
La sezione seguente descrive come utilizzare ed emettere interazioni rispettivamente con InteractionSource e MutableInteractionSource.
Consuma ed emetti Interaction
InteractionSource rappresenta uno stream di sola lettura di Interactions: non è possibile emettere un Interaction in un InteractionSource. Per emettere Interaction, devi utilizzare un MutableInteractionSource, che si estende da InteractionSource.
I modificatori e i componenti possono consumare, emettere o consumare ed emettere Interactions.
Le sezioni seguenti descrivono come utilizzare ed emettere interazioni sia dai modificatori che dai componenti.
Esempio di modificatore di consumo
Per un modificatore che disegna un bordo per lo stato attivo, devi solo osservare
Interactions, in modo da poter accettare un InteractionSource:
fun Modifier.focusBorder(interactionSource: InteractionSource): Modifier { // ... }
Dalla firma della funzione è chiaro che questo modificatore è un consumer: può consumare Interaction, ma non può emetterli.
Esempio di modificatore di produzione
Per un modificatore che gestisce gli eventi di passaggio del mouse come Modifier.hoverable, devi emettere Interactions e accettare un MutableInteractionSource come parametro:
fun Modifier.hover(interactionSource: MutableInteractionSource, enabled: Boolean): Modifier { // ... }
Questo modificatore è un producer: può utilizzare l'oggetto MutableInteractionSource fornito per emettere HoverInteractions quando viene passato il mouse sopra o fuori.
Crea componenti che utilizzano e producono
I componenti di alto livello, come un Button di Material, fungono sia da producer che da consumer. Gestiscono gli eventi di input e di stato attivo e modificano anche il loro aspetto in risposta a questi eventi, ad esempio mostrando un effetto onda o animando l'elevazione. Di conseguenza, espongono direttamente MutableInteractionSource come parametro, in modo che tu possa fornire la tua istanza memorizzata:
@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() */ }
In questo modo è possibile eseguire l'hoisting di
MutableInteractionSource dal componente e osservare tutti i
Interaction prodotti dal componente. Puoi utilizzarlo per controllare l'aspetto di quel componente o di qualsiasi altro componente della UI.
Se stai creando i tuoi componenti interattivi di alto livello, ti consigliamo di esporre MutableInteractionSource come parametro in questo modo. Oltre a seguire le best practice per l'innalzamento dello stato, questo semplifica anche la lettura e il controllo dello stato visivo di un componente nello stesso modo in cui è possibile leggere e controllare qualsiasi altro tipo di stato (ad esempio lo stato abilitato).
Compose segue un approccio architetturale a livelli,
quindi i componenti Material di alto livello sono basati su elementi di base
che producono gli Interaction di cui hanno bisogno per controllare gli effetti ripple e altri
effetti visivi. La libreria Foundation fornisce modificatori di interazione di alto livello come Modifier.hoverable, Modifier.focusable e Modifier.draggable.
Per creare un componente che risponda agli eventi di passaggio del mouse, puoi semplicemente utilizzare Modifier.hoverable e passare un MutableInteractionSource come parametro.
Ogni volta che passi il mouse sopra il componente, vengono emessi HoverInteraction, che puoi utilizzare per modificare l'aspetto 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!") }
Per rendere attivabile anche questo componente, puoi aggiungere Modifier.focusable e passare
lo stesso MutableInteractionSource come parametro. Ora, sia HoverInteraction.Enter/Exit che FocusInteraction.Focus/Unfocus vengono emessi tramite lo stesso MutableInteractionSource e puoi personalizzare l'aspetto per entrambi i tipi di interazione nello stesso posto:
// 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 è un'astrazione di livello ancora più elevato rispetto a hoverable e focusable: affinché un componente sia cliccabile, deve essere implicitamente attivabile con il passaggio del mouse e i componenti su cui è possibile fare clic devono anche essere attivabili. Puoi utilizzare Modifier.clickable per creare un componente che gestisce le interazioni di passaggio del mouse, stato attivo e pressione, senza dover combinare API di livello inferiore. Se vuoi rendere cliccabile anche il componente, puoi sostituire hoverable e focusable con 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!") }
Lavora con InteractionSource
Se hai bisogno di informazioni di basso livello sulle interazioni con un componente, puoi utilizzare le API di flusso standard per il InteractionSource di quel componente.
Ad esempio, supponiamo che tu voglia mantenere un elenco delle interazioni di pressione e trascinamento per un InteractionSource. Questo codice fa metà del lavoro, aggiungendo
le nuove pressioni all'elenco man mano che arrivano:
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) } } } }
Tuttavia, oltre ad aggiungere le nuove interazioni, devi anche rimuoverle quando terminano (ad esempio, quando l'utente solleva il dito dal componente). È facile farlo, poiché le interazioni finali contengono sempre un riferimento all'interazione iniziale associata. Questo codice mostra come rimuovere le interazioni terminate:
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) } } } }
Ora, se vuoi sapere se il componente è attualmente premuto o trascinato,
devi solo controllare se interactions è vuoto:
val isPressedOrDragged = interactions.isNotEmpty()
Se vuoi sapere qual è stata l'interazione più recente, guarda l'ultimo elemento dell'elenco. Ad esempio, ecco come l'implementazione dell'effetto ripple di Compose determina l'overlay di stato appropriato da utilizzare per l'interazione più recente:
val lastInteraction = when (interactions.lastOrNull()) { is DragInteraction.Start -> "Dragged" is PressInteraction.Press -> "Pressed" else -> "No state" }
Poiché tutti i Interaction seguono la stessa struttura, non c'è molta differenza nel codice quando si lavora con diversi tipi di interazioni utente: il pattern complessivo è lo stesso.
Tieni presente che gli esempi precedenti in questa sezione rappresentano lo Flow delle interazioni utilizzando State, il che semplifica l'osservazione dei valori aggiornati, poiché la lettura del valore dello stato causerà automaticamente le ricomposizioni. Tuttavia, la composizione viene eseguita in batch prima del frame. Ciò significa che se lo stato cambia e poi torna allo stato precedente nello stesso frame, i componenti che osservano lo stato non vedranno la modifica.
Questo è importante per le interazioni, in quanto possono iniziare e terminare regolarmente
all'interno dello stesso frame. Ad esempio, utilizzando l'esempio precedente con Button:
val interactionSource = remember { MutableInteractionSource() } val isPressed by interactionSource.collectIsPressedAsState() Button(onClick = { /* do something */ }, interactionSource = interactionSource) { Text(if (isPressed) "Pressed!" else "Not pressed") }
Se una pressione inizia e termina nello stesso frame, il testo non verrà mai visualizzato come "Premuto". Nella maggior parte dei casi, questo non è un problema: mostrare un effetto visivo per un periodo di tempo così breve comporterà uno sfarfallio e non sarà molto evidente per l'utente. In alcuni casi, ad esempio per mostrare un effetto onda o un'animazione simile, potresti voler mostrare l'effetto per almeno un periodo di tempo minimo, anziché interromperlo immediatamente se il pulsante non viene più premuto. Per farlo, puoi avviare e interrompere le animazioni direttamente dall'interno della funzione lambda collect, anziché scrivere in uno stato. Un esempio di questo pattern è disponibile nella sezione Crea un Indication avanzato con bordo animato.
Esempio: crea un componente con gestione delle interazioni personalizzata
Per vedere come creare componenti con una risposta personalizzata all'input, ecco un esempio di pulsante modificato. In questo caso, supponiamo che tu voglia un pulsante che risponda alla pressione cambiando aspetto:
Per farlo, crea un componibile personalizzato basato su Button e aggiungi un parametro icon aggiuntivo per disegnare l'icona (in questo caso, un carrello degli acquisti). Chiami collectIsPressedAsState() per monitorare se l'utente passa il mouse sopra il pulsante. In questo caso, aggiungi l'icona. Ecco come si presenta il codice:
@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() } }
Ecco come si presenta l'utilizzo del nuovo composable:
PressIconButton( onClick = {}, icon = { Icon(Icons.Filled.ShoppingCart, contentDescription = null) }, text = { Text("Add to cart") } )
Poiché questo nuovo PressIconButton è basato sul Button Material esistente, reagisce alle interazioni degli utenti in tutti i modi consueti. Quando l'utente
preme il pulsante, l'opacità cambia leggermente, proprio come un normale
Button di Material.
Crea e applica un effetto personalizzato riutilizzabile con Indication
Nelle sezioni precedenti hai imparato a modificare parte di un componente in risposta a diversi Interaction, ad esempio mostrando un'icona quando viene premuto. Questo stesso approccio può essere utilizzato per modificare il valore dei parametri forniti a un componente o per modificare i contenuti visualizzati all'interno di un componente, ma questo è applicabile solo per singolo componente. Spesso, un'applicazione o un sistema di progettazione ha un sistema generico per gli effetti visivi con stato, ovvero un effetto che deve essere applicato a tutti i componenti in modo coerente.
Se stai creando questo tipo di sistema di progettazione, personalizzare un componente e riutilizzare questa personalizzazione per altri componenti può essere difficile per i seguenti motivi:
- Ogni componente del sistema di progettazione richiede lo stesso boilerplate
- È facile dimenticare di applicare questo effetto ai componenti appena creati e ai componenti cliccabili personalizzati.
- Potrebbe essere difficile combinare l'effetto personalizzato con altri effetti
Per evitare questi problemi e scalare facilmente un componente personalizzato nel sistema, puoi utilizzare Indication.
Indication rappresenta un effetto visivo riutilizzabile che può essere applicato ai componenti di un'applicazione o di un sistema di progettazione. Indication è diviso in due
parti:
IndicationNodeFactory: una factory che crea istanzeModifier.Nodeche eseguono il rendering degli effetti visivi per un componente. Per implementazioni più semplici che non cambiano tra i componenti, può essere un singleton (oggetto) e riutilizzato nell'intera applicazione.Queste istanze possono essere stateful o stateless. Poiché vengono creati per componente, possono recuperare i valori da un
CompositionLocalper modificare il modo in cui vengono visualizzati o si comportano all'interno di un determinato componente, come con qualsiasi altroModifier.Node.Modifier.indication: un modificatore che disegnaIndicationper un componente.Modifier.clickablee altri modificatori di interazione di alto livello accettano direttamente un parametro di indicazione, quindi non solo emettonoInteraction, ma possono anche disegnare effetti visivi per iInteractionche emettono. Pertanto, per i casi semplici, puoi utilizzareModifier.clickablesenza aver bisogno diModifier.indication.
Sostituisci l'effetto con un Indication
Questa sezione descrive come sostituire un effetto di scalabilità manuale applicato a un pulsante specifico con un'indicazione equivalente che può essere riutilizzata in più componenti.
Il seguente codice crea un pulsante che si riduce quando viene premuto:
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") }
Per convertire l'effetto di scalabilità nello snippet riportato sopra in un Indication, segui
questi passaggi:
Crea l'
Modifier.Noderesponsabile dell'applicazione dell'effetto di scalabilità. Una volta collegato, il nodo osserva l'origine dell'interazione, in modo simile agli esempi precedenti. L'unica differenza è che avvia direttamente le animazioni anziché convertire le interazioni in entrata in stato.Il nodo deve implementare
DrawModifierNodein modo da poter eseguire l'override diContentDrawScope#draw()ed eseguire il rendering di un effetto di scalabilità utilizzando gli stessi comandi di disegno di qualsiasi altra API grafica in Compose.La chiamata a
drawContent()disponibile dal ricevitoreContentDrawScopedisegnerà il componente effettivo a cui deve essere applicatoIndication, quindi devi solo chiamare questa funzione all'interno di una trasformazione di scalabilità. Assicurati che le tue implementazioni diIndicationchiamino sempredrawContent()a un certo punto, altrimenti il componente a cui stai applicandoIndicationnon verrà disegnato.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
IndicationNodeFactory. La sua unica responsabilità è creare una nuova istanza del nodo per un'origine di interazione fornita. Poiché non esistono parametri per configurare l'indicazione, la factory può essere un oggetto: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.clickableutilizzaModifier.indicationinternamente, quindi per creare un componente cliccabile conScaleIndication, devi solo fornireIndicationcome parametro perclickable:Box( modifier = Modifier .size(100.dp) .clickable( onClick = {}, indication = ScaleIndication, interactionSource = null ) .background(Color.Blue), contentAlignment = Alignment.Center ) { Text("Hello!", color = Color.White) }
In questo modo è anche facile creare componenti riutilizzabili di alto livello utilizzando un
Indicationpersonalizzato. Un pulsante potrebbe avere il seguente aspetto:@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 ) }
Puoi quindi utilizzare il pulsante nel seguente modo:
ScaleButton(onClick = {}) { Icon(Icons.Filled.ShoppingCart, "") Spacer(Modifier.padding(10.dp)) Text(text = "Add to cart!") }
Indication personalizzato.Crea un Indication avanzato con bordo animato
Indication non si limita agli effetti di trasformazione, come la scalabilità di un componente. Poiché IndicationNodeFactory restituisce un Modifier.Node, puoi disegnare qualsiasi tipo di effetto sopra o sotto i contenuti, come con altre API di disegno. Ad esempio, puoi disegnare un bordo animato attorno al componente e un overlay sopra il componente quando viene premuto:
Indication.L'implementazione di Indication qui è molto simile all'esempio precedente: crea semplicemente un nodo con alcuni parametri. Poiché il bordo animato dipende dalla forma e dal bordo del componente per cui viene utilizzato Indication, l'implementazione di Indication richiede anche che la forma e lo spessore bordo vengano forniti come parametri:
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 ) } }
Anche l'implementazione di Modifier.Node è concettualmente la stessa, anche se il codice di disegno è più complicato. Come prima, osserva InteractionSource
quando viene collegato, avvia le animazioni e implementa DrawModifierNode per disegnare
l'effetto sopra i contenuti:
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 differenza principale è che ora esiste una durata minima per l'animazione con la funzione animateToResting(), quindi anche se la pressione viene rilasciata immediatamente, l'animazione della pressione continuerà. Vengono gestite anche le pressioni rapide multiple all'inizio di animateToPressed: se una pressione si verifica durante un'animazione di pressione o di riposo esistente, l'animazione precedente viene annullata e l'animazione di pressione inizia dall'inizio. Per supportare più effetti simultanei (ad esempio con le onde, dove una nuova animazione di onda viene disegnata sopra le altre), puoi monitorare le animazioni in un elenco, anziché annullare quelle esistenti e avviarne di nuove.
Consigliati per te
- Nota: il testo del link viene visualizzato quando JavaScript è disattivato
- Informazioni sui gesti
- Kotlin per Jetpack Compose
- Componenti e layout di Material