Processar restrições de ponta a ponta no Android 15

1. Antes de começar

O SociaLite ensina a usar várias APIs da plataforma Android para implementar recursos comuns de apps de redes sociais, aproveitando uma variedade de APIs do Jetpack para acessar funcionalidades complexas que funcionam de forma confiável em mais dispositivos e exigem menos código.

Este codelab explica o processo para tornar o app SociaLite compatível com a exibição de ponta a ponta no Android 15, e de forma que ele também seja compatível com versões anteriores. Após esse processo, o SociaLite terá a seguinte aparência, dependendo do dispositivo e modo de navegação:

O app SociaLite na navegação com três botões.

O app SociaLite na navegação por gestos.

SociaLite na navegação com três botões.

SociaLite na navegação por gestos.

O app SociaLite em um dispositivo de tela grande.

SociaLite em um dispositivo de tela grande.

Pré-requisitos

  • Conhecimentos básicos sobre Kotlin.
  • Ter concluído o codelab Configurar o Android Studio ou ter familiaridade com o uso do Android Studio e apps de teste em um emulador ou dispositivo físico com o Android 15.

O que você vai aprender

  • Como processar mudanças de ponta a ponta no Android 15.
  • Como mostrar seu app de ponta a ponta de maneira compatível com versões anteriores.

O que é necessário

  • Ter a versão mais recente do Android Studio.
  • Ter um dispositivo de teste ou emulador com o Android 15 Beta 1 ou versão mais recente.
  • Ter um SDK Android 15 Beta 1 ou mais recente.

2. Acessar o código inicial

  1. Faça o download do código inicial (link em inglês) no GitHub.

Você também pode clonar o repositório e conferir a ramificação codelab_improve_android_experience_2024.

$ git clone git@github.com:android/socialite.git
$ cd socialite
$ git checkout codelab_improve_android_experience_2024
  1. Abra o SociaLite no Android Studio e execute o app no seu dispositivo ou emulador com o Android 15. Uma tela parecida com uma destas opções será mostrada:

SociaLite na navegação com três botões.

SociaLite na navegação por gestos.

Navegação com três botões.

Navegação por gestos.

SociaLite em um dispositivo de tela grande.

Tela grande.

  1. Na página Chats, selecione uma das conversas, como a que tem uma imagem de cachorro.

Mensagem na conversa sobre cachorros na navegação com três botões.

Mensagem na conversa sobre cachorros na navegação por gestos.

Mensagem na conversa sobre cachorros na navegação com três botões.

Mensagem na conversa sobre cachorros na navegação por gestos.

3. Mostrar seu app de ponta a ponta no Android 15

O que é ponta a ponta?

Os apps podem mostrar informações por trás das barras de sistema, permitindo uma experiência do usuário aprimorada e uso completo do espaço da tela. Isso é chamado de exibição de ponta a ponta.

GIF de um app sendo mostrado de ponta a ponta.

Como processar mudanças de ponta a ponta no Android 15

Antes do Android 15, por padrão, a interface do seu app era restrita de modo a evitar as áreas das barras de sistema, como as barras de status e navegação. Os apps permitiam escolher a exibição de ponta a ponta. Dependendo do app, implementar esse tipo de exibição podia ser simples ou complexo.

Do Android 15 em diante, seu app será mostrado de ponta a ponta por padrão. Confira alguns dos novos padrões:

  • A barra de navegação com três botões é translúcida.
  • A barra de navegação por gestos é transparente.
  • A barra de status é transparente.
  • A menos que o conteúdo aplique encartes ou padding, ele será mostrado por trás das barras de sistema, como as de navegação, status e legenda.

Isso garante que a exibição de ponta a ponta não seja ignorada como uma maneira de aumentar a qualidade do app e reduz o trabalho necessário para mostrar o app dessa forma. No entanto, essa mudança pode impactar seu app negativamente. Você verá dois exemplos de impactos negativos no SociaLite após o upgrade do SDK de destino para o Android 15.

Mudar o valor do SDK de destino para o Android 15

  1. No arquivo build.gradle do app SociaLite, mude o destino e compile as versões do SDK para o Android 15 ou VanillaIceCream.

Se você estiver lendo este codelab antes do lançamento estável do Android 15, o código terá esta aparência:

android {
    namespace = "com.google.android.samples.socialite"
    compileSdkPreview = "VanillaIceCream"

    defaultConfig {
        applicationId = "com.google.android.samples.socialite"
        minSdk = 21
        targetSdkPreview = "VanillaIceCream"
        ...
    }
...
}

Se for após o lançamento estável do Android 15, o código terá esta aparência:

android {
    namespace = "com.google.android.samples.socialite"
    compileSdk = 35

    defaultConfig {
        applicationId = "com.google.android.samples.socialite"
        minSdk = 21
        targetSdk = 35
        ...
    }
...
}
  1. Recrie o SociaLite e observe estes problemas:
  • A proteção em segundo plano da navegação com três botões não corresponde à barra de navegação. Na navegação por gestos, a tela Chats é mostrada de ponta a ponta sem nenhum tipo de intervenção da sua parte. No entanto, há uma proteção em segundo plano da navegação com três botões, que precisa ser removida.

Tela "Chats" na navegação com três botões.

Tela "Chats" na navegação por gestos.

Tela Chats na navegação com três botões.

Tela Chats na navegação por gestos.

  • Interface ocultada. Os elementos da interface na parte de baixo de uma conversa são ocultados pelas barras de navegação. Isso fica mais aparente na navegação com três botões.

Mensagem na conversa sobre cachorros na navegação com três botões.

Mensagem na conversa sobre cachorros na navegação por gestos.

Mensagem na conversa sobre cachorros na navegação com três botões.

Mensagem na conversa sobre cachorros na navegação por gestos.

Corrigir o SociaLite

Para remover a proteção em segundo plano padrão da navegação com três botões, siga estas etapas:

  1. No arquivo MainActivity.kt, configure a propriedade window.isNavigationBarContrastEnforced como falsa para remover a proteção em segundo plano padrão.
class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        installSplashScreen()
        super.onCreate(savedInstanceState)
        setContent {
            // Add this block:
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
                window.isNavigationBarContrastEnforced = false
            }
        }
    }
    ...
}

A window.isNavigationBarContrastEnforced garante que a barra de navegação tenha contraste suficiente quando um plano de fundo totalmente transparente é solicitado. Ao definir esse atributo como falso, você configura o plano de fundo da navegação com três botões como transparente. A window.isNavigationBarContrastEnforced só afeta a navegação com três botões e não tem impacto na navegação por gestos.

  1. Execute o app novamente e confira uma das conversas no seu dispositivo Android 15. As telas Timeline, Chats e Settings agora são mostradas de ponta a ponta. A NavigationBar do app (com os botões Timeline, Chats e Settings) é mostrada por trás da barra de navegação com três botões transparente do sistema.

Tela "Chats" na navegação com três botões e remoção das faixas de cor.

Conversa sobre cachorros na navegação por gestos.

Tela Chats com remoção das faixas de cor.

Não há mudanças na navegação por gestos.

No entanto, observe que a InputBar da conversa ainda está ocultada pelas barras de sistema. Você precisa processar os encartes adequadamente para corrigir esse problema.

Conversa sobre cachorros na navegação com três botões.

Conversa sobre cachorros na navegação por gestos.

Conversa sobre cachorros na navegação com três botões. O campo de entrada na parte de baixo foi ocultado pela barra de navegação do sistema.

Conversa sobre cachorros na navegação por gestos. O campo de entrada na parte de baixo foi ocultado pela barra de navegação do sistema.

No SociaLite, a InputBar fica esmaecida. Na prática, você poderá encontrar elementos esmaecidos nos quatro cantos da tela ao mudar para o modo paisagem ou usar um dispositivo de tela grande. É preciso programar o processamento de encartes em todos esses casos de uso. Para o SociaLite, padding é aplicado para destacar o conteúdo tocável da InputBar.

Para aplicar encartes e corrigir interfaces ocultadas, siga estas etapas:

  1. Navegue até o arquivo ui/chat/ChatScreen.kt e encontre o elemento combinável ChatContent por volta da linha 178, que contém a interface da tela de conversa. O ChatContent usa o Scaffold para criar a interface com facilidade. Por padrão, o Scaffold fornece informações sobre a interface do sistema, como a profundidade das barras de sistema, na forma de encartes que você pode consumir com valores do padding do Scaffold (o parâmetro innerPadding). Adicione padding usando o innerPadding do Scaffold como InputBar.
  2. Encontre a InputBar dentro do ChatContent por volta da linha 214. Esse é um elemento combinável que cria a interface para usuários escreverem mensagens. A prévia é semelhante a esta:

A PreviewInputBar.

A InputBar usa um contentPadding para aplicar como padding ao elemento combinável Row que contém o restante da interface. Esse padding será aplicado a todos os lados do elemento combinável Row. Você pode conferir esse detalhe por volta da linha 432. Para referência, confira o elemento combinável InputBar (não adicione este código):

// Don't add this code because it's only for reference.
@Composable
private fun InputBar(
    contentPadding: PaddingValues,
    ...,
) {
    Surface(...) {
        Row(
            modifier = Modifier
                .padding(contentPadding)
            ...
        ) {
            IconButton(...) { ... } // take picture
            IconButton(...) { ... } // attach picture
            TextField(...) // write message
            FilledIconButton(...){ ... } // send message
            }
        }
    }
}
  1. Volte à InputBar dentro de ChatContent e mude o contentPadding para consumir os encartes da barra de sistema. Faça isso por volta da linha 220.
InputBar(
    ...
    contentPadding = innerPadding, //Add this line.
    // contentPadding = PaddingValues(0.dp), // Remove this line.
    ...
 )
  1. Execute o app novamente no seu dispositivo Android 15.

Conversa sobre cachorros na navegação com três botões.

Conversa sobre cachorros na navegação por gestos.

Conversa sobre cachorros na navegação com três botões com encartes aplicados incorretamente.

Conversa sobre cachorros na navegação por gestos com encartes aplicados incorretamente.

O padding da parte de baixo foi aplicado para que os botões não ficassem mais esmaecidos pelas barras de sistema, mas o padding da parte de cima também foi aplicado. Ele abrange a profundidade da TopAppBar e a barra de sistema. O Scaffold transmite os valores do padding ao conteúdo para evitar a barra de apps da parte de cima, bem como as barras de sistema.

  1. Para corrigir o padding da parte de cima, crie uma cópia dos PaddingValues do innerPadding, defina o padding da parte de cima como 0.dp e transmita sua cópia modificada para contentPadding.
InputBar(
    ...
    contentPadding = innerPadding.copy(layoutDirection, top = 0.dp), //Add this line.
    // contentPadding = innerPadding, // Remove this line.
    ...
 )
  1. Execute o app novamente no seu dispositivo Android 15.

Conversa sobre cachorros na navegação com três botões.

Conversa sobre cachorros na navegação por gestos.

Conversa sobre cachorros na navegação com três botões com encartes aplicados corretamente.

Conversa sobre cachorros na navegação por gestos com encartes aplicados corretamente.

Parabéns! Você tornou o SociaLite compatível com as mudanças da plataforma de ponta a ponta do Android 15. A seguir, você vai aprender a mostrar o SociaLite de ponta a ponta de maneira compatível com versões anteriores.

4. Mostrar o SociaLite de ponta a ponta de maneira compatível com versões anteriores

O SociaLite agora é mostrado de ponta a ponta no Android 15, mas ainda não tem essa funcionalidade em dispositivos Android mais antigos. Para mostrar o SociaLite de ponta a ponta em dispositivos Android mais antigos, chame enableEdgeToEdge antes de definir o conteúdo no arquivo MainActivity.kt.

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        installSplashScreen()
        enableEdgeToEdge() // Add this line.
        window.isNavigationBarContrastEnforced = false
        super.onCreate(savedInstanceState)
        setContent {... }
    }
}

A importação do enableEdgeToEdge é import androidx.activity.enableEdgeToEdge. A dependência é AndroidX Activity 1.8.0 ou uma versão mais recente.

Para conferir uma descrição aprofundada do processo para mostrar seu app de ponta a ponta de maneira compatível com versões anteriores e processar encartes, confira estes guias:

Isso conclui a parte do Programa de treinamentos sobre a exibição de ponta a ponta. A próxima seção é opcional e discute outras considerações sobre a exibição de ponta a ponta que podem ser relevantes para o seu app.

5. Opcional: outras considerações sobre a exibição de ponta a ponta

Como processar encartes em várias arquiteturas

Componentes

Talvez você tenha notado que vários componentes do SociaLite não mudaram após a troca do valor do SDK de destino. O SociaLite é arquitetado com práticas recomendadas, então é fácil processar essa mudança de plataforma. As práticas recomendadas incluem:

Como rolar conteúdo

Seu app pode conter listas, e o último item da lista pode ser ocultado pelas barras de navegação do sistema com a mudança do Android 15.

App com o último item da lista ocultado pela navegação com três botões.

Mostra que o último item da lista está ocultado pela navegação com três botões.

Como rolar conteúdo com o Compose

No Compose, use o contentPadding da LazyColumn para adicionar um espaço ao último item, a menos que você esteja usando o TextField:

Scaffold { innerPadding ->
    LazyColumn(
        contentPadding = innerPadding
    ) {
        // Content that does not contain TextField
    }
}

App com último item da lista não ocultado pela navegação com três botões.

Mostra que o último item da lista não está ocultado pela navegação com três botões.

No caso do TextField, use um Spacer para mostrar o último TextField em uma LazyColumn. Para saber mais, consulte Consumo de encartes.

LazyColumn(
    Modifier.imePadding()
) {
    // Content with TextField
    item {
        Spacer(
            Modifier.windowInsetsBottomHeight(
                WindowInsets.systemBars
            )
        )
    }
}

Como rolar conteúdo com o Views

Para RecyclerView ou NestedScrollView, adicione android:clipToPadding="false".

<androidx.recyclerview.widget.RecyclerView
    android:id="@+id/recycler"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:clipToPadding="false"
    app:layoutManager="LinearLayoutManager" />

Forneça paddings dos encartes de janela em todos os lados, usando setOnApplyWindowInsetsListener:

ViewCompat.setOnApplyWindowInsetsListener(binding.recycler) { v, insets ->
    val i = insets.getInsets(
        WindowInsetsCompat.Type.systemBars() + WindowInsetsCompat.Type.displayCutout()
    )
    v.updatePadding(
        left = i.left,
        right = i.right,
        bottom = i.bottom + bottomPadding,
    )
    WindowInsetsCompat.CONSUMED
}

Como usar o LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS

Antes de ser destinado ao SDK 35, o SocialLite tinha esta aparência no modo paisagem, com uma grande caixa branca na borda esquerda para compensar o corte da câmera. Na navegação com três botões, os botões ficam do lado direito.

O app SociaLite no modo paisagem.

Depois de ser destinado ao SDK 35, o SociaLite terá esta aparência, com a borda esquerda livre, sem precisar compensar o corte da câmera. Para conseguir esse efeito, o Android define LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS automaticamente. O app SociaLite no modo paisagem.

Dependendo do app, convém processar encartes nesta parte.

Para fazer isso no SociaLite, siga estas etapas:

  1. No arquivo ui/ContactRow.kt, encontre o elemento combinável Row.
  2. Modifique o padding para considerar o corte da tela.
@Composable
fun ChatRow(
   chat: ChatDetail,
   onClick: (() -> Unit)?,
   modifier: Modifier = Modifier,
) {
   // Add layoutDirection, displayCutout, startPadding, and endPadding.
   val layoutDirection = LocalLayoutDirection.current
   val displayCutout = WindowInsets.displayCutout.asPaddingValues()
   val startPadding = displayCutout.calculateStartPadding(layoutDirection)
   val endPadding = displayCutout.calculateEndPadding(layoutDirection)
   Row(
       modifier = modifier
           ...
           // .padding(16.dp) // Remove this line.
           // Add this block:
           .padding(
               PaddingValues(
                   top = 16.dp,
                   bottom = 16.dp,
                   // Ensure content is not occluded by display cutouts
                   // when rotating the device.
                   start = startPadding.coerceAtLeast(16.dp),
                   end = endPadding.coerceAtLeast(16.dp)
               )
           ),
       ...
   ) { ... }

Após o processamento de cortes de tela, o SociaLite terá esta aparência:

O app SociaLite no modo paisagem.

Teste várias configurações de corte da tela na seção Opções do desenvolvedor em Corte da tela.

Caso uma janela não flutuante (por exemplo, uma atividade) do seu app esteja usando LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT, LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER ou LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES, o Android vai interpretar esses modos de corte como LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS do Android 15 Beta 2 em diante. No Android 15 Beta 1, isso causaria uma falha no app.

Barras de legenda também são barras de sistema

Uma barra de legenda também é uma barra de sistema, já que descreve a decoração da janela da interface do sistema de uma janela com formato livre, como a barra de título da parte de cima. Você pode conferir a barra de legenda em um emulador de computador no Android Studio. Na captura de tela abaixo, a barra de legenda está na parte de cima do app.

Emulador mostrando uma barra de legenda.

No Compose, se você estiver usando PaddingValues, safeContent, safeDrawing ou as WindowInsets.systemBars integradas do Scaffold, seu app será mostrado como esperado. No entanto, se você estiver processando encartes com a statusBar, o conteúdo do app talvez não seja mostrado como esperado, já que a barra de status não considera a barra de legenda.

No Views, se você estiver processando encartes manualmente usando WindowInsetsCompat.systemBars, seu app será mostrado como esperado. Se você estiver processando encartes manualmente usando WindowInsetsCompat.statusBars, talvez o app não seja mostrado como esperado, já que barras de status não são barras de legenda.

Apps no modo imersivo

Telas no modo imersivo não são afetadas pelas restrições de ponta a ponta do Android 15, já que apps imersivos já são mostrados dessa forma.

Como proteger as barras de sistema

Talvez você queira que o app tenha uma barra transparente na navegação por gestos, mas translúcida ou opaca na navegação com três botões.

No Android 15, uma navegação com três botões translúcida é o padrão, já que a plataforma define a propriedade window.isNavigationBarContrastEnforced como true. A navegação por gestos permanece transparente.

Um app na navegação com três botões.

A navegação com três botões é translúcida por padrão.

No geral, uma navegação com três botões translúcida será suficiente. No entanto, em alguns casos, seu app pode exigir uma navegação com três botões opaca. Primeiro, defina a propriedade window.isNavigationBarContrastEnforced como false. Em seguida, use WindowInsetsCompat.tappableElement para o Views ou WindowInsets.tappableElement para o Compose. Se o conteúdo for 0, isso significa que o usuário está utilizando a navegação por gestos. Do contrário, o usuário está utilizando a navegação com três botões. Se esse for o caso, mostre uma visualização ou caixa por trás da barra de navegação. Confira um possível exemplo de composição:

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            window.isNavigationBarContrastEnforced = false
            MyTheme {
                Surface(...) {
                    MyContent(...)
                    ProtectNavigationBar()
                }
            }
        }
    }
}

// Use only if required.
@Composable
fun ProtectNavigationBar(modifier: Modifier = Modifier) {
   val density = LocalDensity.current
   val tappableElement = WindowInsets.tappableElement
   val bottomPixels = tappableElement.getBottom(density)
   val usingTappableBars = remember(bottomPixels) {
       bottomPixels != 0
   }
   val barHeight = remember(bottomPixels) {
       tappableElement.asPaddingValues(density).calculateBottomPadding()
   }

   Column(
       modifier = modifier.fillMaxSize(),
       verticalArrangement = Arrangement.Bottom
   ) {
       if (usingTappableBars) {
           Box(
               modifier = Modifier
                   .background(MaterialTheme.colorScheme.background)
                   .fillMaxWidth()
                   .height(barHeight)
           )
       }
   }
}

Um app na navegação com três botões.

Navegação com três botões opaca.

6. Analisar o código da solução

O método onCreate do arquivo MainActivity.kt tem esta aparência:

class MainActivity : ComponentActivity() {
   override fun onCreate(savedInstanceState: Bundle?) {
       installSplashScreen()
       enableEdgeToEdge()
       window.isNavigationBarContrastEnforced = false
       super.onCreate(savedInstanceState)
       setContent {
           Main(
               shortcutParams = extractShortcutParams(intent),
           )
       }
   }
}

O elemento combinável ChatContent dentro do arquivo ChatScreen.kt processa encartes desta forma:

private fun ChatContent(...) {
   ...
   Scaffold(...) { innerPadding ->
       Column {
           ...
           InputBar(
               input = input,
               onInputChanged = onInputChanged,
               onSendClick = onSendClick,
               onCameraClick = onCameraClick,
               onPhotoPickerClick = onPhotoPickerClick,
               contentPadding = innerPadding.copy(
                    layoutDirection, top = 0.dp
                ),
               sendEnabled = sendEnabled,
               modifier = Modifier
                   .fillMaxWidth()
                   .windowInsetsPadding(
                       WindowInsets.ime.exclude(WindowInsets.navigationBars)
                    ),
            )
       }
   }
}

O código de solução está disponível na ramificação principal. Se você já tiver baixado o SociaLite:

git checkout main

Caso contrário, baixe o código novamente de forma direta ou pelo git para visualizar a ramificação principal:

git clone git@github.com:android/socialite.git