Отображение превью с камеры — обычный вариант использования у любого приложения для съемки фото и видео. Однако сих пор это было довольно трудно сделать правильно, в основном из-за сложностей, связанных с граничными случаями API camera2 и поведением конкретных устройств. PreviewView, часть библиотеки CameraX Jetpack, упрощает отображение превью камеры, предоставляя удобный разработчикам, последовательный и стабильный API для широкого спектра устройств Android.

Знакомство с PreviewView

PreviewView — это кастомный View, который позволяет отображать ленту камеры. Он был спроектирован, чтобы уменьшить нагрузку по настройке и обработке поверхности превью, которая используется камерой.

Если вам нужно отобразить базовое превью камеры в приложении, рекомендуется использовать PreviewView, так как он:

  • Проще в использовании: PreviewView — это View. Он осуществляет всю работу, необходимую для отображения того, что видит камера в вашем макете, управляя поверхностью Surface, применяемой в варианте использования Preview.
  • Мало весит: PreviewView фокусируется исключительно на превью. Все его внутренние ресурсы направлены на то, чтобы настроить превью камеры и управлять поверхностью превью во время ее использования. Такое разделение ответственности позволяет коду PreviewView оставаться чистым.
  • Исчерпывающий: PreviewView обрабатывает все сложные детали отображения ленты камеры на экране, включая соотношение сторон, масштабирование и вращение. Он также содержит исправления совместимости и обходные пути, которые обеспечивают плавное взаимодействие со множеством различных устройств, разрешений экрана, уровней аппаратной поддержки камеры и конфигураций дисплея, таких как режим разделенного экрана, фиксированная ориентация и мульти-окно свободной формы.

PreviewView — режимы реализации

PreviewView — это подкласс FrameLayout. Для отображения ленты камеры он использует либо SurfaceView, либо TextureView, предоставляет камере готовую поверхность превью, пытается сохранить ее в актуальном состоянии, пока камера использует ее, и при преждевременном освобождении предоставляет новую поверхность, если камера все еще используется.

SurfaceView, как правило, лучше TextureView, когда речь заходит о некоторых ключевых метриках, включающих заряд и время ожидания, поэтому PreviewView пытается использовать SurfaceView по умолчанию. Однако некоторые устройства (в основном устаревшие) аварийно завершают работу, когда поверхность превью освобождается преждевременно. К сожалению, при использовании SurfaceView невозможно контролировать, когда поверхность освобождается, так как это контролируется иерархией View. На таких устройствах PreviewView вынужденно возвращается к использованию TextureView. Вы также должны настроить PreviewView на использование TextureView в тех случаях, когда требуется вращение превью, прозрачность или анимация.

Вы можете в явном виде задать ту реализацию, которую хотите использовать в PreviewView, вызвав PreviewView.setPreferredImplementationMode(ImplementationMode), где ImplementationMode — это либо SURFACE_VIEW, либо TEXTURE_VIEW. PreviewView старается уважать ваш выбор и гарантирует использование указанного режима.

⚠️Перед запуском превью обязательно удостоверьтесь, что установили предпочтительный режим реализации, вызвав Preview.setSurfaceProvider(PreviewView.createSurfaceProvider()).

Ниже показано, как установить предпочтительный режим реализации PreviewView:

// Установите предпочтительный режим реализации, прежде чем запускать превью
previewView.preferredImplementationMode = ImplementationMode.SURFACE_VIEW

// Присоедините вариант использования превью и previewView, чтобы запустить превью
preview.setSurfaceProvider(previewView.createSurfaceProvider(cameraInfo))

PreviewView — превью

PreviewView обрабатывает “винтики и шестеренки” создания SurfaceProvider, который необходим в варианте использования Preview для запуска потока превью. SurfaceProvider подготавливает поверхность, которая будет предоставлена камере для отображения потока превью, и при необходимости заботится о ее воссоздании. Preview.createSurfaceProvider(CameraInfo) принимает нуллифицированный экземпляр CameraInfo. PreviewView использует его наряду с предпочтительным режимом реализации и возможностями камеры, чтобы определить реализацию для внутреннего использования. Если вы передаете null CameraInfo, PreviewView использует реализацию TextureView, так как он не может определить, будет ли выбранная камера работать с SurfaceView.

Как только вы сконструируете вариант использования превью и любые другие необходимые вам варианты использования, свяжите их с LifecycleOwner, используйте CameraInfo привязанной камеры для создания SurfaceProvider, а затем прикрепите к варианту использования Preview для запуска потока превью, вызвав Preview.setSurfaceProvider(SurfaceProvider).

Ниже показано, как прикрепить PreviewView к Preview, чтобы запустить поток предварительного просмотра:

// Создайте экземпляр варианта использования Preview
val preview = Preview.Builder().build()

// Привяжите вариант использования preview и другие необходимые варианты использования к lifecycle
val camera = cameraProvider.bindToLifecycle(lifecycleOwner, cameraSelector, preview, imageAnalysis, imageCapture)

// Создайте surfaceProvider, используя cameraInfo привязанной камеры
val surfaceProvider = previewView.createSurfaceProvider(camera.cameraInfo)

// Прикрепите surfaceProvider к варианту использования preview, чтобы запустить превью
preview.setSurfaceProvider(surfaceProvider)

PreviewView — способы масштабирования

PreviewView предоставляет API, который позволяет управлять тем, как должно выглядеть превью и где оно должно располагается в контейнере.

  • Параметр How определяет, должно ли превью вмещаться внутрь своего контейнера (FIT) или заполнять (FILL) его.
  • Параметр where определяет, должно ли превью находиться в верхнем левом углу (START), в центре (CENTER) или в нижнем правом углу (END) своего контейнера.

Доступные значения способов масштабирования, поддерживаемые PreviewView, представляют собой комбинации из “how” и “where”: FIT_START, FIT_CENTER, FIT_END, FILL_START, FILL_CENTER и FILL_END. Чаще всего используются FIT_CENTER, который эквивалентен леттербоксингу превью, и FILL_CENTER, который центрирует превью в контейнере.

Установить способ масштабирования можно двумя путями.

В XML-макете с помощью атрибута scaleType в PreviewView, как показано в следующем примере:

<androidx.camera.view.PreviewView
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    app:scaleType="fitEnd" />

Программно через вызов PreviewView.setScaleType(ScaleType), как показано ниже:

previewView.setScaleType(ScaleType.FIT_CENTER)

Чтобы узнать текущий способ масштабирования, используемый PreviewView, вызовите PreviewView.getScaleType().

PreviewView — управление камерой

В зависимости от ориентации датчика камеры, поворота устройства, режима отображения и типа шкалы предварительного просмотра PreviewView может масштабировать, поворачивать и преобразовывать превью-кадры, полученные от камеры, чтобы правильно отображать поток превью в пользовательском интерфейсе. Вот почему так важно иметь возможность конвертировать координаты пользовательского интерфейса в координаты датчика камеры. В CameraX такое преобразование выполняется с помощью MeteringPointFactory. PreviewView предоставляет API для его создания: PreviewView.createMeteringPointFactory(cameraSelector), где CameraSelector выступает в качестве камеры, транслирующей превью.

MeteringPointFactory в PreviewView очень удобен, когда нужно реализовать tap-to-focus. Несмотря на то, что в превью камеры автофокус включен по умолчанию (если камера поддерживает его), вы также можете управлять тем, какой объект будет выбран целью фокусировки при нажатии на PreviewView. MeteringPointFactory преобразует координаты объекта фокусировки в координаты датчика камеры, что затем позволяет камере фокусироваться на нужной области.

В следующем примере показано, как реализовать tap-to-focus с помощью тач-слушателя, установленного в PreviewView:

fun onTouch(x: Float, y: Float) {
    // Создайте factory для создания MeteringPoint
    val factory = previewView.createMeteringPointFactory(cameraSelector)
  
    // Преобразуйте UI-координаты в координаты датчиков камеры
    val point = factory.createPoint(x, y)
  
    // Подготовьте действие фокусировки для запуска
    val action = FocusMeteringAction.Builder(point).build()
  
    // Выполните действие фокусировки
    cameraControl.startFocusAndMetering(action)
}

Еще одна распространенная функция превью камеры — это pinch-to-zoom, что позволяет камере приближать и отдалять изображение, когда вы делаете щипок по превью. Чтобы реализовать эту функцию с помощью PreviewView, добавьте тач-слушатель в PreviewView и прикрепите его к слушателю жестов масштабирования. Это позволит перехватывать жесты щипка и соответствующим образом обновлять коэффициент увеличения камеры.

В следующем примере показано, как реализовать pinch-to-zoom с помощью PreviewView.

// Создайте слушатель с обратным вызовом, который запускается, когда происходит событие жеста
val listener = object : ScaleGestureDetector.SimpleOnScaleGestureListener() {
    override fun onScale(detector: ScaleGestureDetector): Boolean {
        // Получите текущий коэффициент масштабирования камеры
        val currentZoomRatio: Float = cameraInfo.zoomRatio.value ?: 1F
      
        // Посмотрите, насколько сильно изменился масштаб из-за жеста щипка пользователя
        val delta = detector.scaleFactor
      
        // Обновите коэффициент увеличения камеры
        cameraControl.setZoomRatio(currentZoomRatio * delta)
        return true
    }
}

// Прикрепите тач-слушатель событий PreviewView к слушателю жестов масштабирования
val scaleGestureDetector = ScaleGestureDetector(context, listener)

// Передайте тач-события от PreviewView слушателю жестов масштабирования
previewView.setOnTouchListener { _, event ->
    scaleGestureDetector.onTouchEvent(event)
    return@setOnTouchListener true
}

PreviewView — как он тестируется

PreviewView обеспечивает согласованное поведение камеры на широком диапазоне устройств Android. Это стало возможным благодаря инвестициям CameraX в тестирование PreviewView и других API-интерфейсов в лаборатории автоматизированного тестирования. Эти тесты делятся на две основные категории:

  • Модульные тесты, которые проверяют поведение PreviewView в отношении режимов реализации, способов масштабирования и MeteringPointFactory. Они также помогают удостоверится, что PreviewView правильно подстраивает превью, когда это необходимо, например, если изменяется размер его контейнера, если обновляется макет дисплея, или если он прикрепляется (первично или повторно) к Window.
  • Интеграционные тесты, которые гарантируют, что PreviewView ведет себя правильно, когда является частью приложения, и соответственно отображает или останавливает поток превью. Эти тесты включают в себя проверку состояния превью во время работы приложения, после его многократного закрытия и повторного открытия, после переключения объектива камеры вперед и назад, а также после уничтожения и перезапуска жизненного цикла приложения. В настоящее время эти тесты в основном охватывают реализацию PreviewView с TextureView, поскольку получение от SurfaceView сигнала о том, когда превью запущено или остановлено, оказалось сложной задачей.

Заключение

Подводя итог:

  • PreviewView — это пользовательский View, который упрощает отображение превью камеры.
  • PreviewView обрабатывает поверхность предварительного просмотра с помощью SurfaceView по умолчанию, но при необходимости или по запросу может вернуться к использованию TextureView.
  • Свяжите другие ваши варианты использования, такие как ImageCapture и ImageAnalysis, с LifecycleOwner, а затем запустите превью камеры, прикрепив SurfaceProvider из PreviewView к связанному варианту использования Preview.
  • Управление отображением предварительного просмотра осуществляется в PreviewView путем определения способа масштабирования.
  • Реализуйте tap-to-focus в вашем превью, получив MeteringPointFactory из PreviewView.
  • Реализуйте pinch-to-zoom в вашем превью, настроив в PreviewView слушатель жестов.

Хотите узнать больше о CameraX? Загляните сюда:

Спасибо за чтение!

Читайте также:

Читайте нас в Telegram, VK и Дзен


Перевод статьи Husayn Hakeem: Display a camera preview with PreviewView

Предыдущая статьяОт рекомендаций по проведению ревью кода к общечеловеческим ценностям
Следующая статьяКогда и зачем использовать оператор := в Python