Suporte para modos de exibição dobráveis

Dispositivos dobráveis oferecem experiências de visualização únicas. Modo de tela traseira e o modo Dual Screen permite criar recursos especiais de exibição para dispositivos dobráveis como a prévia de selfie de câmera traseira e imagens simultâneas, mas diferentes exibidos nas telas interna e externa.

Modo de tela traseira

Normalmente, quando um dispositivo dobrável é aberto, apenas a tela interna fica ativa. O modo de tela traseira permite mover uma atividade para a tela externa de um dispositivo dobrável que geralmente fica voltada para o lado oposto ao usuário enquanto o dispositivo está desdobrado. O display interno é desligado automaticamente.

Um aplicativo novo é exibir a visualização da câmera na tela externa, para que os usuários podem tirar selfies com a câmera traseira, o que geralmente oferece uma experiência desempenho de tirar fotos melhor do que a câmera frontal.

Para ativar o modo de tela traseira, os usuários respondem a uma caixa de diálogo que permite a troca de tela pelo app. Por exemplo:

Figura 1. Caixa de diálogo do sistema para permitir o início do modo de tela traseira.

O sistema cria a caixa de diálogo. Assim, ela não precisa ser desenvolvida por você. Diferentes caixas de diálogo aparecem dependendo do estado do dispositivo. Por exemplo, o sistema orienta os usuários a abrir o dispositivo se ele estiver fechado. Não é possível personalizar a caixa de diálogo, e ela pode variar de acordo com os dispositivos de diferentes OEMs.

Você pode testar o modo de tela traseira com o app de câmera do Pixel Fold. Ver um exemplo implementação no codelab Otimizar o app de câmera em dispositivos dobráveis usando Jetpack WindowManager.

Modo Dual Screen

O modo Dual Screen permite mostrar conteúdo nas duas telas de um dispositivo dobrável ao mesmo tempo. O modo Dual Screen está disponível no Pixel Fold com o Android 14 (nível 34 da API) ou mais recente.

Um exemplo de caso de uso é o intérprete com Dual Screen.

Figura 2. Intérprete com Dual Screen mostrando conteúdo diferente nas telas frontal e traseira.

Ativar os modos de forma programática

Você pode acessar o modo de tela traseira e o modo de tela dupla pelas APIs Jetpack WindowManager da versão 1.2.0-beta03 da biblioteca em diante.

Adicione a dependência WindowManager ao arquivo build.gradle do módulo do app:

Groovy

dependencies {
    implementation "androidx.window:window:1.2.0-beta03"
}

Kotlin

dependencies {
    implementation("androidx.window:window:1.2.0-beta03")
}

O ponto de entrada é o WindowAreaController, que fornece informações e o comportamento ao mover janelas entre telas ou entre áreas de exibição em um dispositivo. WindowAreaController permite consultar a lista de objetos WindowAreaInfo disponíveis.

Use WindowAreaInfo para acessar a WindowAreaSession, uma interface que representa um recurso de área da janela ativa. Use WindowAreaSession para determinar a disponibilidade de uma WindowAreaCapability específica.

Cada recurso está relacionado a uma WindowAreaCapability.Operation específica. Na versão 1.2.0-beta03, a API Jetpack WindowManager tem suporte a dois tipos de operações:

Confira um exemplo de como declarar variáveis para o modo de tela traseira e Dual Screen na atividade principal do app:

Kotlin

private lateinit var windowAreaController: WindowAreaController
private lateinit var displayExecutor: Executor
private var windowAreaSession: WindowAreaSession? = null
private var windowAreaInfo: WindowAreaInfo? = null
private var capabilityStatus: WindowAreaCapability.Status =
    WindowAreaCapability.Status.WINDOW_AREA_STATUS_UNSUPPORTED

private val dualScreenOperation = WindowAreaCapability.Operation.OPERATION_PRESENT_ON_AREA
private val rearDisplayOperation = WindowAreaCapability.Operation.OPERATION_TRANSFER_ACTIVITY_TO_AREA

Java

private WindowAreaControllerCallbackAdapter windowAreaController = null;
private Executor displayExecutor = null;
private WindowAreaSessionPresenter windowAreaSession = null;
private WindowAreaInfo windowAreaInfo = null;
private WindowAreaCapability.Status capabilityStatus  =
        WindowAreaCapability.Status.WINDOW_AREA_STATUS_UNSUPPORTED;

private WindowAreaCapability.Operation dualScreenOperation =
        WindowAreaCapability.Operation.OPERATION_PRESENT_ON_AREA;
private WindowAreaCapability.Operation rearDisplayOperation =
        WindowAreaCapability.Operation.OPERATION_TRANSFER_ACTIVITY_TO_AREA;

Confira como inicializar as variáveis no método onCreate() da sua atividade:

Kotlin

displayExecutor = ContextCompat.getMainExecutor(this)
windowAreaController = WindowAreaController.getOrCreate()

lifecycleScope.launch(Dispatchers.Main) {
    lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
        windowAreaController.windowAreaInfos
            .map { info -> info.firstOrNull { it.type == WindowAreaInfo.Type.TYPE_REAR_FACING } }
            .onEach { info -> windowAreaInfo = info }
            .map { it?.getCapability(operation)?.status ?: WindowAreaCapability.Status.WINDOW_AREA_STATUS_UNSUPPORTED }
            .distinctUntilChanged()
            .collect {
                capabilityStatus = it
            }
    }
}

Java

displayExecutor = ContextCompat.getMainExecutor(this);
windowAreaController = new WindowAreaControllerCallbackAdapter(WindowAreaController.getOrCreate());
windowAreaController.addWindowAreaInfoListListener(displayExecutor, this);

windowAreaController.addWindowAreaInfoListListener(displayExecutor,
  windowAreaInfos -> {
    for(WindowAreaInfo newInfo : windowAreaInfos){
        if(newInfo.getType().equals(WindowAreaInfo.Type.TYPE_REAR_FACING)){
            windowAreaInfo = newInfo;
            capabilityStatus = newInfo.getCapability(presentOperation).getStatus();
            break;
        }
    }
});

Antes de iniciar uma operação, verifique a disponibilidade do capacidade:

Kotlin

when (capabilityStatus) {
    WindowAreaCapability.Status.WINDOW_AREA_STATUS_UNSUPPORTED -> {
      // The selected display mode is not supported on this device.
    }
    WindowAreaCapability.Status.WINDOW_AREA_STATUS_UNAVAILABLE -> {
      // The selected display mode is not available.
    }
    WindowAreaCapability.Status.WINDOW_AREA_STATUS_AVAILABLE -> {
      // The selected display mode is available and can be enabled.
    }
    WindowAreaCapability.Status.WINDOW_AREA_STATUS_ACTIVE -> {
      // The selected display mode is already active.
    }
    else -> {
      // The selected display mode status is unknown.
    }
}

Java

if (capabilityStatus.equals(WindowAreaCapability.Status.WINDOW_AREA_STATUS_UNSUPPORTED)) {
  // The selected display mode is not supported on this device.
}
else if (capabilityStatus.equals(WindowAreaCapability.Status.WINDOW_AREA_STATUS_UNAVAILABLE)) {
  // The selected display mode is not available.
}
else if (capabilityStatus.equals(WindowAreaCapability.Status.WINDOW_AREA_STATUS_AVAILABLE)) {
  // The selected display mode is available and can be enabled.
}
else if (capabilityStatus.equals(WindowAreaCapability.Status.WINDOW_AREA_STATUS_ACTIVE)) {
  // The selected display mode is already active.
}
else {
  // The selected display mode status is unknown.
}

Modo de tela dupla

O exemplo a seguir fecha a sessão se o recurso já estiver ativo ou chama a função presentContentOnWindowArea():

Kotlin

fun toggleDualScreenMode() {
    if (windowAreaSession != null) {
        windowAreaSession?.close()
    }
    else {
        windowAreaInfo?.token?.let { token ->
            windowAreaController.presentContentOnWindowArea(
                token = token,
                activity = this,
                executor = displayExecutor,
                windowAreaPresentationSessionCallback = this
            )
        }
    }
}

Java

private void toggleDualScreenMode() {
    if(windowAreaSession != null) {
        windowAreaSession.close();
    }
    else {
        Binder token = windowAreaInfo.getToken();
        windowAreaController.presentContentOnWindowArea( token, this, displayExecutor, this);
    }
}

Observe o uso da atividade principal do app como Argumento WindowAreaPresentationSessionCallback.

A API usa uma abordagem de listener: ao fazer uma solicitação para apresentar o conteúdo na outra tela de um dispositivo dobrável, você inicia uma sessão que é retornada usando o método onSessionStarted(). Quando você fechar o você vai receber uma confirmação no método onSessionEnded().

Para criar o listener, implemente a interface WindowAreaPresentationSessionCallback:

Kotlin

class MainActivity : AppCompatActivity(), windowAreaPresentationSessionCallback

Java

public class MainActivity extends AppCompatActivity implements WindowAreaPresentationSessionCallback

O listener precisa implementar os métodos onSessionStarted(), onSessionEnded(), e onContainerVisibilityChanged(). Os métodos de callback notificam o status da sessão e permitem que você atualize o app adequadamente.

O callback onSessionStarted() recebe um WindowAreaSessionPresenter como argumento. O argumento é o contêiner que permite acessar uma área da janela e mostrar conteúdo. A apresentação poderá ser dispensada automaticamente pelo sistema quando o usuário sair da janela principal do app ou ser fechada chamando WindowAreaSessionPresenter#close().

Para os outros callbacks, você pode simplificar o processo verificando se há erros no corpo da função e registrando o estado:

Kotlin

override fun onSessionStarted(session: WindowAreaSessionPresenter) {
    windowAreaSession = session
    val view = TextView(session.context)
    view.text = "Hello world!"
    session.setContentView(view)
}

override fun onSessionEnded(t: Throwable?) {
    if(t != null) {
        Log.e(logTag, "Something was broken: ${t.message}")
    }
}

override fun onContainerVisibilityChanged(isVisible: Boolean) {
    Log.d(logTag, "onContainerVisibilityChanged. isVisible = $isVisible")
}

Java

@Override
public void onSessionStarted(@NonNull WindowAreaSessionPresenter session) {
    windowAreaSession = session;
    TextView view = new TextView(session.getContext());
    view.setText("Hello world, from the other screen!");
    session.setContentView(view);
}

@Override public void onSessionEnded(@Nullable Throwable t) {
    if(t != null) {
        Log.e(logTag, "Something was broken: ${t.message}");
    }
}

@Override public void onContainerVisibilityChanged(boolean isVisible) {
    Log.d(logTag, "onContainerVisibilityChanged. isVisible = " + isVisible);
}

Para manter a consistência em todo o ecossistema, use o Dual Screen (em inglês) oficial para indicar aos usuários como ativar ou desativar o modo Dual Screen.

Para um exemplo funcional, consulte DualScreenActivity.kt.

Modo de tela traseira

Semelhante ao exemplo do modo Dual Screen, o exemplo a seguir de um A função toggleRearDisplayMode() fecha a sessão se o recurso é já está ativo ou chama transferActivityToWindowArea() função:

Kotlin

fun toggleRearDisplayMode() {
    if(capabilityStatus == WindowAreaCapability.Status.WINDOW_AREA_STATUS_ACTIVE) {
        if(windowAreaSession == null) {
            windowAreaSession = windowAreaInfo?.getActiveSession(
                operation
            )
        }
        windowAreaSession?.close()
    } else {
        windowAreaInfo?.token?.let { token ->
            windowAreaController.transferActivityToWindowArea(
                token = token,
                activity = this,
                executor = displayExecutor,
                windowAreaSessionCallback = this
            )
        }
    }
}

Java

void toggleDualScreenMode() {
    if(capabilityStatus == WindowAreaCapability.Status.WINDOW_AREA_STATUS_ACTIVE) {
        if(windowAreaSession == null) {
            windowAreaSession = windowAreaInfo.getActiveSession(
                operation
            )
        }
        windowAreaSession.close()
    }
    else {
        Binder token = windowAreaInfo.getToken();
        windowAreaController.transferActivityToWindowArea(token, this, displayExecutor, this);
    }
}

Nesse caso, a atividade mostrada é usada como um WindowAreaSessionCallback, que é mais simples de implementar porque o callback não recebe um apresentador que permita mostrar conteúdo em uma área de janela, mas transfere toda a atividade para outra área:

Kotlin

override fun onSessionStarted() {
    Log.d(logTag, "onSessionStarted")
}

override fun onSessionEnded(t: Throwable?) {
    if(t != null) {
        Log.e(logTag, "Something was broken: ${t.message}")
    }
}

Java

@Override public void onSessionStarted(){
    Log.d(logTag, "onSessionStarted");
}

@Override public void onSessionEnded(@Nullable Throwable t) {
    if(t != null) {
        Log.e(logTag, "Something was broken: ${t.message}");
    }
}

Para manter a consistência em todo o ecossistema, use o ícone oficial de câmera traseira para indicar aos usuários como ativar ou desativar o modo de exibição traseira.

Outros recursos