1. Antes de começar
Existem dispositivos Android de todos os formatos e tamanhos. Por isso, você precisa adequar o layout do seu app a diferentes tamanhos de tela para que ele seja disponibilizado a usuários e dispositivos variados com um único pacote Android (APK) ou Android App Bundle (AAB). Para isso, é necessário definir o app com um layout responsivo e adaptável, em vez de usar dimensões estáticas que pressuponham um determinado tamanho de tela ou proporção. Os layouts adaptáveis mudam de acordo com o espaço de tela disponível.
Este codelab ensina os conceitos básicos de como criar IUs adaptáveis e ajustar um app que mostra uma lista de esportes e detalhes sobre cada um deles de modo a oferecer suporte a dispositivos com telas grandes. O app esportivo consiste em três telas: inicial, de favoritos e de configurações. A tela inicial mostra uma lista de esportes e um marcador de posição para notícias quando você seleciona um esporte na lista. As telas de favoritos e de configurações também mostram textos de marcador de posição. Para trocar de tela, basta selecionar o item associado no menu de navegação da parte de baixo.
A princípio, o app tem estes problemas de layout em telas grandes:
- Ele não pode ser usado na orientação retrato.
- Ele mostra muitos espaços em branco em telas grandes.
- Ele sempre mostra o menu de navegação da parte de baixo em telas grandes.
Ao tornar o app adaptável, você permite que ele:
- tenha suporte à orientação paisagem e retrato;
- mostre a lista de esportes e notícias sobre cada esporte lado a lado quando houver espaço horizontal suficiente;
- mostre o componente de navegação de acordo com as diretrizes do Material Design.
O app tem apenas uma atividade com vários fragmentos. Você vai usar estes arquivos:
- O arquivo
AndroidManifest.xml
, que fornece os metadados sobre o app esportivo. - O arquivo
MainActivity.kt
, que contém um código gerado com o arquivoactivity_main.xml
, a anotaçãooverride
, uma classeenum
que representa a classe de tamanho de janela para a largura e uma definição de método para recuperar essa classe para a janela do app. O menu de navegação da parte de baixo da tela é inicializado quando a atividade é criada. - O arquivo
activity_main.xml
, que define o layout padrão da atividadeMain
. - O arquivo
layout-sw600dp/activity_main.xml
, que define um layout alternativo para a atividadeMain
. O layout alternativo é eficaz quando a largura da janela do app é maior que ou igual a um valor de600dp
. O conteúdo é igual ao layout padrão no ponto de partida. - O arquivo
SportsListFragment.kt
, que contém a implementação da lista de esportes e a navegação de retorno personalizada. - O arquivo
fragment_sports_list.xml
, que define um layout para a lista de esportes. - O arquivo
navigation_menu.xml
, que define os itens do menu de navegação da parte de baixo da tela.
Figura 1. O app esportivo tem suporte a diferentes tamanhos de janela com um único APK ou AAB.
Pré-requisitos
- Conhecimento básico de desenvolvimento de IU baseado em visualização.
- Experiência com a sintaxe do Kotlin, incluindo funções lambda.
- Conclusão do codelab Noções básicas do Jetpack Compose.
O que você vai aprender
- Como oferecer suporte a mudanças de configuração.
- Como adicionar layouts alternativos com menos modificações de código.
- Como implementar uma IU de detalhes da lista que se comporte de maneira diferente para tamanhos de janela variados.
O que você vai criar
Um app Android com suporte a:
- orientação paisagem do dispositivo;
- tablets, computadores e dispositivos móveis;
- comportamento de detalhes da lista para diferentes tamanhos de tela.
O que é necessário
- Android Studio Bumblebee | 2021.1.1 ou versão mais recente.
- Um emulador ou tablet Android.
2. Começar a configuração
Faça o download do código para este codelab e configure o projeto:
- Na linha de comando, clone o código deste repositório do GitHub (link em inglês):
$ git clone https://github.com/android/add-adaptive-layouts
- No Android Studio, abra o projeto
AddingAdaptiveLayout
. Ele está disponível em várias ramificações do GitHub:
- A ramificação
main
contém o código inicial para este projeto. Você vai fazer edições nessa ramificação para concluir o codelab. - A ramificação
end
contém a solução deste codelab.
3. Migrar o componente de navegação da parte de cima da tela para o Compose
O app esportivo usa um menu de navegação na parte de baixo da tela como componente da parte de cima. Esse componente é implementado com a classe BottomNavigationView
. Nesta seção, você vai migrar o componente de navegação da parte de cima para o Compose.
Representar o conteúdo do recurso de menu associado como uma classe selada
- Crie uma classe
MenuItem
selada para representar o conteúdo do arquivonavigation_menu.xml
e, em seguida, transmita um parâmetroiconId
,labelId
edestinationId
. - Adicione um objeto
Home
,Favorite
eSettings
como subclasses que correspondam aos seus destinos.
MenuItem.kt
sealed class MenuItem(
// Resource ID of the icon for the menu item
@DrawableRes val iconId: Int,
// Resource ID of the label text for the menu item
@StringRes val labelId: Int,
// ID of a destination to navigate users
@IdRes val destinationId: Int
) {
object Home: MenuItem(
R.drawable.ic_baseline_home_24,
R.string.home,
R.id.SportsListFragment
)
object Favorites: MenuItem(
R.drawable.ic_baseline_favorite_24,
R.string.favorites,
R.id.FavoritesFragment
)
object Settings: MenuItem(
R.drawable.ic_baseline_settings_24,
R.string.settings,
R.id.SettingsFragment
)
}
Implementar o menu de navegação da parte de baixo da tela como uma função de composição
- Defina uma função de composição
BottomNavigationBar
que use estes três parâmetros: um objetomenuItems
definido com um valorList<MenuItem>
, ummodifier
definido com um valorModifier = Modifier
e umonMenuSelected
definido como uma função lambda(MenuItem) -> Unit = {}
. - No corpo da função de composição
BottomNavigationBar
, chame uma funçãoNavigationBar()
que use um parâmetromodifier
. - Na função lambda transmitida para a função
NavigationBar()
, chame o métodoforEach()
no parâmetromenuItems
e, em seguida, chame uma funçãoNavigationBarItem()
na função lambda definida como a chamada de métodoforeach()
. - Transmita para a função
NavigationBarItem()
um parâmetroselected
definido com um valorfalse
; umonClick
definido como uma função lambda que contém uma funçãoonMenuSelected
com um parâmetroMenuItem
; umicon
definido como uma função lambda que contém uma funçãoIcon
com um parâmetropainter = painterResource(id = menuItem.iconId)
e umcontentDescription = null
; e umlabel
definido como uma função lambda que contém uma funçãoText
com um parâmetro(text = stringResource(id = menuItem.labelId)
.
Navigation.kt
@Composable
fun BottomNavigationBar(
menuItems: List<MenuItem>,
modifier: Modifier = Modifier,
onMenuSelected: (MenuItem) -> Unit = {}
) {
NavigationBar(modifier = modifier) {
menuItems.forEach { menuItem ->
NavigationBarItem(
selected = false,
onClick = { onMenuSelected(menuItem) },
icon = {
Icon(
painter = painterResource(id = menuItem.iconId),
contentDescription = null)
},
label = { Text(text = stringResource(id = menuItem.labelId))}
)
}
}
}
Substituir o elemento BottomNavigationView
pelo ComposeView
no arquivo de recurso de layout
- No arquivo
activity_main.xml
, substitua o elementoBottomNavigationView
pelo elementoComposeView
. Essa mudança incorpora o componente de navegação da parte de baixo da tela a um layout de IU baseado em visualização usando o Compose.
activity_main.xml
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior">
<androidx.fragment.app.FragmentContainerView
android:id="@+id/nav_host_fragment_content_main"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="0dp"
android:layout_height="0dp"
app:defaultNavHost="true"
app:layout_constraintBottom_toTopOf="@id/top_navigation"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:navGraph="@navigation/nav_graph" />
<androidx.compose.ui.platform.ComposeView
android:id="@+id/navigation"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:menu="@menu/top_navigation" />
</androidx.constraintlayout.widget.ConstraintLayout>
Integrar o menu de navegação da parte de baixo da tela baseado no Compose a um layout de IU com base em visualização
- No arquivo
MainActivity.kt
, defina uma variávelnavigationMenuItems
configurada como uma lista de objetosMenuItem
. Esses objetosMenuItems
aparecem no menu de navegação da parte de baixo da tela na ordem da lista. - Chame a função
BottomNavigationBar()
para incorporar o menu de navegação da parte de baixo da tela ao objetoComposeView
. - Navegue até o destino associado ao item selecionado pelos usuários na função de callback transmitida para a função
BottomNavigationBar
.
MainActivity.kt
val navigationMenuItems = listOf(
MenuItem.Home,
MenuItem.Favorites,
MenuItem.Settings
)
binding.navigation.setContent {
MaterialTheme {
BottomNavigationBar(menuItems = navigationMenuItems){ menuItem ->
navController.navigate(screen.destinationId)
}
}
}
4. Suporte à orientação horizontal
Caso seu app tenha suporte a um dispositivo de tela grande, é esperado que ele funcione na orientação paisagem e retrato. No momento, o app tem apenas uma atividade: MainActivity
.
A orientação da tela da atividade é definida no arquivo AndroidManifest.xml
com o atributo android:screenOrientation
, configurado com o valor portrait
.
Defina o app para que ele tenha suporte à orientação paisagem:
- Defina o atributo
android:screenOrientation
com o valorfullUser
. Com essa configuração, os usuários podem bloquear a orientação da tela. A orientação é determinada pelo sensor do dispositivo para qualquer uma das quatro opções possíveis.
AndroidManifest.xml
<activity
android:name=".MainActivity"
android:exported="true"
android:screenOrientation="fullUser">
Figura 2. O app é executado na orientação horizontal depois que você atualiza o arquivo AndroidManifest.xml
.
5. Classes de tamanho de janela
Esses são os valores de ponto de interrupção que ajudam a classificar o tamanho da janela em classes predefinidas (compacta, média e expandida) com o tamanho bruto da janela disponível para o app. Essas classes de tamanho seriam usadas ao projetar, desenvolver e testar o layout adaptável.
A largura e altura disponíveis são particionadas de forma independente para que o app seja sempre associado a duas classes de tamanho de janela: uma para a largura e outra para a altura.
Figura 3. Classes de tamanho de janela para a largura e pontos de interrupção associados.
Figura 4. As classes de tamanho de janela para a altura e os pontos de interrupção associados.
As classes de tamanho de janela representam o tamanho atual da janela do app. Em outras palavras, não é possível determinar a classe de tamanho de janela pelo tamanho do dispositivo físico. Ainda que seu app seja executado no mesmo dispositivo, a classe de tamanho de janela associada vai mudar de acordo com a configuração, como quando você executa o app no modo de tela dividida. Isso tem duas consequências importantes:
- Os dispositivos físicos não garantem uma classe de tamanho de janela específica.
- A classe de tamanho de janela pode mudar durante o ciclo de vida do app.
Depois de adicionar layouts adaptáveis ao app, você o testa em todas as variações, especialmente em classes de tamanho de janela compacta, média e expandida. Os testes para cada classe de tamanho de janela são necessários, mas não suficientes em muitos casos. É importante testar o app em vários tamanhos de janela para garantir que a IU seja dimensionada corretamente. Para ver mais informações, consulte Layouts em tela grande (link em inglês) e Qualidade dos apps em telas grandes.
6. Posicionar os painéis de lista e de detalhes lado a lado em telas grandes
Uma IU de detalhes da lista pode precisar ter um comportamento diferente de acordo com a classe de tamanho da janela para largura definida atualmente. Quando a classe de tamanho de janela com largura média ou expandida é associada ao app, isso significa que ele pode ter espaço suficiente para mostrar o painel de lista e o de detalhes lado a lado para que os usuários possam ver a lista de itens e os detalhes do item selecionado sem a necessidade de transição da tela. No entanto, telas menores podem ficar muito sobrecarregadas com essa exibição. Nesse caso, é melhor mostrar um painel por vez começando pelo de lista. O painel de detalhes mostra os detalhes do item selecionado quando os usuários tocam em um item da lista. A classe SlidingPaneLayout
gerencia a lógica para determinar qual dessas duas experiências do usuário é adequada para o tamanho atual da janela.
Configurar o layout do painel de lista
A classe SlidingPaneLayout
é um componente de IU baseado em visualização. Você vai modificar o arquivo de recurso de layout do painel de lista, que é o arquivo fragment_sports_list.xml
deste codelab.
A classe SlidingPaneLayout
usa dois elementos filhos. Os atributos de largura e peso de cada elemento filho são os principais fatores para a classe SlidingPaneLayout
determinar se a janela é grande o suficiente para mostrar as duas visualizações lado a lado. Se não for, a lista em tela cheia será substituída pela IU de detalhes em tela cheia. Os valores de peso são usados para dimensionar os dois painéis proporcionalmente quando o tamanho da janela é maior do que o requisito mínimo para mostrar os painéis lado a lado.
A classe SlidingPaneLayout
foi aplicada ao arquivo fragment_sports_list.xml
. O painel de lista está configurado para ter uma largura 1280dp
. É por isso que esse painel e o de detalhes não são mostrados lado a lado.
Faça com que os painéis sejam mostrados lado a lado quando a largura da tela for maior que 580dp
:
- Defina a
RecyclerView
com uma largura de280dp
.
fragment_sports_list.xml
<androidx.slidingpanelayout.widget.SlidingPaneLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/sliding_pane_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".SportsListFragment">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycler_view"
android:layout_width="280dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:clipToPadding="false"
android:padding="8dp"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" />
<androidx.fragment.app.FragmentContainerView
android:layout_height="match_parent"
android:layout_width="300dp"
android:layout_weight="1"
android:id="@+id/detail_container"
android:name="com.example.android.sports.NewsDetailsFragment"/>
</androidx.slidingpanelayout.widget.SlidingPaneLayout>
Figura 5. Os painéis de lista e de detalhes aparecem lado a lado depois que você atualiza o arquivo de recurso de layout.
Trocar o painel de detalhes
Agora, os painéis de lista e de detalhes aparecem lado a lado quando a classe de tamanho de janela com largura média ou expandida está associada ao app. No entanto, a tela vai mudar totalmente para o painel de detalhes quando os usuários selecionarem um item no painel de lista.
Figura 6. A tela faz a transição para o painel de detalhes depois que você seleciona um esporte na lista.
Esse problema é causado pela navegação acionada quando os usuários selecionam um item no painel de lista. O código relevante está disponível no arquivo SportsListFragment.kt
.
SportsListFragment.kt
val adapter = SportsAdapter {
sportsViewModel.updateCurrentSport(it)
// Navigate to the details pane.
val action =
SportsListFragmentDirections.actionSportsListFragmentToNewsFragment()
this.findNavController().navigate(action)
}
Confira se a tela só faz a transição completa para o painel de detalhes quando não há espaço suficiente para mostrar os painéis de lista e de detalhes lado a lado:
- Na variável de função
adapter
, adicione uma instruçãoif
que verifica se o atributoisSlidable
da classeSlidingPaneLayout
é verdadeiro e se o atributoisOpen
da classeSlidingPaneLayout
é falso.
SportsListFragment.kt
val adapter = SportsAdapter {
sportsViewModel.updateCurrentSport(it)
if(slidingPaneLayout.isSlidable && !slidingPaneLayout.isOpen){
// Navigate to the details pane.
val action =
SportsListFragmentDirections.actionSportsListFragmentToNewsFragment()
this.findNavController().navigate(action)
}
}
7. Escolher o componente de navegação correto de acordo com a classe de tamanho de janela para a largura
O Material Design espera que seu app escolha os componentes de forma adaptável. Nesta seção, você escolhe um componente para a barra de navegação da parte de cima da tela com base na classe de tamanho de janela atual. Esta tabela descreve o componente de navegação esperado para cada classe de tamanho de janela:
Classe de tamanho de janela para a largura | Componente de navegação |
Compacta | Navegação na parte de baixo da tela |
Média | Coluna de navegação |
Expandida | Gaveta de navegação permanente |
Implementar a coluna de navegação
- Crie uma função de composição
NavRail()
que use três parâmetros: um objetomenuItems
definido com um valorList<MenuItem>
, ummodifier
definido com um valorModifier
e uma função lambdaonMenuSelected
. - No corpo da função, chame uma
NavigationRail()
que use o objetomodifier
como um parâmetro. - Chame uma função
NavigationRailItem()
para cadaMenuItem
no objetomenuItems
, como você fez com a funçãoNavigationBarItem
naBottomNavigationBar()
.
Implementar a gaveta de navegação permanente
- Crie uma função de composição
NavigationDrawer()
que use estes três parâmetros: um objetomenuItems
definido com um valorList<MenuItem>
, ummodifier
definido com um valorModifier
e uma função lambdaonMenuSelected
. - No corpo da função, chame uma
Column()
que use o objetomodifier
como um parâmetro. - Chame uma função
Row()
para cadaMenuItem
no objetomenuItems
. - No corpo da função
Row()
, adicione os identificadoresicon
etext
como você fez com a funçãoNavigationBarItem
naBottomNavigationBar()
.
Selecionar o componente de navegação correto de acordo com a classe de tamanho de janela para a largura
- Para recuperar a classe de tamanho de janela atual para a largura, chame a função
rememberWidthSizeClass()
dentro da função de composição transmitida ao métodosetContent()
no objetoComposeView
. - Crie uma ramificação condicional para escolher o componente de navegação com base na classe de tamanho de janela recuperada para a largura e chame a que for selecionada.
- Transmita um objeto
Modifier
para a funçãoNavigationDrawer
para especificar a largura com um valor de256dp
.
ActivityMain.kt
binding.navigation.setContent {
MaterialTheme {
when(rememberWidthSizeClass()){
WidthSizeClass.COMPACT ->
BottomNavigationBar(menuItems = navigationMenuItems){ menuItem ->
navController.navigate(screen.destinationId)
}
WidthSizeClass.MEDIUM ->
NavRail(menuItems = navigationMenuItems){ menuItem ->
navController.navigate(screen.destinationId)
}
WidthSizeClass.EXPANDED ->
NavigationDrawer(
menuItems = navigationMenuItems,
modifier = Modifier.width(256.dp)
) { menuItem ->
navController.navigate(screen.destinationId)
}
}
}
}
Colocar o componente de navegação na posição correta
Agora, seu app pode escolher o componente de navegação correto com base na classe de tamanho de janela atual para a largura, mas os componentes selecionados não podem ser posicionados como esperado. Isso ocorre porque o elemento ComposeView
é colocado no FragmentViewContainer
.
Atualize o recurso de layout alternativo para a classe MainActivity
:
- Abra o arquivo
layout-sw600dp/activity_main.xml
. - Atualize a restrição para dispor o elemento
ComposeView
e oFragmentContainer
horizontalmente.
layout-sw600dp/activity_main.xml
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior">
<androidx.fragment.app.FragmentContainerView
android:id="@+id/nav_host_fragment_content_main"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="0dp"
android:layout_height="0dp"
app:defaultNavHost="true"
app:layout_constraintLeft_toRightOf="@+id/top_navigation"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:navGraph="@navigation/nav_graph" />
<androidx.compose.ui.platform.ComposeView
android:id="@+id/top_navigation"
android:layout_width="wrap_content"
android:layout_height="0dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:menu="@menu/top_navigation" />
</androidx.constraintlayout.widget.ConstraintLayout>
Após a modificação, o app escolhe o componente de navegação correto com base na classe de tamanho de janela associada para a largura. A primeira captura mostra a tela para a classe de tamanho de janela com largura média. A segunda captura mostra a tela para a classe de tamanho de janela com largura expandida.
Figura 7. Telas das classes de tamanho de janela com largura média e expandida.
8. Parabéns
Parabéns! Você concluiu este codelab e aprendeu a usar o Compose para adicionar layouts adaptáveis a um app Android baseado em visualização. Ao fazer isso, você aprendeu sobre a classe SlidingPaneLayout
, as classes de tamanho de janela e a seleção do componente de navegação com base nas classes de tamanho de janela para a largura.