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.
Siga estas etapas:
- Abra e crie o app no Android Studio.
- 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.
- Execute o app. A lista que você vê agrupa as músicas nas seleções Recommended (recomendadas) e Albums (álbuns).
- Clique em Recommended e selecione uma música da lista.
- 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:
- Barra de navegação: abra
res/values-29/styles.xml
e definanavigationBarColor
comocolor/transparent
. - Barra de status: também defina
statusBarColor
comocolor/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:
- Abra a classe
MainActivity.kt
e localize o métodoonCreate()
. Acesse uma instância dofragmentContainer
. - Defina os elementos abaixo como
content.systemUiVisibility
:
View.SYSTEM_UI_FLAG_LAYOUT_STABLE
View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
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:
- Execute o app, navegue até a tela do player e selecione uma música para tocar.
- Verifique se os controles do player são exibidos embaixo da barra de navegação, dificultando o acesso a eles:
- Vá até as configurações do sistema, volte para o modo de navegação com três botões e retorne ao app.
- 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. - 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:
- Acesse uma instância de
playerLayout
na instância do objetoview
. - Adicione um
OnApplyWindowInsetsListener
àplayerView
. - 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
}
- 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.
- Para corrigir isso, alterne para
FragmentContainerView
, que resolve esse problema automaticamente. Abraactivity_main.xml
e mudeFrameLayout
paraFragmentContainerView
.
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"/>
- 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:
- 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
- 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
}
- 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:
- Abra o
fragment_nowplaying.xml
Alterne para a visualização "Design" e selecioneSeekBar
na parte de baixo:
- Alterne para a visualização "Code".
- Para mover a
SeekBar
até a parte de cima doplayerLayout
, mude olayout_constraintTop_toBottomOf
da barra de busca paraparent
. - Para restringir outros itens da
playerView
à parte de baixo daSeekBar
, mudelayout_constraintTop_toTopOf
do pai para@+id/seekBar
emmedia_button
,title
eposition
.
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>
- 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:
- Crie um novo pacote com o nome
view
. - Para chamar essa API, crie uma nova classe com o nome
MySeekBar
e estendaAppCompatSeekBar
.
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) {
}
- Crie um novo método com o nome
updateGestureExclusion()
.
Veja abaixo o exemplo de código de MySeekBar.kt
:
private fun updateGestureExclusion() {
}
- 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
}
- 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()
}
}
- Chame
systemGestureExclusionRects()
com as listasgestureExclusionRects
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
}
- Chame o método
updateGestureExclusion()
emonDraw()
ouonLayout()
. SubstituaonDraw()
e adicione uma chamada paraupdateGestureExclusion
.
Veja abaixo o exemplo de código de MySeekBar.kt
:
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
updateGestureExclusion()
}
- É preciso atualizar as referências de
SeekBar
. Para começar, abrafragment_nowplaying.xml
. - Mude
SeekBar
paracom.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" />
- Para atualizar as referências de
SeekBar
emNowPlayingFragment.kt
, abraNowPlayingFragment.kt
e mude o tipo depositionSeekBar
paraMySeekBar
. Para associar o tipo de variável, mude aSeekBar
genérica da chamadafindViewById
paraMySeekBar
.
Veja abaixo o exemplo de código de NowPlayingFragment.kt
:
val positionSeekBar: MySeekBar = view.findViewById<MySeekBar>(
R.id.seekBar
).apply { progress = 0 }
- Execute o app e interaja com a
SeekBar
. Se ainda houver conflitos de gesto, tente mudar os limites da miniatura emMySeekBar
. 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
- WindowInsets: listeners para layouts (link em inglês)
- Navegação por gestos: de ponta a ponta (link em inglês)
- Navegação por gestos: como gerenciar sobreposições visuais (link em inglês)
- Navegação por gestos: como lidar com conflitos de gesto (link em inglês)
- Garantir a compatibilidade com a navegação por gestos