Suporte para modos de exibição dobráveis

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

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á aberto. O display interno é desligado automaticamente.

Um aplicativo inovador deve mostrar a prévia da câmera na tela externa. Assim, os usuários poderão tirar selfies com a câmera traseira, o que geralmente oferece uma qualidade de fotos muito 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. Portanto, não é necessário desenvolver nada da sua parte. 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. Confira um exemplo de implementação no codelab Otimizar o app de câmera em dispositivos dobráveis com o Jetpack WindowManager.

Modo de tela dupla

Esse modo permite mostrar conteúdo nas duas telas do dispositivo dobrável ao mesmo tempo. Ele está disponível no Pixel Fold com o Android 14 (nível 34 da API) e em versões mais recentes.

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 Dual Screen pelas APIs Jetpack WindowManager, a partir da versão 1.2.0-beta03 da biblioteca.

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 as informações e o comportamento relacionados ao movimento de janelas entre telas ou á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 recurso específico:

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 abaixo 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 o 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(). Ao encerrar a sessão, você recebe 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 você sobre o status da sessão e permitem atualizar 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 ícone oficial de Dual Screen para indicar aos usuários como ativar ou desativar esse modo.

Para um exemplo funcional, consulte DualScreenActivity.kt.

Modo de tela traseira

Semelhante ao exemplo do modo Dual Screen, o exemplo a seguir de uma função toggleRearDisplayMode() fecha a sessão se o capability já estiver ativo ou chama a função transferActivityToWindowArea():

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 esse modo.

Outros recursos