Navigazione tramite gesti ed esperienza edge-to-edge

1. Introduzione

Per Android 10 o versioni successive, i gesti di navigazione sono supportati come nuova modalità. In questo modo, la tua app può utilizzare l'intero schermo e offrire un'esperienza di visualizzazione più immersiva. Quando l'utente scorre verso l'alto dal bordo inferiore dello schermo, viene visualizzata la schermata Home di Android. Quando scorrono verso l'interno dai bordi sinistro o destro, tornano alla schermata precedente.

Con questi due gesti, la tua app può sfruttare lo spazio nella parte inferiore dello schermo. Tuttavia, se la tua app utilizza i gesti o ha controlli nelle aree dei gesti di sistema, potrebbe creare conflitti con i gesti a livello di sistema.

Questo codelab ha lo scopo di insegnarti a utilizzare gli inset per evitare conflitti di gesti. Inoltre, questo codelab ha lo scopo di insegnarti a utilizzare l'API Gesture Exclusion per i controlli, ad esempio le maniglie di trascinamento, che devono trovarsi nelle zone dei gesti.

Cosa imparerai

  • Come utilizzare i listener di inserimento nelle visualizzazioni
  • Come utilizzare l'API Gesture Exclusion
  • Come si comporta la modalità immersiva quando i gesti sono attivi

Questo codelab ha lo scopo di rendere la tua app compatibile con le gesture di sistema. Concetti e blocchi di codice non pertinenti sono trattati solo superficialmente e sono forniti solo per operazioni di copia e incolla.

Cosa creerai

Universal Android Music Player (UAMP) è un'app di esempio per la riproduzione di musica per Android scritta in Kotlin. Configurerai UAMP per la navigazione tramite gesti.

  • Utilizzare gli inset per allontanare i controlli dalle aree dei gesti
  • Utilizza l'API Gesture Exclusion per disattivare il gesto Indietro per i controlli in conflitto
  • Utilizzare le build per esplorare le modifiche al comportamento della modalità immersiva con la navigazione tramite gesti

Che cosa ti serve

  • Un dispositivo o un emulatore con Android 10 o versioni successive
  • Android Studio

2. Panoramica app

Universal Android Music Player (UAMP) è un'app di esempio per la riproduzione di musica per Android scritta in Kotlin. Supporta funzionalità che includono la riproduzione in background, la gestione della messa a fuoco audio, l'integrazione dell'assistente e più piattaforme come Wear, TV e Auto.

Figura 1: un flusso in UAMP

UAMP carica un catalogo musicale da un server remoto e consente all'utente di sfogliare gli album e i brani. L'utente tocca un brano e lo riproduce tramite cuffie o altoparlanti connessi. L'app non è progettata per funzionare con i gesti di sistema. Pertanto, quando esegui UAMP su un dispositivo con Android 10 o versioni successive, inizialmente riscontri alcuni problemi.

3. Configurazione

Per ottenere l'app di esempio, clona il repository da GitHub e passa al branch starter:

$  git clone https://github.com/googlecodelabs/android-gestural-navigation/

In alternativa, puoi scaricare il repository come file ZIP, decomprimerlo e aprirlo in Android Studio.

Completa i seguenti passaggi:

  1. Apri e crea l'app in Android Studio.
  2. Crea un nuovo dispositivo virtuale e seleziona livello API 29. In alternativa, puoi collegare un dispositivo reale con livello API 29 o superiore.
  3. Esegui l'app. L'elenco che vedi raggruppa i brani nelle sezioni Consigliati e Album.
  4. Fai clic su Consigliati e seleziona un brano dall'elenco.
  5. L'app inizia la riproduzione del brano.

Attivare la navigazione tramite gesti

Se esegui una nuova istanza dell'emulatore con il livello API 29, la navigazione tramite gesti potrebbe non essere attivata per impostazione predefinita. Per attivare la navigazione tramite gesti, seleziona Impostazioni di sistema > Sistema > Navigazione del sistema > Navigazione tramite gesti.

Eseguire l'app con la navigazione tramite gesti

Se esegui l'app con la navigazione tramite gesti attivata e avvii la riproduzione di un brano, potresti notare che i controlli del player sono molto vicini alle aree dei gesti Home e Indietro.

4. Passare alla visualizzazione edge-to-edge

Che cos'è edge-to-edge?

Le app eseguite su Android 10 o versioni successive possono offrire un'esperienza a schermo intero, indipendentemente dal fatto che i gesti o i pulsanti siano abilitati per la navigazione. Per offrire un'esperienza edge-to-edge, le app devono essere disegnate dietro le barre di navigazione e di stato trasparenti.

Disegna dietro la barra di navigazione

Affinché la tua app esegua il rendering dei contenuti sotto la barra di navigazione, devi prima rendere trasparente lo sfondo della barra di navigazione. Dopodiché, devi rendere trasparente la barra di stato. In questo modo, l'app può essere visualizzata per l'intera altezza dello schermo.

Per modificare il colore della barra di navigazione e della barra di stato:

  1. Barra di navigazione:apri res/values-29/styles.xml e imposta navigationBarColor su color/transparent.
  2. Barra di stato:imposta statusBarColor su color/transparent.

Esamina il seguente esempio di codice di res/values-29/styles.xml:

<!-- change navigation bar color -->
<item name="android:navigationBarColor">
    @android:color/transparent
</item>

<!-- change status bar color -->
<item name="android:statusBarColor">
    @android:color/transparent
</item>

Flag di visibilità della UI di sistema

Devi anche impostare i flag di visibilità dell'interfaccia utente di sistema per indicare al sistema di disporre l'app sotto le barre di sistema. Le API systemUiVisibility nella classe View consentono di impostare una serie di flag. Segui questi passaggi:

  1. Apri la classe MainActivity.kt e trova il metodo onCreate(). Ottieni un'istanza di fragmentContainer.
  2. Imposta i seguenti valori su content.systemUiVisibility:

Esamina il seguente esempio di codice di MainActivity.kt:

  val content: FrameLayout = findViewById(R.id.fragmentContainer)
  content.systemUiVisibility = View.SYSTEM_UI_FLAG_LAYOUT_STABLE or
            View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or
            View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION

Quando imposti questi flag insieme, indichi al sistema che vuoi che la tua app venga visualizzata a schermo intero come se le barre di navigazione e di stato non fossero presenti. Segui questi passaggi:

  1. Esegui l'app e, per passare alla schermata del player, seleziona un brano da riprodurre.
  2. Verifica che i controlli del player siano disegnati sotto la barra di navigazione, rendendoli difficili da raggiungere:

  1. Vai alle impostazioni di sistema, torna alla modalità di navigazione con tre pulsanti e torna all'app.
  2. Verifica che i controlli siano ancora più difficili da usare con la barra di navigazione a tre pulsanti: nota che il pulsante SeekBar è nascosto dietro la barra di navigazione e che il pulsante Riproduci/Pausa è coperto quasi interamente dalla barra di navigazione.
  3. Esplora e sperimenta un po'. Al termine, vai alle impostazioni di sistema e torna alla navigazione tramite gesti:

741ef664e9be5e7f.gif

L'app ora viene visualizzata da bordo a bordo, ma ci sono problemi di usabilità, controlli dell'app in conflitto e sovrapposti, che devono essere risolti.

5. Inset

WindowInsets indica all'app dove viene visualizzata l'interfaccia utente di sistema sopra i contenuti, nonché le regioni dello schermo in cui i gesti di sistema hanno la priorità sui gesti in-app. Gli inserti sono rappresentati dalla classe WindowInsets e dalla classe WindowInsetsCompat in Jetpack. Ti consigliamo vivamente di utilizzare WindowInsetsCompat per garantire un comportamento coerente in tutti i livelli API.

Inserti di sistema e inserti di sistema obbligatori

Le seguenti API di inset sono i tipi di inset più comunemente utilizzati:

  • Inset della finestra di sistema:indicano dove viene visualizzata la UI di sistema sopra l'app. Spieghiamo come utilizzare gli inset di sistema per allontanare i controlli dalle barre di sistema.
  • Inset dei gesti di sistema:restituiscono tutte le aree dei gesti. I controlli di scorrimento in-app in queste regioni possono attivare accidentalmente i gesti di sistema.
  • Margini interni dei gesti obbligatori:sono un sottoinsieme dei margini interni dei gesti di sistema e non possono essere sostituiti. Indicano le aree dello schermo in cui il comportamento dei gesti di sistema avrà sempre la priorità sui gesti in-app.

Utilizzare gli inserti per spostare i controlli delle app

Ora che hai maggiori informazioni sulle API inset, puoi correggere i controlli delle app, come descritto nei passaggi seguenti:

  1. Ottieni un'istanza di playerLayout dall'istanza dell'oggetto view.
  2. Aggiungi un OnApplyWindowInsetsListener a playerView.
  3. Allontana la visualizzazione dall'area dei gesti: trova il valore di inserimento del sistema per la parte inferiore e aumenta il padding della visualizzazione di questo valore. Per aggiornare il padding della visualizzazione di conseguenza, aggiungi [il valore associato al padding inferiore dell'app] a [il valore associato al valore di rientro inferiore del sistema].

Esamina il seguente esempio di codice di NowPlayingFragment.kt:

playerView = view.findViewById(R.id.playerLayout)
playerView.setOnApplyWindowInsetsListener { view, insets ->
   view.updatePadding(
      bottom = insets.systemWindowInsetBottom + view.paddingBottom
   )
   insets
}
  1. Esegui l'app e seleziona un brano. Nota che non sembra cambiare nulla nei controlli del player. Se aggiungi un punto di interruzione ed esegui il debug dell'app, vedrai che il listener non viene chiamato.
  2. Per risolvere il problema, passa a FragmentContainerView, che lo gestisce automaticamente. Apri activity_main.xml e modifica FrameLayout in FragmentContainerView.

Esamina il seguente esempio di codice di activity_main.xml:

<androidx.fragment.app.FragmentContainerView xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/fragmentContainer"
    tools:context="com.example.android.uamp.MainActivity"
    android:layout_width="match_parent"
    android:layout_height="match_parent"/>
  1. Esegui di nuovo l'app e vai alla schermata del player. I controlli del player in basso sono spostati lontano dall'area dei gesti in basso.

I controlli dell'app ora funzionano con la navigazione tramite gesti, ma si spostano più del previsto. Devi risolvere il problema.

Mantenere l'imbottitura e i margini attuali

Se passi ad altre app o vai alla schermata Home e torni all'app senza chiuderla, noterai che i controlli del player si spostano verso l'alto ogni volta.

Questo perché l'app attiva requestApplyInsets() ogni volta che l'attività inizia. Anche senza questa chiamata, WindowInsets può essere inviato più volte in qualsiasi momento durante il ciclo di vita di una visualizzazione.

L'attuale InsetListener su playerView funziona perfettamente la prima volta quando aggiungi l'importo del valore del rientro inferiore al valore del padding inferiore dell'app dichiarato in activity_main.xml. Le chiamate successive, tuttavia, continuano ad aggiungere il valore del margine interno inferiore al padding inferiore della visualizzazione già aggiornata.

Per risolvere il problema, segui questi passaggi:

  1. Registra il valore iniziale del padding della visualizzazione. Crea un nuovo valore e memorizza il valore iniziale del padding della visualizzazione di playerView, appena prima del codice del listener.

Esamina il seguente esempio di codice di NowPlayingFragment.kt:

   val initialPadding = playerView.paddingBottom
  1. Utilizza questo valore iniziale per aggiornare il padding inferiore della visualizzazione, in modo da evitare l'utilizzo del valore corrente del padding inferiore dell'app.

Esamina il seguente esempio di codice di NowPlayingFragment.kt:

   playerView.setOnApplyWindowInsetsListener { view, insets ->
            view.updatePadding(bottom = insets.systemWindowInsetBottom + initialPadding)
            insets
        }
  1. Esegui di nuovo l'app. Passare da un'app all'altra e andare alla schermata Home. Quando torni all'app, i controlli del player si trovano appena sopra l'area dei gesti.

Riprogettazione dei controlli delle app

La barra di ricerca del player è troppo vicina all'area dei gesti in basso, il che significa che l'utente può attivare accidentalmente il gesto della home page quando completa uno scorrimento orizzontale. Se aumenti ulteriormente il padding, il problema potrebbe risolversi, ma il player potrebbe anche spostarsi più in alto di quanto desiderato.

L'utilizzo degli inset ti consente di risolvere i conflitti di gesti, ma a volte, con piccole modifiche al design, puoi evitarli del tutto. Per riprogettare i controlli del player ed evitare conflitti con i gesti, segui questi passaggi:

  1. Apri fragment_nowplaying.xml. Passa alla visualizzazione Struttura e seleziona SeekBar in basso:

74918dec3926293f.png

  1. Passa alla Vista codice.
  2. Per spostare SeekBar nella parte superiore di playerLayout, modifica layout_constraintTop_toBottomOf della barra di ricerca in parent.
  3. Per vincolare altri elementi in playerView alla parte inferiore di SeekBar, modifica layout_constraintTop_toTopOf da elemento principale a @+id/seekBar in media_button, title e position.

Esamina il seguente esempio di codice di fragment_nowplaying.xml:

<androidx.constraintlayout.widget.ConstraintLayout
   android:layout_width="match_parent"
   android:layout_height="wrap_content"
   android:padding="8dp"
   android:layout_gravity="bottom"
   android:background="@drawable/media_overlay_background"
   android:id="@+id/playerLayout">

   <ImageButton
       android:id="@+id/media_button"
       android:layout_width="@dimen/exo_media_button_width"
       android:layout_height="@dimen/exo_media_button_height"
       android:background="?attr/selectableItemBackground"
       android:scaleType="centerInside"
       app:layout_constraintLeft_toLeftOf="parent"
       app:layout_constraintTop_toTopOf="@+id/seekBar"
       app:srcCompat="@drawable/ic_play_arrow_black_24dp"
       tools:ignore="ContentDescription" />

   <TextView
       android:id="@+id/title"
       android:layout_width="0dp"
       android:layout_height="wrap_content"
       android:layout_marginTop="8dp"
       android:layout_marginStart="@dimen/text_margin"
       android:layout_marginEnd="@dimen/text_margin"
       android:ellipsize="end"
       android:maxLines="1"
       android:textAppearance="@style/TextAppearance.Uamp.Title"
       app:layout_constraintTop_toTopOf="@+id/seekBar"
       app:layout_constraintLeft_toRightOf="@id/media_button"
       app:layout_constraintRight_toLeftOf="@id/position"
       tools:text="Song Title" />

   <TextView
       android:id="@+id/subtitle"
       android:layout_width="0dp"
       android:layout_height="wrap_content"
       android:layout_marginStart="@dimen/text_margin"
       android:layout_marginEnd="@dimen/text_margin"
       android:ellipsize="end"
       android:maxLines="1"
       android:textAppearance="@style/TextAppearance.Uamp.Subtitle"
       app:layout_constraintTop_toBottomOf="@+id/title"
       app:layout_constraintLeft_toRightOf="@id/media_button"
       app:layout_constraintRight_toLeftOf="@id/position"
       tools:text="Artist" />

   <TextView
       android:id="@+id/position"
       android:layout_width="0dp"
       android:layout_height="wrap_content"
       android:layout_marginTop="8dp"
       android:layout_marginStart="@dimen/text_margin"
       android:layout_marginEnd="@dimen/text_margin"
       android:ellipsize="end"
       android:maxLines="1"
       android:textAppearance="@style/TextAppearance.Uamp.Title"
       app:layout_constraintTop_toTopOf="@+id/seekBar"
       app:layout_constraintRight_toRightOf="parent"
       tools:text="0:00" />

   <TextView
       android:id="@+id/duration"
       android:layout_width="0dp"
       android:layout_height="wrap_content"
       android:layout_marginStart="@dimen/text_margin"
       android:layout_marginEnd="@dimen/text_margin"
       android:ellipsize="end"
       android:maxLines="1"
       android:textAppearance="@style/TextAppearance.Uamp.Subtitle"
       app:layout_constraintTop_toBottomOf="@id/position"
       app:layout_constraintRight_toRightOf="parent"
       tools:text="0:00" />

   <SeekBar
       android:id="@+id/seekBar"
       android:layout_width="match_parent"
       android:layout_height="wrap_content"
       app:layout_constraintBottom_toBottomOf="parent"
       app:layout_constraintEnd_toEndOf="parent"
       app:layout_constraintStart_toStartOf="parent"
       app:layout_constraintTop_toBottomOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>
  1. Esegui l'app e interagisci con il player e la barra di ricerca.

Queste modifiche minime al design migliorano notevolmente l'app.

6. API Gesture Exclusion

I controlli di riproduzione per i conflitti di gesture nell'area delle gesture della casa sono stati corretti. L'area del gesto Indietro può anche creare conflitti con i controlli dell'app. Il seguente screenshot mostra che la barra di ricerca del player si trova attualmente nelle aree dei gesti indietro a destra e a sinistra:

e6d98e94dcf83dde.png

SeekBar gestisce automaticamente i conflitti di gesture. Tuttavia, potresti dover utilizzare altri componenti UI che attivano conflitti di gesti. In questi casi, puoi utilizzare Gesture Exclusion API per disattivare parzialmente il gesto Indietro.

Utilizzare l'API Gesture Exclusion

Per creare una zona di esclusione dei gesti, chiama setSystemGestureExclusionRects() nella visualizzazione con un elenco di oggetti rect. Questi oggetti rect corrispondono alle coordinate delle aree rettangolari escluse. Questa chiamata deve essere eseguita nei metodi onLayout() o onDraw() della visualizzazione. Per farlo, segui questi passaggi:

  1. Crea un nuovo pacchetto denominato view.
  2. Per chiamare questa API, crea una nuova classe denominata MySeekBar ed estendi AppCompatSeekBar.

Esamina il seguente esempio di codice di MySeekBar.kt:

class MySeekBar @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyle: Int = android.R.attr.seekBarStyle
) : androidx.appcompat.widget.AppCompatSeekBar(context, attrs, defStyle) {

}
  1. Crea un nuovo metodo denominato updateGestureExclusion().

Esamina il seguente esempio di codice di MySeekBar.kt:

private fun updateGestureExclusion() {

}
  1. Aggiungi un controllo per ignorare questa chiamata al livello API 28 o inferiore.

Esamina il seguente esempio di codice di MySeekBar.kt:

private fun updateGestureExclusion() {
        // Skip this call if we're not running on Android 10+
        if (Build.VERSION.SDK_INT < 29) return
}
  1. Poiché l'API Gesture Exclusion ha un limite di 200 dp, escludi solo il pollice della barra di ricerca. Ottieni una copia dei limiti della barra di ricerca e aggiungi ogni oggetto a un elenco modificabile.

Esamina il seguente esempio di codice di MySeekBar.kt:

private val gestureExclusionRects = mutableListOf<Rect>()

private fun updateGestureExclusion() {
    // Skip this call if we're not running on Android 10+
    if (Build.VERSION.SDK_INT < 29) return

    thumb?.also { t ->
        gestureExclusionRects += t.copyBounds()
    }
}
  1. Chiama systemGestureExclusionRects() con gli elenchi gestureExclusionRects che crei.

Esamina il seguente esempio di codice di MySeekBar.kt:

private val gestureExclusionRects = mutableListOf<Rect>()

private fun updateGestureExclusion() {
    // Skip this call if we're not running on Android 10+
    if (Build.VERSION.SDK_INT < 29) return

    thumb?.also { t ->
        gestureExclusionRects += t.copyBounds()
    }
    // Finally pass our updated list of rectangles to the system
    systemGestureExclusionRects = gestureExclusionRects
}
  1. Chiama il metodo updateGestureExclusion() da onDraw() o onLayout(). Ignora onDraw() e aggiungi una chiamata a updateGestureExclusion.

Esamina il seguente esempio di codice di MySeekBar.kt:

override fun onDraw(canvas: Canvas) {
    super.onDraw(canvas)
    updateGestureExclusion()
}
  1. Devi aggiornare i riferimenti SeekBar. Per iniziare, apri fragment_nowplaying.xml.
  2. Cambia SeekBar in com.example.android.uamp.view.MySeekBar.

Esamina il seguente esempio di codice di fragment_nowplaying.xml:

<com.example.android.uamp.view.MySeekBar
    android:id="@+id/seekBar"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    app:layout_constraintBottom_toBottomOf="parent"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toBottomOf="parent" />
  1. Per aggiornare i riferimenti SeekBar in NowPlayingFragment.kt, apri NowPlayingFragment.kt e modifica il tipo di positionSeekBar in MySeekBar. Per far corrispondere il tipo di variabile, modifica i generici SeekBar per la chiamata findViewById in MySeekBar.

Esamina il seguente esempio di codice di NowPlayingFragment.kt:

val positionSeekBar: MySeekBar = view.findViewById<MySeekBar>(
     R.id.seekBar
).apply { progress = 0 }
  1. Esegui l'app e interagisci con SeekBar. Se i conflitti di gesture persistono, puoi sperimentare e modificare i limiti del pollice in MySeekBar. Fai attenzione a non creare una zona di esclusione dei gesti più grande del necessario, perché ciò limita altre potenziali chiamate di esclusione dei gesti e crea un comportamento incoerente per l'utente.

7. Complimenti

Complimenti! Hai imparato a evitare e risolvere i conflitti con i gesti di sistema.

Hai fatto in modo che la tua app utilizzi lo schermo intero quando hai esteso la visualizzazione edge-to-edge e hai utilizzato gli inset per allontanare i controlli dell'app dalle zone dei gesti. Hai anche imparato a disattivare il gesto Indietro del sistema sui controlli delle app.

Ora conosci i passaggi chiave necessari per far funzionare le tue app con i gesti di sistema.

Materiali aggiuntivi

Documentazione di riferimento