Orientações da câmera

Se o app Android usa câmeras, há algumas considerações especiais ao processar orientações. Este documento pressupõe que você entenda os conceitos básicos da API camera2 do Android. Leia nossa postagem do blog ou resumo para uma visão geral do camera2. Também recomendamos que você tente escrever um app de câmera antes de ler este documento.

Contexto

O processamento de orientações em apps de câmera Android é complicado e precisa considerar os seguintes fatores:

  • Orientação natural: a orientação da tela quando o dispositivo está na posição "normal" para o design dele. Geralmente, é a orientação retrato para smartphones e paisagem para laptops.
  • Orientação do sensor: a orientação do sensor montado fisicamente no dispositivo.
  • Rotação da tela: quanto o dispositivo é girado fisicamente em relação à orientação natural.
  • Tamanho do visor: o tamanho do visor usado para mostrar a prévia da câmera.
  • Tamanho da imagem gerada pela câmera.

Esses fatores combinados introduzem um grande número de possíveis configurações de UI e visualização para apps de câmera. Este documento demonstra como os desenvolvedores podem navegar por essas orientações e processar corretamente as orientações da câmera em apps Android.

Para simplificar, vamos considerar que todos os exemplos envolvem uma câmera traseira, a menos que seja mencionado o contrário. Além disso, todas as fotos a seguir são simuladas para tornar as ilustrações visualmente mais claras.

Tudo sobre orientações

Orientação natural

A orientação natural é definida como a orientação da tela quando o dispositivo está na posição em que normalmente é esperado que ele esteja. Para smartphones, a orientação natural costuma ser retrato. Em outras palavras, os smartphones têm larguras menores e alturas maiores. Para laptops, a orientação natural é paisagem, ou seja, eles têm larguras maiores e alturas menores. Os tablets são um pouco mais complicados do que isso: eles podem ser usados na vertical ou na horizontal.

Ilustração de orientação natural com um smartphone, um laptop e um objeto do lado do observador

Orientação do sensor

Formalmente falando, a orientação do sensor é medida pelos graus que uma imagem de saída do sensor precisa ser girada no sentido horário para corresponder à orientação natural do dispositivo. Em outras palavras, a orientação do sensor é o número de graus que um sensor é girado no sentido anti-horário antes de ser montado no dispositivo. Ao olhar para a tela, a rotação parece estar no sentido horário, porque o sensor da câmera traseira está instalado na parte de trás do dispositivo.

De acordo com a Definição de compatibilidade do Android 10 7.5.5 Orientação da câmera, as câmeras frontais e traseiras "PRECISAM estar orientadas de forma que a dimensão longa da câmera se alinhe à dimensão longa da tela".

Os buffers de saída das câmeras têm tamanho paisagem. Como a orientação natural dos smartphones geralmente é retrato, a orientação do sensor normalmente é de 90 ou 270 graus em relação à orientação natural para que o lado longo do buffer de saída corresponda ao lado longo da tela. A orientação do sensor é diferente para dispositivos cuja orientação natural é paisagem, como os Chromebooks. Nesses dispositivos, os sensores de imagem são colocados novamente para que o lado longo do buffer de saída corresponda ao lado longo da tela. Como ambos têm tamanho paisagem, as orientações correspondem e a orientação do sensor é de 0 ou 180 graus.

Ilustração de orientação natural com um smartphone, um laptop e um objeto do lado do observador

As ilustrações a seguir mostram como as coisas aparecem do ponto de vista de um observador olhando para a tela do dispositivo:

Ilustração da orientação do sensor com um smartphone, um laptop e um objeto do lado do observador

Considere a seguinte cena:

Uma cena com uma figura fofa do Android (bugdroid)

Telefone Laptop
Ilustração de imagem ao olhar pelo sensor da câmera traseira de um smartphone Ilustração de imagem ao olhar pelo sensor da câmera traseira em um laptop

Como a orientação do sensor geralmente é de 90 ou 270 graus em smartphones, sem considerar essa orientação, as imagens ficariam assim:

Telefone Laptop
Ilustração de imagem ao olhar pelo sensor da câmera traseira de um smartphone Ilustração de imagem ao olhar pelo sensor da câmera traseira em um laptop

Suponha que a orientação do sensor no sentido anti-horário esteja armazenada na variável sensorOrientation. Para compensar a orientação do sensor, gire os buffers de saída em `sensorOrientation` no sentido horário para alinhar a orientação com a orientação natural do dispositivo.

No Android, os apps podem usar TextureView ou SurfaceView para mostrar a visualização da câmera. Ambos podem processar a orientação do sensor se os apps os usarem corretamente. Vamos detalhar como considerar a orientação do sensor nas seções a seguir.

Rotação da tela

A rotação da tela é definida formalmente pela rotação dos gráficos desenhados na tela, que é a direção oposta à rotação física do dispositivo em relação à orientação natural. As seções a seguir pressupõem que todas as rotações de tela são múltiplos de 90. Se você extrair a rotação da tela pelos graus absolutos, arredonde para o mais próximo de {0, 90, 180, 270}.

"Orientação da tela" nas seções a seguir se refere a se um dispositivo é mantido fisicamente na posição paisagem ou retrato e é diferente de "rotação da tela".

Suponha que você gire os dispositivos 90 graus no sentido anti-horário em relação às posições anteriores, conforme demonstrado na figura a seguir:

Ilustração de rotação de tela em 90 graus com um smartphone, um laptop e um objeto do lado do observador

Supondo que os buffers de saída já estejam girados com base na orientação do sensor, você teria os seguintes buffers de saída:

Telefone Laptop
Ilustração de imagem ao olhar pelo sensor da câmera traseira de um smartphone Ilustração de imagem ao olhar pelo sensor da câmera traseira em um laptop

Se a rotação da tela estiver armazenada na variável displayRotation, para conseguir a imagem correta, gire os buffers de saída no sentido anti-horário pelo displayRotation.

Para câmeras frontais, a rotação da tela atua nos buffers de imagem na direção oposta à tela. Se você estiver usando uma câmera frontal, gire os buffers no sentido horário de acordo com displayRotation.

Avisos

A rotação da tela mede a rotação no sentido anti-horário do dispositivo. Isso não é válido para todas as APIs de orientação/rotação.

Por exemplo,

O importante aqui é que a rotação da tela é relativa à orientação natural. Por exemplo, se você girar um smartphone em 90 ou 270 graus, a tela vai ficar no formato paisagem. Em comparação, você teria uma tela em formato retrato se girasse um laptop na mesma quantidade. Os apps precisam sempre ter isso em mente e nunca presumir a orientação natural de um dispositivo.

Exemplos

Vamos usar as figuras anteriores para ilustrar o que são as orientações e rotações.

Ilustração de orientação combinada com um smartphone e um laptop não girados, além de um objeto

Telefone Laptop
Orientação natural = retrato Orientação natural = paisagem
Orientação do sensor = 90 Orientação do sensor = 0
Rotação da tela = 0 Rotação da tela = 0
Orientação da tela = retrato Orientação da tela = paisagem

Ilustração de orientação combinada com um smartphone e um laptop não girados, além de um objeto

Telefone Laptop
Orientação natural = retrato Orientação natural = paisagem
Orientação do sensor = 90 Orientação do sensor = 0
Rotação da tela = 90 Rotação da tela = 90
Orientação da tela = paisagem Orientação da tela = retrato

Tamanho do visor

Os apps precisam sempre redimensionar o visor com base na orientação, na rotação e na resolução da tela. Em geral, os apps precisam fazer com que a orientação do visor seja idêntica à orientação da tela atual. Em outras palavras, os apps precisam alinhar a borda longa do visor com a borda longa da tela.

Tamanho da saída de imagem por câmera

Ao escolher o tamanho da saída de imagem para a prévia, selecione um tamanho igual ou um pouco maior que o do visor, sempre que possível. Em geral, não é recomendável aumentar os buffers de saída, o que causaria pixelização. Também não é recomendável escolher um tamanho muito grande, o que pode reduzir o desempenho e usar mais bateria.

Orientação JPEG

Vamos começar com uma situação comum: capturar uma foto JPEG. Na API camera2, é possível transmitir JPEG_ORIENTATION na solicitação de captura para especificar o quanto você quer que os JPEGs de saída sejam girados no sentido horário.

Um resumo rápido do que mencionamos:

  • Para processar a orientação do sensor, gire o buffer de imagem em sensorOrientation no sentido horário.
  • Para processar a rotação da tela, gire um buffer em displayRotation no sentido anti-horário para câmeras traseiras e no sentido horário para câmeras frontais.

Somando os dois fatores, o valor que você quer girar no sentido horário é

  • sensorOrientation - displayRotation para câmeras traseiras.
  • sensorOrientation + displayRotation para câmeras frontais.

Confira o exemplo de código dessa lógica na documentação de JPEG_ORIENTATION. Observe que deviceOrientation no exemplo de código da documentação usa a rotação no sentido horário do dispositivo. Por isso, os sinais para rotação da tela são invertidos.

Visualização

E a prévia da câmera? Há duas maneiras principais de um app mostrar uma visualização da câmera: SurfaceView e TextureView. Cada um exige abordagens diferentes para processar a orientação corretamente.

SurfaceView

Em geral, o SurfaceView é recomendado para visualizações da câmera, desde que você não precise processar ou animar os buffers de visualização. Ele tem mais desempenho e exige menos recursos do que o TextureView.

O SurfaceView também é relativamente mais fácil de criar. Você só precisa se preocupar com a proporção da SurfaceView em que a visualização da câmera está sendo mostrada.

Origem

A plataforma Android gira os buffers de saída abaixo da SurfaceView para corresponder à orientação da tela do dispositivo. Em outras palavras, ele considera tanto a orientação do sensor quanto a rotação da tela. Em outras palavras, quando a tela está na horizontal, a prévia também fica na horizontal, e o mesmo acontece com a vertical.

Isso é ilustrado na tabela a seguir. É importante lembrar que a rotação da tela sozinha não determina a orientação da fonte.

Rotação da tela Smartphone (orientação natural = retrato) Laptop (orientação natural = paisagem)
0 Uma imagem em formato de retrato com a cabeça do bugdroid apontada para cima Uma imagem em formato paisagem com a cabeça do bugdroid apontada para cima
90 Uma imagem em formato paisagem com a cabeça do bugdroid apontada para cima Uma imagem em formato de retrato com a cabeça do bugdroid apontada para cima
180 Uma imagem em formato de retrato com a cabeça do bugdroid apontada para cima Uma imagem em formato paisagem com a cabeça do bugdroid apontada para cima
270 Uma imagem em formato paisagem com a cabeça do bugdroid apontada para cima Uma imagem em formato de retrato com a cabeça do bugdroid apontada para cima

Layout

Como você pode ver, o SurfaceView já processa algumas das coisas complicadas para nós. Mas agora você precisa considerar o tamanho do visor ou o tamanho que quer que a prévia tenha na tela. A SurfaceView dimensiona automaticamente o buffer de origem para se ajustar às dimensões. Verifique se a proporção do visor é idêntica à do sourcebuffer. Por exemplo, se você tentar ajustar uma prévia no formato retrato em uma SurfaceView no formato paisagem, vai aparecer algo distorcido assim:

Ilustração de uma imagem mostrando um bugdroid esticado como resultado do ajuste de uma prévia em formato retrato em um visor em formato paisagem

Em geral, a proporção (ou seja, largura/altura) do visor precisa ser idêntica à da fonte. Se você não quiser cortar a imagem no visor, removendo alguns pixels para corrigir a exibição, há dois casos a considerar: quando aspectRatioActivity é maior que aspectRatioSource e quando é menor ou igual a aspectRatioSource.

aspectRatioActivity > aspectRatioSource

Pense no caso como a atividade sendo "mais ampla". A seguir, consideramos um exemplo em que você tem uma atividade 16:9 e uma fonte 4:3.

aspectRatioActivity = 16/9 ≈ 1.78
aspectRatioSource = 4/3 ≈ 1.33

Primeiro, você precisa que o visor também seja 4:3. Em seguida, ajuste a origem e o visor na atividade da seguinte maneira:

Ilustração de uma atividade cuja proporção é maior que a do visor interno

Nesse caso, faça com que a altura do visor corresponda à altura da atividade, mantendo a proporção do visor idêntica à da fonte. O pseudocódigo é este:

viewfinderHeight = activityHeight;
viewfinderWidth = activityHeight * aspectRatioSource;
aspectRatioActivity ≤ aspectRatioSource

O outro caso é quando a atividade é "mais estreita" ou "mais alta". Podemos reutilizar o exemplo anterior, exceto que, no exemplo a seguir, você gira o dispositivo em 90 graus, tornando a atividade 9:16 e a origem 3:4.

aspectRatioActivity = 9/16 = 0.5625
aspectRatioSource = 3/4 = 0.75

Nesse caso, você quer ajustar a origem e o visor à atividade da seguinte maneira:

Ilustração de uma atividade cuja proporção é menor que a do visor interno

A largura do visor precisa corresponder à largura da atividade (e não à altura, como no caso anterior), e a proporção do visor precisa ser idêntica à da fonte. Pseudocódigo:

viewfinderWidth = activityWidth;
viewfinderHeight = activityWidth / aspectRatioSource;
recortes

O AutoFitSurfaceView.kt (github) das amostras do Camera2 substitui o SurfaceView e processa proporções incompatíveis usando uma imagem igual ou "apenas maior" que a atividade em ambas as dimensões e corta o conteúdo que transborda. Isso é útil para apps que querem que a prévia cubra toda a atividade ou preencha completamente uma visualização de dimensões fixas sem distorcer a imagem.

Ressalva

A amostra anterior tenta maximizar o espaço na tela deixando a prévia um pouco maior que a atividade para que nenhum espaço fique sem preenchimento. Isso depende do fato de que as partes excedentes são cortadas pelo layout pai (ou ViewGroup) por padrão. O comportamento é consistente com RelativeLayout e LinearLayout, mas NÃO com ConstraintLayout. Um ConstraintLayout pode redimensionar as visualizações secundárias para que elas se encaixem no layout, o que quebraria o efeito "center-crop" pretendido e causaria prévias esticadas. Você pode consultar este commit como referência.

TextureView

A TextureView oferece controle máximo sobre o conteúdo da visualização da câmera, mas tem um custo de desempenho. Também é preciso mais trabalho para que a prévia da câmera seja exibida corretamente.

Origem

A plataforma Android gira os buffers de saída de acordo com a orientação do sensor para corresponder à orientação natural do dispositivo. Embora a TextureView processe a orientação do sensor, ela não processa as rotações da tela. Ele alinha os buffers de saída com a orientação natural do dispositivo, o que significa que você precisa processar as rotações da tela por conta própria.

Isso é ilustrado na tabela a seguir. Tente girar as figuras de acordo com a rotação da tela correspondente. Você vai receber as mesmas figuras no SurfaceView.

Rotação da tela Smartphone (orientação natural = retrato) Laptop (orientação natural = paisagem)
0 Uma imagem em formato de retrato com a cabeça do bugdroid apontada para cima Uma imagem em formato paisagem com a cabeça do bugdroid apontada para cima
90 Uma imagem em formato retrato com a cabeça do bugdroid apontada para a direita Uma imagem em formato paisagem com a cabeça do bugdroid apontada para a direita
180 Uma imagem em formato paisagem com a cabeça do bugdroid apontada para cima Uma imagem em formato de retrato com a cabeça do bugdroid apontada para cima
270 Uma imagem em formato paisagem com a cabeça do bugdroid apontada para cima Uma imagem em formato de retrato com a cabeça do bugdroid apontada para cima

Layout

O layout é um pouco complicado para o caso de TextureView. Já foi sugerido o uso de uma matriz de transformação para TextureView, mas esse método não funciona em todos os dispositivos. Sugerimos que você siga as etapas descritas aqui.

O processo de três etapas para criar layouts de visualizações corretamente em uma TextureView:

  1. Defina o tamanho da TextureView para ser idêntico ao tamanho da visualização escolhido.
  2. Ajuste a escala da TextureView potencialmente esticada de volta às dimensões originais da prévia.
  3. Gire a TextureView em displayRotation no sentido anti-horário.

Suponha que você tenha um smartphone com uma rotação de tela de 90 graus.

Ilustração de um smartphone com rotação de tela de 90 graus e um objeto

1. Defina o tamanho da TextureView para ser idêntico ao tamanho da visualização escolhida

Suponha que o tamanho da prévia escolhido seja previewWidth × previewHeight, em que previewWidth > previewHeight (a saída do sensor é em formato paisagem por natureza). Ao configurar uma sessão de captura, chame SurfaceTexture#setDefaultBufferSize(int width, height) para especificar o tamanho da visualização (previewWidth × previewHeight).

Antes de chamar setDefaultBufferSize, é importante definir também o tamanho da TextureView como `previewWidth × previewHeight` com View#setLayoutParams(android.view.ViewGroup.LayoutParams). Isso acontece porque a TextureView chama SurfaceTexture#setDefaultBufferSize(int width, height) com a largura e a altura medidas. Se o tamanho da TextureView não for definido explicitamente antes, isso poderá causar uma condição de disputa. Isso é atenuado definindo explicitamente o tamanho da TextureView primeiro.

Agora, a TextureView pode não corresponder às dimensões da origem. No caso dos smartphones, a origem tem formato retrato, mas a TextureView tem formato paisagem devido aos layoutParams que você acabou de definir. Isso resultaria em prévias esticadas, como ilustrado aqui:

Ilustração de uma prévia em formato retrato esticada para caber em uma TextureView do mesmo tamanho da prévia escolhida

2. Redimensione a TextureView potencialmente esticada de volta às dimensões originais da prévia

Considere o seguinte para dimensionar a prévia esticada de volta às dimensões da origem.

As dimensões da origem (sourceWidth × sourceHeight) são:

  • previewHeight × previewWidth, se a orientação natural for retrato ou retrato invertido (a orientação do sensor for de 90 ou 270 graus)
  • previewWidth × previewHeight, se a orientação natural for paisagem ou paisagem invertida (a orientação do sensor é 0 ou 180 graus)

Correção do alongamento usando View#setScaleX(float) e View#setScaleY(float)

  • setScaleX(sourceWidth / previewWidth)
  • setScaleY(sourceHeight / previewHeight)

Ilustração mostrando o procedimento de redução da prévia esticada para as dimensões originais

3. Gire a prévia em `displayRotation` no sentido anti-horário.

Como mencionado anteriormente, gire a prévia em displayRotation no sentido anti-horário para compensar a rotação da tela.

Para fazer isso, View#setRotation(float)

  • setRotation(-displayRotation), porque ele faz uma rotação no sentido horário.

Ilustração mostrando o procedimento de rotação da prévia para corresponder à orientação da tela do dispositivo

Exemplo

Observação:se você já usou a matriz de transformação para TextureView no seu código, a visualização pode não aparecer corretamente em um dispositivo naturalmente horizontal, como Chromebooks. É provável que sua matriz de transformação assuma incorretamente que a orientação do sensor é de 90 ou 270 graus. Consulte este commit no GitHub para uma solução alternativa, mas recomendamos migrar seu app para usar o método descrito aqui.