Navegação por gestos e a experiência de ponta a ponta

Para o Android 10 ou versões mais recentes, um novo modo oferece suporte aos gestos de navegação. Isso permite que o app use a tela inteira e forneça uma experiência de exibição mais imersiva. Quando o usuário deslizar da borda de baixo da tela para cima, a tela inicial do Android vai ser exibida. Quando deslizar da borda esquerda ou direita para o centro, o usuário vai ser levado à tela anterior.

Com esses dois gestos, o app pode aproveitar toda a área de baixo da tela. No entanto, se o app usar gestos ou tiver controles nas áreas de gesto do sistema, isso poderá criar conflitos com os gestos do sistema.

O objetivo deste codelab é ensinar você a usar encartes para evitar conflitos de gestos. Além disso, este codelab pretende ensinar você a usar a API Gesture Exclusion para controles que precisam ficar nas zonas de gestos, como as alças de arrastar.

O que você vai aprender

  • Como usar listeners de encarte nas visualizações.
  • Como usar a API Gesture Exclusion.
  • Como o modo imersivo se comporta quando os gestos estão ativos.

Este codelab pretende tornar seu app compatível com os gestos do sistema. Conceitos e blocos de código irrelevantes são resumidos e apresentados para você copiar e colar.

O que você vai criar

O Universal Android Music Player (UAMP) é um exemplo de app de player de música para Android escrito em Kotlin. Você vai configurar o UAMP para usar a navegação por gestos.

  • Use encartes para afastar os controles das áreas de gesto.
  • Use a API Gesture Exclusion a fim de desativar o gesto "voltar" para controles que criam conflito.
  • Use os builds para explorar as mudanças de comportamento do modo imersivo com a navegação por gestos.

O que é necessário

  • Um dispositivo ou emulador com o Android 10 ou mais recente
  • Android Studio

O Universal Android Music Player (UAMP) é um exemplo de app de player de música para Android escrito em Kotlin. Ele oferece suporte a recursos que incluem reprodução em segundo plano, manuseio de seleção de áudio, integração do Google Assistente e várias plataformas, como Wear, TV e Auto.

Figura 1: um fluxo no UAMP

O UAMP carrega um catálogo de músicas em um servidor remoto e permite que os usuários naveguem pelos álbuns e músicas. O usuário toca em uma música e a reproduz pelos alto-falantes ou fones de ouvido conectados. O app não foi projetado para funcionar com gestos do sistema. Quando o UAMP for executado em um dispositivo com o Android 10 ou mais recente, você vai enfrentar alguns problemas inicialmente.

Para fazer o download do app de exemplo, clone o repositório do GitHub e alterne para a ramificação starter:

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


Se preferir, você pode fazer o download do repositório como um arquivo ZIP, descompactá-lo e abrir no Android Studio.

Fazer o download do ZIP

Siga estas etapas:

  1. Abra e crie o app no Android Studio.
  2. Crie um novo dispositivo virtual e selecione API level 29. Se preferir, você pode conectar um dispositivo real com o nível 29 da API ou mais recente.
  3. Execute o app. A lista que você vê agrupa as músicas nas seleções Recommended (recomendadas) e Albums (álbuns).
  4. Clique em Recommended e selecione uma música da lista.
  5. O app vai tocar a música.

Ativar a navegação por gestos

Se você executar uma nova instância do emulador com o nível 29 da API, a navegação por gestos talvez não seja ativada por padrão. Para ativar a navegação por gestos, selecione Configurações do sistema > Sistema > Navegação do sistema > Navegação por gestos.

Executar o app com a navegação por gestos

Se você executar o app com a navegação por gestos ativada e iniciar a reprodução de uma música, talvez perceba que os controles do player estão bem perto das áreas para gestos de ir até a tela inicial e de voltar.

O que é ponta a ponta?

Os apps executados no Android 10 ou versões mais recentes podem oferecer uma experiência completa de tela de ponta a ponta, independente dos gestos ou botões estarem ativados para navegação. Para oferecer uma experiência de ponta a ponta, os apps precisam ser mostrados atrás das barras de status e de navegação transparentes.

Mostrar atrás da barra de navegação

Para que o app renderize conteúdo na barra de navegação, primeiro você precisa deixar o fundo da barra de navegação transparente. Depois, você precisa deixar a barra de status transparente. Isso permite que o app seja exibido na altura inteira da tela.

Para mudar a cor da barra de navegação e da barra de status, siga estas etapas:

  1. Barra de navegação: abra res/values-29/styles.xml e defina navigationBarColor como color/transparent.
  2. Barra de status: também defina statusBarColor como color/transparent.

Revise o exemplo de código abaixo de 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>

Sinalizadores de visibilidade da IU do sistema

Também é possível definir os sinalizadores de visibilidade da IU do sistema para que o app fique embaixo das barras de sistema. As APIs systemUiVisibility na classe View permitem definir vários sinalizadores. Siga estas etapas:

  1. Abra a classe MainActivity.kt e localize o método onCreate(). Acesse uma instância do fragmentContainer.
  2. Defina os elementos abaixo como content.systemUiVisibility:

Veja abaixo o exemplo de código de 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 esses sinalizadores são definidos juntos, você informa ao sistema que gostaria de exibir o app em tela cheia, como se as barras de status e navegação não estivessem presentes. Siga estas etapas:

  1. Execute o app, navegue até a tela do player e selecione uma música para tocar.
  2. Verifique se os controles do player são exibidos embaixo da barra de navegação, dificultando o acesso a eles:

  1. Vá até as configurações do sistema, volte para o modo de navegação com três botões e retorne ao app.
  2. Veja que os controles estão ainda mais difíceis de usar com o modo de navegação com três botões: observe que a SeekBar está oculta atrás da barra de navegação e que o botão de Tocar/pausar está coberto pela barra de navegação.
  3. Explore e experimente um pouco. Quando terminar, vá até as configurações do sistema e volte para a navegação por gestos:

O app agora é exibido de ponta a ponta agora, mas existem problemas de usabilidade que precisam ser resolvidos, como controles do app que criam conflito e se sobrepõem.

A classe WindowInsets informa ao app onde a IU do sistema é mostrada por cima do seu conteúdo e também quais regiões da tela são priorizadas pelos gestos do sistema com relação aos gestos no app. Os encartes são representados pelas classes WindowInsets e WindowInsetsCompat no Jetpack. Recomendamos que você use WindowInsetsCompat para ter um comportamento consistente em todos os níveis da API.

Encartes do sistema e encartes obrigatórios do sistema

As APIs de encarte abaixo são os tipos de encarte usados com mais frequência:

  • Encartes de janela do sistema: informam onde a IU do sistema é exibida por cima do app. Vamos discutir como é possível usar encartes do sistema para afastar controles das barras de sistema.
  • Encartes de gestos do sistema: retornam todas as áreas de gesto. Os controles de deslizamento no app dessas regiões podem acionar gestos do sistema acidentalmente.
  • Encartes de gesto obrigatórios: são um subconjunto dos encartes de gestos do sistema e não podem ser substituídos. Eles informam as áreas da tela onde o comportamento dos gestos do sistema sempre terá prioridade sobre os gestos do app.

Usar encartes para mover controles do app

Agora que você sabe mais sobre as APIs de encarte, pode corrigir os controles do app como descrito nas próximas etapas:

  1. Acesse uma instância de playerLayout na instância do objeto view.
  2. Adicione um OnApplyWindowInsetsListener à playerView.
  3. Afaste a visualização da área de gesto: encontre o valor do encarte do sistema da parte de baixo da tela e aumente o padding da visualização por esse valor. A fim de atualizar o padding da visualização corretamente, para o [valor associado ao padding da parte de baixo do app], adicione [o valor associado ao valor da parte de baixo do encarte do sistema].

Veja abaixo o exemplo de código de NowPlayingFragment.kt:

playerView = view.findViewById(R.id.playerLayout)
playerView.setOnApplyWindowInsetsListener { view, insets ->
   view.updatePadding(
      bottom = insets.systemWindowInsetBottom + view.paddingBottom
   )
   insets
}
  1. Execute o app e selecione uma música. Observe que nada parece mudar nos controles do player. Se você adicionar um ponto de interrupção e executar o app no modo de depuração, vai ver que o listener não é chamado.
  2. Para corrigir isso, alterne para FragmentContainerView, que resolve esse problema automaticamente. Abra activity_main.xml e mude FrameLayout para FragmentContainerView.

Veja abaixo o exemplo de código de 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. Execute o app novamente e navegue até a tela do player. Os controles da parte de baixo do player são afastados da área de gesto de baixo.

Os controles do app agora funcionam com a navegação por gestos, mas os controles se moveram mais do que o esperado. É preciso resolver isso.

Manter o padding e as margens atuais

Se você alternar para outros apps ou for até a tela inicial e voltar para o app sem o fechar, vai perceber que os controles do player se movem para cima toda vez que o app é retomado.

Isso acontece porque o app aciona requestApplyInsets() sempre que a atividade começa. Mesmo sem essa chamada, a classe WindowInsets pode ser enviada várias vezes a qualquer momento durante o ciclo de vida de uma visualização.

O InsetListener atual na playerView funciona perfeitamente na primeira vez em que você adiciona o valor do encarte de baixo ao valor do padding de baixo do app declarado em activity_main.xml. No entanto, as chamadas subsequentes continuam adicionando o valor do encarte de baixo ao padding já atualizado da visualização.

Para resolver isso, siga estas etapas:

  1. Registre o valor inicial do padding da visualização. Crie um novo valor e armazene o valor inicial do padding da visualização playerView, logo antes do código do listener.

Veja abaixo o exemplo de código de NowPlayingFragment.kt:

   val initialPadding = playerView.paddingBottom
  1. Use esse valor inicial para atualizar o padding de baixo da visualização, o que permite evitar o uso do valor de padding atual do app.

Veja abaixo o exemplo de código de NowPlayingFragment.kt:

   playerView.setOnApplyWindowInsetsListener { view, insets ->
            view.updatePadding(bottom = insets.systemWindowInsetBottom + initialPadding)
            insets
        }
  1. Execute o app novamente. Navegue entre apps e vá para a tela inicial. Quando você retornar ao app, os controles do player vão estar no lugar certo, logo acima da área de gesto.

Reprojetar controles do app

A barra de busca do player está muito perto da área de gesto de baixo, o que significa que o usuário pode acionar acidentalmente o gesto de ir até a tela inicial quando terminar um deslizamento horizontal. Se você aumentar o padding ainda mais, o problema vai ser resolvido, mas também pode ser que o player seja movido para um lugar mais alto do que o desejado.

O uso de encartes permite resolver conflitos de gesto, mas, às vezes, com pequenas mudanças de design, é possível evitar conflitos de gesto por completo. Para reprojetar os controles do player e evitar conflitos de gesto, siga estas etapas:

  1. Abra o fragment_nowplaying.xml Alterne para a visualização "Design" e selecione SeekBar na parte de baixo:

  1. Alterne para a visualização "Code".
  2. Para mover a SeekBar até a parte de cima do playerLayout, mude o layout_constraintTop_toBottomOf da barra de busca para parent.
  3. Para restringir outros itens da playerView à parte de baixo da SeekBar, mude layout_constraintTop_toTopOf do pai para @+id/seekBar em media_button, title e position.

Veja abaixo o exemplo de código de 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. Execute o app e interaja com o player e a barra de busca.

Essas mudanças mínimas de design melhoram significativamente o app.

Os controles do player para conflitos na área de gestos de voltar à tela inicial foram resolvidos. A área de gestos "voltar" também pode criar conflitos com os controles do app. A captura de tela abaixo mostra que a barra de busca do player atualmente fica nas áreas de gesto "voltar" da direita e da esquerda:

A SeekBar lida automaticamente com conflitos de gesto. Mesmo assim, talvez seja preciso usar outros componentes de IU que causam conflitos de gesto. Nesses casos, você pode usar a Gesture Exclusion API para não autorizar parcialmente o gesto de voltar.

Usar a API Gesture Exclusion

Para criar uma zona de exclusão de gesto, chame o método setSystemGestureExclusionRects() na visualização com uma lista de objetos rect. Esses objetos rect mapeiam para coordenadas das áreas retangulares excluídas. Essa chamada precisa ser feita nos métodos onLayout() ou onDraw() da visualização. Para fazer isso, siga estas etapas:

  1. Crie um novo pacote com o nome view.
  2. Para chamar essa API, crie uma nova classe com o nome MySeekBar e estenda AppCompatSeekBar.

Veja abaixo o exemplo de código de 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. Crie um novo método com o nome updateGestureExclusion().

Veja abaixo o exemplo de código de MySeekBar.kt:

private fun updateGestureExclusion() {

}
  1. Adicione uma verificação para pular essa chamada no nível 28 da API ou versões anteriores.

Veja abaixo o exemplo de código de MySeekBar.kt:

private fun updateGestureExclusion() {
        // Skip this call if we're not running on Android 10+
        if (Build.VERSION.SDK_INT < 29) return
}
  1. Como a API Gesture Exclusion tem um limite de 200 dp, exclua somente a miniatura da barra de busca. Extraia uma cópia dos limites da barra de busca e adicione cada objeto a uma lista mutável.

Veja abaixo o exemplo de código de 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. Chame systemGestureExclusionRects() com as listas gestureExclusionRects criadas.

Veja abaixo o exemplo de código de 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. Chame o método updateGestureExclusion() em onDraw() ou onLayout(). Substitua onDraw() e adicione uma chamada para updateGestureExclusion.

Veja abaixo o exemplo de código de MySeekBar.kt:

override fun onDraw(canvas: Canvas) {
    super.onDraw(canvas)
    updateGestureExclusion()
}
  1. É preciso atualizar as referências de SeekBar. Para começar, abra fragment_nowplaying.xml.
  2. Mude SeekBar para com.example.android.uamp.view.MySeekBar.

Veja abaixo o exemplo de código de 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. Para atualizar as referências de SeekBar em NowPlayingFragment.kt, abra NowPlayingFragment.kt e mude o tipo de positionSeekBar para MySeekBar. Para associar o tipo de variável, mude a SeekBar genérica da chamada findViewById para MySeekBar.

Veja abaixo o exemplo de código de NowPlayingFragment.kt:

val positionSeekBar: MySeekBar = view.findViewById<MySeekBar>(
     R.id.seekBar
).apply { progress = 0 }
  1. Execute o app e interaja com a SeekBar. Se ainda houver conflitos de gesto, tente mudar os limites da miniatura em MySeekBar. Tome cuidado para não criar uma zona de exclusão de gesto maior do que o necessário, porque isso limita outras possíveis chamadas de exclusão de gesto e cria um comportamento inconsistente para o usuário.

Parabéns! Você aprendeu a evitar e solucionar conflitos com os gestos do sistema.

Você fez o app usar a tela cheia, o estendeu de ponta a ponta e usou encartes para afastar os controles dele das zonas de gesto. Você também aprendeu a desativar o gesto "voltar" do sistema nos controles do app.

Agora você sabe as principais etapas necessárias para fazer seus apps funcionarem com gestos do sistema.

Outros recursos

Documentos de referência