Optimiza la experiencia con la cámara en dispositivos plegables

1. Antes de comenzar

¿Qué tienen de especial los dispositivos plegables?

Los dispositivos plegables son una innovación única de esta generación. Brindan una experiencia exclusiva y, con ella, oportunidades únicas que permiten satisfacer los gustos de los usuarios con funciones como IU en el modo de mesa para usarlos con la modalidad de manos libres.

Requisitos previos

  • Conocimientos básicos sobre el desarrollo de apps para Android
  • Conocimientos básicos sobre el framework de inyección de dependencias Hilt

Qué compilarás

En este codelab, crearás una app de cámara con diseños optimizados para dispositivos plegables.

Captura de pantalla de la app ejecutándose

Comenzarás con una app de cámara básica que no responde a ninguna posición del dispositivo ni aprovecha la mejor cámara posterior para selfies mejoradas. Actualizarás el código fuente para mover la vista previa a la pantalla más pequeña cuando el dispositivo no esté desplegado y que reaccione cuando el teléfono se disponga en modo de mesa.

Aunque la aplicación de cámara es el caso de uso más conveniente para esta API, las funciones que aprenderás en este codelab se pueden aplicar a cualquier app.

Qué aprenderás

  • Cómo usar Jetpack Window Manager para responder a los cambios de posición
  • Cómo mover tu app a la pantalla más pequeña de un dispositivo plegable

Requisitos

  • Una versión reciente de Android Studio
  • Contar con un dispositivo plegable o un emulador de dispositivo plegable

2. Prepárate

Obtén el código de partida

  1. Si tienes Git instalado, simplemente puedes ejecutar el comando que se indica abajo. Para comprobarlo, escribe git --version en la terminal o línea de comandos y verifica que se ejecute de forma correcta.
git clone https://github.com/android/large-screen-codelabs.git
  1. Si no tienes Git, puedes hacer clic en el siguiente botón para descargar todo el código de este codelab: (opcional).

Abre el primer módulo

  • En Android Studio, abre el primer módulo en /step1.

Captura de pantalla de Android Studio mostrando el código que se relaciona con este codelab

Si se te pide que uses la versión más reciente de Gradle, actualízala.

3. Ejecuta y observa

  1. Ejecuta el código del módulo step1.

Como puedes ver, es una app de cámara sencilla en la que puedes alternar entre la cámara frontal y la posterior y ajustar la relación de aspecto. Sin embargo, por el momento, el primer botón de la izquierda no hace nada, pero será el punto de entrada para el modo de selfie con la cámara posterior.

Captura de pantalla de la app con el ícono del modo de selfie con la cámara posterior destacado

  1. Ahora, intenta colocar el dispositivo en una posición semiabierta en la cual la bisagra forme un ángulo de 90 grados.

Como puedes ver, la app no responde a las diferentes posturas del dispositivo, por lo que el diseño no cambia y deja la bisagra en medio del visor.

4. Obtén información sobre WindowManager de Jetpack

La biblioteca WindowManager de Jetpack permite que los desarrolladores de apps creen experiencias optimizadas para los dispositivos plegables. Contiene la clase FoldingFeature que describe un pliegue en una pantalla flexible o una bisagra entre dos paneles físicos de la pantalla. Su API brinda acceso a información importante que se relaciona con el dispositivo:

La clase FoldingFeature incluye información adicional, como occlusionType() o isSeparating(), pero este codelab no lo aborda en detalle.

A partir de la versión 1.1.0-beta01, la biblioteca usa WindowAreaController, una API que permite que el modo de pantalla posterior mueva la ventana actual a la pantalla que se alinea con la cámara posterior, lo que es excelente para tomar selfies con esa cámara y para muchos otros casos de uso.

Agrega dependencias

  • Para usar WindowManager de Jetpack en tu app, debes agregar las siguientes dependencias al archivo de build.gradle del nivel de módulo:

step1/build.gradle

def work_version = '1.1.0-beta01'
implementation "androidx.window:window:$work_version"
implementation "androidx.window:window-java:$work_version"
implementation "androidx.window:window-core:$work_version"

Ahora puedes acceder a las clases FoldingFeature y WindowAreaController en tu app y usarlas para crear la mejor experiencia de cámara plegable.

5. Implementa el modo de selfie con la cámara posterior

Comienza con el modo de pantalla posterior. WindowAreaControllerJavaAdapter es la API que habilita este modo, el cual necesita un Executor y muestra una WindowAreaSession que guarda el estado actual. Esta WindowAreaSession se debe retener cuando la Activity se borra y se vuelve a crear, de modo que puedas guardarla de forma segura en el ViewModel entre cambios de configuración.

  1. Declara estas variables en MainActivity:

step1/MainActivity.kt

private lateinit var windowAreaController: WindowAreaControllerJavaAdapter
private lateinit var displayExecutor: Executor
  1. Inicialízalas en el método onCreate():

step1/MainActivity.kt

windowInfoTracker = WindowInfoTracker.getOrCreate(this)
displayExecutor = ContextCompat.getMainExecutor(this)
windowAreaController = WindowAreaControllerJavaAdapter(WindowAreaController.getOrCreate())

Ahora tu Activity está lista para mover el contenido en la pantalla más pequeña, pero debes guardar la sesión.

  1. Para guardar la sesión, abre CameraViewModel y declara la siguiente variable dentro del elemento:

step1/CameraViewModel.kt

var rearDisplaySession: WindowAreaSession? = null
        private set

Necesitas que rearDisplaySession sea una variable, ya que cambia cada vez que creas una, pero quieres asegurarte de que no pueda actualizarse desde el exterior porque ahora creas un método que la actualiza cuando es necesario.

  1. Pega este código en CameraViewModel:

step1/CameraViewModel.kt

fun updateSession(newSession: WindowAreaSession? = null) {
        rearDisplaySession = newSession
}

Este método se invoca cada vez que el código debe actualizar la sesión y es útil para encapsularla en un único punto de acceso.

La API de la pantalla posterior funciona con un enfoque de objeto de escucha: cuando se solicita mover el contenido a una pantalla más pequeña, se inicia una sesión que se muestra a través del método de onSessionStarted() del objeto de escucha. Por el contrario; si quieres volver a la pantalla interior (y más grande), cierra la sesión para recibir una confirmación en el método de onSessionEnded(). Aprovecha estos métodos para actualizar el rearDisplaySession en el CameraViewModel. Si quieres crear un objeto de escucha de este tipo, tienes que implementar la interfaz de WindowAreaSessionCallback.

  1. Modifica la declaración MainActivity para implementar la interfaz de WindowAreaSessionCallback:

step1/MainActivity.kt

class MainActivity : AppCompatActivity(), WindowAreaSessionCallback

Ahora, implementa los métodos onSessionStarted y onSessionEnded en la MainActivity. En el primero, quieres guardar la WindowAreaSession y, en el segundo, la restableces a null. Esto resulta muy útil, ya que WindowAreaSession te permite decidir si iniciar una sesión o cerrar una existente.

step1/MainActivity.kt

override fun onSessionEnded() {
    viewModel.updateSession(null)
}

override fun onSessionStarted(session: WindowAreaSession) {
    viewModel.updateSession(session)
}
  1. En el archivo MainActivity.kt, escribe el último fragmento de código necesario para que esta API funcione:

step1/MainActivity.kt

private fun startRearDisplayMode() {
   if (viewModel.rearDisplaySession != null) {
      viewModel.rearDisplaySession?.close()
   } else {
      windowAreaController.startRearDisplayModeSession(
         this,
         displayExecutor,
         this
      )
   }
}

Como se mencionó anteriormente, para entender qué medidas tomar, tienes que revisar que rearDisplaySession esté en CameraViewModel. Si no aparece como null, entonces la sesión ya está en curso y se cierra. Por otro lado, si aparece como null, usa windowAreaController para iniciar una nueva sesión y pasar Activity dos veces. La primera vez se utiliza como Context y la segunda como un objeto de escucha de WindowAreaSessionCallback.

  1. Ahora, compila y ejecuta la app. Si luego despliegas tu teléfono y presionas el botón de la pantalla posterior, aparecerá un mensaje como el siguiente:

Captura de pantalla de la solicitud del usuario en la que se muestra cuando se inicia el modo de pantalla posterior

  1. Haz clic en Cambiar de pantalla ahora para ver el contenido en la pantalla externa.

6. Implementa el modo de mesa

Ahora es el momento de hacer que tu app funcione en dispositivos plegables. Para ello, mueve el contenido a un lado o por encima de la bisagra del dispositivo según la orientación del pliegue. En ese caso, actuarás dentro de FoldingStateActor, de modo que tu código quede desvinculado de Activity para facilitar la legibilidad.

La parte central de esta API consiste en la interfaz de WindowInfoTracker, que se crea con un método estático que requiere Activity:

step1/CameraCodelabDependencies.kt

@Provides
fun provideWindowInfoTracker(activity: Activity) =
        WindowInfoTracker.getOrCreate(activity)

No necesitas escribir este código porque ya existe, pero es útil comprender cómo se crea WindowInfoTracker.

  1. Para detectar cualquier cambio en la ventana, hazlo en el método onResume() de tu Activity:

step1/MainActivity.kt

lifecycleScope.launch {
    foldingStateActor.checkFoldingState(
         this@MainActivity,
         binding.viewFinder
    )
}
  1. Ahora, abre el archivo FoldingStateActor para completar el método checkFoldingState().

Este se ejecuta en la fase RESUMED de tu Activity y aprovecha la WindowInfoTracker para detectar cualquier cambio en el diseño.

step1/FoldingStateActor.kt

windowInfoTracker.windowLayoutInfo(activity)
      .collect { newLayoutInfo ->
         activeWindowLayoutInfo = newLayoutInfo
         updateLayoutByFoldingState(cameraViewfinder)
      }

Si usas la interfaz de WindowInfoTracker, puedes llamar a windowLayoutInfo() para recopilar un Flow de WindowLayoutInfo que contenga toda la información disponible en DisplayFeature.

El último paso es reaccionar a estos cambios y mover el contenido según corresponda. Esto lo haces en el método updateLayoutByFoldingState(), un paso a la vez.

  1. Asegúrate de que activityLayoutInfo contenga algunas propiedades de DisplayFeature y que al menos una de ellas sea una FoldingFeature. De lo contrario, no realices ninguna acción:

step1/FoldingStateActor.kt

val foldingFeature = activeWindowLayoutInfo?.displayFeatures
            ?.firstOrNull { it is FoldingFeature } as FoldingFeature?
            ?: return
  1. Calcula la posición del pliegue para asegurarte de que la posición del dispositivo influye en el diseño y no está fuera de los límites de la jerarquía:

step1/FoldingStateActor.kt

val foldPosition = FoldableUtils.getFeaturePositionInViewRect(
            foldingFeature,
            cameraViewfinder.parent as View
        ) ?: return

Ahora te aseguraste de que cuentas con un FoldingFeature que influye en tu diseño, por lo que tienes que mover el contenido.

  1. Verifica que FoldingFeature esté en HALF_OPEN o, de lo contrario, solo restablecerás la posición del contenido. Si aparece como HALF_OPEN, tendrás que ejecutar otra verificación y realizar otras acciones según la orientación del pliegue:

step1/FoldingStateActor.kt

if (foldingFeature.state == FoldingFeature.State.HALF_OPENED) {
    when (foldingFeature.orientation) {
        FoldingFeature.Orientation.VERTICAL -> {
            cameraViewfinder.moveToRightOf(foldPosition)
        }
        FoldingFeature.Orientation.HORIZONTAL -> {
            cameraViewfinder.moveToTopOf(foldPosition)
        }
    }
} else {
    cameraViewfinder.restore()
}

Si el pliegue es VERTICAL, mueve tu contenido a la derecha. De lo contrario, muévelo a la parte superior de la posición del pliegue.

  1. Compila y ejecuta tu app y, luego, despliega el teléfono y colócalo en el modo de mesa para ver cómo el contenido se mueve según el caso.

7. Felicitaciones

En este codelab, aprendiste lo particular de los dispositivos plegables, los cambios de posición y la API de pantalla posterior.

Lecturas adicionales

Referencia