Если ваше Android-приложение использует камеры, следует учитывать некоторые особенности обработки ориентации. В этом документе предполагается, что вы понимаете основные концепции API Android camera2 . Вы можете ознакомиться с нашей статьей в блоге или кратким обзором camera2. Мы также рекомендуем сначала попробовать написать приложение для работы с камерой, прежде чем приступать к изучению этого документа.
Фон
Управление ориентацией в приложениях камеры Android — сложная задача, требующая учета следующих факторов:
- Естественная ориентация: ориентация дисплея, когда устройство находится в «обычном» положении для его конструкции — обычно это портретная ориентация для мобильных телефонов и альбомная ориентация для ноутбуков.
- Ориентация датчика: ориентация датчика, физически установленного на устройстве.
- Поворот дисплея: Насколько устройство физически повернуто относительно своего естественного положения.
- Размер видоискателя: Размер видоискателя, используемого для отображения предварительного просмотра изображения с камеры.
- Размер изображения, выводимого камерой.
Совокупность этих факторов приводит к большому количеству возможных настроек пользовательского интерфейса и предварительного просмотра для приложений камеры. Цель этого документа — показать, как разработчики могут ориентироваться в этих настройках и корректно обрабатывать ориентацию камеры в приложениях Android.
Для упрощения предположим, что во всех примерах используется задняя камера, если не указано иное. Кроме того, все следующие фотографии являются смоделированными для большей наглядности иллюстраций.
Всё об ориентации
Естественная ориентация
Естественная ориентация определяется как ориентация дисплея, когда устройство находится в том положении, в котором оно обычно должно находиться. Для телефонов естественная ориентация часто является портретной. Другими словами, телефоны имеют меньшую ширину и большую высоту. Для ноутбуков естественная ориентация является альбомной, то есть они имеют большую ширину и меньшую высоту. С планшетами ситуация несколько сложнее — они могут быть как портретными, так и альбомными.

Ориентация датчика
Формально говоря, ориентация сенсора измеряется количеством градусов, на которое необходимо повернуть выходное изображение с сенсора по часовой стрелке, чтобы оно соответствовало естественной ориентации устройства. Иными словами, ориентация сенсора — это количество градусов, на которое сенсор поворачивается против часовой стрелки перед установкой на устройство. При взгляде на экран вращение кажется по часовой стрелке, это потому, что датчик задней камеры установлен на «задней» стороне устройства.
Согласно определению совместимости Android 10 7.5.5 «Ориентация камеры» , фронтальная и тыловая камеры «ДОЛЖНЫ быть ориентированы таким образом, чтобы длинная сторона камеры совпадала с длинной стороной экрана».
Выходные буферы камер имеют альбомную ориентацию. Поскольку естественная ориентация телефонов обычно портретная, ориентация сенсора, как правило, составляет 90 или 270 градусов от естественной ориентации, чтобы длинная сторона выходного буфера совпадала с длинной стороной экрана. Ориентация сенсора отличается для устройств с альбомной ориентацией, таких как Chromebook. На этих устройствах датчики изображения также расположены таким образом, чтобы длинная сторона выходного буфера совпадала с длинной стороной экрана. Поскольку оба устройства имеют альбомную ориентацию, их ориентации совпадают, и ориентация сенсора составляет 0 или 180 градусов.

Следующие иллюстрации показывают, как всё выглядит с точки зрения наблюдателя, смотрящего на экран устройства:

Рассмотрим следующую сцену:

| Телефон | Ноутбук |
|---|---|
![]() | ![]() |
Поскольку на телефонах ориентация сенсора обычно составляет 90 или 270 градусов, без учета ориентации сенсора полученные изображения будут выглядеть примерно так:
| Телефон | Ноутбук |
|---|---|
![]() | ![]() |
Предположим, что ориентация датчика против часовой стрелки хранится в переменной sensorOrientation. Для компенсации ориентации датчика необходимо повернуть выходные буферы на `sensorOrientation` по часовой стрелке , чтобы вернуть ориентацию в соответствие с естественной ориентацией устройства.
В Android приложения могут использовать TextureView или SurfaceView для отображения предварительного просмотра камеры. Оба варианта могут обрабатывать ориентацию сенсора, если приложения используют их правильно. В следующих разделах мы подробно расскажем, как следует учитывать ориентацию сенсора.
Поворот дисплея
Формально поворот дисплея определяется поворотом отображаемой графики на экране, который направлен в противоположную сторону от физического поворота устройства относительно его естественной ориентации. В следующих разделах предполагается, что все повороты дисплея кратны 90. Если вы получаете поворот дисплея в абсолютных градусах, округлите его до ближайшего значения из {0, 90, 180, 270}.
В следующих разделах термин «ориентация дисплея» относится к тому, находится ли устройство в альбомной или портретной ориентации, и отличается от термина «поворот дисплея».
Предположим, вы повернули устройства на 90 градусов против часовой стрелки относительно их предыдущего положения, как показано на следующем рисунке:

Предположим, что выходные буферы уже повернуты относительно ориентации датчика. В таком случае у вас будут следующие выходные буферы:
| Телефон | Ноутбук |
|---|---|
![]() | ![]() |
Если поворот дисплея хранится в переменной displayRotation, то для получения правильного изображения следует повернуть выходные буферы на величину displayRotation против часовой стрелки.
Для фронтальных камер вращение дисплея воздействует на буферы изображений в направлении, противоположном направлению отображения на экране. Если вы работаете с фронтальной камерой, вам следует вращать буферы по часовой стрелке.
Предостережения
Поворот дисплея измеряет вращение устройства против часовой стрелки. Это не относится ко всем API для определения ориентации/поворота.
Например,
- Если вы используете
Display#getRotation(), вы получите вращение против часовой стрелки , как указано в этом документе. - Если вы используете OrientationEventListener#onOrientationChanged(int) , вы получите вращение по часовой стрелке.
Важно отметить, что поворот экрана происходит относительно естественной ориентации устройства. Например, если вы физически повернете телефон на 90 или 270 градусов, вы получите экран альбомной ориентации. В то же время, если вы повернете ноутбук на ту же величину, вы получите экран портретной ориентации. Приложениям всегда следует помнить об этом и никогда не делать предположений о естественной ориентации устройства.
Примеры
Давайте воспользуемся предыдущими рисунками, чтобы проиллюстрировать, что такое ориентации и повороты.

| Телефон | Ноутбук |
|---|---|
| Естественная ориентация = Портрет | Естественная ориентация = Ландшафт |
| Ориентация датчика = 90 | Ориентация датчика = 0 |
| Поворот дисплея = 0 | Поворот дисплея = 0 |
| Ориентация экрана = Портретная | Ориентация экрана = альбомная |

| Телефон | Ноутбук |
|---|---|
| Естественная ориентация = Портрет | Естественная ориентация = Ландшафт |
| Ориентация датчика = 90 | Ориентация датчика = 0 |
| Поворот дисплея = 90 | Поворот дисплея = 90 |
| Ориентация экрана = альбомная | Ориентация экрана = Портретная |
Размер видоискателя
Приложения всегда должны изменять размер видоискателя в зависимости от ориентации, поворота и разрешения экрана. В целом, приложения должны устанавливать ориентацию видоискателя в соответствии с текущей ориентацией дисплея. Другими словами, приложения должны выравнивать длинный край видоискателя с длинным краем экрана.
Размер выходного изображения в зависимости от камеры
При выборе размера выходного изображения для предварительного просмотра следует по возможности выбирать размер, равный или немного превышающий размер видоискателя. Как правило, нежелательно масштабировать выходные буферы, так как это может привести к пикселизации. Также не следует выбирать слишком большой размер, поскольку это может снизить производительность и увеличить расход заряда батареи.
Ориентация JPEG
Начнём с распространённой ситуации — захвата фотографии в формате JPEG. В API camera2 вы можете передать JPEG_ORIENTATION в запросе на захват изображения, чтобы указать, насколько вы хотите повернуть выходные JPEG-файлы по часовой стрелке.
Краткое резюме того, что мы упомянули:
- Для обработки ориентации датчика необходимо повернуть буфер изображения на величину
sensorOrientationпо часовой стрелке. - Для управления поворотом экрана необходимо повернуть буфер с помощью
displayRotationпротив часовой стрелки для задних камер и по часовой стрелке для передних камер.
Сложив эти два фактора, получим величину, на которую нужно повернуть устройство по часовой стрелке:
-
sensorOrientation - displayRotationдля задних камер. -
sensorOrientation + displayRotationдля фронтальных камер.
Пример кода для этой логики можно найти в документации по JPEG_ORIENTATION . Обратите внимание, что в примере кода в документации deviceOrientation использует вращение устройства по часовой стрелке. Следовательно, знаки для поворота дисплея перевернуты.
Предварительный просмотр
А что насчет предварительного просмотра камеры? Существует два основных способа отображения предварительного просмотра камеры в приложении: SurfaceView и TextureView. Для корректной обработки ориентации в каждом из них требуются разные подходы.
SurfaceView
SurfaceView обычно рекомендуется для предварительного просмотра с камеры при условии, что вам не нужно обрабатывать или анимировать буферы предварительного просмотра. Он более производительный и менее ресурсоемкий, чем TextureView.
Кроме того, SurfaceView относительно проще в компоновке. Вам нужно лишь учитывать соотношение сторон SurfaceView, на котором отображается предварительный просмотр с камеры.
Источник
В основе SurfaceView лежит платформа Android, которая поворачивает выходные буферы в соответствии с ориентацией дисплея устройства. Другими словами, она учитывает как ориентацию датчика , так и поворот дисплея . Проще говоря, когда наш дисплей находится в альбомной ориентации, мы получаем предварительный просмотр, который также находится в альбомной ориентации, и наоборот, когда он находится в портретной ориентации.
Это показано в следующей таблице. Важно помнить, что поворот дисплея сам по себе не определяет ориентацию источника.
| Поворот дисплея | Телефон (естественная ориентация = портретный режим) | Ноутбук (естественная ориентация = альбомная) |
|---|---|---|
| 0 | ![]() | ![]() |
| 90 | ![]() | ![]() |
| 180 | ![]() | ![]() |
| 270 | ![]() | ![]() |
Макет
Как видите, SurfaceView уже справляется с некоторыми сложными задачами. Но теперь вам нужно учесть размер видоискателя, или то, насколько большим вы хотите видеть предварительный просмотр на экране. SurfaceView автоматически масштабирует исходный буфер в соответствии со своими размерами. Вам необходимо убедиться, что соотношение сторон видоискателя идентично соотношению сторон исходного буфера. Например, если вы попытаетесь разместить предварительный просмотр в портретной ориентации в SurfaceView альбомной ориентации, вы получите искаженное изображение, подобное этому:

Как правило, желательно , чтобы соотношение сторон (т.е. ширина/высота) видоискателя было идентично соотношению сторон исходного изображения . Если вы не хотите обрезать изображение в видоискателе — удалять некоторые пиксели для исправления отображения, — следует рассмотреть два случая: когда aspectRatioActivity больше aspectRatioSource и когда оно меньше или равно aspectRatioSource
aspectRatioActivity > aspectRatioSource
Можно рассматривать этот случай как "более широкий" охват деятельности. Ниже мы рассмотрим пример, где у вас есть деятельность с соотношением сторон 16:9 и источник с соотношением сторон 4:3.
aspectRatioActivity = 16/9 ≈ 1.78 aspectRatioSource = 4/3 ≈ 1.33
Сначала убедитесь, что ваш видоискатель тоже имеет соотношение сторон 4:3. Затем расположите источник и видоискатель в окне активности следующим образом:

В этом случае высота видоискателя должна соответствовать высоте объекта, а соотношение сторон видоискателя должно быть идентично соотношению сторон исходного изображения. Псевдокод выглядит следующим образом:
viewfinderHeight = activityHeight; viewfinderWidth = activityHeight * aspectRatioSource;
aspectRatioActivity ≤ aspectRatioSource
Другой случай — когда объект «уже» или «выше». Мы можем использовать предыдущий пример, за исключением того, что в следующем примере вы поворачиваете устройство на 90 градусов, в результате чего объект становится 9:16, а источник — 3:4.
aspectRatioActivity = 9/16 = 0.5625 aspectRatioSource = 3/4 = 0.75
В этом случае вам нужно разместить источник и окно видоискателя в пределах активности следующим образом:

Ширина видоискателя должна соответствовать ширине объекта съемки (в отличие от высоты в предыдущем случае), а соотношение сторон видоискателя должно совпадать с соотношением сторон исходного изображения. Псевдокод:
viewfinderWidth = activityWidth; viewfinderHeight = activityWidth / aspectRatioSource;
Срез
Файл AutoFitSurfaceView.kt (github) из примеров Camera2 переопределяет SurfaceView и обрабатывает несоответствие соотношений сторон, используя изображение, равное или «немного большее», чем активность по обоим измерениям, а затем обрезает контент, выходящий за пределы изображения. Это полезно для приложений, которым необходимо, чтобы предварительный просмотр покрывал всю активность или полностью заполнял представление фиксированных размеров без искажения изображения.
Предостережение
В приведенном выше примере предпринимается попытка максимально увеличить пространство экрана за счет того, что размер предварительного просмотра немного превышает размер самого элемента, чтобы не оставалось незаполненного пространства. Это основано на том факте, что выходящие за пределы области по умолчанию обрезаются родительским макетом (или ViewGroup). Такое поведение согласуется с RelativeLayout и LinearLayout, но НЕ с ConstraintLayout . ConstraintLayout может изменять размер дочерних элементов, чтобы они поместились внутри макета, что нарушит желаемый эффект «центрирования» и приведет к растянутому предварительному просмотру. Вы можете обратиться к этому коммиту в качестве примера.
TextureView
TextureView обеспечивает максимальный контроль над содержимым предварительного просмотра камеры, но это сопряжено с затратами на производительность. Кроме того, для корректного отображения предварительного просмотра камеры требуется больше усилий.
Источник
В основе TextureView лежит платформа Android, которая поворачивает выходные буферы в соответствии с ориентацией датчика, чтобы они соответствовали естественной ориентации устройства. Хотя TextureView обрабатывает ориентацию датчика , он не обрабатывает повороты дисплея. Он выравнивает выходные буферы по естественной ориентации устройства, а это значит, что вам придется самостоятельно обрабатывать повороты дисплея.
Это показано в следующей таблице. Попробуйте повернуть изображения в соответствии с их положением на экране — вы получите те же самые изображения в SurfaceView.
| Поворот дисплея | Телефон (естественная ориентация = портретный режим) | Ноутбук (естественная ориентация = альбомная) |
|---|---|---|
| 0 | ![]() | ![]() |
| 90 | ![]() | ![]() |
| 180 | ![]() | ![]() |
| 270 | ![]() | ![]() |
Макет
В случае с TextureView компоновка несколько сложна. Ранее предлагалось использовать матрицу преобразования для TextureView, но этот метод работает не на всех устройствах. Мы рекомендуем вместо этого следовать шагам, описанным здесь.
Процесс из 3 шагов для правильного размещения предварительного просмотра на TextureView:
- Установите размер TextureView равным выбранному размеру предварительного просмотра.
- Уменьшите масштаб потенциально растянутого TextureView до исходных размеров предварительного просмотра.
- Поверните TextureView с помощью
displayRotationпротив часовой стрелки.
Предположим, у вас есть телефон с возможностью поворота дисплея на 90 градусов.

1. Установите размер TextureView равным выбранному размеру предварительного просмотра.
Предположим, что выбранный вами размер предварительного просмотра равен previewWidth × previewHeight где previewWidth > previewHeight (выходные данные датчика по своей природе имеют альбомную форму). При настройке сеанса захвата следует вызвать SurfaceTexture#setDefaultBufferSize(int width, height) чтобы указать размер предварительного просмотра ( previewWidth × previewHeight ).
Перед вызовом метода setDefaultBufferSize важно также установить размер TextureView равным `previewWidth × previewHeight` с помощью View#setLayoutParams(android.view.ViewGroup.LayoutParams) . Причина в том, что TextureView вызывает SurfaceTexture#setDefaultBufferSize(int width, height) с измеренными шириной и высотой. Если размер TextureView не задан явно заранее, это может привести к состоянию гонки. Этого можно избежать, явно задав размер TextureView перед вызовом метода.
Теперь TextureView может не соответствовать размерам источника. В случае телефонов источник имеет портретную ориентацию, а TextureView — альбомную из-за заданных вами параметров компоновки. Это приведет к растянутому предварительному просмотру, как показано здесь:

2. Уменьшите масштаб потенциально растянутого TextureView до исходных размеров предварительного просмотра.
Для масштабирования растянутого предварительного просмотра до размеров исходного изображения рассмотрите следующие варианты.
Размеры источника ( sourceWidth × sourceHeight ) составляют:
-
previewHeight × previewWidth, если естественная ориентация — портретная или перевернутая портретная (ориентация сенсора — 90 или 270 градусов) -
previewWidth × previewHeight, если естественная ориентация — альбомная или обратная альбомная (ориентация датчика — 0 или 180 градусов)
Исправьте искажения, используя View#setScaleX(float) и View#setScaleY(float)
- setScaleX(
sourceWidth / previewWidth) - setScaleY(
sourceHeight / previewHeight)

3. Поверните окно предварительного просмотра против часовой стрелки с помощью команды `displayRotation`.
Как уже упоминалось ранее, для компенсации поворота экрана следует повернуть предварительный просмотр против часовой стрелки с помощью displayRotation .
Это можно сделать с помощью View#setRotation(float)
- setRotation(
-displayRotation), поскольку это выполняет вращение по часовой стрелке.

Образец
-
PreviewViewиз библиотеки camerax в Jetpack обрабатывает компоновку TextureView, как описано ранее. Она настраивает преобразование с помощью PreviewCorrector .
Примечание: Если вы ранее использовали матрицу преобразования для TextureView в своем коде, предварительный просмотр может выглядеть некорректно на устройствах с горизонтальной ориентацией экрана, таких как Chromebook. Вероятно, ваша матрица преобразования ошибочно предполагает ориентацию датчика как 90 или 270 градусов. Вы можете обратиться к этому коммиту на GitHub для решения проблемы, но мы настоятельно рекомендуем вам перейти на использование метода, описанного здесь.





















