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 articulações e dobras. Quando o app reconhece dobras, ele pode adaptar o layout para evitar colocar conteúdo importante na área delas ou de articulações, além de poder usar as dobras e articulações como separadores naturais.

Informações da janela

A interface WindowInfoTracker na 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 da dobra de um dispositivo dobrável. O método getOrCreate() do WindowInfoTracker cria uma instância do WindowInfoTracker.

A WindowManager permite coletar dados WindowLayoutInfo usando fluxos do Kotlin e callbacks do Java.

Fluxos do 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 (iniciado) e interrompido quando o ciclo de vida é STOPPED (parado). 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), pode aproveitar os artefatos que permitem usar Observable ou Flowable para coletar atualizações de WindowLayoutInfo sem usar um fluxo Kotlin (links em inglês).

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 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 ou não, "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 windowInfoRepo 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()
                        .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
            }
        }
    }
}

Modo de mesa

Usando as informações incluídas no objeto FoldingFeature, o app pode oferecer suporte a posições como o modo de 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.

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

Um app de player de vídeo no modo de mesa

Use FoldingFeature.State e FoldingFeature.Orientation para determinar se o dispositivo está no modo 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á no modo de mesa, atualize o layout do app corretamente. Em apps de música, 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.

Exemplos

Modo de livro

Outra posição dobrável exclusiva é o modo de livro, em que o dispositivo fica meio aberto com a articulação na vertical. O modo de livro é ótimo para ler e-books. Com um layout de duas páginas em uma tela dobrável grande, aberta como um livro encadernado, o modo de livro reproduz 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 o modo de livro com as mesmas técnicas usadas no modo 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 devido a uma modificação na configuração do dispositivo. Por exemplo, quando o dispositivo é dobrado, 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 Classes de tamanho de janela.

Outros recursos

Exemplos (em inglês)

Codelabs