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:
- Experiência na criação de apps Android.
- Experiência com Atividades, Fragmentos, Vinculação de visualizações e layouts xml.
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 Studio Arctic Fox ou uma versão mais recente.
- Um dispositivo ou emulador dobrável.
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:
- Para usar um emulador de tela dupla, faça o download do emulador do Microsoft Surface Duo (link em inglês) para sua plataforma (Windows, MacOS ou GNU/Linux).
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.
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.
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:
state()
: fornece a posição atual do dispositivo com base em uma lista de posições definidas (FLAT
eHALF_OPENED
)isSeparating()
: calcula se umFoldingFeature
deve ser considerado como uma divisão da janela em várias áreas físicas que podem ser entendidas pelos usuários como logicamente separadasocclusionType()
: calcula o modo de oclusão para determinar se umFoldingFeature
oculta uma parte da janela.orientation()
: retornaFoldingFeature.Orientation.HORIZONTAL
se a largura deFoldingFeature
for maior que a altura. Caso contrário, o retorno éFoldingFeature.Orientation.VERTICAL
.bounds()
: fornece uma instânciaRect
que contém os limites do recurso do dispositivo, como os de uma articulação física.
Usando a interface WindowInfoTracker
, você pode acessar windowLayoutInfo()
para coletar um Flow
(link em inglês) de WindowLayoutInfo
que contém todos os DisplayFeature
s disponíveis.
4. Configurar
Crie um novo projeto e selecione o modelo "Empty Activity":
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 TextView
s, 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:
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:
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:
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 DisplayFeature
s 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:
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:
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.
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
).
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
.
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:
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:
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.
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 TextView
s 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
- Repositório de exemplos de codelab
- Como criar aplicativos para dispositivos dobráveis
- Suporte a várias janelas
- Arrastar e soltar
- Layouts responsivos para o desenvolvimento de telas grandes
- Jetpack WindowManager
- Exemplo da Jetpack WindowManager (link em inglês)
- Desenvolver apps para dispositivos de tela dupla