Rotações de casos de uso do CameraX

Este tópico mostra como configurar casos de uso do CameraX dentro do seu app para conseguir imagens com as informações de rotação corretas, sejam do caso de uso ImageAnalysis ou ImageCapture. Portanto:

  • O Analyzer do caso de uso ImageAnalysis precisa receber frames com a rotação correta.
  • O caso de uso ImageCapture precisa tirar fotos com a rotação correta.

Terminologia

Neste tópico, usamos a terminologia a seguir, então é importante entender o que cada termo significa:

Orientação da tela
Refere-se a qual lado do dispositivo está na posição para cima e pode ser um dos quatro valores: retrato, paisagem, retrato invertido ou paisagem invertida.
Rotação da tela
É o valor retornado por Display.getRotation() e representa os graus em que o dispositivo é girado no sentido anti-horário a partir da orientação natural.
Rotação desejada
Representa o número de graus que o dispositivo precisa ser girado no sentido horário para chegar à orientação natural.

Como determinar a rotação desejada

Os exemplos a seguir mostram como determinar a rotação desejada para um dispositivo com base na orientação natural dele.

Exemplo 1: orientação natural de retrato

Dispositivo de exemplo: Pixel 3 XL

Orientação natural = retrato
Orientação atual = retrato

Rotação da tela = 0
Rotação desejada = 0

Orientação natural = retrato
Orientação atual = paisagem

Rotação da tela = 90
Rotação desejada = 90

Exemplo 2: orientação natural de paisagem

Dispositivo de exemplo: Pixel C

Orientação natural = paisagem
Orientação atual = paisagem

Rotação da tela = 0
Rotação desejada = 0

Orientação natural = paisagem
Orientação atual = retrato

Rotação da tela = 270
Rotação desejada = 270

Rotação da imagem

Qual lado está para cima? A orientação do sensor é definida no Android como um valor constante, que representa os graus (0, 90, 180, 270) em que o sensor é girado começando pela parte superior do dispositivo quando este está em uma posição natural. Em todos os casos nos diagramas, a rotação da imagem descreve como os dados precisam ser girados no sentido horário para que apareçam na posição vertical.

Os exemplos a seguir mostram qual deve ser a rotação da imagem, dependendo da orientação do sensor da câmera. Eles também pressupõem que a rotação desejada está definida como a rotação da tela.

Exemplo 1: o sensor gira 90 graus

Dispositivo de exemplo: Pixel 3 XL

Rotação da tela = 0
Orientação da tela = retrato
Rotação da imagem = 90

Rotação da tela = 90
Orientação da tela = paisagem
Rotação da imagem = 0

Exemplo 2: o sensor gira 270 graus

Dispositivo de exemplo: Nexus 5X

Rotação da tela = 0
Orientação da tela = retrato
Rotação da imagem = 270

Rotação da tela = 90
Orientação da tela = paisagem
Rotação da imagem = 180

Exemplo 3: o sensor gira 0 grau

Dispositivo de exemplo: Pixel C (Tablet)

Rotação da tela = 0
Orientação da tela = paisagem
Rotação da imagem = 0

Rotação da tela = 270
Orientação da tela = retrato
Rotação da imagem = 90

Como calcular a rotação de uma imagem

ImageAnalysis

O Analyzer de ImageAnalysis recebe imagens da câmera na forma de ImageProxys. Cada imagem contém informações de rotação que podem ser acessadas usando:

val rotation = imageProxy.imageInfo.rotationDegrees

Esse valor representa os graus em que a imagem precisa ser girada em sentido horário para corresponder à rotação desejada de ImageAnalysis. No contexto de um app Android, a rotação desejada de ImageAnalysis normalmente corresponde à orientação da tela.

ImageCapture

Um callback é anexado a uma instância de ImageCapture para sinalizar quando um resultado de captura está pronto. O resultado pode ser a imagem capturada ou um erro.

Quando uma foto é tirada, o callback pode ser de um dos seguintes tipos:

  • OnImageCapturedCallback: recebe uma imagem com acesso na memória na forma de um ImageProxy.
  • OnImageSavedCallback: invocado quando a imagem capturada é armazenada no local especificado por ImageCapture.OutputFileOptions. As opções podem especificar um File, um OutputStream ou um local em MediaStore.

A rotação da imagem capturada, independentemente do formato dela (ImageProxy, File, OutputStream, MediaStore Uri), representa os graus de rotação em que a imagem capturada precisa ser girada no sentido horário para corresponder à rotação desejada de ImageCapture, o que também, no contexto de um app Android, normalmente corresponderia à orientação da tela.

É possível recuperar a rotação da imagem capturada de uma das seguintes maneiras:

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

Verificar a rotação de uma imagem

Os casos de uso ImageAnalysis e ImageCapture recebem ImageProxys da câmera após uma solicitação de captura bem-sucedida. Um ImageProxy encapsula uma imagem e informações sobre ela, incluindo as de rotação. Essas informações de rotação representam os graus em que a imagem precisa ser girada para corresponder à rotação desejada do caso de uso.

Fluxo de verificação da rotação de uma imagem

Diretrizes de rotação desejada de ImageCapture/ImageAnalysis

Como muitos dispositivos não giram até as orientações de retrato invertido ou paisagem invertida por padrão, alguns apps Android não têm suporte a elas. Um app oferecer ou não essa compatibilidade muda a maneira como a rotação desejada dos casos de uso pode ser atualizada.

Veja abaixo duas tabelas que definem como manter a rotação desejada dos casos de uso sincronizada com a rotação da tela. A primeira mostra como fazer isso e, ao mesmo tempo, ser compatível com todas as quatro orientações. A segunda lida apenas com as orientações em que o dispositivo é girado por padrão.

Para escolher as diretrizes a serem seguidas no app:

  1. Verifique se a Activity da câmera do seu app tem uma orientação bloqueada, uma orientação desbloqueada ou se ela substitui as mudanças das configurações de orientação.

  2. Decida se a Activity da câmera do seu app precisa processar todas as quatro orientações do dispositivo (retrato, retrato invertido, paisagem e paisagem invertida) ou se ela só vai processar as orientações às quais o dispositivo tem suporte por padrão.

Compatibilidade com as quatro orientações

Esta tabela menciona determinadas diretrizes que precisam ser seguidas nos casos em que o dispositivo não gira para o modo retrato invertido. O mesmo pode ser aplicado a dispositivos que não giram para o modo paisagem invertida.

Cenário Diretrizes Modo de janela única Modo de tela dividida em várias janelas
Orientação desbloqueada Configure os casos de uso sempre que a Activity for criada, como no callback onCreate() da Activity.
Use onOrientationChanged() de OrientationEventListener. Dentro do callback, atualize a rotação desejada dos casos de uso. Isso processa casos em que o sistema não recria a Activity mesmo depois de uma mudança de orientação, como quando o dispositivo é girado em 180 graus. Também processa casos em que a tela está em uma orientação de retrato invertido e o dispositivo não gira para o modo retrato invertido por padrão. Também processa casos em que a Activity não é recriada quando o dispositivo é girado (por exemplo, em 90 graus). Isso acontece em dispositivos com formato pequeno, quando o app ocupa metade da tela, e em dispositivos maiores, quando o app ocupa dois terços da tela.
Opcional: defina a propriedade screenOrientation do Activity como fullSensor no arquivo AndroidManifest. Isso permite que a IU fique na posição vertical quando o dispositivo está no modo retrato inverso e que Activity seja recriado pelo sistema sempre que o dispositivo é girado em 90 graus. Não afeta dispositivos que não giram para o modo retrato invertido por padrão. O modo de várias janelas não está disponível enquanto a tela está em uma orientação de retrato invertida.
Orientação bloqueada Configure os casos de uso apenas uma vez, quando a Activity for criada pela primeira vez, como no callback onCreate() da Activity.
Use onOrientationChanged() de OrientationEventListener. Dentro do callback, atualize a rotação que quiser dos casos de uso, exceto a visualização. Também processa casos em que a Activity não é recriada quando o dispositivo é girado (por exemplo, em 90 graus). Isso acontece em dispositivos com formato pequeno, quando o app ocupa metade da tela, e em dispositivos maiores, quando o app ocupa dois terços da tela.
Substituição de configChanges da orientação Configure os casos de uso apenas uma vez, quando a Activity for criada pela primeira vez, como no callback onCreate() da Activity.
Use onOrientationChanged() de OrientationEventListener. Dentro do callback, atualize a rotação desejada dos casos de uso. Também processa casos em que a Activity não é recriada quando o dispositivo é girado (por exemplo, em 90 graus). Isso acontece em dispositivos com formato pequeno, quando o app ocupa metade da tela, e em dispositivos maiores, quando o app ocupa dois terços da tela.
Opcional: defina a propriedade screenOrientation da atividade como fullSensor no arquivo AndroidManifest. Permite que a interface fique na posição vertical quando o dispositivo está no modo retrato invertido. Não afeta dispositivos que não giram para o modo retrato invertido por padrão. O modo de várias janelas não é compatível enquanto a tela está em uma orientação de retrato invertido.

Compatibilidade apenas com orientações compatíveis com o dispositivo

Suporte apenas às orientações disponíveis no dispositivo por padrão, que podem ou não incluir a orientação de retrato invertido/paisagem invertida.

Cenário Diretrizes Modo de tela dividida em várias janelas
Orientação desbloqueada Configure os casos de uso sempre que a Activity for criada, como no callback onCreate() da Activity.
Use onDisplayChanged() de DisplayListener. Dentro do callback, atualize a rotação desejada dos casos de uso, como quando o dispositivo é girado em 180 graus. Também processa casos em que a Activity não é recriada quando o dispositivo é girado (por exemplo, em 90 graus). Isso acontece em dispositivos com formato pequeno, quando o app ocupa metade da tela, e em dispositivos maiores, quando o app ocupa dois terços da tela.
Orientação bloqueada Configure os casos de uso apenas uma vez, quando a Activity for criada pela primeira vez, como no callback onCreate() da Activity.
Use onOrientationChanged() de OrientationEventListener. Dentro do callback, atualize a rotação desejada dos casos de uso. Também processa casos em que a Activity não é recriada quando o dispositivo é girado (por exemplo, em 90 graus). Isso acontece em dispositivos com formato pequeno, quando o app ocupa metade da tela, e em dispositivos maiores, quando o app ocupa dois terços da tela.
Substituição de configChanges da orientação Configure os casos de uso apenas uma vez, quando a Activity for criada pela primeira vez, como no callback onCreate() da Activity.
Use onDisplayChanged() de DisplayListener. Dentro do callback, atualize a rotação desejada dos casos de uso, como quando o dispositivo é girado em 180 graus. Também processa casos em que a Activity não é recriada quando o dispositivo é girado (por exemplo, em 90 graus). Isso acontece em dispositivos com formato pequeno, quando o app ocupa metade da tela, e em dispositivos maiores, quando o app ocupa dois terços da tela.

Orientação desbloqueada

Uma Activity tem orientação desbloqueada quando a orientação da tela (como retrato ou paisagem) é igual à orientação física do dispositivo, com exceção de retrato invertido/paisagem invertida, para as quais nem todo dispositivo oferece suporte por padrão. Para forçar o dispositivo a girar para as quatro orientações, defina a propriedade screenOrientation da Activity como fullSensor.

No modo de várias janelas, um dispositivo que não tem suporte às orientações de retrato invertido/paisagem invertida por padrão não vai ser girado para essas orientações, nem mesmo quando a propriedade screenOrientation estiver definida como 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" />

Orientação bloqueada

Uma tela tem orientação bloqueada quando permanece na mesma orientação (como retrato ou paisagem), independentemente da orientação física do dispositivo. Isso pode ser feito especificando a propriedade screenOrientation da Activity na declaração no arquivo AndroidManifest.xml.

Quando a tela tem uma orientação bloqueada, o sistema não destrói nem recria a Activity conforme o dispositivo é girado.

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

Substituição das mudanças de configuração de orientação

Quando uma Activity substitui as mudanças de configuração de orientação, o sistema não a destrói nem a recria quando a orientação física do dispositivo muda. No entanto, o sistema atualiza a IU para corresponder à orientação física do 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" />

Configuração dos casos de uso da câmera

Nos cenários descritos acima, é possível configurar os casos de uso da câmera quando a Activity é criada pela primeira vez.

No caso de uma Activity com orientação desbloqueada, essa configuração é feita sempre que o dispositivo é girado, conforme o sistema destrói e recria a Activity nas mudanças de orientação. Isso faz com que os casos de uso definam a rotação desejada para sempre corresponder à orientação da tela por padrão.

No caso de uma Activity com orientação bloqueada ou que substitua as mudanças de configuração de orientação, essa configuração é feita uma vez, quando a Activity é criada.

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)
   }
}

Configuração do OrientationEventListener

O uso de um OrientationEventListener permite atualizar de maneira contínua a rotação desejada dos casos de uso da câmera conforme as mudanças de orientação do 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()
    }
}

Configuração do DisplayListener

O uso de um DisplayListener permite atualizar a rotação desejada dos casos de uso da câmera em determinadas situações, por exemplo, quando o sistema não destrói nem recria a Activity depois que o dispositivo é girado em 180 graus.

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)
    }
}