Esistono diversi termini e concetti che è importante comprendere quando si lavora sulla gestione dei gesti in un'applicazione. Questa pagina spiega i termini puntatori, eventi puntatore e gesti e introduce i diversi livelli di astrazione per i gesti. Inoltre, approfondisce il consumo e la propagazione degli eventi.
Definizioni
Per comprendere i vari concetti di questa pagina, devi conoscere alcuni dei termini utilizzati:
- Puntatore: un oggetto fisico che puoi utilizzare per interagire con la tua applicazione.
Per i dispositivi mobili, il puntatore più comune è il dito che interagisce con il touchscreen. In alternativa, puoi utilizzare uno stilo al posto del dito.
Per gli schermi di grandi dimensioni, puoi utilizzare un mouse o un trackpad per interagire indirettamente con il display. Un dispositivo di input deve essere in grado di "puntare" a una coordinata per essere considerato un puntatore, quindi una tastiera, ad esempio, non può essere considerata un puntatore. In Compose, il tipo di puntatore è incluso nelle modifiche del puntatore utilizzando
PointerType. - Evento puntatore: descrive un'interazione di basso livello di uno o più puntatori con l'applicazione in un determinato momento. Qualsiasi interazione con il puntatore, ad esempio toccare lo schermo con un dito o trascinare un mouse, attiva un evento. In Compose, tutte le informazioni pertinenti per un evento di questo tipo sono contenute nella classe
PointerEvent. - Gesto: una sequenza di eventi del puntatore che può essere interpretata come una singola azione. Ad esempio, un gesto di tocco può essere considerato una sequenza di un evento di pressione seguito da un evento di rilascio. Esistono gesti comuni utilizzati da molte app, come tocco, trascinamento o trasformazione, ma puoi anche creare un gesto personalizzato quando necessario.
Diversi livelli di astrazione
Jetpack Compose offre diversi livelli di astrazione per la gestione dei gesti.
Al livello più alto si trova l'assistenza per i componenti. I componibili come Button
includono automaticamente il supporto dei gesti. Per aggiungere il supporto dei gesti ai componenti personalizzati, puoi aggiungere modificatori dei gesti come clickable a elementi componibili arbitrari. Infine, se hai bisogno di un gesto personalizzato, puoi utilizzare il modificatore pointerInput.
Come regola generale, crea sul livello di astrazione più alto che offre la funzionalità di cui hai bisogno. In questo modo, puoi usufruire delle best practice incluse nel livello. Ad esempio, Button contiene più informazioni semantiche, utilizzate per l'accessibilità, rispetto a clickable, che contiene più informazioni rispetto a un'implementazione pointerInput non elaborata.
Supporto dei componenti
Molti componenti pronti all'uso in Compose includono un qualche tipo di gestione interna dei gesti. Ad esempio, un LazyColumn risponde ai gesti di trascinamento scorrendo i contenuti, un Button mostra un'onda quando lo premi e il componente SwipeToDismiss contiene la logica di scorrimento per ignorare un elemento. Questo tipo di gestione dei gesti funziona automaticamente.
Oltre alla gestione interna dei gesti, molti componenti richiedono anche che il chiamante
gestisca il gesto. Ad esempio, un Button rileva automaticamente i tocchi e attiva un evento di clic. Passi una funzione lambda onClick a Button per reagire al gesto. Analogamente, aggiungi una funzione lambda onValueChange a un Slider per reagire al trascinamento del cursore da parte dell'utente.
Quando si adattano al tuo caso d'uso, preferisci i gesti inclusi nei componenti, in quanto includono il supporto pronto all'uso per lo stato attivo e l'accessibilità e sono ben testati. Ad esempio, un Button è contrassegnato in modo speciale in modo che i servizi di accessibilità lo descrivano correttamente come un pulsante, anziché come un qualsiasi elemento cliccabile:
// Talkback: "Click me!, Button, double tap to activate" Button(onClick = { /* TODO */ }) { Text("Click me!") } // Talkback: "Click me!, double tap to activate" Box(Modifier.clickable { /* TODO */ }) { Text("Click me!") }
Per scoprire di più sull'accessibilità in Compose, consulta Accessibilità in Compose.
Aggiungi gesti specifici a elementi componibili arbitrari con i modificatori
Puoi applicare i modificatori dei gesti a qualsiasi composable arbitrario per fare in modo che
il composable ascolti i gesti. Ad esempio, puoi consentire a un Box generico di gestire i gesti di tocco rendendolo clickable o consentire a un Column di gestire lo scorrimento verticale applicando verticalScroll.
Esistono molti modificatori per gestire diversi tipi di gesti:
- Gestisci i tocchi e le pressioni con i modificatori
clickable,combinedClickable,selectable,toggleableetriStateToggleable. - Gestisci lo scorrimento con i modificatori
horizontalScroll,verticalScrolle i modificatoriscrollablepiù generici. - Gestisci il trascinamento con i modificatori
draggableeswipeable. - Gestisci i gesti multi-touch come la panoramica, la rotazione e lo zoom con il modificatore
transformable.
Come regola generale, preferisci i modificatori dei gesti pronti all'uso rispetto alla gestione dei gesti personalizzati.
I modificatori aggiungono ulteriori funzionalità alla gestione degli eventi del puntatore puro.
Ad esempio, il modificatore clickable non solo aggiunge il rilevamento di pressioni e tocchi, ma aggiunge anche informazioni semantiche, indicazioni visive sulle interazioni, passaggio del mouse, stato attivo e supporto della tastiera. Puoi controllare il codice sorgente di clickable per vedere come viene aggiunta la funzionalità.
Aggiungi un gesto personalizzato a composabili arbitrari con il modificatore pointerInput
Non tutti i gesti vengono implementati con un modificatore di gesti pronto all'uso. Ad esempio, non puoi utilizzare un modificatore per reagire a un trascinamento dopo una pressione prolungata, un clic con il tasto Ctrl o un tocco con tre dita. Puoi invece scrivere il tuo gestore dei gesti per identificare questi gesti personalizzati. Puoi creare un gestore di gesti con il modificatore pointerInput, che ti consente di accedere agli eventi puntatore non elaborati.
Il seguente codice ascolta gli eventi del puntatore non elaborati:
@Composable private fun LogPointerEvents(filter: PointerEventType? = null) { var log by remember { mutableStateOf("") } Column { Text(log) Box( Modifier .size(100.dp) .background(Color.Red) .pointerInput(filter) { awaitPointerEventScope { while (true) { val event = awaitPointerEvent() // handle pointer event if (filter == null || event.type == filter) { log = "${event.type}, ${event.changes.first().position}" } } } } ) } }
Se suddividi questo snippet, i componenti principali sono:
- Il modificatore
pointerInput. Devi passare una o più chiavi. Quando il valore di una di queste chiavi cambia, la funzione lambda dei contenuti del modificatore viene eseguita di nuovo. L'esempio passa un filtro facoltativo al composable. Se il valore di questo filtro cambia, il gestore di eventi del puntatore deve essere eseguito nuovamente per assicurarsi che vengano registrati gli eventi corretti. awaitPointerEventScopecrea un ambito di coroutine che può essere utilizzato per attendere gli eventi del puntatore.awaitPointerEventsospende la coroutine fino al verificarsi di un evento puntatore successivo.
Sebbene l'ascolto di eventi di input non elaborati sia efficace, è anche complesso scrivere un gesto personalizzato basato su questi dati non elaborati. Per semplificare la creazione di gesti personalizzati, sono disponibili molti metodi di utilità.
Rileva i gesti completi
Anziché gestire gli eventi di puntatore non elaborati, puoi ascoltare gesti specifici
e rispondere di conseguenza. AwaitPointerEventScope fornisce
metodi per l'ascolto di:
- Pressione, tocco, doppio tocco e pressione prolungata:
detectTapGestures - Resistenze:
detectHorizontalDragGestures,detectVerticalDragGestures,detectDragGesturesedetectDragGesturesAfterLongPress - Trasformazioni:
detectTransformGestures
Si tratta di rilevatori di primo livello, quindi non puoi aggiungere più rilevatori all'interno di un modificatore pointerInput. Il seguente snippet rileva solo i tocchi, non i trascinamenti:
var log by remember { mutableStateOf("") } Column { Text(log) Box( Modifier .size(100.dp) .background(Color.Red) .pointerInput(Unit) { detectTapGestures { log = "Tap!" } // Never reached detectDragGestures { _, _ -> log = "Dragging" } } ) }
Internamente, il metodo detectTapGestures blocca la coroutine e il secondo rilevatore non viene mai raggiunto. Se devi aggiungere più di un listener di gesti a un composable, utilizza istanze di modificatore pointerInput separate:
var log by remember { mutableStateOf("") } Column { Text(log) Box( Modifier .size(100.dp) .background(Color.Red) .pointerInput(Unit) { detectTapGestures { log = "Tap!" } } .pointerInput(Unit) { // These drag events will correctly be triggered detectDragGestures { _, _ -> log = "Dragging" } } ) }
Gestisci gli eventi per gesto
Per definizione, i gesti iniziano con un evento pointerdown. Puoi utilizzare il metodo helper awaitEachGesture anziché il loop while(true) che passa attraverso ogni evento non elaborato. Il metodo awaitEachGesture riavvia il blocco contenitore quando tutti i puntatori sono stati sollevati, indicando che il gesto è stato completato:
@Composable private fun SimpleClickable(onClick: () -> Unit) { Box( Modifier .size(100.dp) .pointerInput(onClick) { awaitEachGesture { awaitFirstDown().also { it.consume() } val up = waitForUpOrCancellation() if (up != null) { up.consume() onClick() } } } ) }
In pratica, quasi sempre è preferibile utilizzare awaitEachGesture, a meno che tu non stia rispondendo a eventi del puntatore senza identificare i gesti. Un esempio è hoverable, che non risponde agli eventi di pressione o rilascio del puntatore, ma deve solo sapere quando un puntatore entra o esce dai suoi limiti.
Attendi un evento o un gesto secondario specifico
Esiste un insieme di metodi che aiuta a identificare le parti comuni dei gesti:
- Sospendi finché un puntatore non scende con
awaitFirstDowno attendi che tutti i puntatori salgano conwaitForUpOrCancellation. - Crea un listener di trascinamento di basso livello utilizzando
awaitTouchSlopOrCancellationeawaitDragOrCancellation. Il gestore dei gesti viene prima sospeso finché il puntatore non raggiunge la soglia di scorrimento, quindi viene sospeso finché non viene ricevuto un primo evento di trascinamento. Se ti interessano solo i trascinamenti lungo un singolo asse, utilizzaawaitHorizontalTouchSlopOrCancellationpiùawaitHorizontalDragOrCancellationoawaitVerticalTouchSlopOrCancellationpiùawaitVerticalDragOrCancellation. - Sospendi fino a quando non viene eseguita una pressione prolungata con
awaitLongPressOrCancellation. - Utilizza il metodo
dragper ascoltare continuamente gli eventi di trascinamento oppurehorizontalDragoverticalDragper ascoltare gli eventi di trascinamento su un asse.
Applica i calcoli per gli eventi multi-touch
Quando un utente esegue un gesto multi-touch utilizzando più di un puntatore, è difficile comprendere la trasformazione richiesta in base ai valori non elaborati.
Se il modificatore transformable o i metodi detectTransformGestures non forniscono un controllo sufficientemente granulare per il tuo caso d'uso, puoi ascoltare gli eventi non elaborati e applicare i calcoli su questi. Questi metodi helper sono calculateCentroid, calculateCentroidSize, calculatePan, calculateRotation e calculateZoom.
Invio di eventi e hit test
Non tutti gli eventi del puntatore vengono inviati a ogni modificatore pointerInput. L'invio degli eventi funziona nel seguente modo:
- Gli eventi del puntatore vengono inviati a una gerarchia componibile. Nel momento in cui un nuovo puntatore attiva il suo primo evento puntatore, il sistema avvia l'hit test dei componibili "idonei". Un composable è considerato idoneo quando ha funzionalità di gestione dell'input del puntatore. I flussi di hit test vanno dall'alto verso il basso dell'albero della UI. Un componibile viene "colpito" quando l'evento del puntatore si verifica entro i limiti di quel componibile. Questo processo genera una catena di componibili che superano l'hit test.
- Per impostazione predefinita, quando sono presenti più componibili idonei allo stesso livello dell'albero, viene "colpito" solo il componibile con l'indice z più alto. Ad esempio, quando aggiungi due componibili
Buttonsovrapposti a unBox, solo quello disegnato in alto riceve gli eventi del puntatore. In teoria, puoi eseguire l'override di questo comportamento creando la tua implementazione diPointerInputModifierNodee impostandosharePointerInputWithSiblingssu true. - Gli eventi successivi per lo stesso puntatore vengono inviati alla stessa catena di componibili e seguono la logica di propagazione degli eventi. Il sistema non esegue altri hit test per questo puntatore. Ciò significa che ogni componibile della catena riceve tutti gli eventi per quel puntatore, anche quando si verificano al di fuori dei limiti del componibile. I componibili che non fanno parte della catena non ricevono mai eventi del puntatore, anche quando il puntatore si trova all'interno dei loro limiti.
Gli eventi di passaggio del mouse, attivati dal passaggio del mouse o dello stilo, sono un'eccezione alle regole definite qui. Gli eventi di passaggio del mouse vengono inviati a qualsiasi composable che colpiscono. Quindi, quando un utente passa il puntatore dai limiti di un composable a quello successivo, anziché inviare gli eventi al primo composable, gli eventi vengono inviati al nuovo composable.
Consumo di eventi
Quando a più composabili è assegnato un gestore di gesti, questi gestori non devono entrare in conflitto. Ad esempio, dai un'occhiata a questa UI:
Quando un utente tocca il pulsante dei preferiti, la funzione lambda onClick del pulsante gestisce il gesto. Quando un utente tocca un'altra parte dell'elemento dell'elenco, ListItem
gestisce il gesto e passa all'articolo. In termini di input del puntatore, il pulsante deve utilizzare questo evento, in modo che il relativo elemento principale sappia di non dover più reagire. I gesti inclusi nei componenti pronti all'uso e i modificatori dei gesti comuni includono questo comportamento di consumo, ma se stai scrivendo un gesto personalizzato, devi consumare gli eventi manualmente. A questo scopo, utilizza il metodo PointerInputChange.consume:
Modifier.pointerInput(Unit) { awaitEachGesture { while (true) { val event = awaitPointerEvent() // consume all changes event.changes.forEach { it.consume() } } } }
L'utilizzo di un evento non interrompe la propagazione dell'evento ad altri elementi componibili. Un composable deve invece ignorare esplicitamente gli eventi consumati. Quando scrivi gesti personalizzati, devi verificare se un evento è già stato utilizzato da un altro elemento:
Modifier.pointerInput(Unit) { awaitEachGesture { while (true) { val event = awaitPointerEvent() if (event.changes.any { it.isConsumed }) { // A pointer is consumed by another gesture handler } else { // Handle unconsumed event } } } }
Propagazione degli eventi
Come accennato in precedenza, le modifiche del puntatore vengono passate a ogni composable che colpisce.
Tuttavia, se esistono più composabili di questo tipo, in quale ordine si propagano gli eventi? Se prendiamo l'esempio della sezione precedente, questa UI si traduce nel seguente albero della UI, in cui solo ListItem e Button rispondono agli eventi del puntatore:
Gli eventi del puntatore passano attraverso ciascuno di questi composabili tre volte, durante tre "passaggi":
- Nella fase iniziale, l'evento passa dalla parte superiore dell'albero della UI alla parte inferiore. Questo flusso consente a un elemento padre di intercettare un evento prima che l'elemento figlio possa
utilizzarlo. Ad esempio, i tooltip devono intercettare una pressione prolungata anziché trasmetterla ai relativi elementi secondari. Nel nostro esempio,
ListItemriceve l'evento prima diButton. - Nella fase principale, l'evento passa dai nodi foglia dell'albero della UI alla radice dell'albero della UI. In questa fase, in genere, vengono consumati i gesti ed è il passaggio predefinito durante l'ascolto degli eventi. La gestione dei gesti in questa fase
significa che i nodi foglia hanno la precedenza sui relativi nodi principali, il che è il
comportamento più logico per la maggior parte dei gesti. Nel nostro esempio,
Buttonriceve l'evento prima diListItem. - Nel passaggio finale, l'evento passa ancora una volta dalla parte superiore dell'albero della UI ai nodi foglia. Questo flusso consente agli elementi più in alto nello stack di rispondere al consumo di eventi da parte del relativo elemento principale. Ad esempio, un pulsante rimuove l'indicazione di ripple quando una pressione si trasforma in un trascinamento del relativo elemento padre scorrevole.
Visivamente, il flusso di eventi può essere rappresentato come segue:
Una volta consumata una modifica dell'input, queste informazioni vengono trasmesse da quel punto del flusso in poi:
Nel codice, puoi specificare la tessera che ti interessa:
Modifier.pointerInput(Unit) { awaitPointerEventScope { val eventOnInitialPass = awaitPointerEvent(PointerEventPass.Initial) val eventOnMainPass = awaitPointerEvent(PointerEventPass.Main) // default val eventOnFinalPass = awaitPointerEvent(PointerEventPass.Final) } }
In questo snippet di codice, lo stesso identico evento viene restituito da ciascuna di queste chiamate al metodo await, anche se i dati relativi al consumo potrebbero essere cambiati.
Testa i gesti
Nei metodi di test, puoi inviare manualmente gli eventi del puntatore utilizzando il metodo performTouchInput. In questo modo puoi eseguire gesti completi di livello superiore (come pizzicare o fare clic e tenere premuto) o gesti di livello inferiore (come spostare il cursore di un certo numero di pixel):
composeTestRule.onNodeWithTag("MyList").performTouchInput { swipeUp() swipeDown() click() }
Per altri esempi, consulta la documentazione di performTouchInput.
Scopri di più
Puoi scoprire di più sui gesti in Jetpack Compose nelle seguenti risorse:
Consigliati per te
- Nota: il testo del link viene visualizzato quando JavaScript è disattivato
- Accessibilità in Compose
- Scorrere
- Tocca e premi