Rotaciones de casos de uso de CameraX

En este tema, se muestra el proceso de configuración de casos de uso de CameraX dentro de tu app para obtener imágenes con la información de rotación correcta, ya sea desde el caso de uso del elemento ImageAnalysis o ImageCapture. Entonces:

  • El elemento Analyzer del caso de uso del objeto ImageAnalysis debe recibir marcos con la rotación correcta.
  • El caso de uso del objeto ImageCapture debe tomar fotos con la rotación correcta.

Terminología

En este tema, se utiliza la siguiente terminología, por lo que es importante comprender el significado de cada término:

Orientación de la pantalla
Se refiere a qué lado del dispositivo está en la posición hacia arriba. Puede ser uno de cuatro valores: vertical, horizontal, horizontal inverso o vertical inverso.
Rotación de la pantalla
Es el valor que muestra el objeto Display.getRotation() y representa los grados a los que se rota el dispositivo en sentido contrario a las manecillas del reloj desde la orientación natural.
Rotación objetivo
Representa la cantidad de grados a los que se debe rotar el dispositivo en el sentido de las manecillas del reloj para alcanzar su orientación natural.

Cómo determinar la rotación objetivo

En los siguientes ejemplos, se muestra cómo determinar la rotación objetivo de un dispositivo en función de su orientación natural.

Ejemplo 1: orientación natural vertical

Ejemplo de dispositivo: Pixel 3 XL

Orientación natural = vertical
Orientación actual = vertical

Rotación de la pantalla = 0
Rotación objetivo = 0

Orientación natural = vertical
Orientación actual = horizontal

Rotación de la pantalla = 90
Rotación objetivo = 90

Ejemplo 2: orientación natural horizontal

Ejemplo de dispositivo: Pixel C

Orientación natural = horizontal
Orientación actual = horizontal

Rotación de la pantalla = 0
Rotación objetivo = 0

Orientación natural = horizontal
Orientación actual = vertical

Rotación de la pantalla = 270
Rotación objetivo = 270

Rotación de la imagen

¿Qué extremo está orientado hacia arriba? En Android, la orientación del sensor se define como un valor constante, que representa los grados (0, 90, 180, 270) a los que se rota el sensor desde la parte superior del dispositivo cuando se encuentra en una posición natural. En todos los casos de los diagramas, la rotación de la imagen describe cómo se deben rotar los datos en el sentido de las manecillas del reloj para que se muestren en posición vertical.

En los siguientes ejemplos, se muestra cómo debería rotar la imagen según la orientación del sensor de la cámara. En estos ejemplos, también se presupone que la rotación objetivo se establece en la rotación de la pantalla.

Ejemplo 1: rotación del sensor a 90 grados

Ejemplo de dispositivo: Pixel 3 XL

Rotación de la pantalla = 0
Orientación de la pantalla = vertical
Rotación de la imagen = 90

Rotación de la pantalla = 90
Orientación de la pantalla = horizontal
Rotación de la imagen = 0

Ejemplo 2: rotación del sensor a 270 grados

Ejemplo de dispositivo: Nexus 5X

Rotación de la pantalla = 0
Orientación de la pantalla = vertical
Rotación de la imagen = 270

Rotación de la pantalla = 90
Orientación de la pantalla = horizontal
Rotación de la imagen = 180

Ejemplo 3: rotación del sensor a 0 grados

Ejemplo de dispositivo: Pixel C (tablet)

Rotación de la pantalla = 0
Orientación de la pantalla = horizontal
Rotación de la imagen = 0

Rotación de la pantalla = 270
Orientación de la pantalla = vertical
Rotación de la imagen = 90

Cómo calcular la rotación de una imagen

ImageAnalysis

El elemento Analyzer del objeto ImageAnalysis recibe imágenes de la cámara en forma de ImageProxy. Cada imagen contiene información de rotación, a la que se puede acceder con el siguiente código:

val rotation = imageProxy.imageInfo.rotationDegrees

Este valor representa los grados a los que se debe rotar la imagen en el sentido de las manecillas del reloj para que coincida con la rotación objetivo del elemento ImageAnalysis. En el contexto de una app para Android, en general, la rotación objetivo del elemento ImageAnalysis coincidiría con la orientación de la pantalla.

ImageCapture

Se adjunta una devolución de llamada a una instancia ImageCapture para indicar cuándo está listo un resultado de captura. El resultado puede ser la imagen capturada o un error.

Cuando tomas una foto, la devolución de llamada proporcionada puede ser de uno de los siguientes tipos:

  • OnImageCapturedCallback: Recibe una imagen con acceso en la memoria, en forma de un objeto ImageProxy.
  • OnImageSavedCallback: Se invoca cuando la imagen capturada se almacena correctamente en la ubicación que especificó ImageCapture.OutputFileOptions. Las opciones pueden especificar un File, un OutputStream o una ubicación en MediaStore específicos.

La rotación de la imagen capturada, sin importar su formato (ImageProxy, File, OutputStream, MediaStore Uri) representa los grados de rotación a los que se debe rotar la imagen capturada en sentido horario para que coincida con la rotación objetivo del elemento ImageCapture, que, nuevamente, en el contexto de una app para Android, coincidiría generalmente con la orientación de la pantalla.

Se puede recuperar la rotación de la imagen capturada de una de las siguientes maneras:

ImageProxy

val rotation = imageProxy.imageInfo.rotationDegrees

File

val exif = Exif.createFromFile(file)
val rotation = exif.rotation

OutputStream

val byteArray = outputStream.toByteArray()
val exif = Exif.createFromInputStream(ByteArrayInputStream(byteArray))
val rotation = exif.rotation

MediaStore uri

val inputStream = contentResolver.openInputStream(outputFileResults.savedUri)
val exif = Exif.createFromInputStream(inputStream)
val rotation = exif.rotation

Cómo verificar la rotación de una imagen

Los casos de uso de los objetos ImageAnalysis y ImageCapture reciben el elementos ImageProxy de la cámara después solicitar una captura correctamente. Un elemento ImageProxy une una imagen y la información sobre esta, lo que incluye su rotación. Esta información de rotación representa los grados a los que se debe rotar la imagen para que coincida con la rotación objetivo del caso de uso.

Flujo de verificación de rotación de una imagen

Lineamientos de la rotación objetivo de ImageCapture y de ImageAnalysis

Como muchos dispositivos no rotan al modo vertical inverso ni horizontal inverso de forma predeterminada, algunas apps para Android no admiten estas orientaciones. La forma en que se puede actualizar la rotación objetivo de los casos de uso cambia en función de si una app admite o no estas orientaciones.

A continuación, se muestran dos tablas en las que se define cómo mantener la rotación objetivo de los casos de uso en sincronización con la rotación de la pantalla. En el primer ejemplo, se muestra cómo hacerlo mientras se admiten las cuatro orientaciones; en el segundo, solo se describen las orientaciones a las que rota el dispositivo de forma predeterminada.

Para elegir los lineamientos que se deben cumplir en la app, haz lo siguiente:

  1. Verifica si el objeto Activity de la cámara de tu app tiene una orientación bloqueada, una orientación desbloqueada o si anula los cambios en la configuración de orientación.

  2. Decide si el objeto Activity de la cámara de tu app debe controlar las cuatro orientaciones del dispositivo (vertical, vertical inverso, horizontal y horizontal inverso) o si solo deben controlar las orientaciones que admite el dispositivo de forma predeterminada.

Cómo admitir las cuatro orientaciones

En esta tabla, se incluyen ciertos lineamientos que se deben cumplir en los casos en los que el dispositivo no rota al modo vertical inverso. Estos mismos lineamientos se pueden aplicar en dispositivos que no rotan al modo horizontal inverso.

Situación Lineamientos Modo de ventana única Modo de pantalla dividida en multiventana
Orientación desbloqueada Configura los casos de uso cada vez que se crea el objeto Activity, como en la devolución de llamada del elemento onCreate() del parámetro Activity.
Usa el objeto onOrientationChanged() del elemento OrientationEventListener. Dentro de la devolución de llamada, actualiza la rotación objetivo de los casos de uso. De esta manera, se controlan los casos en los que el sistema no vuelve a crear el objeto Activity incluso después cambiar la orientación, como cuando se rota el dispositivo a 180 grados. También controla cuando la pantalla está en orientación vertical inversa y el dispositivo no rota al modo vertical inverso de forma predeterminada. También controla los casos en los que no se vuelve a crear el objeto Activity cuando se rota el dispositivo (por ejemplo, a 90 grados), lo que sucede en dispositivos pequeños de factor de forma cuando la app ocupa la mitad de la pantalla y en dispositivos más grandes cuando ocupa dos tercios.
Opcional: Configura la propiedad screenOrientation del objeto Activity en el elemento fullSensor del archivo AndroidManifest. De esta manera, se permite que la IU se mantenga en posición vertical cuando el dispositivo se encuentra en modo vertical inverso y que el sistema vuelva a crear el objeto Activity cuando se rota el dispositivo a 90 grados. No tiene ningún efecto en dispositivos que no rotan al modo vertical inverso de forma predeterminada. No se admite el modo multiventana mientras la pantalla se encuentra en orientación vertical inversa.
Orientación bloqueada Configura los casos de uso solo una vez, cuando se crea el objeto Activity por primera vez, como en la devolución de llamada del elemento onCreate() del parámetro Activity.
Usa el objeto onOrientationChanged() del elemento OrientationEventListener. Dentro de la devolución de llamada, actualiza la rotación objetivo de los casos de uso, excepto Preview. También controla los casos en los que no se vuelve a crear el objeto Activity cuando se rota el dispositivo (por ejemplo, a 90 grados), lo que sucede en dispositivos pequeños de factor de forma cuando la app ocupa la mitad de la pantalla y en dispositivos más grandes cuando ocupa dos tercios.
Anulación de los cambios en la configuración de orientación Configura los casos de uso solo una vez, cuando se crea el objeto Activity por primera vez, como en la devolución de llamada del elemento onCreate() del parámetro Activity.
Usa el objeto onOrientationChanged() del elemento OrientationEventListener. Dentro de la devolución de llamada, actualiza la rotación objetivo de los casos de uso. También controla los casos en los que no se vuelve a crear el objeto Activity cuando se rota el dispositivo (por ejemplo, a 90 grados), lo que sucede en dispositivos pequeños de factor de forma cuando la app ocupa la mitad de la pantalla y en dispositivos más grandes cuando ocupa dos tercios.
Opcional: Configura la propiedad screenOrientation del objeto Activity en el elemento fullSensor del archivo AndroidManifest. Permite que la IU se mantenga en posición vertical cuando el dispositivo se encuentra en modo vertical inverso. No tiene ningún efecto en dispositivos que no rotan al modo vertical inverso de forma predeterminada. No se admite el modo multiventana mientras la pantalla se encuentra en orientación vertical inversa.

Cómo admitir solo orientaciones compatibles con el dispositivo

Admite solo las orientaciones compatibles con el dispositivo de forma predeterminada (que puede incluir el modo vertical y horizontal inverso).

Situación Lineamientos Modo de pantalla dividida en multiventana
Orientación desbloqueada Configura los casos de uso cada vez que se crea el objeto Activity, como en la devolución de llamada del elemento onCreate() del parámetro Activity.
Usa el objeto onDisplayChanged() del elemento DisplayListener. Dentro de la devolución de llamada, actualiza la rotación objetivo de los casos de uso, como cuando se rota el dispositivo a 180 grados. También controla los casos en los que no se vuelve a crear el objeto Activity cuando se rota el dispositivo (por ejemplo, a 90 grados), lo que sucede en dispositivos pequeños de factor de forma cuando la app ocupa la mitad de la pantalla y en dispositivos más grandes cuando ocupa dos tercios.
Orientación bloqueada Configura los casos de uso solo una vez, cuando se crea el objeto Activity por primera vez, como en la devolución de llamada del elemento onCreate() del parámetro Activity.
Usa el objeto onOrientationChanged() del elemento OrientationEventListener. Dentro de la devolución de llamada, actualiza la rotación objetivo de los casos de uso. También controla los casos en los que no se vuelve a crear el objeto Activity cuando se rota el dispositivo (por ejemplo, a 90 grados), lo que sucede en dispositivos pequeños de factor de forma cuando la app ocupa la mitad de la pantalla y en dispositivos más grandes cuando ocupa dos tercios.
Anulación de los cambios en la configuración de orientación Configura los casos de uso solo una vez, cuando se crea el objeto Activity por primera vez, como en la devolución de llamada del elemento onCreate() del parámetro Activity.
Usa el objeto onDisplayChanged() del elemento DisplayListener. Dentro de la devolución de llamada, actualiza la rotación objetivo de los casos de uso, como cuando se rota el dispositivo a 180 grados. También controla los casos en los que no se vuelve a crear el objeto Activity cuando se rota el dispositivo (por ejemplo, a 90 grados), lo que sucede en dispositivos pequeños de factor de forma cuando la app ocupa la mitad de la pantalla y en dispositivos más grandes cuando ocupa dos tercios.

Orientación desbloqueada

Un objeto Activity tiene una orientación desbloqueada cuando su orientación de la pantalla (por ejemplo, horizontal o vertical) coincide con la orientación física del dispositivo, a excepción de la orientación horizontal o vertical inversa, que algunos dispositivos no admiten de forma predeterminada. Para forzar que el dispositivo rote a las cuatro orientaciones, establece la propiedad screenOrientation del objeto Activity en el elemento fullSensor.

En el modo multiventana, un dispositivo que no admite la orientación horizontal o vertical inversa de forma predeterminada no rotará al modo horizontal o vertical, incluso cuando se configure su propiedad screenOrientation en el elemento fullSensor.

<!-- The Activity has an unlocked orientation, but might not rotate to reverse
portrait/landscape in single-window mode if the device doesn't support it by
default. -->
<activity android:name=".UnlockedOrientationActivity" />

<!-- The Activity has an unlocked orientation, and will rotate to all four
orientations in single-window mode. -->
<activity
   android:name=".UnlockedOrientationActivity"
   android:screenOrientation="fullSensor" />

Orientación bloqueada

Una pantalla tiene una orientación bloqueada cuando se mantiene en la misma orientación de la pantalla (por ejemplo, horizontal o vertical), sin importar la orientación física del dispositivo. Para ello, especifica la propiedad screenOrientation del objeto Activity dentro de su declaración en el archivo AndroidManifest.xml.

Cuando la pantalla tiene una orientación bloqueada, el sistema no destruye ni vuelve a crear el objeto Activity cuando se rota el dispositivo.

<!-- The Activity keeps a portrait orientation even as the device rotates. -->
<activity
   android:name=".LockedOrientationActivity"
   android:screenOrientation="portrait" />

Anulación de los cambios en la configuración de orientación

Cuando un objeto Activity anula los cambios en la configuración de orientación, el sistema no lo destruye ni vuelve a crearlo cuando cambia la orientación física del dispositivo. Sin embargo, el sistema actualiza la IU para que coincida con la orientación física del dispositivo.

<!-- The Activity's UI might not rotate in reverse portrait/landscape if the
device doesn't support it by default. -->
<activity
   android:name=".OrientationConfigChangesOverriddenActivity"
   android:configChanges="orientation|screenSize" />

<!-- The Activity's UI will rotate to all 4 orientations in single-window
mode. -->
<activity
   android:name=".OrientationConfigChangesOverriddenActivity"
   android:configChanges="orientation|screenSize"
   android:screenOrientation="fullSensor" />

Configuración de los casos de uso de la cámara

En las situaciones que se describen anteriormente, se pueden configurar los casos de uso de la cámara cuando se crea el objeto Activity por primera vez.

En el caso de un objeto Activity con una orientación desbloqueada, se realiza esta configuración cada vez que se rota el dispositivo, ya que el sistema destruye y vuelve a crear el objeto Activity con los cambios de orientación. Como resultado, los casos de uso configuran su rotación objetivo para que coincida con la orientación de la pantalla de forma predeterminada cada vez que se rote el dispositivo.

En el caso de un objeto Activity con una orientación bloqueada o una que anule los cambios en la configuración de orientación, se realiza esta configuración una vez, cuando se crea el objeto Activity por primera vez.

class CameraActivity : AppCompatActivity() {
   override fun onCreate(savedInstanceState: Bundle?) {
       super.onCreate(savedInstanceState)

       val cameraProcessFuture = ProcessCameraProvider.getInstance(this)
       cameraProcessFuture.addListener(Runnable {
          val cameraProvider = cameraProcessFuture.get()

          // By default, the use cases set their target rotation to match the
          // display’s rotation.
          val preview = buildPreview()
          val imageAnalysis = buildImageAnalysis()
          val imageCapture = buildImageCapture()

          cameraProvider.bindToLifecycle(
              this, cameraSelector, preview, imageAnalysis, imageCapture)
       }, mainExecutor)
   }
}

Configuración de OrientationEventListener

Usar un objeto OrientationEventListener te permite actualizar, de manera continua, la rotación objetivo de los casos de uso de la cámara a medida que cambia la orientación del dispositivo.

class CameraActivity : AppCompatActivity() {

    private val orientationEventListener by lazy {
        object : OrientationEventListener(this) {
            override fun onOrientationChanged(orientation: Int) {
                if (orientation == ORIENTATION_UNKNOWN) {
                    return
                }

                val rotation = when (orientation) {
                     in 45 until 135 -> Surface.ROTATION_270
                     in 135 until 225 -> Surface.ROTATION_180
                     in 225 until 315 -> Surface.ROTATION_90
                     else -> Surface.ROTATION_0
                 }

                 imageAnalysis.targetRotation = rotation
                 imageCapture.targetRotation = rotation
            }
        }
    }

    override fun onStart() {
        super.onStart()
        orientationEventListener.enable()
    }

    override fun onStop() {
        super.onStop()
        orientationEventListener.disable()
    }
}

Configuración de DisplayListener

Usar un objeto DisplayListener te permite actualizar la rotación objetivo de los casos de uso de la cámara en situaciones particulares, por ejemplo, cuando el sistema no destruye ni vuelve a crear el objeto Activity después de que se rota el dispositivo a 180 grados.

class CameraActivity : AppCompatActivity() {

    private val displayListener = object : DisplayManager.DisplayListener {
        override fun onDisplayChanged(displayId: Int) {
            if (rootView.display.displayId == displayId) {
                val rotation = rootView.display.rotation
                imageAnalysis.targetRotation = rotation
                imageCapture.targetRotation = rotation
            }
        }

        override fun onDisplayAdded(displayId: Int) {
        }

        override fun onDisplayRemoved(displayId: Int) {
        }
    }

    override fun onStart() {
        super.onStart()
        val displayManager = getSystemService(Context.DISPLAY_SERVICE) as DisplayManager
        displayManager.registerDisplayListener(displayListener, null)
    }

    override fun onStop() {
        super.onStop()
        val displayManager = getSystemService(Context.DISPLAY_SERVICE) as DisplayManager
        displayManager.unregisterDisplayListener(displayListener)
    }
}