Navigazione tramite gesti ed esperienza edge-to-edge

1. Introduzione

Per Android 10 o versioni successive, i gesti di navigazione sono supportati come una 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 indirizzato alla schermata Home di Android. Quando l'utente scorre verso l'interno dai bordi sinistro o destro, l'utente viene indirizzato alla schermata precedente.

Con questi due gesti, la tua app può sfruttare lo spazio che occupa nella parte inferiore dello schermo. Tuttavia, se l'app utilizza i gesti o dispone di controlli nelle aree di sistema relative ai gesti, potrebbero verificarsi conflitti con i gesti a livello di sistema.

Questo codelab ha lo scopo di insegnarti a usare i riquadri per evitare conflitti di gesti. Inoltre, questi codelab hanno lo scopo di insegnarti a utilizzare l'API Gesto Esclusione per i controlli, come i punti di manipolazione di trascinamento, che devono trovarsi nelle zone dei gesti.

Cosa imparerai

  • Come utilizzare gli ascoltatori inseriti nelle visualizzazioni
  • Come utilizzare l'API Empower Exclusion
  • Comportamento della modalità immersiva quando i gesti sono attivi

Questo codelab ha lo scopo di rendere la tua app compatibile con i gesti di sistema. Concetti irrilevanti e blocchi di codice sono trattati solo superficialmente e sono disponibili per essere copiati e incollati.

Cosa creerai

L'Universal Android Music Player (UAMP) è un'app di esempio di lettore musicale per Android scritta in Kotlin. Configurerai UAMP per la navigazione gestuale.

  • Usa i riquadri per allontanare i controlli dalle aree dei gesti
  • Utilizza l'API Driven Exclusion per disattivare il gesto Indietro per i controlli in conflitto
  • Usa le tue build per esplorare i cambiamenti del 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

L'Universal Android Music Player (UAMP) è un'app di esempio di musica per Android scritta in Kotlin. Supporta funzionalità che includono riproduzione in background, gestione del focus audio, integrazione con l'assistente e diverse 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 album e brani. L'utente tocca un brano e questo viene riprodotto tramite altoparlanti o cuffie connessi. L'app non è progettata per supportare i Gesti di sistema. Pertanto, quando esegui UAMP su un dispositivo con Android 10 o versioni successive, inizialmente riscontri alcuni problemi.

3. Configurazione

Per scaricare l'app di esempio, clona il repository da GitHub e passa al ramo 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 connettere un dispositivo reale con livello API 29 o superiore.
  3. Esegui l'app. L'elenco visualizzato raggruppa i brani nelle selezioni Consigliati e Album.
  4. Fai clic su Consigliati e seleziona un brano dall'elenco.
  5. L'app avvia la riproduzione del brano.

Attiva la navigazione tramite gesti

Se esegui una nuova istanza dell'emulatore con livello API 29, la navigazione tramite gesti potrebbe non essere attiva 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 con i gesti della schermata Home e Indietro.

4. Vai a livello perimetrale

Che cos'è edge-to-edge?

Le app eseguite su Android 10 o versioni successive possono offrire un'esperienza con lo schermo completa, 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 visualizzate dietro le barre di navigazione e di stato trasparenti.

Disegna dietro la barra di navigazione

Affinché l'app visualizzi i contenuti sotto la barra di navigazione, devi prima rendere trasparente lo sfondo della barra di navigazione. Devi quindi rendere trasparente la barra di stato. In questo modo la tua app potrà visualizzare la tua app per l'intera altezza dello schermo.

Per modificare il colore della barra di navigazione e della barra di stato, procedi nel seguente modo:

  1. Barra di navigazione: apri res/values-29/styles.xml e imposta navigationBarColor su color/transparent.
  2. Barra di stato: allo stesso modo, 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à dell'UI di sistema

Devi anche impostare i flag di visibilità dell'interfaccia utente di sistema per indicare al sistema di sovrapporre l'app sotto le barre di sistema. Le API systemUiVisibility nella classe View consentono di impostare una serie di flag. Procedi nel seguente modo:

  1. Apri il corso MainActivity.kt e trova il metodo onCreate(). Recupera un'istanza di fragmentContainer.
  2. Imposta il seguente valore 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 insieme questi flag, 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. Procedi nel seguente modo:

  1. Esegui l'app e, per andare alla schermata del player, seleziona un brano da riprodurre.
  2. Verifica che i controlli del player siano tracciati sotto la barra di navigazione; in questo modo è difficile accedervi:

  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 utilizzare con la barra di navigazione con tre pulsanti: tieni presente che SeekBar è nascosto dietro la barra di navigazione e che la funzione Riproduci/Pausa è coperta in gran parte dalla barra di navigazione.
  3. Esplora e sperimenta. Al termine, vai alle Impostazioni di sistema e torna alla Navigazione tramite gesti:

741ef664e9be5e7f.gif

L'app è ora in modalità edge-to-edge, ma sono presenti problemi di usabilità e controlli delle app che sono in conflitto e che si sovrappongono e questi problemi devono essere risolti.

5. Inserti

WindowInsets indica all'app dove viene visualizzata l'UI di sistema nella parte superiore dei contenuti e quali aree dello schermo hanno la priorità sui gesti in-app. Gli insiemi sono rappresentati dalla classe WindowInsets e dalla classe WindowInsetsCompat in Jetpack. Ti consigliamo vivamente di utilizzare WindowInsetsCompat per avere un comportamento coerente in tutti i livelli API.

Inserisci di sistema e riquadri di sistema obbligatori

Le seguenti API integrate sono i tipi di inserti più utilizzati:

  • Riquadri delle finestre di sistema:indicano dove viene visualizzata l'UI di sistema sopra l'app. Abbiamo parlato di come utilizzare gli insiemi di sistema per allontanare i controlli dalle barre del sistema.
  • Elementi con gesti di sistema:restituiscono tutte le aree corrispondenti. Tutti i controlli di scorrimento in-app in queste aree possono attivare accidentalmente i Gesti di sistema.
  • Inserti di gesti obbligatori:sono un sottoinsieme degli insiemi di 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.

Usare i riquadri per spostare i controlli delle app

Ora che conosci meglio le API integrate, puoi correggere i controlli dell'app, come descritto nei seguenti passaggi:

  1. Recupera un'istanza di playerLayout dall'istanza dell'oggetto view.
  2. Aggiungi una OnApplyWindowInsetsListener a playerView.
  3. Allontana la visualizzazione dall'area del gesto: trova il valore del riquadro di sistema per la parte inferiore e aumenta la spaziatura interna della visualizzazione di questo valore. Per aggiornare di conseguenza la spaziatura interna della vista a [valore associato alla spaziatura interna inferiore dell'app], aggiungi [il valore associato al valore inferiore inserito nel 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. Tieni presente che non sembra cambiare nulla nei controlli del player. Se aggiungi un punto di interruzione ed esegui l'app nel debug, vedrai che il listener non è stato chiamato.
  2. Per risolvere il problema, passa all'app FragmentContainerView, che gestisce il problema automaticamente. Apri activity_main.xml e cambia 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 in basso del player sono spostati dall'area dei gesti in basso.

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

Mantenere la spaziatura interna e i margini correnti

Se passi ad altre app o apri la schermata Home e torni all'app senza chiuderla, tieni presente che i controlli del player vengono spostati verso l'alto ogni volta.

Il motivo è che l'app attiva requestApplyInsets() ogni volta che viene avviata l'attività. Anche senza questa chiamata, WindowInsets può essere inviato più volte e in qualsiasi momento durante il ciclo di vita di una vista.

L'attuale InsetListener in playerView funziona perfettamente la prima volta quando aggiungi il valore minimo inserito al valore di spaziatura interna inferiore dell'app dichiarato in activity_main.xml. Nelle chiamate successive, tuttavia, continua ad aggiungere il valore inferiore del riquadro alla spaziatura interna inferiore della vista già aggiornata.

Per risolverlo, segui questi passaggi:

  1. Registra il valore di spaziatura interna della visualizzazione iniziale. Crea un nuovo valore e memorizza il valore di spaziatura interna della visualizzazione iniziale pari a playerView, subito prima del codice listener.

Esamina il seguente esempio di codice di NowPlayingFragment.kt:

   val initialPadding = playerView.paddingBottom
  1. Utilizza questo valore iniziale per aggiornare la spaziatura interna inferiore della visualizzazione, in modo da evitare di utilizzare il valore attuale di spaziatura interna 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. Spostati tra le app e vai alla schermata Home. Quando torni nell'app, i controlli del player si trovano subito sopra l'area del gesto.

Riprogettare i controlli dell'app

La barra di scorrimento del player è troppo vicina all'area in basso del gesto, il che significa che l'utente può attivare accidentalmente il gesto Home quando esegue uno scorrimento orizzontale. Se aumenti ulteriormente la spaziatura interna, il problema potrebbe risolversi, ma il player potrebbe anche essere spostato più in alto del previsto.

L'uso dei riquadri consente di risolvere i conflitti di gesti, ma a volte, con piccole modifiche al design, puoi evitare del tutto questi conflitti. Per riprogettare i controlli del player ed evitare conflitti di gesti, procedi nel seguente modo:

  1. Apri fragment_nowplaying.xml. Passa alla Vista struttura e seleziona SeekBar in basso:

74918dec3926293f.png

  1. Passa alla Vista codice.
  2. Per spostare SeekBar in cima alla playerLayout, modifica layout_constraintTop_toBottomOf della SeekBar in parent.
  3. Per vincolare altri elementi in playerView alla parte inferiore di SeekBar, modifica layout_constraintTop_toTopOf da principale a @+id/seekBar su 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 scorrimento.

Queste modifiche minime al design migliorano notevolmente l'app.

6. API Gesti Esclusione

I controlli del player per i conflitti di gesti nell'area dei gesti della schermata Home sono fissi. L'area del gesto Indietro può anche creare conflitti con i controlli dell'app. Il seguente screenshot mostra che la barra di scorrimento del player si trova attualmente nell'area dei gesti Indietro a destra e a sinistra:

e6d98e94dcf83dde.png

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

Utilizzare l'API MANAGE Exclusion

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

  1. Crea un nuovo pacchetto denominato view.
  2. Per chiamare questa API, crea un nuovo corso denominato 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 saltare questa chiamata con livello API 28 o precedente.

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 Gesti Esclusione ha un limite di 200 dp, escludi solo il pollice della barra di scorrimento. Recupera una copia dei limiti della barra di scorrimento e aggiungi ciascun 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 hai creato.

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(). Esegui l'override di 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 l'SeekBar. Se continui a riscontrare conflitti di gesti, 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é questo 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 usare la tua app a schermo intero estendendo l'area da bordo a bordo e utilizzato gli riquadri per allontanare i controlli delle app dalle zone dei gesti. Hai anche imparato a disattivare il gesto Indietro del sistema nei controlli delle app.

Ora sai quali sono i passaggi chiave necessari per far funzionare le tue app con i Gesti di sistema.

Materiali aggiuntivi

Documenti di riferimento