Cómo optimizar tu app de cámara en dispositivos plegables con Jetpack WindowManager

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 inserción de dependencias Hilt

Qué compilarás

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

c5e52933bcd81859.png

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 coloque 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

Qué necesitarás

  • 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 inicio

  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 que muestra 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.

149e3f9841af7726.png

  1. Ahora, intenta colocar el dispositivo en una posición semiabierta en la que 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. Más información sobre WindowManager de Jetpack

La biblioteca de Jetpack WindowManager 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.2.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.

Cómo agregar 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.2.0'
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.

La API de WindowAreaController te permite usar este modo y proporciona la información y el comportamiento en torno al movimiento de ventanas entre pantallas o áreas de visualización en un dispositivo.

Además, te permite consultar la lista de WindowAreaInfo que están disponibles actualmente para interactuar.

Con WindowAreaInfo, puedes acceder a WindowAreaSession, una interfaz para representar una función del área de ventana activa y el estado de disponibilidad de un WindowAreaCapability. específico.

  1. Declara estas variables en tu MainActivity:

step1/MainActivity.kt

private lateinit var windowAreaController: WindowAreaController
private lateinit var displayExecutor: Executor
private var rearDisplaySession: WindowAreaSession? = null
private var rearDisplayWindowAreaInfo: WindowAreaInfo? = null
private var rearDisplayStatus: WindowAreaCapability.Status =
    WindowAreaCapability.Status.WINDOW_AREA_STATUS_UNSUPPORTED
private val rearDisplayOperation = WindowAreaCapability.Operation.OPERATION_TRANSFER_ACTIVITY_TO_AREA
  1. Inicialízalas en el método onCreate():

step1/MainActivity.kt

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 -> rearDisplayWindowAreaInfo = info }
      .map{it?.getCapability(rearDisplayOperation)?.status?:  WindowAreaCapability.Status.WINDOW_AREA_STATUS_UNSUPPORTED }
      .distinctUntilChanged()
      .collect {
           rearDisplayStatus = it
           updateUI()
      }
  }
}
  1. Ahora implementa la función updateUI() para habilitar o inhabilitar el botón de selfie posterior, según el estado actual:

step1/MainActivity.kt

private fun updateUI() {
    if(rearDisplaySession != null) {
        binding.rearDisplay.isEnabled = true
        // A session is already active, clicking on the button will disable it
    } else {
        when(rearDisplayStatus) {
            WindowAreaCapability.Status.WINDOW_AREA_STATUS_UNSUPPORTED -> {
                binding.rearDisplay.isEnabled = false
                // RearDisplay Mode is not supported on this device"
            }
            WindowAreaCapability.Status.WINDOW_AREA_STATUS_UNAVAILABLE -> {
                binding.rearDisplay.isEnabled = false
                // RearDisplay Mode is not currently available
            }
            WindowAreaCapability.Status.WINDOW_AREA_STATUS_AVAILABLE -> {
                binding.rearDisplay.isEnabled = true
                // You can enable RearDisplay Mode
            }
            WindowAreaCapability.Status.WINDOW_AREA_STATUS_ACTIVE -> {
                binding.rearDisplay.isEnabled = true
                // You can disable RearDisplay Mode
            }
            else -> {
                binding.rearDisplay.isEnabled = false
                // RearDisplay status is unknown
            }
        }
    }
}

Este último paso es opcional, pero es muy útil para aprender todos los estados posibles de una WindowAreaCapability..

  1. Ahora implementa la función toggleRearDisplayMode, que cerrará la sesión si la función ya está activa, o llama a la función transferActivityToWindowArea:

step1/CameraViewModel.kt

private fun toggleRearDisplayMode() {
    if(rearDisplayStatus == WindowAreaCapability.Status.WINDOW_AREA_STATUS_ACTIVE) {
        if(rearDisplaySession == null) {
            rearDisplaySession = rearDisplayWindowAreaInfo?.getActiveSession(rearDisplayOperation)
        }
        rearDisplaySession?.close()
    } else {
        rearDisplayWindowAreaInfo?.token?.let { token ->
            windowAreaController.transferActivityToWindowArea(
                token = token,
                activity = this,
                executor = displayExecutor,
                windowAreaSessionCallback = this
            )
        }
    }
}

Observa el uso de MainActivity como WindowAreaSessionCallback.

La API de Rear Display funciona con un enfoque de objeto de escucha: cuando solicitas mover el contenido a la otra pantalla, inicias una sesión que se muestra a través del método onSessionStarted() del objeto de escucha. En cambio, 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(). Para crear un objeto de escucha de este tipo, debes implementar la interfaz 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 dentro de MainActivity. Esos métodos de devolución de llamada son extremadamente útiles para recibir notificaciones sobre el estado de la sesión y actualizar la app según corresponda.

Pero esta vez, para mayor simplicidad, solo verifica en el cuerpo de la función si hay algún error y registra el estado.

step1/MainActivity.kt

override fun onSessionEnded(t: Throwable?) {
    if(t != null) {
        Log.d("Something was broken: ${t.message}")
    }else{
        Log.d("rear session ended")
    }
}

override fun onSessionStarted(session: WindowAreaSession) {
    Log.d("rear session started [session=$session]")
}
  1. Compila y ejecuta la app. Si luego despliegas el dispositivo y presionas el botón de la pantalla posterior, aparecerá un mensaje como el siguiente:

ba878f120b7c8d58.png

  1. Selecciona Switch screens now 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 una 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 sobre algunas capacidades que son exclusivas de los dispositivos plegables, como el modo de pantalla posterior o el modo de mesa, y cómo desbloquearlas con Jetpack WindowManager.

Estás listo para implementar experiencias del usuario excelentes en tu app de cámara.

Lecturas adicionales

Referencia