Suporte a tamanhos de tela diferentes

Existem dispositivos Android de todos os formatos e tamanhos. Por isso, o layout do app precisa ser responsivo e adaptável. Em vez de definir o layout com dimensões estáticas que pressuponham um determinado tamanho de tela ou proporção, crie o app para acomodar diferentes tamanhos e orientações.

Ao oferecer suporte ao maior número possível de telas, o app é disponibilizado para diversos usuários com diferentes dispositivos em um único APK ou AAB. Além disso, flexibilizar o app para diferentes tamanhos de tela garante que ele possa processar mudanças de configuração da janela no dispositivo, por exemplo, quando o usuário acessa o modo de várias janelas ou quando o app é executado em um dispositivo dobrável, em que o tamanho e a proporção da tela podem mudar durante a execução.

No entanto, o suporte a diferentes tamanhos e orientações de tela não torna o app compatível com todos os formatos do Android. Siga outras etapas para oferecer suporte a dispositivos Wear OS, TV, Auto e Chrome OS.

Para ver informações sobre a criação de IUs para telas diferentes, consulte as noções básicas sobre layout (link em inglês) do Material Design.

Classes de tamanho de janela

Classes de tamanho de janela são um conjunto de pontos de interrupção específicos de janela de visualização para você projetar, desenvolver e testar layouts de aplicativos redimensionáveis. Elas foram escolhidas especificamente para equilibrar a simplicidade do layout com a flexibilidade de otimizar o app para casos únicos.

As classes de tamanho de janela particionam o tamanho bruto da janela disponível para seu app em buckets mais gerenciáveis e significativos. Há três buckets: compacto, médio e expandido. A largura e a altura disponíveis são particionadas individualmente. Portanto, em qualquer momento, o app tem duas classes de tamanho associadas: uma de largura e outra de altura.

Embora as classes de tamanho de janela sejam especificadas para largura e altura, a largura disponível geralmente é mais importante do que a altura disponível devido à ubiquidade de rolagem vertical. Portanto, a classe de tamanho da janela de largura provavelmente vai ser mais relevante para a IU do seu app.

Representações de classes de tamanho de janela baseadas em largura.
Figura 1. Representações de classes de tamanho de janela baseadas em largura.
Representações de classes de tamanho de janela baseadas em altura.
Figura 2. Representações de classes de tamanho de janela baseadas em altura.

Conforme visualizado acima, esses pontos de interrupção permitem que você continue pensando nos layouts em termos de dispositivos e configurações. Cada ponto de interrupção de classe de tamanho representa um caso majoritário para cenários de dispositivos típicos, que podem servir de referência útil enquanto você pensa sobre o design dos seus layouts baseados em pontos de interrupção.

Classe de tamanho Ponto de interrupção Representação do dispositivo
Largura compacta < 600 dp 99,96% dos smartphones no modo retrato
Largura média 600 dp+ 93,73% dos tablets no modo retrato

Telas internas grandes desdobradas no modo retrato

Largura expandida 840 dp+ 97,22% dos tablets no modo paisagem

Telas internas grandes desdobradas no modo paisagem

A maioria dos apps pode parar por aqui
Altura compacta < 480 dp 99,78% dos smartphones no modo paisagem
Altura média 480 dp+ 96,56% dos tablets no modo paisagem

97,59% de smartphones no modo retrato

Altura expandida 900 dp+ 94,25% de tablets no modo retrato

Embora seja útil visualizar classes de tamanho com dispositivos físicos, as classes de tamanho de janela não são explicitamente determinadas pelo tamanho físico da tela. Em outras palavras, as classes de tamanho de janela não representam a lógica "isTablet". Em vez disso, as classes de tamanho de janela são determinadas pelo tamanho da janela disponível para o aplicativo.

Isso tem duas consequências importantes:

  • Os dispositivos físicos não garantem uma classe de tamanho de janela específica. Há muitos motivos para o fato de o espaço da tela disponível para o app ser diferente do tamanho da tela física do dispositivo. Em dispositivos móveis, o modo de tela dividida pode separar a tela entre vários apps. No Chrome OS, os apps Android podem ser apresentados em janelas de formato livre e redimensionáveis arbitrariamente. Dispositivos dobráveis podem ter várias telas físicas, e a dobra do dispositivo muda qual delas fica visível.

  • A classe de tamanho de janela pode mudar durante todo o ciclo de vida do app. Enquanto ele está aberto, a rotação dos dispositivos, a realização de várias tarefas e o dobramento podem mudar a quantidade de espaço disponível na tela. Portanto, a classe de tamanho da janela é dinâmica e precisa se adaptar de acordo com a IU do app.

Use os pontos de interrupção do tamanho da janela para tomar grandes decisões de layout para o aplicativo, por exemplo, decidir usar um layout canônico específico para usar mais espaço na tela. Eles também mapeiam os pontos de interrupção do layout do Material Design para mudanças na coluna da grade de layout.

Para calcular as classes de tamanho de janela, os apps precisam consultar as métricas atuais disponíveis, conforme fornecido pela biblioteca WindowManager do Jetpack.

Visualizações

enum class WindowSizeClass { COMPACT, MEDIUM, EXPANDED }

class MainActivity : Activity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        // ...

        // Replace with a known container that you can safely add a View
        // to where it won't affect the layout and the view won't be
        // replaced.
        val container: ViewGroup = binding.container

        // Add a utility view to the container to hook into
        // View.onConfigurationChanged.
        // This is required for all activities, even those that don't
        // handle configuration changes.
        // We also can't use Activity.onConfigurationChanged, since there
        // are situations where that won't be called when the configuration
        // changes.
        // View.onConfigurationChanged is called in those scenarios.
        container.addView(object : View(this) {
            override fun onConfigurationChanged(newConfig: Configuration?) {
                super.onConfigurationChanged(newConfig)
                computeWindowSizeClasses()
            }
        })

        computeWindowSizeClasses()
    }

    private fun computeWindowSizeClasses() {
        val metrics = WindowMetricsCalculator.getOrCreate()
            .computeCurrentWindowMetrics(this)

        val widthDp = metrics.bounds.width() /
            resources.displayMetrics.density
        val widthWindowSizeClass = when {
            widthDp < 600f -> WindowSizeClass.COMPACT
            widthDp < 840f -> WindowSizeClass.MEDIUM
            else -> WindowSizeClass.EXPANDED
        }

        val heightDp = metrics.bounds.height() /
            resources.displayMetrics.density
        val heightWindowSizeClass = when {
            heightDp < 480f -> WindowSizeClass.COMPACT
            heightDp < 900f -> WindowSizeClass.MEDIUM
            else -> WindowSizeClass.EXPANDED
        }

        // Use widthWindowSizeClass and heightWindowSizeClass
    }
}

Visualizações

public enum WindowSizeClass { COMPACT, MEDIUM, EXPANDED }

public class MainActivity extends Activity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        // ...

        // Replace with a known container that you can safely add a View
        // to where it won't affect the layout and the view won't be
        // replaced.
        ViewGroup container = binding.container;

        // Add a utility view to the container to hook into
        // View.onConfigurationChanged.
        // This is required for all activities, even those that don't
        // handle configuration changes.
        // We also can't use Activity.onConfigurationChanged, since there
        // are situations where that won't be called when the configuration
        // changes.
        // View.onConfigurationChanged is called in those scenarios.
        container.addView(new View(this) {
            @Override
            protected void onConfigurationChanged(Configuration newConfig) {
                super.onConfigurationChanged(newConfig);
                computeWindowSizeClasses();
            }
        });

        computeWindowSizeClasses();
    }

    private void computeWindowSizeClasses() {
        WindowMetrics metrics = WindowMetricsCalculator.getOrCreate()
                .computeCurrentWindowMetrics(this);

        float widthDp = metrics.getBounds().width() /
                getResources().getDisplayMetrics().density;
        WindowSizeClass widthWindowSizeClass;

        if (widthDp < 600f) {
            widthWindowSizeClass = WindowSizeClass.COMPACT;
        } else if (widthDp < 840f) {
            widthWindowSizeClass = WindowSizeClass.MEDIUM;
        } else {
            widthWindowSizeClass = WindowSizeClass.EXPANDED;
        }

        float heightDp = metrics.getBounds().height() /
                getResources().getDisplayMetrics().density;
        WindowSizeClass heightWindowSizeClass;

        if (heightDp < 480f) {
            heightWindowSizeClass = WindowSizeClass.COMPACT;
        } else if (heightDp < 900f) {
            heightWindowSizeClass = WindowSizeClass.MEDIUM;
        } else {
            heightWindowSizeClass = WindowSizeClass.EXPANDED;
        }

        // Use widthWindowSizeClass and heightWindowSizeClass
    }
}

Compose

enum class WindowSizeClass { COMPACT, MEDIUM, EXPANDED }

@Composable
fun Activity.rememberWindowSizeClass() {
    val configuration = LocalConfiguration.current
    val windowMetrics = remember(configuration) {
        WindowMetricsCalculator.getOrCreate()
            .computeCurrentWindowMetrics(this)
    }
    val windowDpSize = with(LocalDensity.current) {
        windowMetrics.bounds.toComposeRect().size.toDpSize()
    }
    val widthWindowSizeClass = when {
        windowDpSize.width < 600.dp -> WindowSizeClass.COMPACT
        windowDpSize.width < 840.dp -> WindowSizeClass.MEDIUM
        else -> WindowSizeClass.EXPANDED
    }

    val heightWindowSizeClass = when {
        windowDpSize.height < 480.dp -> WindowSizeClass.COMPACT
        windowDpSize.height < 900.dp -> WindowSizeClass.MEDIUM
        else -> WindowSizeClass.EXPANDED
    }

    // Use widthWindowSizeClass and heightWindowSizeClass
}

Quando estiver observando as classes de tamanho de janela no seu app, você está pronto para começar a mudar o layout com base na classe de tamanho atual. Para saber como usar classes de tamanho de janela para tornar os layouts baseados em visualização responsivos, consulte Migrar a IU para layouts responsivos. Para saber como usar classes de tamanho de janela para tornar os layouts baseados no Compose responsivos, consulte Criar layouts adaptáveis.

Lista de verificação para suporte a diferentes classes de tamanho de janela

Ao fazer mudanças, teste o comportamento do layout em todos os tamanhos de janela, especialmente nas larguras compactas, médias e expandidas.

Se você já tiver um layout para telas menores, primeiro ele deve ser otimizado para a classe de tamanho de largura expandida, porque isso fornece um espaço maior para mais conteúdo ou mudanças de layout. Em seguida, veja qual layout faz sentido para a classe de largura média e considere adicionar um layout especializado nesse tamanho. Para oferecer uma experiência do usuário ainda melhor, adicione funcionalidades que façam sentido para seu app, por exemplo, suporte a posturas dobráveis ou otimização para suporte a entradas de teclado, mouse e stylus.

Para saber mais sobre o que faz um app funcionar bem em todos os dispositivos e tamanhos de tela, consulte Qualidade de apps para telas grandes.

Criar um layout flexível

Independentemente de para qual perfil de hardware você quer oferecer suporte primeiro, você precisa criar um layout que seja responsivo às menores variações possíveis de tamanho de tela.

Usar ConstraintLayout

A melhor forma de criar um layout responsivo para diferentes tamanhos de tela é usar ConstraintLayout como o layout básico na IU. ConstraintLayout permite especificar a posição e o tamanho de cada visualização de acordo com as relações espaciais com outras visualizações no layout. Dessa forma, todas as visualizações podem se mover e expandir juntas à medida que o tamanho da tela muda.

A maneira mais fácil de criar um layout com o ConstraintLayout é usar o Layout Editor no Android Studio. Ele permite que você arraste novas visualizações para o layout, anexe as limitações da visualização mãe e outras visualizações irmãs e edite as propriedades da visualização, tudo isso sem editar qualquer XML manualmente (veja a Figura 1).

Para ver mais informações, consulte Criar uma IU responsiva com o ConstraintLayout.

Figura 1. O Layout Editor no Android Studio mostrando um arquivo ConstraintLayout.

O ConstraintLayout não resolve todos os cenários de layout, principalmente em listas carregadas dinamicamente, para as quais é necessário usar o RecyclerView. No entanto, independentemente do layout usado, você sempre precisa evitar a codificação de tamanhos de layout. Consulte a próxima seção.

Evitar tamanhos de layout codificados

Para garantir que o layout seja flexível e se adapte a diferentes tamanhos de tela, use "wrap_content" ou "match_parent" para a largura e a altura da maioria dos componentes de visualização, em vez de tamanhos codificados.

"wrap_content" instrui a visualização a definir o próprio tamanho conforme necessário para ajustar o conteúdo a ela.

"match_parent" faz a visualização expandir o máximo possível dentro da visualização mãe.

Exemplo:

<TextView
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:text="@string/lorem_ipsum" />

Embora o layout dessa visualização dependa de outros atributos da visualização mãe e de todas as visualizações irmãs, o objetivo do TextView é definir a largura para preencher todo o espaço disponível (match_parent) e definir a altura para exatamente o espaço necessário para a altura do texto (wrap_content). Isso permite que a visualização se adapte a diferentes tamanhos de tela e de texto.

A Figura 2 mostra como a largura da visualização de texto usando "match_parent" se ajusta conforme a largura da tela muda pela orientação do dispositivo.

Figura 2. Uma visualização de texto flexível

Se você usar um LinearLayout, também poderá expandir as visualizações filhas com ponderação do layout para que cada visualização preencha o espaço restante proporcional ao valor ponderado. No entanto, para usar ponderações em um LinearLayout aninhado, é necessário que o sistema realize várias transmissões de layout para determinar o tamanho de cada visualização, diminuindo a velocidade do desempenho da IU. Felizmente, o ConstraintLayout pode atingir quase todos os layouts possíveis com LinearLayout sem impactos no desempenho. Por isso, tente converter seu layout em ConstraintLayout. Depois, você pode definir layouts ponderados com cadeias de restrição.

Usar SlidingPaneLayout para IUs de lista/detalhes

Uma IU de lista/detalhes pode ter um comportamento diferente em diferentes tamanhos de tela. Em uma tela grande, há espaço suficiente para a lista e os painéis de detalhes ficarem lado a lado. Clique em um item da lista para ver os detalhes no painel de detalhes. No entanto, isso pode ficar sobrecarregado em telas menores. Em vez de exibir os dois painéis, é melhor exibi-los um de cada vez. Inicialmente, o painel de lista é mostrado preenchendo a janela. Quando o usuário toca em um item, o painel de lista é substituído pelo painel de detalhes desse item, que também preenche a janela.

É possível usar SlidingPaneLayout para gerenciar a lógica a fim de determinar qual dessas duas experiências do usuário é adequada para o tamanho atual da janela:

<?xml version="1.0" encoding="utf-8"?>
<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:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/recycler_view"
        android:layout_width="280dp"
        android:layout_height="match_parent"
        android:layout_gravity="start" />

    <androidx.fragment.app.FragmentContainerView
        android:id="@+id/nav_host_fragment"
        android:name="androidx.navigation.fragment.NavHostFragment"
        android:layout_width="300dp"
        android:layout_height="match_parent"
        android:layout_weight="1"
        app:defaultNavHost="true"
        app:navGraph="@navigation/item_navigation" />

</androidx.slidingpanelayout.widget.SlidingPaneLayout>

As larguras e pesos aqui são os principais fatores que determinam o comportamento. Se a janela for grande o suficiente para exibir as duas visualizações (pelo menos 580 dp), será usada IU do modo lado a lado. No entanto, se ela for menor que isso, a lista de tela cheia é substituída pela IU de detalhes em tela cheia.

No modo lado a lado, a janela pode ser maior que o requisito mínimo de 580 dp nesse exemplo. Os valores ponderados são usados para dimensionar os dois painéis proporcionalmente. No exemplo, o painel de lista vai ser sempre 280 dp, e o painel de detalhes preenche o espaço restante. A única exceção é ao usar o SlidingPaneLayout V1.2.0-alpha01 e versões mais recentes em dispositivos dobráveis. Nesses casos, o SlidingPaneLayout ajusta automaticamente o tamanho dos painéis para que fiquem em qualquer lado da dobra ou articulação.

Criar layouts alternativos

Embora seu layout precise sempre responder a diferentes tamanhos de tela esticando o espaço dentro e em torno das visualizações, isso pode não oferecer a melhor experiência do usuário em todos os tamanhos de tela. Por exemplo, a IU criada para um smartphone provavelmente não oferece uma boa experiência em um tablet. Portanto, seu app também precisa oferecer recursos de layout alternativos para otimizar o design da IU para determinados tamanhos de tela.

Figura 3. O mesmo app usa diferentes layouts para diferentes tamanhos de tela.

Você pode fornecer layouts específicos para cada tela criando outros diretórios res/layout/, um para cada configuração de tela que precisa de um layout diferente. Em seguida, anexe um qualificador de configuração de tela ao nome do diretório layout, por exemplo, layout-w600dp para telas com 600 dp de largura disponível.

Esses qualificadores de configuração representam o espaço de tela visível disponível para a IU do seu app. O sistema considera todas as decorações, por exemplo, a barra de navegação, e mudanças na configuração de janelas, por exemplo, quando o usuário ativa o modo de várias janelas ao selecionar o layout do seu app.

Para criar um layout alternativo no Android Studio (versão 3.0 ou mais recente), siga estas etapas:

  1. Abra o layout padrão e clique em Orientation for Preview na barra de ferramentas.
  2. Na lista suspensa, clique para criar uma variante sugerida, por exemplo, Create Landscape Variant ou clique em Create Other.
  3. Se você tiver selecionado Create Other, o Select Resource Directory será exibido. Selecione um qualificador de tela à esquerda e o adicione à lista de Chosen qualifiers. Quando terminar de adicionar qualificadores, clique em OK. Consulte as seções a seguir para ver informações sobre os qualificadores de tamanho de tela.

Isso cria um arquivo de layout duplicado no diretório adequado para que você possa começar a personalizar o layout para a variante de tela.

Usar o qualificador de menor largura

O qualificador de tamanho de tela de "menor largura" permite que você forneça layouts alternativos para telas que tenham uma largura mínima medida em pixels de densidade independente (dp ou dip).

Ao descrever o tamanho da tela como uma medida em pixels de densidade independente, o Android permite que você crie layouts voltados a dimensões de tela muito específicas e, ao mesmo tempo, evita quaisquer preocupações sobre diferentes densidades de pixel.

Por exemplo, é possível criar um layout chamado main_activity otimizado para celulares e tablets criando versões diferentes do arquivo nos diretórios da seguinte forma:

res/layout/main_activity.xml           # For handsets (smaller than 600dp available width)
res/layout-sw600dp/main_activity.xml   # For 7” tablets (600dp wide and bigger)

O qualificador de menor largura especifica o menor dos dois lados da tela, independentemente da orientação atual do dispositivo. Portanto, essa é uma forma simples de especificar o tamanho de tela disponível para seu layout.

Veja como outros valores de menor largura correspondem a tamanhos de tela típicos:

  • 320 dp: uma tela típica de smartphone (240 x 320 ldpi, 320 x 480 mdpi, 480 x 800 hdpi etc).
  • 480 dp: uma tela grande de smartphone (aprox. 5 pol.) (480 x 480 mdpi)
  • 600 dp: um tablet de 7 pol. (600 x 1024 mdpi).
  • 720 dp: um tablet de 10 pol. (720 x 1280 mdpi, 800 x 1280 mdpi etc).

A Figura 4 fornece uma visão mais detalhada de como diferentes larguras de tela em dp correspondem a diferentes tamanhos e orientações de tela.

Figura 4. Pontos de interrupção de largura recomendados para suporte a tamanhos diferentes de tela.

Todos os valores para o qualificador de menor largura estão em pixels de densidade independente, porque o importante é a quantidade de espaço disponível na tela após o sistema considerar a densidade do pixel, e não a resolução bruta de pixels.

Usar o qualificador de largura disponível

Em vez de mudar o layout com base na menor largura da tela, é possível mudá-lo com base na largura ou altura atualmente disponível. Por exemplo, se você tem um layout de dois painéis, pode querer usá-lo sempre que a tela fornecer pelo menos 600 dp de largura, o que pode mudar dependendo da orientação do dispositivo (paisagem ou retrato). Nesse caso, use o qualificador "largura disponível" da seguinte forma:

res/layout/main_activity.xml         # For handsets (smaller than 600dp available width)
res/layout-w600dp/main_activity.xml  # For 7” tablets or any screen with 600dp
                                     #   available width (possibly landscape handsets)

Se a altura disponível for uma preocupação, você pode fazer o mesmo usando o qualificador "altura disponível". Por exemplo, layout-h600dp para telas com pelo menos 600 dp de altura.

Adicionar qualificadores de orientação

Embora seja possível oferecer suporte a todas as variações de tamanho usando apenas combinações dos qualificadores "menor largura" e "largura disponível", você também pode querer mudar a experiência do usuário quando ele alternar entre as orientações de retrato e paisagem.

Para isso, adicione o qualificador port ou land aos nomes do diretório de recursos. Esses qualificadores precisam ser posicionados depois dos outros qualificadores de tamanho. Exemplo:

res/layout/main_activity.xml                # For handsets
res/layout-land/main_activity.xml           # For handsets in landscape
res/layout-sw600dp/main_activity.xml        # For 7” tablets
res/layout-sw600dp-land/main_activity.xml   # For 7” tablets in landscape

Para ver mais informações sobre todos os qualificadores de configuração de tela, consulte a Tabela 2 no guia Fornecimento de recursos.

Modularizar componentes de IU com fragmentos

Ao criar seu app para vários tamanhos de tela, confira se você não está duplicando desnecessariamente o comportamento da IU nas suas atividades. Portanto, você precisa usar fragmentos para extrair a lógica da sua IU em componentes diferentes. Em seguida, você pode combinar fragmentos para criar layouts de vários painéis ao executá-los em uma tela grande ou colocá-los em atividades separadas ao executar o app em um celular.

Por exemplo, um app de notícias em um tablet pode exibir uma lista de artigos no lado esquerdo e um artigo completo no lado direito, com a seleção de um artigo à esquerda atualizando a visualização à direita. No entanto, em um celular, esses dois componentes precisam aparecer em telas separadas, considerando que selecionar um artigo de uma lista muda toda a tela para mostrá-lo.

Para saber mais, consulte Criar uma IU dinâmica com fragmentos.

Testar em todos os tamanhos de tela

É importante testar seu app em vários tamanhos de tela para garantir que sua IU seja dimensionada corretamente.

O Android 10 (API de nível 29) e versões mais recentes são compatíveis com uma ampla variedade de proporções. Com dispositivos dobráveis, os formatos podem variar de telas muito longas e finas (por exemplo, 21:9 para um dispositivo dobrado) até 1:1.

Para oferecer compatibilidade com o maior número possível de dispositivos, teste seus apps com o máximo possível destas proporções de tela:

Se o app não for compatível com algumas dessas proporções, use maxAspectRatio (como antes) e minAspectRatio para indicar as proporções mais altas e mais baixas que podem ser gerenciadas pelo seu app. Em casos com telas que excedem esses limites, seu app poderá ser colocado no modo de compatibilidade.

Quando há cinco ícones na visualização de navegação inferior, dispositivos com Android 10 (API de nível 29) e versões mais recentes têm um tamanho mínimo de alvo de toque de 2 polegadas. Consulte a documentação da definição de compatibilidade.

Se você não tem acesso a dispositivos físicos para todos os diferentes tamanhos de tela, pode usar o Android Emulator para emular qualquer tamanho de tela.

Caso você prefira testar em um dispositivo físico, mas não queira comprar dispositivos, use o Firebase Test Lab para acessar dispositivos em um data center do Google.

Declarar compatibilidade com tamanho de tela específico

Se você não quiser que o app seja executado em alguns tamanhos de tela, pode definir limites de até quanto sua tela pode ser redimensionada ou até mesmo limitar quais dispositivos podem instalar o app com base na configuração de tela. Para saber mais, consulte Declaração de suporte restrito à tela.