Ротация вариантов использования CameraX

В этом разделе показано, как настроить варианты использования CameraX внутри вашего приложения для получения изображений с правильной информацией о повороте, будь то из варианта использования ImageAnalysis или ImageCapture . Так:

  • Analyzer варианта использования ImageAnalysis должен получать кадры с правильным поворотом.
  • Вариант использования ImageCapture должен делать снимки с правильным поворотом.

Терминология

В этом разделе используется следующая терминология, поэтому важно понимать, что означает каждый термин:

Ориентация дисплея
Это относится к тому, какая сторона устройства находится вверху, и может иметь одно из четырех значений: книжная, альбомная, перевернутая книжная или перевернутая альбомная.
Поворот дисплея
Это значение, возвращаемое Display.getRotation() , и представляет собой степень, на которую устройство поворачивается против часовой стрелки от его естественной ориентации.
Целевое вращение
Это количество градусов, на которое нужно повернуть устройство по часовой стрелке, чтобы достичь его естественной ориентации.

Как определить целевую ротацию

В следующих примерах показано, как определить целевое вращение устройства на основе его естественной ориентации.

Пример 1. Портретная естественная ориентация.

Пример устройства: Pixel 3 XL

Естественная ориентация = Портрет
Текущая ориентация = Портрет.

Поворот дисплея = 0
Целевое вращение = 0

Естественная ориентация = Портрет
Текущая ориентация = Альбомная.

Поворот дисплея = 90
Целевое вращение = 90

Пример 2: Пейзаж, естественная ориентация

Пример устройства: Pixel C

Естественная ориентация = Пейзаж
Текущая ориентация = Альбомная.

Поворот дисплея = 0
Целевое вращение = 0

Естественная ориентация = Пейзаж
Текущая ориентация = Портрет.

Поворот дисплея = 270
Целевое вращение = 270

Поворот изображения

Какой конец вверх? Ориентация датчика определяется в Android как постоянное значение, которое представляет собой градусы (0, 90, 180, 270), на которые датчик поворачивается относительно верхней части устройства, когда устройство находится в естественном положении. Для всех случаев на диаграммах поворот изображения описывает, как данные следует повернуть по часовой стрелке, чтобы они выглядели вертикально.

Следующие примеры показывают, каким должен быть поворот изображения в зависимости от ориентации датчика камеры. Они также предполагают, что целевое вращение установлено на вращение дисплея.

Пример 1: Датчик повернут на 90 градусов.

Пример устройства: Pixel 3 XL

Поворот дисплея = 0
Ориентация дисплея = Портретная
Поворот изображения = 90

Поворот дисплея = 90
Ориентация дисплея = Альбомная
Поворот изображения = 0

Пример 2: Датчик повернут на 270 градусов.

Пример устройства: Nexus 5X.

Поворот дисплея = 0
Ориентация дисплея = Портретная
Поворот изображения = 270

Поворот дисплея = 90
Ориентация дисплея = Альбомная
Поворот изображения = 180

Пример 3: Датчик повернут на 0 градусов

Пример устройства: Pixel C (планшет)

Поворот дисплея = 0
Ориентация дисплея = Альбомная
Поворот изображения = 0

Поворот дисплея = 270
Ориентация дисплея = Портретная
Поворот изображения = 90

Вычисление поворота изображения

Анализ изображений

Analyzer ImageAnalysis получает изображения с камеры в форме ImageProxy . Каждое изображение содержит информацию о повороте, доступную через:

val rotation = imageProxy.imageInfo.rotationDegrees

Это значение представляет собой степень, на которую изображение необходимо повернуть по часовой стрелке, чтобы оно соответствовало целевому повороту ImageAnalysis . В контексте приложения Android целевой поворот ImageAnalysis обычно соответствует ориентации экрана.

Захват изображения

К экземпляру ImageCapture присоединяется обратный вызов, сигнализирующий о готовности результата захвата. Результатом может быть либо захваченное изображение, либо ошибка.

При съемке изображения предоставляемый обратный вызов может быть одного из следующих типов:

  • OnImageCapturedCallback : получает изображение с доступом в памяти в форме ImageProxy .
  • OnImageSavedCallback : вызывается, когда захваченное изображение было успешно сохранено в расположении, указанном ImageCapture.OutputFileOptions . Параметры могут указывать File , OutputStream или местоположение в MediaStore .

Вращение захваченного изображения, независимо от его формата ( ImageProxy , File , OutputStream , MediaStore Uri ), представляет собой степень вращения, на которую захваченное изображение необходимо повернуть по часовой стрелке, чтобы оно соответствовало целевому вращению ImageCapture , что опять же в контексте приложение Android обычно соответствует ориентации экрана.

Получить поворот захваченного изображения можно одним из следующих способов:

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

Проверьте поворот изображения

Сценарии использования ImageAnalysis и ImageCapture получают ImageProxy от камеры после успешного запроса на захват. ImageProxy оборачивает изображение и информацию о нем, включая его вращение. Эта информация о повороте представляет собой степень, на которую необходимо повернуть изображение, чтобы оно соответствовало целевому повороту варианта использования.

Порядок проверки поворота изображения

Рекомендации по ротации целевых объектов ImageCapture/ImageAnalysis

Поскольку многие устройства по умолчанию не поворачиваются в обратную книжную или альбомную ориентацию, некоторые приложения Android не поддерживают эти ориентации. Независимо от того, поддерживает ли приложение это или нет, способ обновления целевой ротации вариантов использования может быть изменен.

Ниже приведены две таблицы, определяющие, как синхронизировать целевое вращение вариантов использования с вращением дисплея. Первый показывает, как это сделать, поддерживая все четыре ориентации; второй обрабатывает только те ориентации, в которых устройство вращается по умолчанию.

Чтобы выбрать, каким рекомендациям следовать в своем приложении:

  1. Убедитесь, что Activity камеры вашего приложения имеет заблокированную ориентацию, разблокированную ориентацию или переопределяет изменения конфигурации ориентации.

  2. Решите, должна ли Activity камеры вашего приложения обрабатывать все четыре ориентации устройства (книжная, обратная книжная, альбомная и обратная альбомная) или она должна обрабатывать только те ориентации, которые устройство, на котором оно работает, поддерживает по умолчанию.

Поддержка всех четырех ориентаций

В этой таблице приведены определенные рекомендации, которым следует следовать в случаях, когда устройство не поворачивается в обратную книжную ориентацию. То же самое можно применить к устройствам, которые не поворачиваются в обратном направлении.

Сценарий Рекомендации Режим одного окна Многооконный режим разделенного экрана
Разблокированная ориентация Настраивайте варианты использования каждый раз при создании Activity , например, в обратном вызове onCreate() Activity .
Используйте OrientationEventListener onOrientationChanged() . Внутри обратного вызова обновите целевую ротацию вариантов использования. Это обрабатывает случаи, когда система не воссоздает Activity даже после изменения ориентации, например, когда устройство поворачивается на 180 градусов. Также обрабатывается ситуация, когда дисплей находится в обратной книжной ориентации, а устройство по умолчанию не поворачивается в обратную книжную ориентацию. Также обрабатывает случаи, когда Activity не воссоздается при повороте устройства (например, на 90 градусов). Это происходит на устройствах малого форм-фактора, когда приложение занимает половину экрана, и на устройствах большего размера, когда приложение занимает две трети экрана.
Необязательно: установите для свойства screenOrientation Activity значение fullSensor в файле AndroidManifest . Это позволяет размещать пользовательский интерфейс в вертикальном положении, когда устройство находится в перевернутом портретном положении, и позволяет системе воссоздавать Activity всякий раз, когда устройство поворачивается на 90 градусов. Не оказывает никакого влияния на устройства, которые по умолчанию не поворачиваются в обратную портретную ориентацию. Многооконный режим не поддерживается, если дисплей находится в обратной книжной ориентации.
Заблокированная ориентация Настраивайте варианты использования только один раз, при первом создании Activity , например, в обратном вызове onCreate() Activity .
Используйте OrientationEventListener onOrientationChanged() . Внутри обратного вызова обновите целевую ротацию вариантов использования, кроме предварительного просмотра. Также обрабатывает случаи, когда Activity не воссоздается при повороте устройства (например, на 90 градусов). Это происходит на устройствах малого форм-фактора, когда приложение занимает половину экрана, и на устройствах большего размера, когда приложение занимает две трети экрана.
Изменения конфигурации ориентации переопределены Настраивайте варианты использования только один раз, при первом создании Activity , например, в обратном вызове onCreate() Activity .
Используйте OrientationEventListener onOrientationChanged() . Внутри обратного вызова обновите целевую ротацию вариантов использования. Также обрабатывает случаи, когда Activity не воссоздается при повороте устройства (например, на 90 градусов). Это происходит на устройствах малого форм-фактора, когда приложение занимает половину экрана, и на устройствах большего размера, когда приложение занимает две трети экрана.
Необязательно: установите для свойства screenOrientation действия значение fullSensor в файле AndroidManifest. Позволяет размещать пользовательский интерфейс в вертикальном положении, когда устройство находится в перевернутой портретной ориентации. Не влияет на устройства, которые по умолчанию не поворачиваются в обратную портретную ориентацию. Многооконный режим не поддерживается, если дисплей находится в обратной книжной ориентации.

Поддержка только ориентаций, поддерживаемых устройством

Поддерживаются только те ориентации, которые устройство поддерживает по умолчанию (которые могут включать или не включать перевернутую книжную/перевернутую альбомную ориентацию).

Сценарий Рекомендации Многооконный режим разделенного экрана
Разблокированная ориентация Настраивайте варианты использования каждый раз при создании Activity , например, в обратном вызове onCreate() Activity .
Используйте DisplayListener onDisplayChanged() . Внутри обратного вызова обновите целевой поворот вариантов использования, например, когда устройство поворачивается на 180 градусов. Также обрабатывает случаи, когда Activity не воссоздается при повороте устройства (например, на 90 градусов). Это происходит на устройствах малого форм-фактора, когда приложение занимает половину экрана, и на устройствах большего размера, когда приложение занимает две трети экрана.
Заблокированная ориентация Настраивайте варианты использования только один раз, при первом создании Activity , например, в обратном вызове onCreate() Activity .
Используйте OrientationEventListener onOrientationChanged() . Внутри обратного вызова обновите целевую ротацию вариантов использования. Также обрабатывает случаи, когда Activity не воссоздается при повороте устройства (например, на 90 градусов). Это происходит на устройствах малого форм-фактора, когда приложение занимает половину экрана, и на устройствах большего размера, когда приложение занимает две трети экрана.
Изменения конфигурации ориентации переопределены Настраивайте варианты использования только один раз, при первом создании Activity , например, в обратном вызове onCreate() Activity .
Используйте DisplayListener onDisplayChanged() . Внутри обратного вызова обновите целевой поворот вариантов использования, например, когда устройство поворачивается на 180 градусов. Также обрабатывает случаи, когда Activity не воссоздается при повороте устройства (например, на 90 градусов). Это происходит на устройствах малого форм-фактора, когда приложение занимает половину экрана, и на устройствах большего размера, когда приложение занимает две трети экрана.

Разблокированная ориентация

Activity имеет разблокированную ориентацию, когда его ориентация отображения (например, книжная или альбомная) соответствует физической ориентации устройства, за исключением обратной книжной/альбомной ориентации, которую некоторые устройства не поддерживают по умолчанию. Чтобы заставить устройство вращаться во всех четырех ориентациях, установите для свойства screenOrientation Activity значение fullSensor .

В многооконном режиме устройство, которое по умолчанию не поддерживает обратную книжную/альбомную ориентацию, не будет поворачиваться в обратную книжную/альбомную ориентацию, даже если для его свойства screenOrientation установлено значение 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" />

Заблокированная ориентация

Ориентация дисплея заблокирована, если он остается в одной и той же ориентации (например, книжной или альбомной) независимо от физической ориентации устройства. Это можно сделать, указав свойство screenOrientation Activity внутри его объявления в файле AndroidManifest.xml .

Если ориентация дисплея заблокирована, система не уничтожает и не воссоздает Activity при повороте устройства.

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

Изменения конфигурации ориентации отменены

Когда Activity переопределяет изменения конфигурации ориентации, система не уничтожает и не воссоздает его при изменении физической ориентации устройства. Однако система обновляет пользовательский интерфейс, чтобы он соответствовал физической ориентации устройства.

<!-- 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" />

Настройка вариантов использования камеры

В описанных выше сценариях варианты использования камеры можно настроить при первом создании Activity .

В случае Activity с разблокированной ориентацией эта настройка выполняется каждый раз при повороте устройства, поскольку система уничтожает и воссоздает Activity при изменении ориентации. Это приводит к тому, что в вариантах использования каждый раз по умолчанию устанавливается целевое вращение, соответствующее ориентации дисплея.

В случае Activity с заблокированной ориентацией или действия, которое переопределяет изменения конфигурации ориентации, эта настройка выполняется один раз при первом создании Activity .

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

Настройка прослушивателя событий ориентации

Использование OrientationEventListener позволяет постоянно обновлять целевое вращение вариантов использования камеры при изменении ориентации устройства.

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

Настройка DisplayListener

Использование DisplayListener позволяет обновлять целевое вращение вариантов использования камеры в определенных ситуациях, например, когда система не уничтожает и не воссоздает Activity после поворота устройства на 180 градусов.

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