Fazer com que seu app reconheça um dispositivo dobrável

Telas grandes desdobradas e estados dobrados exclusivos permitem novas experiências do usuário em dispositivos dobráveis. Para que o app reconheça um dispositivo dobrável, use a biblioteca Jetpack WindowManager, que tem uma superfície de API para recursos de janela de dispositivos dobráveis, como dobras e articulações. Quando o app reconhece dobras, ele pode adaptar o layout para evitar colocar conteúdo importante na área de dobras ou articulações e usar as dobras e articulações como separadores naturais.

Entender se um dispositivo oferece suporte a configurações, como postura de mesa ou livro, pode orientar decisões sobre como oferecer suporte a diferentes layouts ou fornecer recursos específicos.

Informações da janela

A interface WindowInfoTracker no Jetpack WindowManager expõe informações de layout de janelas. O método windowLayoutInfo() da interface retorna um fluxo de dados do WindowLayoutInfo que informa ao app sobre o estado de dobra de um dispositivo dobrável. O método WindowInfoTracker#getOrCreate() cria uma instância de WindowInfoTracker.

A WindowManager oferece suporte à coleta de dados WindowLayoutInfo usando fluxos Kotlin e callbacks do Java.

Fluxos Kotlin

Para iniciar e interromper a coleta de dados de WindowLayoutInfo, use uma corrotina reiniciável que reconhece o ciclo de vida, em que o bloco de código repeatOnLifecycle é executado quando o ciclo de vida é de pelo menos STARTED e é interrompido quando o ciclo de vida é STOPPED. A execução do bloco de código é reiniciada automaticamente quando o ciclo de vida é STARTED (iniciado) novamente. No exemplo abaixo, o bloco de código coleta e usa dados de WindowLayoutInfo:

class DisplayFeaturesActivity : AppCompatActivity() {

   
private lateinit var binding: ActivityDisplayFeaturesBinding

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

        binding
= ActivityDisplayFeaturesBinding.inflate(layoutInflater)
        setContentView
(binding.root)

        lifecycleScope
.launch(Dispatchers.Main) {
            lifecycle
.repeatOnLifecycle(Lifecycle.State.STARTED) {
               
WindowInfoTracker.getOrCreate(this@DisplayFeaturesActivity)
                   
.windowLayoutInfo(this@DisplayFeaturesActivity)
                   
.collect { newLayoutInfo ->
                       
// Use newLayoutInfo to update the layout.
                   
}
           
}
       
}
   
}
}

Callbacks do Java

A camada de compatibilidade de callback incluída na dependência androidx.window:window-java permite coletar atualizações de WindowLayoutInfo sem usar um fluxo Kotlin. O artefato inclui a classe WindowInfoTrackerCallbackAdapter, que adapta um WindowInfoTracker para oferecer suporte ao registro (e ao cancelamento) de callbacks para receber atualizações de WindowLayoutInfo, por exemplo:

public class SplitLayoutActivity extends AppCompatActivity {

   
private WindowInfoTrackerCallbackAdapter windowInfoTracker;
   
private ActivitySplitLayoutBinding binding;
   
private final LayoutStateChangeCallback layoutStateChangeCallback =
           
new LayoutStateChangeCallback();

   
@Override
   
protected void onCreate(@Nullable Bundle savedInstanceState) {
       
super.onCreate(savedInstanceState);

       binding
= ActivitySplitLayoutBinding.inflate(getLayoutInflater());
       setContentView
(binding.getRoot());

       windowInfoTracker
=
               
new WindowInfoTrackerCallbackAdapter(WindowInfoTracker.getOrCreate(this));
   
}

   
@Override
   
protected void onStart() {
       
super.onStart();
       windowInfoTracker
.addWindowLayoutInfoListener(
               
this, Runnable::run, layoutStateChangeCallback);
   
}

   
@Override
   
protected void onStop() {
       
super.onStop();
       windowInfoTracker
           
.removeWindowLayoutInfoListener(layoutStateChangeCallback);
   
}

   
class LayoutStateChangeCallback implements Consumer<WindowLayoutInfo> {
       
@Override
       
public void accept(WindowLayoutInfo newLayoutInfo) {
           
SplitLayoutActivity.this.runOnUiThread( () -> {
               
// Use newLayoutInfo to update the layout.
           
});
       
}
   
}
}

Suporte ao RxJava

Se você já usa o RxJava (versão 2 ou 3), aproveite os artefatos que permitem usar um Observable ou Flowable para coletar atualizações de WindowLayoutInfo sem usar um fluxo Kotlin.

A camada de compatibilidade fornecida pelas dependências de androidx.window:window-rxjava2 e androidx.window:window-rxjava3 inclui os métodos WindowInfoTracker#windowLayoutInfoFlowable() e WindowInfoTracker#windowLayoutInfoObservable(), que permitem que o app receba atualizações de WindowLayoutInfo, por exemplo:

class RxActivity: AppCompatActivity {

   
private lateinit var binding: ActivityRxBinding

   
private var disposable: Disposable? = null
   
private lateinit var observable: Observable<WindowLayoutInfo>

   
@Override
   
protected void onCreate(@Nullable Bundle savedInstanceState) {
       
super.onCreate(savedInstanceState);

       binding
= ActivitySplitLayoutBinding.inflate(getLayoutInflater());
       setContentView
(binding.getRoot());

       
// Create a new observable.
        observable
= WindowInfoTracker.getOrCreate(this@RxActivity)
           
.windowLayoutInfoObservable(this@RxActivity)
   
}

   
@Override
   
protected void onStart() {
       
super.onStart();

       
// Subscribe to receive WindowLayoutInfo updates.
        disposable
?.dispose()
        disposable
= observable
           
.observeOn(AndroidSchedulers.mainThread())
           
.subscribe { newLayoutInfo ->
           
// Use newLayoutInfo to update the layout.
       
}
   
}

   
@Override
   
protected void onStop() {
       
super.onStop();

       
// Dispose of the WindowLayoutInfo observable.
        disposable
?.dispose()
   
}
}

Recursos de telas dobráveis

A classe WindowLayoutInfo da Jetpack WindowManager disponibiliza os recursos de uma janela de exibição como uma lista de elementos DisplayFeature.

Um FoldingFeature é um tipo de DisplayFeature que fornece informações sobre telas dobráveis, incluindo o seguinte:

  • state: o estado dobrado do dispositivo, FLAT ou HALF_OPENED.

  • orientation: a orientação da dobra ou articulação, HORIZONTAL ou VERTICAL.

  • occlusionType: indica se a dobra ou articulação oculta parte da tela, NONE ou FULL.

  • isSeparating: se a dobra ou articulação cria duas áreas de exibição lógicas, "true" ou "false".

Um dispositivo dobrável que está HALF_OPENED sempre informa isSeparating como "true" porque a tela é separada em duas áreas de exibição. Além disso, isSeparating é sempre "true" em um dispositivo de tela dupla quando o app abrange as duas telas.

A propriedade FoldingFeature bounds (herdada de DisplayFeature) representa o retângulo delimitador de um recurso dobrável, como uma dobra ou articulação. Os limites podem ser usados para posicionar elementos na tela em relação ao recurso:

override fun onCreate(savedInstanceState: Bundle?) {
   
...
    lifecycleScope
.launch(Dispatchers.Main) {
        lifecycle
.repeatOnLifecycle(Lifecycle.State.STARTED) {
           
// Safely collects from WindowInfoTracker when the lifecycle is
           
// STARTED and stops collection when the lifecycle is STOPPED.
           
WindowInfoTracker.getOrCreate(this@MainActivity)
               
.windowLayoutInfo(this@MainActivity)
               
.collect { layoutInfo ->
                   
// New posture information.
                   
val foldingFeature = layoutInfo.displayFeatures
                       
.filterIsInstance<FoldingFeature>()
                       
.firstOrNull()
                   
// Use information from the foldingFeature object.
               
}

       
}
   
}
}
private WindowInfoTrackerCallbackAdapter windowInfoTracker;
private final LayoutStateChangeCallback layoutStateChangeCallback =
               
new LayoutStateChangeCallback();

@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
   
...
    windowInfoTracker
=
           
new WindowInfoTrackerCallbackAdapter(WindowInfoTracker.getOrCreate(this));
}

@Override
protected void onStart() {
   
super.onStart();
    windowInfoTracker
.addWindowLayoutInfoListener(
           
this, Runnable::run, layoutStateChangeCallback);
}

@Override
protected void onStop() {
   
super.onStop();
    windowInfoTracker
.removeWindowLayoutInfoListener(layoutStateChangeCallback);
}

class LayoutStateChangeCallback implements Consumer<WindowLayoutInfo> {
   
@Override
   
public void accept(WindowLayoutInfo newLayoutInfo) {
       
// Use newLayoutInfo to update the Layout.
       
List<DisplayFeature> displayFeatures = newLayoutInfo.getDisplayFeatures();
       
for (DisplayFeature feature : displayFeatures) {
           
if (feature instanceof FoldingFeature) {
               
// Use information from the feature object.
           
}
       
}
   
}
}

Posição de mesa

Usando as informações incluídas no objeto FoldingFeature, o app pode oferecer suporte a posições como uma mesa, em que o smartphone está em uma superfície, a articulação está em uma posição horizontal e a tela dobrável está meio aberta.

A postura de mesa oferece aos usuários a conveniência de operar o smartphone sem segurar o dispositivo nas mãos. A postura de mesa é ótima para assistir conteúdo de mídia, tirar fotos e fazer videochamadas.

Figura 1. Um app de player de vídeo na posição de mesa.

Use FoldingFeature.State e FoldingFeature.Orientation para determinar se o dispositivo está na posição de mesa:


fun isTableTopPosture(foldFeature : FoldingFeature?) : Boolean {
    contract
{ returns(true) implies (foldFeature != null) }
   
return foldFeature?.state == FoldingFeature.State.HALF_OPENED &&
            foldFeature
.orientation == FoldingFeature.Orientation.HORIZONTAL
}


boolean isTableTopPosture(FoldingFeature foldFeature) {
   
return (foldFeature != null) &&
           
(foldFeature.getState() == FoldingFeature.State.HALF_OPENED) &&
           
(foldFeature.getOrientation() == FoldingFeature.Orientation.HORIZONTAL);
}

Quando você detectar que o dispositivo está na posição de mesa, atualize o layout do app corretamente. Em apps de mídia, isso normalmente significa colocar a reprodução acima da dobra e posicionar os controles e o conteúdo suplementar logo abaixo para uma experiência de visualização ou escuta viva-voz.

No Android 15 (nível 35 da API) e versões mais recentes, é possível invocar uma API síncrona para detectar se um dispositivo oferece suporte à postura de mesa, independente do estado atual dele.

A API fornece uma lista de posturas com suporte do dispositivo. Se a lista contiver a postura de mesa, você poderá dividir o layout do app para oferecer suporte a ela e executar testes A/B na interface do app para layouts de mesa e tela cheia.

if (WindowSdkExtensions.getInstance().extensionsVersion >= 6) {
   
val postures = WindowInfoTracker.getOrCreate(context).supportedPostures
   
if (postures.contains(TABLE_TOP)) {
       
// Device supports tabletop posture.
   
}
}
if (WindowSdkExtensions.getInstance().getExtensionVersion() >= 6) {
   
List<SupportedPosture> postures = WindowInfoTracker.getOrCreate(context).getSupportedPostures();
   
if (postures.contains(SupportedPosture.TABLETOP)) {
       
// Device supports tabletop posture.
   
}
}

Exemplos

Posição de livro

Outro recurso dobrável exclusivo é a postura de livro, em que o dispositivo fica meio aberto com a articulação na vertical. A posição de livro é ótima para ler e-books. Com um layout de duas páginas em uma tela dobrável grande aberta como um livro encadernado, a postura do livro captura a experiência de ler um livro real.

Ele também pode ser usado para fotografia se você quiser capturar uma proporção diferente ao tirar fotos por viva-voz.

Implemente a postura de livro com as mesmas técnicas usadas para a postura de mesa. A única diferença é que o código precisa conferir se a orientação do recurso dobrável é vertical em vez de horizontal:

fun isBookPosture(foldFeature : FoldingFeature?) : Boolean {
    contract
{ returns(true) implies (foldFeature != null) }
   
return foldFeature?.state == FoldingFeature.State.HALF_OPENED &&
            foldFeature
.orientation == FoldingFeature.Orientation.VERTICAL
}
boolean isBookPosture(FoldingFeature foldFeature) {
   
return (foldFeature != null) &&
           
(foldFeature.getState() == FoldingFeature.State.HALF_OPENED) &&
           
(foldFeature.getOrientation() == FoldingFeature.Orientation.VERTICAL);
}

Mudanças no tamanho das janelas

A área de exibição de um app pode mudar como resultado de uma mudança na configuração do dispositivo, por exemplo, quando o dispositivo é dobrado ou desdobrado, girado ou uma janela é redimensionada no modo de várias janelas.

A classe WindowMetricsCalculator da Jetpack WindowManager permite extrair as métricas atuais e máximas da janela. Semelhante à plataforma WindowMetrics introduzida no nível 30 da API, a WindowMetrics da biblioteca WindowManager fornece os limites de janela, mas a API é compatível com versões anteriores até o nível 14 da API.

Consulte Usar classes de tamanho de janela.

Outros recursos

Amostras

Codelabs