Cómo brindar compatibilidad con modos de pantalla plegable

Los dispositivos plegables ofrecen experiencias de visualización únicas. El modo de pantalla posterior y el modo de pantalla doble te permiten crear funciones especiales de pantalla para dispositivos plegables, como la vista previa de selfie con cámara posterior y pantallas simultáneas pero diferentes en las pantallas internas y externas.

Modo de pantalla posterior

Por lo general, cuando un dispositivo plegable está desplegado, solo está activa la pantalla interna. El modo de pantalla posterior te permite mover una actividad a la pantalla externa de un dispositivo plegable, que generalmente se orienta hacia el lado opuesto al del usuario mientras el dispositivo está desplegado. La pantalla interior se apaga automáticamente.

Una aplicación novedosa consiste en mostrar la vista previa de la cámara en la pantalla exterior para que los usuarios puedan tomar selfies con la cámara posterior, lo que generalmente proporciona un rendimiento de toma de fotos mucho mejor que la cámara frontal.

Para activar el modo de pantalla posterior, los usuarios responden a un diálogo para permitir que la app cambie de pantalla, por ejemplo:

Figura 1: Diálogo del sistema para permitir el inicio del modo de pantalla posterior.

El sistema crea el diálogo, por lo que no se requiere desarrollo de tu parte. Aparecerán diferentes diálogos según el estado del dispositivo. Por ejemplo, el sistema les indica a los usuarios que desplieguen el dispositivo si este está cerrado. No puedes personalizar el diálogo, y este puede variar según los dispositivos de diferentes OEMs.

Puedes probar el modo de pantalla posterior con la app de cámara del Pixel Fold. Consulta un ejemplo de implementación en el codelab Cómo optimizar tu app de cámara en dispositivos plegables con Jetpack WindowManager.

Modo Dual Screen

El modo Dual Screen te permite mostrar contenido en las dos pantallas de un dispositivo plegable al mismo tiempo. El modo Dual Screen está disponible en Pixel Fold con Android 14 (nivel de API 34) o versiones posteriores.

Un ejemplo de caso de uso es el intérprete de Dual Screen.

Figura 2: Intérprete de doble pantalla que muestra contenido diferente en las pantallas frontal y posterior.

Cómo habilitar los modos de manera programática

Puedes acceder al modo de pantalla posterior y al modo de pantalla doble con las APIs de Jetpack WindowManager, a partir de la biblioteca versión 1.2.0-beta03.

Agrega la dependencia de WindowManager al archivo build.gradle del módulo de tu app:

Groovy

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

Kotlin

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

El punto de entrada es WindowAreaController, que proporciona la información y el comportamiento en relación con el movimiento de ventanas entre pantallas o áreas de visualización en un dispositivo. WindowAreaController te permite consultar la lista de objetos WindowAreaInfo disponibles.

Usa WindowAreaInfo para acceder a WindowAreaSession, una interfaz que representa una función de área de ventana activa. Usa WindowAreaSession para determinar la disponibilidad de una WindowAreaCapability específica.

Cada función está relacionada con una WindowAreaCapability.Operation en particular. En la versión 1.2.0-beta03, Jetpack WindowManager admite dos tipos de operaciones:

A continuación, se muestra un ejemplo de cómo declarar variables para el modo de pantalla posterior y el modo de pantalla doble en la actividad principal de tu 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;

A continuación, se muestra cómo inicializar las variables en el método onCreate() de tu actividad:

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 una operación, verifica la disponibilidad de la capacidad en particular:

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

En el siguiente ejemplo, se cierra la sesión si la función ya está activa o, de lo contrario, llama a la función 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);
    }
}

Observa el uso de la actividad principal de la app como argumento de WindowAreaPresentationSessionCallback.

La API usa un enfoque de objeto de escucha: cuando realizas una solicitud para presentar el contenido a la otra pantalla de un dispositivo plegable, se inicia una sesión que se muestra a través del método onSessionStarted() del objeto de escucha. Cuando cierres la sesión, recibirás una confirmación en el método onSessionEnded().

Para crear el objeto de escucha, implementa la interfaz WindowAreaPresentationSessionCallback:

Kotlin

class MainActivity : AppCompatActivity(), windowAreaPresentationSessionCallback

Java

public class MainActivity extends AppCompatActivity implements WindowAreaPresentationSessionCallback

El objeto de escucha debe implementar los métodos onSessionStarted(), onSessionEnded(), y onContainerVisibilityChanged(). Los métodos de devolución de llamada te notifican el estado de la sesión y te permiten actualizar la app según corresponda.

La devolución de llamada onSessionStarted() recibe un WindowAreaSessionPresenter como argumento. El argumento es el contenedor que te permite acceder a un área de la ventana y mostrar contenido. El sistema puede descartar automáticamente la presentación cuando el usuario sale de la ventana principal de la aplicación, o bien llamar a WindowAreaSessionPresenter#close() para cerrarla.

Para las otras devoluciones de llamada, por cuestiones de simplicidad, solo debes comprobar si hay errores en el cuerpo de la función y registrar el 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 mantener la coherencia en todo el ecosistema, usa el ícono oficial de Dual Screen para indicar a los usuarios cómo habilitar o inhabilitar este modo.

Para ver una muestra funcional, consulta DualScreenActivity.kt.

Modo de pantalla posterior

Al igual que en el caso del modo de pantalla doble, el siguiente ejemplo de una función toggleRearDisplayMode() cierra la sesión si la función ya está activa o, de lo contrario, llama a la función 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);
    }
}

En este caso, la actividad que se muestra se usa como WindowAreaSessionCallback, que es más fácil de implementar porque la devolución de llamada no recibe un presentador que permita mostrar contenido en un área de ventana, sino que transfiere toda la actividad a otra á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 mantener la coherencia en todo el ecosistema, usa el ícono oficial de la cámara posterior para indicar a los usuarios cómo habilitar o inhabilitar el modo de pantalla posterior.

Recursos adicionales