Oferecer compatibilidade com dispositivos dobráveis e de duas telas usando a biblioteca Jetpack WindowManager

1. Antes de começar

Este codelab prático ensinará os conceitos básicos de desenvolvimento para dispositivos dobráveis e de tela dupla. Quando terminar, o app vai oferecer suporte a dispositivos dobráveis, como Pixel Fold, Microsoft Surface Duo, Samsung Galaxy Z Fold 5 etc.

Pré-requisitos

Para concluir este codelab, você precisa ter:

O que você vai fazer

Criar um app simples que:

  • Mostra os recursos do dispositivo.
  • Detecta quando o aplicativo está sendo executado em um dispositivo dobrável ou de tela dupla.
  • Determina o estado do dispositivo.
  • Usa a biblioteca Jetpack WindowManager para trabalhar com novos formatos de dispositivos.

O que é necessário

O Android Emulator v30.0.6+ inclui suporte para dispositivos dobráveis com sensor de articulação virtual e visualização em 3D. Existem alguns emuladores dobráveis que podem ser usados, conforme mostrado na imagem abaixo:

ca76200cc00b6ce6.png

2. Dispositivos de tela única versus dobráveis

Os dispositivos dobráveis oferecem uma tela maior e uma interface do usuário mais versátil do que aquela disponível anteriormente em um dispositivo móvel. Quando dobrados, esses dispositivos costumam ser menores do que um tablet de tamanho normal, o que os torna mais portáteis e funcionais.

No momento da criação deste codelab, existem dois tipos de dispositivos dobráveis:

  • Dispositivos dobráveis de tela única, que pode ser dobrada. Os usuários podem executar vários apps na mesma tela, ao mesmo tempo, usando o modo multi-window.
  • Dispositivos dobráveis de duas telas, unidas por uma articulação. Esses dispositivos também podem ser dobrados, mas têm duas regiões lógicas de tela.

9ff347a7c8483fed.png

Da mesma forma que tablets e outros dispositivos móveis de tela única, os dobráveis podem:

  • executar um app em uma das regiões de exibição;
  • executar dois apps lado a lado, cada um em uma região de exibição diferente, usando o modo multi-window.

Ao contrário dos dispositivos de tela única, os dobráveis também são compatíveis com diferentes posições. Elas podem ser usadas para mostrar conteúdo de maneiras diferentes.

bac1d8089687c0c2.png

Os dispositivos dobráveis podem oferecer várias posições quando um app é estendido (mostrado) em toda a região de exibição (usando todas as regiões em dispositivos dobráveis de tela dupla).

Os dispositivos dobráveis também podem oferecer posições dobradas, como o modo de mesa, para que você possa ter uma divisão lógica entre a parte da tela plana e aquela inclinada para você, e o modo tenda, que permite a visualização do conteúdo como se o dispositivo estivesse usando um gadget de apoio.

3. Jetpack WindowManager

A biblioteca Jetpack WindowManager ajuda desenvolvedores de aplicativos a oferecer suporte a novos formatos de dispositivo e fornece uma superfície de API comum para vários recursos da WindowManager em versões antigas e novas da plataforma.

Principais recursos

A versão 1.1.0 da Jetpack WindowManager contém a classe FoldingFeature, que descreve uma dobra em telas flexíveis ou uma articulação entre dois painéis de exibição física. A API dela fornece acesso a informações importantes relacionadas ao dispositivo:

Usando a interface WindowInfoTracker, você pode acessar windowLayoutInfo() para coletar um Flow (link em inglês) de WindowLayoutInfo que contém todos os DisplayFeatures disponíveis.

4. Configurar

Crie um novo projeto e selecione o modelo "Empty Activity":

a5ce5c7fb033ec4c.png

Deixe todos os parâmetros como padrão.

Declarar dependências

Para usar a Jetpack WindowManager, adicione a dependência ao arquivo build.gradle do app ou módulo:

app/build.gradle (link em inglês)

dependencies {
    ext.windowmanager_version = "1.1.0"

    implementation "androidx.window:window:$windowmanager_version"
    androidTestImplementation "androidx.window:window-testing:$windowmanager_version"

    // Needed to use lifecycleScope to collect the WindowLayoutInfo flow
    implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.6.2"
}

Usar a WindowManager

Acesse os recursos da janela pela interface WindowInfoTracker da WindowManager.

Abra o arquivo de origem MainActivity.kt e chame WindowInfoTracker.getOrCreate(this@MainActivity) para inicializar a instância WindowInfoTracker associada à atividade atual:

MainActivity.kt (link em inglês)

import androidx.window.layout.WindowInfoTracker

private lateinit var windowInfoTracker: WindowInfoTracker

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

        windowInfoTracker = WindowInfoTracker.getOrCreate(this@MainActivity)
}

Acesse as informações sobre o estado atual da janela do dispositivo com a instância WindowInfoTracker.

5. Configurar a interface do aplicativo

Com a Jetpack WindowManager, é possível acessar informações sobre métricas da janela, layout e configurações de exibição. Mostre isso no layout da atividade principal, usando uma TextView para cada elemento.

Crie um ConstraintLayout, com três TextViews, centralizado na tela.

Abra o arquivo activity_main.xml e cole este conteúdo:

activity_main.xml (link em inglês)

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

    <TextView
        android:id="@+id/window_metrics"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:padding="20dp"
        tools:text="Window metrics"
        android:textSize="20sp"
        app:layout_constraintBottom_toTopOf="@+id/layout_change"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintVertical_chainStyle="packed" />

    <TextView
        android:id="@+id/layout_change"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:padding="20dp"
        tools:text="Layout change"
        android:textSize="20sp"
        app:layout_constrainedWidth="true"
        app:layout_constraintBottom_toTopOf="@+id/configuration_changed"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/window_metrics" />

    <TextView
        android:id="@+id/configuration_changed"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:padding="20dp"
        tools:text="Using one logic/physical display - unspanned"
        android:textSize="20sp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/layout_change" />

</androidx.constraintlayout.widget.ConstraintLayout>

Agora, vamos conectar esses elementos da interface ao código usando a vinculação de visualizações. Para isso, vamos ativá-la no arquivo build.gradle do aplicativo:

app/build.gradle (link em inglês)

android {
   // Other configurations

   buildFeatures {
      viewBinding true
   }
}

Sincronize o projeto do Gradle conforme sugerido pelo Android Studio e use a vinculação de visualizações no arquivo MainActivity.kt com este código:

MainActivity.kt (link em inglês)

class MainActivity : AppCompatActivity() {
    private lateinit var windowInfoTracker: WindowInfoTracker
    private lateinit var binding: ActivityMainBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)

        windowInfoTracker = WindowInfoTracker.getOrCreate(this@MainActivity)
    }
}

6. Conferir as informações da WindowMetrics

No método onCreate da MainActivity, chame uma função para receber e mostrar as informações de WindowMetrics. Adicione uma chamada obtainWindowMetrics() ao método onCreate:

MainActivity.kt (link em inglês)

override fun onCreate(savedInstanceState: Bundle?) {
   super.onCreate(savedInstanceState)
   binding = ActivityMainBinding.inflate(layoutInflater)
   setContentView(binding.root)

   windowInfoTracker = WindowInfoTracker.getOrCreate(this@MainActivity)

   obtainWindowMetrics()
}

Implemente o método obtainWindowMetrics:

MainActivity.kt (link em inglês)

import androidx.window.layout.WindowMetricsCalculator

private fun obtainWindowMetrics() {
   val wmc = WindowMetricsCalculator.getOrCreate()
   val currentWM = wmc.computeCurrentWindowMetrics(this).bounds.flattenToString()
   val maximumWM = wmc.computeMaximumWindowMetrics(this).bounds.flattenToString()
   binding.windowMetrics.text =
       "CurrentWindowMetrics: ${currentWM}\nMaximumWindowMetrics: ${maximumWM}"
}

Acesse uma instância de WindowMetricsCalculator pela função complementar getOrCreate().

Usando essa instância de WindowMetricsCalculator, defina as informações na windowMetrics TextView. Use os valores que as funções computeCurrentWindowMetrics.bounds e computeMaximumWindowMetrics.bounds retornam.

Esses valores fornecem informações úteis sobre as métricas da área ocupada pela janela.

Execute o app. Em um emulador de tela dupla (imagem abaixo), você acessa as CurrentWindowMetrics que se ajustam às dimensões do dispositivo espelhado pelo emulador (link em inglês). Também é possível conferir as métricas quando o app é executado no modo de tela única:

f6f0deff678fd722.png

Quando o app é estendido para as telas, as métricas da janela mudam, como na imagem abaixo. Elas refletem a maior área de janela usada pelo app:

f1ce73d7198b4990.png

A métricas atual e a máxima da janela têm os mesmos valores, já que o app está sempre em execução e ocupando toda a área de exibição disponível, tanto na tela única quanto na dupla.

Em um emulador dobrável com uma dobra horizontal, os valores diferem quando o app ocupa toda a tela física e quando é executado no modo de várias janelas:

d00e53154f32d7df.png

Como você pode notar na imagem à esquerda, as duas métricas têm o mesmo valor, já que o app em execução está usando toda a área de exibição, que é a atual e a máxima disponível.

Por outro lado, na imagem à direita, com o app executado no modo de várias janelas, é possível observar como as métricas atuais mostram as dimensões da área em que o app é executado na área específica (acima) do modo de tela dividida. Também observamos como as métricas máximas mostram a área máxima de exibição do dispositivo.

As métricas fornecidas pelo WindowMetricsCalculator são muito úteis para determinar a área da janela que o app está usando ou pode usar.

7. Conferir as informações do FoldingFeature

Faça o registro para receber mudanças de layout de janela com as características e os limites dos DisplayFeatures do emulador ou dispositivo.

Para coletar as informações de WindowInfoTracker#windowLayoutInfo(), use o lifecycleScope definido para cada objeto Lifecycle. Toda corrotina iniciada nesse escopo será cancelada quando o ciclo de vida for destruído. É possível acessar o escopo da corrotina do ciclo de vida pelas propriedades lifecycle.coroutineScope ou lifecycleOwner.lifecycleScope.

No método onCreate da MainActivity, chame uma função para receber e mostrar as informações de WindowInfoTracker. Comece adicionando uma chamada onWindowLayoutInfoChange() ao método onCreate:

MainActivity.kt (link em inglês)

override fun onCreate(savedInstanceState: Bundle?) {
   super.onCreate(savedInstanceState)
   binding = ActivityMainBinding.inflate(layoutInflater)
   setContentView(binding.root)

   windowInfoTracker = WindowInfoTracker.getOrCreate(this@MainActivity)

   obtainWindowMetrics()
   onWindowLayoutInfoChange()
}

Use a implementação dessa função para receber informações sempre que uma nova configuração de layout for modificada.

Defina a assinatura da função e o esqueleto.

MainActivity.kt (link em inglês)

private fun onWindowLayoutInfoChange() {
}

Com o parâmetro que a função recebe, um WindowInfoTracker, você pode acessar os dados de WindowLayoutInfo. WindowLayoutInfo contém a lista de DisplayFeatures na janela. Por exemplo, uma articulação ou dobra de tela pode dividir a janela. Nesse caso, pode ser uma boa ideia separar o conteúdo visual e os elementos interativos em dois grupos (por exemplo, detalhes de lista ou controles de visualização).

Apenas os recursos presentes nos limites da janela atual são informados. As posições e os tamanhos deles podem ser modificados se a janela for movida ou redimensionada na tela.

Com o lifecycleScope definido na dependência lifecycle-runtime-ktx, você pode receber um flow de WindowLayoutInfo que contém uma lista de todos os recursos de exibição. Adicione o corpo de onWindowLayoutInfoChange:

MainActivity.kt (link em inglês)

import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch

private fun onWindowLayoutInfoChange() {
    lifecycleScope.launch(Dispatchers.Main) {
        lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
            windowInfoTracker.windowLayoutInfo(this@MainActivity)
                .collect { value ->
                    updateUI(value)
                }
        }
    }
}

A função updateUI está sendo chamada em collect. Implemente essa função para mostrar as informações recebidas do flow de WindowLayoutInfo. Confira se os dados de WindowLayoutInfo têm recursos de exibição. Nesse caso, o recurso está interagindo de alguma forma com a interface do nosso app. Se os dados de WindowLayoutInfo não tiverem recursos de exibição, o app está sendo executado em um dispositivo ou modo de tela única ou no modo de várias janelas.

MainActivity.kt (link em inglês)

import androidx.window.layout.WindowLayoutInfo

private fun updateUI(newLayoutInfo: WindowLayoutInfo) {
    binding.layoutChange.text = newLayoutInfo.toString()
    if (newLayoutInfo.displayFeatures.isNotEmpty()) {
        binding.configurationChanged.text = "Spanned across displays"
    } else {
        binding.configurationChanged.text = "One logic/physical display - unspanned"
    }
}

Execute o app. Em um emulador de tela dupla, você tem:

a6f6452155742925.png

WindowLayoutInfo está vazio Ela tem uma List<DisplayFeature> vazia. Entretanto, se você tem um emulador com uma articulação no meio, por que não extrai as informações da WindowManager?

A WindowManager usa o WindowInfoTracker para fornecer os dados de WindowLayoutInfo (posição, tipos e limites de recursos do dispositivo) somente quando o app for estendido para várias telas (físicas ou não). Na figura anterior, em que o app é executado no modo de tela única, a WindowLayoutInfo está vazia.

Com essas informações, é possível saber em qual modo o app está sendo executado (modo de tela única ou estendido entre telas), para que você possa fazer modificações na interface/UX, proporcionando uma melhor experiência do usuário adaptada a essas configurações específicas.

Em dispositivos que não têm duas telas físicas (normalmente, eles não têm uma articulação física), os apps podem ser executados lado a lado no modo de várias janelas. Nesses dispositivos, quando o app é executado nesse modo, ele atua da mesma forma que em uma tela única, como no exemplo anterior. Quando o app é executado em todas as telas lógicas, ele age como quando é estendido. Confira a próxima figura:

eacdd758eefb6c3c.png

Quando o app é executado no modo de várias janelas, a WindowManager fornece uma List<LayoutInfo> vazia.

Em resumo, você vai receber dados de WindowLayoutInfo somente quando o app for executado em todas as telas lógicas, passando pelo recurso do dispositivo (dobra ou articulação). Em todos os outros casos, você não vai receber informações. 32e4190913b452e4.png

O que acontece quando você estende o app para as telas? Em um emulador de tela dupla, a WindowLayoutInfo terá um objeto FoldingFeature que fornece dados sobre o recurso do dispositivo: uma HINGE, os limites desse recurso Rect (0, 0 - 1434, 1800) e a posição (estado) do dispositivo (FLAT).

586f15def7d23ffd.png

Confira o que cada campo significa:

  • type = TYPE_HINGE: esse emulador de duas telas espelha um dispositivo Surface Duo real com uma articulação física. É isso que a WindowManager informa.
  • Bounds [0, 0 - 1434, 1800]: representa o retângulo delimitador do recurso dentro da janela do aplicativo no espaço de coordenadas dela. Se você ler as especificações de dimensão do dispositivo Surface Duo (link em inglês), vai descobrir que a articulação está exatamente na posição informada por esses limites (esquerda, cima, direita, baixo).
  • State: há dois valores que representam a posição (estado) do dispositivo.
  • HALF_OPENED: a articulação do dispositivo dobrável está em uma posição intermediária entre o estado aberto e fechado. Há um ângulo não plano entre as partes da tela flexível ou entre os painéis da tela física.
  • FLAT: o dispositivo dobrável está completamente aberto e o espaço em tela apresentado ao usuário é plano.

Por padrão, o emulador fica aberto em 180 graus, de modo que a posição retornada pela WindowManager é FLAT.

Se você mudar a posição do emulador usando os sensores virtuais para a posição meio aberta, a WindowManager vai enviar uma notificação sobre a nova posição: HALF_OPENED.

cba02ab39d6d346b.png

Como usar a WindowManager para adaptar sua interface/UX

Como mostrado nas figuras, as informações de layout da janela foram cortadas pelo recurso de exibição, como você pode conferir abaixo também:

ff2caf93916f1682.png

Essa não é uma experiência ideal para o usuário. Você pode usar as informações fornecidas pela WindowManager para ajustar sua interface/UX.

Quando seu app é estendido por todas as regiões de exibição, ele também passa pelo recurso de dobra do dispositivo. A WindowManager fornece informações de layout de janela como estado e limites de exibição. Quando o app é estendido para várias janelas, você precisa usar essas informações e ajustar sua interface/UX.

O que você vai fazer é ajustar a interface/UX que tem atualmente durante a execução quando o app for estendido, para que nenhuma informação importante seja cortada/oculta pelo recurso de exibição. Você vai criar uma visualização que espelha o recurso de exibição do dispositivo e será usada como referência para restringir a TextView que é cortada/oculta, para que você não perca mais informações.

Para fins de aprendizado, acrescente cores a essa nova visualização para que ela fique visível especificamente no mesmo lugar em que o recurso de exibição do dispositivo real está localizado e tem as mesmas dimensões.

Adicione a nova visualização que você vai usar como referência do recurso do dispositivo no activity_main.xml:

activity_main.xml (link em inglês)

<!-- It's not important where this view is placed by default, it will be positioned dynamically at runtime -->
<View
    android:id="@+id/folding_feature"
    android:layout_width="0dp"
    android:layout_height="0dp"
    android:background="@android:color/holo_red_dark"
    android:visibility="gone"
    tools:ignore="MissingConstraints" />

No arquivo MainActivity.kt, acesse a função updateUI() usada para mostrar as informações de WindowLayoutInfo e adicione uma nova chamada de função no caso "if-else" em que havia um recurso de exibição:

MainActivity.kt (link em inglês)

private fun updateUI(newLayoutInfo: WindowLayoutInfo) {
   binding.layoutChange.text = newLayoutInfo.toString()
   if (newLayoutInfo.displayFeatures.isNotEmpty()) {
       binding.configurationChanged.text = "Spanned across displays"
       alignViewToFoldingFeatureBounds(newLayoutInfo)
   } else {
       binding.configurationChanged.text = "One logic/physical display - unspanned"
   }
}

Você adicionou a função alignViewToFoldingFeatureBounds que recebe WindowLayoutInfo como parâmetro.

Crie essa função. Dentro dela, crie seu ConstraintSet para aplicar as novas restrições às suas visualizações. Agora, acesse os limites do recurso de exibição usando a WindowLayoutInfo. Como WindowLayoutInfo retorna uma lista de DisplayFeature que é apenas uma interface, transmita-a ao FoldingFeature para ter acesso a todas as informações:

MainActivity.kt (link em inglês)

import androidx.constraintlayout.widget.ConstraintSet
import androidx.window.layout.FoldingFeature

private fun alignViewToFoldingFeatureBounds(newLayoutInfo: WindowLayoutInfo) {
   val constraintLayout = binding.constraintLayout
   val set = ConstraintSet()
   set.clone(constraintLayout)

   // Get and translate the feature bounds to the View's coordinate space and current
   // position in the window.
   val foldingFeature = newLayoutInfo.displayFeatures[0] as FoldingFeature
   val bounds = getFeatureBoundsInWindow(foldingFeature, binding.root)

   // Rest of the code to be added in the following steps
}

Defina uma função getFeatureBoundsInWindow() para traduzir os limites do recurso ao espaço de coordenadas da visualização e a posição atual na janela.

MainActivity.kt (link em inglês)

import android.graphics.Rect
import android.view.View
import androidx.window.layout.DisplayFeature

/**
 * Get the bounds of the display feature translated to the View's coordinate space and current
 * position in the window. This will also include view padding in the calculations.
 */
private fun getFeatureBoundsInWindow(
    displayFeature: DisplayFeature,
    view: View,
    includePadding: Boolean = true
): Rect? {
    // Adjust the location of the view in the window to be in the same coordinate space as the feature.
    val viewLocationInWindow = IntArray(2)
    view.getLocationInWindow(viewLocationInWindow)

    // Intersect the feature rectangle in window with view rectangle to clip the bounds.
    val viewRect = Rect(
        viewLocationInWindow[0], viewLocationInWindow[1],
        viewLocationInWindow[0] + view.width, viewLocationInWindow[1] + view.height
    )

    // Include padding if needed
    if (includePadding) {
        viewRect.left += view.paddingLeft
        viewRect.top += view.paddingTop
        viewRect.right -= view.paddingRight
        viewRect.bottom -= view.paddingBottom
    }

    val featureRectInView = Rect(displayFeature.bounds)
    val intersects = featureRectInView.intersect(viewRect)

    // Checks to see if the display feature overlaps with our view at all
    if ((featureRectInView.width() == 0 && featureRectInView.height() == 0) ||
        !intersects
    ) {
        return null
    }

    // Offset the feature coordinates to view coordinate space start point
    featureRectInView.offset(-viewLocationInWindow[0], -viewLocationInWindow[1])

    return featureRectInView
}

Com as informações sobre os limites do recurso de exibição, você pode usá-las para definir e mover corretamente a altura para sua visualização de referência.

O código completo dos alignViewToFoldingFeatureBounds será este:

MainActivity.kt (link em inglês) - alignViewToFoldingFeatureBounds

private fun alignViewToFoldingFeatureBounds(newLayoutInfo: WindowLayoutInfo) {
    val constraintLayout = binding.constraintLayout
    val set = ConstraintSet()
    set.clone(constraintLayout)

    // Get and Translate the feature bounds to the View's coordinate space and current
    // position in the window.
    val foldingFeature = newLayoutInfo.displayFeatures[0] as FoldingFeature
    val bounds = getFeatureBoundsInWindow(foldingFeature, binding.root)

    bounds?.let { rect ->
        // Some devices have a 0px width folding feature. We set a minimum of 1px so we
        // can show the view that mirrors the folding feature in the UI and use it as reference.
        val horizontalFoldingFeatureHeight = (rect.bottom - rect.top).coerceAtLeast(1)
        val verticalFoldingFeatureWidth = (rect.right - rect.left).coerceAtLeast(1)

        // Sets the view to match the height and width of the folding feature
        set.constrainHeight(
            R.id.folding_feature,
            horizontalFoldingFeatureHeight
        )
        set.constrainWidth(
            R.id.folding_feature,
            verticalFoldingFeatureWidth
        )

        set.connect(
            R.id.folding_feature, ConstraintSet.START,
            ConstraintSet.PARENT_ID, ConstraintSet.START, 0
        )
        set.connect(
            R.id.folding_feature, ConstraintSet.TOP,
            ConstraintSet.PARENT_ID, ConstraintSet.TOP, 0
        )

        if (foldingFeature.orientation == FoldingFeature.Orientation.VERTICAL) {
            set.setMargin(R.id.folding_feature, ConstraintSet.START, rect.left)
            set.connect(
                R.id.layout_change, ConstraintSet.END,
                R.id.folding_feature, ConstraintSet.START, 0
            )
        } else {
            // FoldingFeature is Horizontal
            set.setMargin(
                R.id.folding_feature, ConstraintSet.TOP,
                rect.top
            )
            set.connect(
                R.id.layout_change, ConstraintSet.TOP,
                R.id.folding_feature, ConstraintSet.BOTTOM, 0
            )
        }

        // Set the view to visible and apply constraints
        set.setVisibility(R.id.folding_feature, View.VISIBLE)
        set.applyTo(constraintLayout)
    }
}

Agora, a TextView que entrou em conflito com o recurso de exibição de dispositivo considera onde o recurso está localizado, para que o conteúdo dela nunca seja cortado ou oculto:

67b41810704d0011.png

No emulador de tela dupla (imagem acima e à esquerda), observamos como a TextView que mostrava o conteúdo nas telas e era cortada pela articulação não é mais cortada, então não há informações ausentes.

Em um emulador dobrável (acima e à direita), há uma linha vermelha clara que representa onde está o recurso de exibição, além da TextView colocada abaixo do recurso. Quando o dispositivo for dobrado (por exemplo, em 90 graus em uma posição de laptop), nenhuma informação será afetada pelo recurso.

Se você está se perguntando onde o recurso de exibição fica no emulador de tela dupla (já que esse é um dispositivo articulado), a visualização que representa o recurso é oculta pela articulação. No entanto, se o app mudar de estendido para não estendido, ele vai aparecer na mesma posição que o recurso, com a altura e a largura corretas.

1a309ab775c49a6a.png

8. Outros artefatos da Jetpack WindowManager

A WindowManager também oferece outros artefatos úteis além do principal, que ajudam você a interagir com o componente de outra forma, considerando o ambiente atual que você está usando ao criar seus apps.

Artefato Java

Se você estiver usando a linguagem de programação Java em vez de Kotlin ou se ouvir eventos usando callbacks for uma abordagem melhor para sua arquitetura, o artefato Java da WindowManager pode ser útil, já que fornece uma API compatível com Java para registrar e cancelar o registro de listeners para eventos usando callbacks.

Artefato(s) RxJava

Se você já está usando o RxJava (versão 2 ou 3), pode utilizar artefatos específicos que vão ajudar a manter a consistência no código, usando Observables ou Flowables (links em inglês).

9. Como testar usando a Jetpack WindowManager

Testar as posturas dobráveis em qualquer emulador ou dispositivo é muito útil para saber como os elementos da interface podem ser posicionados ao redor do FoldingFeature.

Para isso, a WindowManger oferece um ótimo artefato para testes instrumentados.

Vamos conferir como usá-lo.

Com a dependência principal da WindowManager, adicionamos o artefato de teste ao arquivo build.gradle do app: androidx.window:window-testing

O artefato window-testing apresenta uma nova TestRule (link em inglês) chamada WindowLayoutInfoPublisherRule que ajuda a testar o consumo de um stream de valores WindowLayoutInfo. A WindowLayoutInfoPublisherRule permite que você envie diferentes valores de WindowLayoutInfo sob demanda.

Para usar essa regra e depois criar um exemplo que facilita testes de interface com esse novo artefato, atualize a classe de teste criada pelo modelo do Android Studio. Substitua todo o código na classe ExampleInstrumentedTest pelo seguinte:

ExampleInstrumentedTest.kt (link em inglês)

import androidx.test.ext.junit.rules.ActivityScenarioRule
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.window.testing.layout.WindowLayoutInfoPublisherRule
import org.junit.Rule
import org.junit.rules.RuleChain
import org.junit.rules.TestRule
import org.junit.runner.RunWith

@RunWith(AndroidJUnit4::class)
class MainActivityTest {
    private val activityRule = ActivityScenarioRule(MainActivity::class.java)
    private val publisherRule = WindowLayoutInfoPublisherRule()

    @get:Rule
    val testRule: TestRule

    init {
        testRule = RuleChain.outerRule(publisherRule).around(activityRule)
    }
}

A regra mencionada foi encadeada com uma ActvityScenarioRule.

Para simular um FoldingFeature, o novo artefato oferece algumas funções incríveis. Confira abaixo a função mais simples, que fornece alguns valores padrão.

Na MainActivity, as TextViews são alinhadas à esquerda do recurso de dobra. Crie um teste que verifique se isso foi implementado corretamente.

Crie um teste com o nome testText_is_left_of_Vertical_FoldingFeature:

ExampleInstrumentedTest.kt (link em inglês)

import androidx.window.layout.FoldingFeature.Orientation.Companion.VERTICAL
import androidx.window.layout.FoldingFeature.State.Companion.FLAT
import androidx.window.testing.layout.FoldingFeature
import androidx.window.testing.layout.TestWindowLayoutInfo
import org.junit.Test

@Test
fun testText_is_left_of_Vertical_FoldingFeature() {
   activityRule.scenario.onActivity { activity ->
       val hinge = FoldingFeature(
           activity = activity,
           state = FLAT,
           orientation = VERTICAL,
           size = 2
       )

       val expected = TestWindowLayoutInfo(listOf(hinge))
       publisherRule.overrideWindowLayoutInfo(expected)
   }

   // Add Assertion with EspressoMatcher here

}

O FoldingFeature de teste tem um estado FLAT, e a orientação é VERTICAL. Definimos um tamanho específico porque queremos que o FoldingFeature falso seja mostrado na interface nos testes para podermos conferir onde ele está localizado no dispositivo.

Usamos a WindowLayoutInfoPublishRule que instanciamos antes para publicar o FoldingFeaure falso, de forma que ele possa ser acessado como faríamos com dados reais de WindowLayoutInfo:

A etapa final é apenas testar se os elementos da interface estão localizados onde precisam ser alinhados, evitando o FoldingFeature. Para isso, basta usar EspressoMatchers, adicionando a declaração ao final do teste que acabamos de criar:

ExampleInstrumentedTest.kt (link em inglês)

import androidx.test.espresso.assertion.PositionAssertions
import androidx.test.espresso.matcher.ViewMatchers.withId

onView(withId(R.id.layout_change)).check(
    PositionAssertions.isCompletelyLeftOf(withId(R.id.folding_feature))
)

O teste completo ficará assim:

ExampleInstrumentedTest.kt (link em inglês)

@Test
fun testText_is_left_of_Vertical_FoldingFeature() {
    activityRule.scenario.onActivity { activity ->
        val hinge = FoldingFeature(
            activity = activity,
            state = FoldingFeature.State.FLAT,
            orientation = FoldingFeature.Orientation.VERTICAL,
            size = 2
        )
        val expected = TestWindowLayoutInfo(listOf(hinge))
        publisherRule.overrideWindowLayoutInfo(expected)
    }
    onView(withId(R.id.layout_change)).check(
        PositionAssertions.isCompletelyLeftOf(withId(R.id.folding_feature))
    )
}
val horizontal_hinge = FoldingFeature(
   activity = activity,
   state = FLAT,
   orientation = HORIZONTAL,
   size = 2
)

Agora, você pode executar o teste em um dispositivo ou emulador para verificar se o aplicativo se comporta como esperado. Você não precisa de um dispositivo ou emulador dobrável para executar esse teste.

10. Parabéns!

A Jetpack WindowManager ajuda os desenvolvedores com novos formatos de dispositivos, por exemplo, os dobráveis.

As informações fornecidas pela WindowManager ajudam a adaptar apps Android a dispositivos dobráveis para melhorar a experiência do usuário.

Em resumo, você aprendeu neste codelab:

  • O que são dispositivos dobráveis.
  • Diferenças entre os vários dispositivos dobráveis.
  • Diferenças entre dispositivos dobráveis, de tela única e tablets.
  • Sobre a API Jetpack WindowManager.
  • Como usar a Jetpack WindowManager e adaptar nossos apps a novos formatos de dispositivo.
  • Como testar usando a Jetpack WindowManager.

Saiba mais