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:

Kotlin

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.
                }

        }
    }
}

Java

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:

Kotlin

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

Java

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.

Kotlin

if (WindowSdkExtensions.getInstance().extensionsVersion >= 6) {
    val postures = WindowInfoTracker.getOrCreate(context).supportedPostures
    if (postures.contains(TABLE_TOP)) {
        // Device supports tabletop posture.
   }
}

Java

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:

Kotlin

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

Java

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