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 um desempenho 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. Assim, ela não precisa ser desenvolvida por você. Diferentes caixas de diálogo aparecem dependendo do estado do dispositivo. Por exemplo, o sistema instrui o usuário 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 Dual Screen

O modo Dual Screen permite mostrar conteúdo nos dois displays 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 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 Dual Screen 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:

Kotlin

dependencies {
    // Define window_version in your project's build configuration.
    implementation("androidx.window:window:$window_version")
}

Groovy

dependencies {
    // TODO: Define window_version in your project's build configuration.
    implementation "androidx.window:window:$window_version"
}

O ponto de entrada é o WindowAreaController, que fornece as 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 o WindowAreaSession, uma interface que representa um recurso de área de janela ativa. Use WindowAreaSession para determinar a disponibilidade de um WindowAreaCapability específico.

Cada recurso está relacionado a um WindowAreaCapability.Operation específico. 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 os modos 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 da capability específica:

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 Dual Screen

O exemplo a seguir fecha a sessão caso a capability já esteja ativa. Se não estiver, ele vai chamar 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 WindowAreaPresentationSessionCallback argumento.

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ê fecha a sessão, recebe uma confirmação no onSessionEnded() método.

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 onSessionStarted(), onSessionEnded(), e onContainerVisibilityChanged() métodos. Os métodos de callback notificam você sobre o status da sessão e permitem atualizar o app adequadamente.

O onSessionStarted() callback recebe um WindowAreaSessionPresenter como argumento. O argumento é o contêiner que permite acessar uma área de 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 deDual Screen para indicar aos usuários como ativar ou desativar esse modo.

Para um exemplo funcional, consulte DualScreenActivity.kt (link em inglês).

Modo de tela traseira

Semelhante ao exemplo do modo Dual Screen, o seguinte exemplo de uma toggleRearDisplayMode() função fecha a sessão caso o recurso já esteja ativo ou chama a 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 toggleRearDisplayMode() {
    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 exibida é usada como um WindowAreaSessionCallback, que é mais simples de implementar porque o callback não recebe um apresentador que permite 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 da câmera traseira para indicar aos usuários como ativar ou desativar esse modo.

Outros recursos