Если ваше приложение использует оригинальный класс Camera ("Camera1"), который устарел с Android 5.0 (уровень API 21) , мы настоятельно рекомендуем обновить его до современного API камеры Android. Android предлагает CameraX (стандартизированный, надежный API камеры Jetpack ) и Camera2 (низкоуровневый API фреймворка). В большинстве случаев мы рекомендуем перевести ваше приложение на CameraX. Вот почему:
- Простота использования: CameraX берет на себя низкоуровневые детали, позволяя вам меньше сосредотачиваться на создании интерфейса камеры с нуля и больше — на выделении вашего приложения.
- CameraX решает проблему фрагментации: CameraX снижает долгосрочные затраты на обслуживание и количество кода, специфичного для конкретных устройств, обеспечивая пользователям более высокое качество работы. Подробнее об этом читайте в нашей статье в блоге «Улучшенная совместимость устройств с CameraX» .
- Расширенные возможности: CameraX тщательно разработан для того, чтобы упростить интеграцию расширенных функций в ваше приложение. Например, с помощью расширений CameraX вы можете легко применять эффекты боке, ретушь лица, HDR (расширенный динамический диапазон) и ночной режим съемки с осветлением при слабом освещении к вашим фотографиям .
- Возможность обновления: Android в течение года выпускает новые возможности и исправляет ошибки в CameraX. Перейдя на CameraX, ваше приложение получит доступ к новейшим технологиям камеры Android с каждым новым релизом CameraX , а не только с ежегодными обновлениями версий Android.
В этом руководстве вы найдете типичные сценарии использования камер. Для сравнения приведены примеры реализации Camera1 и CameraX.
Когда дело доходит до миграции, иногда требуется дополнительная гибкость для интеграции с существующей кодовой базой. Весь код CameraX в этом руководстве имеет реализацию CameraController — отлично, если вам нужен более простой способ использования CameraX — а также реализацию CameraProvider — отлично, если вам нужна большая гибкость. Чтобы помочь вам решить, какой вариант подходит именно вам, вот преимущества каждого:
CameraController | Поставщик камер |
| Требуется минимум кода настройки. | Обеспечивает больший контроль |
| Благодаря тому, что CameraX берет на себя большую часть процесса настройки, такие функции, как фокусировка касанием и масштабирование жестом «щипка», работают автоматически. | Поскольку настройку выполняет разработчик приложения, появляется больше возможностей для персонализации конфигурации, например, включение поворота выходного изображения или установка формата выходного изображения в ImageAnalysis |
Требование использования PreviewView для предварительного просмотра изображения с камеры позволяет CameraX обеспечивать бесшовную сквозную интеграцию, как, например, в нашей интеграции с ML Kit, которая может напрямую сопоставлять координаты результатов модели машинного обучения (например, ограничивающие рамки лиц) с координатами предварительного просмотра. | Возможность использовать пользовательскую `Surface` для предварительного просмотра с камеры обеспечивает большую гибкость, например, позволяет использовать существующий код `Surface` в качестве входных данных для других частей вашего приложения. |
Если у вас возникнут проблемы с миграцией, свяжитесь с нами в группе обсуждений CameraX .
Перед миграцией
Сравните использование CameraX и Camera1.
Хотя код может выглядеть по-разному, основные концепции Camera1 и CameraX очень похожи. CameraX абстрагирует общие функции камеры в сценарии использования , и в результате многие задачи, которые в Camera1 были возложены на разработчика, обрабатываются CameraX автоматически. В CameraX есть четыре сценария UseCase ), которые можно использовать для различных задач, связанных с камерой: Preview , ImageCapture , VideoCapture и ImageAnalysis .
Одним из примеров того, как CameraX обрабатывает низкоуровневые детали для разработчиков, является ViewPort , который используется совместно всеми активными UseCase . Это гарантирует, что все UseCase видят абсолютно одинаковые пиксели. В Camera1 вам приходится управлять этими деталями самостоятельно. Учитывая переменное соотношение сторон на разных устройствах, сопоставление предварительного просмотра с захваченным медиафайлом затруднительно.
В качестве еще одного примера, CameraX автоматически обрабатывает обратные вызовы Lifecycle в предоставленном вами экземпляре Lifecycle . Благодаря этой архитектуре CameraX обрабатывает подключение вашего приложения к камере на протяжении всего жизненного цикла активности Android , включая следующие случаи: закрытие камеры, когда ваше приложение переходит в фоновый режим; удаление предварительного просмотра камеры, когда экран больше не нуждается в его отображении; и приостановка предварительного просмотра камеры, когда другая активность получает приоритет на переднем плане, например, входящий видеозвонок.
Наконец, CameraX обрабатывает вращение и масштабирование без необходимости написания дополнительного кода с вашей стороны. В случае Activity с разблокированной ориентацией настройка UseCase выполняется каждый раз при повороте устройства, поскольку система уничтожает и создает Activity заново при изменении ориентации. Это приводит к тому, что UseCases каждый раз устанавливают целевое вращение в соответствии с ориентацией дисплея по умолчанию. Подробнее о вращении в CameraX можно прочитать здесь .
Прежде чем перейти к деталям, давайте в общих чертах рассмотрим сценарии UseCase CameraX и то, как приложение Camera1 будет с ними связано. (Концепции CameraX выделены синим цветом , а концепции Camera1 — зеленым .)
КамераX | |||
| Конфигурация CameraController / CameraProvider | |||
| ↓ | ↓ | ↓ | ↓ |
| Предварительный просмотр | ImageCapture | Видеозахват | Анализ изображений |
| ⁞ | ⁞ | ⁞ | ⁞ |
| Настройте предварительный просмотр поверхности и установите его для камеры. | Установите PictureCallback и вызовите takePicture() для камеры. | Настройте параметры камеры и видеорегистратора в указанном порядке. | Пользовательский код анализа, созданный на основе предварительной версии Surface. |
| ↑ | ↑ | ↑ | ↑ |
| Код, специфичный для устройства | |||
| ↑ | |||
| Управление ротацией и масштабированием устройств | |||
| ↑ | |||
| Управление сеансами съемки (выбор камеры, управление жизненным циклом) | |||
Камера1 | |||
Совместимость и производительность в CameraX
CameraX поддерживает устройства под управлением Android 5.0 (уровень API 21) и выше. Это составляет более 98% существующих устройств Android. CameraX разработан для автоматической обработки различий между устройствами, что снижает необходимость в коде, специфичном для конкретного устройства, в вашем приложении. Кроме того, в нашей тестовой лаборатории CameraX мы протестировали более 150 физических устройств на всех версиях Android, начиная с 5.0. Вы можете ознакомиться с полным списком устройств в тестовой лаборатории .
CameraX использует Executor для управления стеком камеры. Вы можете установить собственный исполнитель в CameraX, если ваше приложение имеет специфические требования к многопоточности. Если он не установлен, CameraX создает и использует оптимизированный внутренний Executor по умолчанию. Многие API платформы, на которых построен CameraX, требуют блокирующего межпроцессного взаимодействия (IPC) с оборудованием, ответ на которое иногда может занимать сотни миллисекунд. По этой причине CameraX вызывает эти API только из фоновых потоков, что гарантирует, что основной поток не будет заблокирован и пользовательский интерфейс останется плавным. Подробнее о потоках .
Если целевая аудитория вашего приложения — устройства низкого класса, CameraX предоставляет способ сократить время настройки с помощью ограничителя камер . Поскольку процесс подключения к аппаратным компонентам может занимать значительное время, особенно на устройствах низкого класса, вы можете указать набор камер, необходимых вашему приложению. CameraX подключается к этим камерам только во время настройки. Например, если приложение использует только задние камеры, оно может установить эту конфигурацию с помощью DEFAULT_BACK_CAMERA , и тогда CameraX избежит инициализации передних камер, чтобы уменьшить задержку.
концепции разработки под Android
Данное руководство предполагает общее знакомство с разработкой под Android. Помимо основ, перед тем как приступить к написанию кода, полезно понять несколько важных концепций:
- Метод View Binding генерирует класс привязки для ваших XML-файлов разметки, позволяя вам ссылаться на ваши представления в Activity , как показано в нескольких последующих фрагментах кода. Существуют некоторые различия между View Binding и
findViewById()(предыдущий способ ссылки на представления), но в следующем коде вы сможете заменить строки, относящиеся к View Binding, аналогичным вызовомfindViewById(). - Асинхронные сопрограммы — это шаблон проектирования параллельного программирования, добавленный в Kotlin 1.3, который можно использовать для обработки методов CameraX, возвращающих
ListenableFuture. Начиная с версии 1.1.0, это упрощено благодаря библиотеке Jetpack Concurrent . Чтобы добавить асинхронную сопрограмму в ваше приложение:- Добавьте в свой файл Gradle строку
implementation("androidx.concurrent:concurrent-futures-ktx:1.1.0"). - Поместите любой код CameraX, возвращающий
ListenableFuture, в блокlaunchили приостанавливающую функцию . - Добавьте вызов
await()к вызову функции, возвращающейListenableFuture. - Для более глубокого понимания принципа работы сопрограмм см. руководство «Запуск сопрограммы» .
- Добавьте в свой файл Gradle строку
Миграция распространенных сценариев
В этом разделе объясняется, как перенести распространенные сценарии с Camera1 на CameraX. Каждый сценарий охватывает реализацию Camera1, реализацию CameraProvider в CameraX и реализацию CameraController в CameraX.
Выбор камеры
В вашем приложении для камеры одним из первых шагов может стать предоставление возможности выбора различных камер.
Камера1
В Camera1 вы можете либо вызвать Camera.open() без параметров, чтобы открыть первую заднюю камеру, либо передать целочисленный идентификатор камеры, которую хотите открыть. Вот пример того, как это может выглядеть:
// Camera1: select a camera from id. // Note: opening the camera is a non-trivial task, and it shouldn't be // called from the main thread, unlike CameraX calls, which can be // on the main thread since CameraX kicks off background threads // internally as needed. private fun safeCameraOpen(id: Int): Boolean { return try { releaseCameraAndPreview() camera = Camera.open(id) true } catch (e: Exception) { Log.e(TAG, "failed to open camera", e) false } } private fun releaseCameraAndPreview() { preview?.setCamera(null) camera?.release() camera = null }
CameraX: CameraController
В CameraX выбор камеры осуществляется классом CameraSelector . CameraX упрощает распространенный случай использования камеры по умолчанию. Вы можете указать, хотите ли вы использовать фронтальную или тыловую камеру по умолчанию. Кроме того, объект CameraControl в CameraX позволяет установить уровень масштабирования для вашего приложения, поэтому, если ваше приложение работает на устройстве, поддерживающем логические камеры , оно переключится на соответствующий объектив.
Вот код CameraX для использования стандартной задней камеры с CameraController :
// CameraX: select a camera with CameraController var cameraController = LifecycleCameraController(baseContext) val selector = CameraSelector.Builder() .requireLensFacing(CameraSelector.LENS_FACING_BACK).build() cameraController.cameraSelector = selector
CameraX: Поставщик камер
Вот пример выбора фронтальной камеры по умолчанию с помощью CameraProvider (можно использовать как фронтальную, так и заднюю камеру с CameraController или CameraProvider ):
// CameraX: select a camera with CameraProvider. // Use await() within a suspend function to get CameraProvider instance. // For more details on await(), see the preceding "Android development concepts" // section. private suspend fun startCamera() { val cameraProvider = ProcessCameraProvider.getInstance(this).await() // Set up UseCases (more on UseCases in later scenarios) var useCases:Array= ... // Set the cameraSelector to use the default front-facing (selfie) // camera. val cameraSelector = CameraSelector.DEFAULT_FRONT_CAMERA try { // Unbind UseCases before rebinding. cameraProvider.unbindAll() // Bind UseCases to camera. This function returns a camera // object which can be used to perform operations like zoom, // flash, and focus. var camera = cameraProvider.bindToLifecycle( this, cameraSelector, useCases) } catch(exc: Exception) { Log.e(TAG, "UseCase binding failed", exc) } }) ... // Call startCamera in the setup flow of your app, such as in onViewCreated. override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) ... lifecycleScope.launch { startCamera() } }
Если вам нужно контролировать выбор камеры, это также возможно в CameraX, если вы используете CameraProvider , вызвав метод getAvailableCameraInfos() , который предоставляет объект CameraInfo для проверки определенных свойств камеры, таких как isFocusMeteringSupported() . Затем вы можете преобразовать его в CameraSelector для использования, как показано в предыдущих примерах с помощью метода CameraInfo.getCameraSelector() .
Более подробную информацию о каждой камере можно получить, используя класс Camera2CameraInfo . Вызовите метод getCameraCharacteristic() с ключом, содержащим нужные данные о камере. Список всех ключей, по которым можно выполнить запрос, можно найти в классе CameraCharacteristics .
Вот пример использования пользовательской функции checkFocalLength() , которую вы можете определить самостоятельно:
// CameraX: get a cameraSelector for first camera that matches the criteria // defined in checkFocalLength(). val cameraInfo = cameraProvider.getAvailableCameraInfos() .first { cameraInfo -> val focalLengths = Camera2CameraInfo.from(cameraInfo) .getCameraCharacteristic( CameraCharacteristics.LENS_INFO_AVAILABLE_FOCAL_LENGTHS ) return checkFocalLength(focalLengths) } val cameraSelector = cameraInfo.getCameraSelector()
Отображение предварительного просмотра
В большинстве приложений для работы с камерой необходимо в какой-то момент отображать видеопоток на экране. В случае с Camera1 вам нужно правильно управлять обратными вызовами жизненного цикла, а также определять поворот и масштабирование для предварительного просмотра.
Кроме того, в Camera1 вам нужно решить, использовать ли TextureView или SurfaceView в качестве поверхности предварительного просмотра. Оба варианта имеют свои компромиссы, и в любом случае Camera1 требует корректной обработки вращения и масштабирования. С другой стороны, PreviewView в CameraX имеет базовые реализации как для TextureView , так и SurfaceView . CameraX определяет, какая реализация лучше, в зависимости от таких факторов, как тип устройства и версия Android, на которой работает ваше приложение. Если какая-либо из реализаций совместима, вы можете указать свои предпочтения с помощью PreviewView.ImplementationMode . Параметр COMPATIBLE использует TextureView для предварительного просмотра, а значение PERFORMANCE использует SurfaceView (если это возможно).
Камера1
Для отображения предварительного просмотра необходимо написать собственный класс Preview с реализацией интерфейса android.view.SurfaceHolder.Callback , который используется для передачи данных изображения с камеры в приложение. Затем, прежде чем можно будет начать предварительный просмотр изображения в реальном времени, класс Preview необходимо передать объекту Camera .
// Camera1: set up a camera preview. class Preview( context: Context, private val camera: Camera ) : SurfaceView(context), SurfaceHolder.Callback { private val holder: SurfaceHolder = holder.apply { addCallback(this@Preview) setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS) } override fun surfaceCreated(holder: SurfaceHolder) { // The Surface has been created, now tell the camera // where to draw the preview. camera.apply { try { setPreviewDisplay(holder) startPreview() } catch (e: IOException) { Log.d(TAG, "error setting camera preview", e) } } } override fun surfaceDestroyed(holder: SurfaceHolder) { // Take care of releasing the Camera preview in your activity. } override fun surfaceChanged(holder: SurfaceHolder, format: Int, w: Int, h: Int) { // If your preview can change or rotate, take care of those // events here. Make sure to stop the preview before resizing // or reformatting it. if (holder.surface == null) { return // The preview surface does not exist. } // Stop preview before making changes. try { camera.stopPreview() } catch (e: Exception) { // Tried to stop a non-existent preview; nothing to do. } // Set preview size and make any resize, rotate or // reformatting changes here. // Start preview with new settings. camera.apply { try { setPreviewDisplay(holder) startPreview() } catch (e: Exception) { Log.d(TAG, "error starting camera preview", e) } } } } class CameraActivity : AppCompatActivity() { private lateinit var viewBinding: ActivityMainBinding private var camera: Camera? = null private var preview: Preview? = null override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) viewBinding = ActivityMainBinding.inflate(layoutInflater) setContentView(viewBinding.root) // Create an instance of Camera. camera = getCameraInstance() preview = camera?.let { // Create the Preview view. Preview(this, it) } // Set the Preview view as the content of the activity. val cameraPreview: FrameLayout = viewBinding.cameraPreview cameraPreview.addView(preview) } }
CameraX: CameraController
В CameraX разработчику приходится управлять гораздо меньшим количеством вещей. Если вы используете CameraController , то вам также необходимо использовать PreviewView . Это означает, что UseCase Preview подразумевается, что значительно упрощает настройку:
// CameraX: set up a camera preview with a CameraController. class MainActivity : AppCompatActivity() { private lateinit var viewBinding: ActivityMainBinding override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) viewBinding = ActivityMainBinding.inflate(layoutInflater) setContentView(viewBinding.root) // Create the CameraController and set it on the previewView. var cameraController = LifecycleCameraController(baseContext) cameraController.bindToLifecycle(this) val previewView: PreviewView = viewBinding.cameraPreview previewView.controller = cameraController } }
CameraX: Поставщик камер
При использовании CameraProvider из CameraX вам не обязательно использовать PreviewView , но это все равно значительно упрощает настройку предварительного просмотра по сравнению с Camera1. В демонстрационных целях в этом примере используется PreviewView , но вы можете написать собственный SurfaceProvider для передачи в setSurfaceProvider() если у вас более сложные потребности.
В данном случае, в отличие от CameraController , UseCase Preview не подразумевается, поэтому его необходимо настроить:
// CameraX: set up a camera preview with a CameraProvider. // Use await() within a suspend function to get CameraProvider instance. // For more details on await(), see the preceding "Android development concepts" // section. private suspend fun startCamera() { val cameraProvider = ProcessCameraProvider.getInstance(this).await() // Create Preview UseCase. val preview = Preview.Builder() .build() .also { it.setSurfaceProvider( viewBinding.viewFinder.surfaceProvider ) } // Select default back camera. val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA try { // Unbind UseCases before rebinding. cameraProvider.unbindAll() // Bind UseCases to camera. This function returns a camera // object which can be used to perform operations like zoom, // flash, and focus. var camera = cameraProvider.bindToLifecycle( this, cameraSelector, useCases) } catch(exc: Exception) { Log.e(TAG, "UseCase binding failed", exc) } }) ... // Call startCamera() in the setup flow of your app, such as in onViewCreated. override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) ... lifecycleScope.launch { startCamera() } }
Нажмите для фокусировки
Когда на экране отображается предварительный просмотр изображения с камеры, распространенным способом управления является установка точки фокусировки при касании пользователем предварительного просмотра.
Камера1
Для реализации фокусировки касанием в Camera1 необходимо рассчитать оптимальную Area фокусировки, указывающую, куда Camera должна попытаться сфокусироваться. Эта Area передается в setFocusAreas() . Кроме того, необходимо установить совместимый режим фокусировки для Camera . Область фокусировки действует только в том случае, если текущий режим фокусировки — FOCUS_MODE_AUTO , FOCUS_MODE_MACRO , FOCUS_MODE_CONTINUOUS_VIDEO или FOCUS_MODE_CONTINUOUS_PICTURE .
Каждая Area представляет собой прямоугольник с заданным весом. Вес — это значение от 1 до 1000, и он используется для определения приоритета Areas фокусировки, если задано несколько областей. В этом примере используется только одна Area , поэтому значение веса не имеет значения. Координаты прямоугольника находятся в диапазоне от -1000 до 1000. Верхняя левая точка — (-1000, -1000). Нижняя правая точка — (1000, 1000). Направление задается относительно ориентации датчика, то есть того, что видит датчик. Направление не изменяется при повороте или зеркальном отображении Camera.setDisplayOrientation() , поэтому необходимо преобразовать координаты события касания в координаты датчика.
// Camera1: implement tap-to-focus. class TapToFocusHandler : Camera.AutoFocusCallback { private fun handleFocus(event: MotionEvent) { val camera = camera ?: return val parameters = try { camera.getParameters() } catch (e: RuntimeException) { return } // Cancel previous auto-focus function, if one was in progress. camera.cancelAutoFocus() // Create focus Area. val rect = calculateFocusAreaCoordinates(event.x, event.y) val weight = 1 // This value's not important since there's only 1 Area. val focusArea = Camera.Area(rect, weight) // Set the focus parameters. parameters.setFocusMode(Parameters.FOCUS_MODE_AUTO) parameters.setFocusAreas(listOf(focusArea)) // Set the parameters back on the camera and initiate auto-focus. camera.setParameters(parameters) camera.autoFocus(this) } private fun calculateFocusAreaCoordinates(x: Int, y: Int) { // Define the size of the Area to be returned. This value // should be optimized for your app. val focusAreaSize = 100 // You must define functions to rotate and scale the x and y values to // be values between 0 and 1, where (0, 0) is the upper left-hand side // of the preview, and (1, 1) is the lower right-hand side. val normalizedX = (rotateAndScaleX(x) - 0.5) * 2000 val normalizedY = (rotateAndScaleY(y) - 0.5) * 2000 // Calculate the values for left, top, right, and bottom of the Rect to // be returned. If the Rect would extend beyond the allowed values of // (-1000, -1000, 1000, 1000), then crop the values to fit inside of // that boundary. val left = max(normalizedX - (focusAreaSize / 2), -1000) val top = max(normalizedY - (focusAreaSize / 2), -1000) val right = min(left + focusAreaSize, 1000) val bottom = min(top + focusAreaSize, 1000) return Rect(left, top, left + focusAreaSize, top + focusAreaSize) } override fun onAutoFocus(focused: Boolean, camera: Camera) { if (!focused) { Log.d(TAG, "tap-to-focus failed") } } }
CameraX: CameraController
CameraController прослушивает события касания PreviewView для автоматической обработки фокусировки касанием. Вы можете включить и отключить фокусировку касанием с помощью setTapToFocusEnabled() , а также проверить значение с помощью соответствующего геттера isTapToFocusEnabled() .
Метод getTapToFocusState() возвращает объект LiveData для отслеживания изменений состояния фокуса в CameraController .
// CameraX: track the state of tap-to-focus over the Lifecycle of a PreviewView, // with handlers you can define for focused, not focused, and failed states. val tapToFocusStateObserver = Observer{ state -> when (state) { CameraController.TAP_TO_FOCUS_NOT_STARTED -> Log.d(TAG, "tap-to-focus init") CameraController.TAP_TO_FOCUS_STARTED -> Log.d(TAG, "tap-to-focus started") CameraController.TAP_TO_FOCUS_FOCUSED -> Log.d(TAG, "tap-to-focus finished (focus successful)") CameraController.TAP_TO_FOCUS_NOT_FOCUSED -> Log.d(TAG, "tap-to-focus finished (focused unsuccessful)") CameraController.TAP_TO_FOCUS_FAILED -> Log.d(TAG, "tap-to-focus failed") } } cameraController.getTapToFocusState().observe(this, tapToFocusStateObserver)
CameraX: Поставщик камер
При использовании CameraProvider требуется некоторая настройка для работы функции фокусировки касанием. В этом примере предполагается, что вы используете PreviewView . В противном случае вам необходимо адаптировать логику для применения к вашей пользовательской Surface .
Вот шаги, которые необходимо выполнить при использовании PreviewView :
- Настройте детектор жестов для обработки событий касания.
- При возникновении события касания создайте объект
MeteringPoint, используяMeteringPointFactory.createPoint(). - Для объекта
MeteringPointсоздайте объектFocusMeteringAction. - Имея объект
CameraControlна вашейCamera(возвращаемый функциейbindToLifecycle()), вызовитеstartFocusAndMetering(), передав в негоFocusMeteringAction. - (Необязательно) Ответьте на запрос
FocusMeteringResult. - Настройте детектор жестов для реагирования на события касания в
PreviewView.setOnTouchListener().
// CameraX: implement tap-to-focus with CameraProvider. // Define a gesture detector to respond to tap events and call // startFocusAndMetering on CameraControl. If you want to use a // coroutine with await() to check the result of focusing, see the // preceding "Android development concepts" section. val gestureDetector = GestureDetectorCompat(context, object : SimpleOnGestureListener() { override fun onSingleTapUp(e: MotionEvent): Boolean { val previewView = previewView ?: return val camera = camera ?: return val meteringPointFactory = previewView.meteringPointFactory val focusPoint = meteringPointFactory.createPoint(e.x, e.y) val meteringAction = FocusMeteringAction .Builder(meteringPoint).build() lifecycleScope.launch { val focusResult = camera.cameraControl .startFocusAndMetering(meteringAction).await() if (!result.isFocusSuccessful()) { Log.d(TAG, "tap-to-focus failed") } } } } ) ... // Set the gestureDetector in a touch listener on the PreviewView. previewView.setOnTouchListener { _, event -> // See pinch-to-zoom scenario for scaleGestureDetector definition. var didConsume = scaleGestureDetector.onTouchEvent(event) if (!scaleGestureDetector.isInProgress) { didConsume = gestureDetector.onTouchEvent(event) } didConsume }
Масштабирование с помощью жеста «щипок»
Увеличение и уменьшение масштаба предварительного просмотра — ещё один распространённый способ прямого управления предварительным просмотром камеры. С увеличением количества камер в устройствах пользователи также ожидают, что в результате масштабирования автоматически будет выбран объектив с оптимальным фокусным расстоянием.
Камера1
Существует два способа масштабирования с помощью Camera1. Метод Camera.startSmoothZoom() анимирует переход от текущего уровня масштабирования к уровню масштабирования, указанному в параметре. Метод Camera.Parameters.setZoom() переходит непосредственно к указанному уровню масштабирования. Перед использованием любого из этих методов вызовите методы isSmoothZoomSupported() или isZoomSupported() соответственно, чтобы убедиться, что необходимые методы масштабирования доступны для вашей камеры.
Для реализации масштабирования с помощью жеста «щипка» в этом примере используется setZoom() поскольку обработчик событий касания на поверхности предварительного просмотра постоянно генерирует события при выполнении жеста «щипка», поэтому уровень масштабирования обновляется немедленно каждый раз. Класс ZoomTouchListener определен далее в этом разделе, и его следует установить в качестве функции обратного вызова для обработчика событий касания вашей поверхности предварительного просмотра.
// Camera1: implement pinch-to-zoom. // Define a scale gesture detector to respond to pinch events and call // setZoom on Camera.Parameters. val scaleGestureDetector = ScaleGestureDetector(context, object : ScaleGestureDetector.OnScaleGestureListener { override fun onScale(detector: ScaleGestureDetector): Boolean { val camera = camera ?: return false val parameters = try { camera.parameters } catch (e: RuntimeException) { return false } // In case there is any focus happening, stop it. camera.cancelAutoFocus() // Set the zoom level on the Camera.Parameters, and set // the Parameters back onto the Camera. val currentZoom = parameters.zoom parameters.setZoom(detector.scaleFactor * currentZoom) camera.setParameters(parameters) return true } } ) // Define a View.OnTouchListener to attach to your preview view. class ZoomTouchListener : View.OnTouchListener { override fun onTouch(v: View, event: MotionEvent): Boolean = scaleGestureDetector.onTouchEvent(event) } // Set a ZoomTouchListener to handle touch events on your preview view // if zoom is supported by the current camera. if (camera.getParameters().isZoomSupported()) { view.setOnTouchListener(ZoomTouchListener()) }
CameraX: CameraController
Подобно функции фокусировки касанием, CameraController отслеживает события касания PreviewView для автоматической обработки масштабирования с помощью жеста «щипок». Вы можете включить и отключить масштабирование с помощью жеста «щипок» с помощью setPinchToZoomEnabled() и проверить значение с помощью соответствующего геттера isPinchToZoomEnabled() .
Метод getZoomState() возвращает объект LiveData для отслеживания изменений состояния ZoomState в CameraController .
// CameraX: track the state of pinch-to-zoom over the Lifecycle of // a PreviewView, logging the linear zoom ratio. val pinchToZoomStateObserver = Observer{ state -> val zoomRatio = state.getZoomRatio() Log.d(TAG, "ptz-zoom-ratio $zoomRatio") } cameraController.getZoomState().observe(this, pinchToZoomStateObserver)
CameraX: Поставщик камер
Для работы функции масштабирования с помощью CameraProvider требуется некоторая настройка. Если вы не используете PreviewView , вам нужно адаптировать логику под вашу пользовательскую Surface .
Вот шаги, которые необходимо выполнить при использовании PreviewView :
- Настройте детектор жестов масштабирования для обработки событий сжатия.
- Получите значение
ZoomStateиз объектаCamera.CameraInfo, который возвращается при вызовеCamerabindToLifecycle(). - Если в
ZoomStateуказано значениеzoomRatio, сохраните его как текущее соотношение масштабирования. ЕслиzoomRatioвZoomStateотсутствует, используйте стандартное соотношение масштабирования камеры (1.0). - Для определения нового коэффициента масштабирования возьмите произведение текущего коэффициента масштабирования на коэффициент
scaleFactorи передайте это значение в методCameraControl.setZoomRatio(). - Настройте детектор жестов для реагирования на события касания в
PreviewView.setOnTouchListener().
// CameraX: implement pinch-to-zoom with CameraProvider. // Define a scale gesture detector to respond to pinch events and call // setZoomRatio on CameraControl. val scaleGestureDetector = ScaleGestureDetector(context, object : SimpleOnGestureListener() { override fun onScale(detector: ScaleGestureDetector): Boolean { val camera = camera ?: return val zoomState = camera.cameraInfo.zoomState val currentZoomRatio: Float = zoomState.value?.zoomRatio ?: 1f camera.cameraControl.setZoomRatio( detector.scaleFactor * currentZoomRatio ) } } ) ... // Set the scaleGestureDetector in a touch listener on the PreviewView. previewView.setOnTouchListener { _, event -> var didConsume = scaleGestureDetector.onTouchEvent(event) if (!scaleGestureDetector.isInProgress) { // See pinch-to-zoom scenario for gestureDetector definition. didConsume = gestureDetector.onTouchEvent(event) } didConsume }
Фотографирование
В этом разделе показано, как запустить фотосъемку, независимо от того, нужно ли это сделать по нажатию кнопки спуска затвора, по истечении таймера или по любому другому выбранному вами событию.
Камера1
В Camera1 сначала определяется Camera.PictureCallback для управления данными изображения по запросу. Вот простой пример PictureCallback для обработки данных изображения JPEG:
// Camera1: define a Camera.PictureCallback to handle JPEG data. private val picture = Camera.PictureCallback { data, _ -> val pictureFile: File = getOutputMediaFile(MEDIA_TYPE_IMAGE) ?: run { Log.d(TAG, "error creating media file, check storage permissions") return@PictureCallback } try { val fos = FileOutputStream(pictureFile) fos.write(data) fos.close() } catch (e: FileNotFoundException) { Log.d(TAG, "file not found", e) } catch (e: IOException) { Log.d(TAG, "error accessing file", e) } }
Затем, когда вам нужно сделать снимок, вы вызываете метод takePicture() у экземпляра Camera . Метод takePicture() имеет три разных параметра для разных типов данных. Первый параметр предназначен для ShutterCallback (который не определен в этом примере). Второй параметр предназначен для PictureCallback для обработки необработанных (несжатых) данных с камеры. Третий параметр используется в этом примере, поскольку это PictureCallback для обработки данных изображения JPEG.
// Camera1: call takePicture on Camera instance, passing our PictureCallback. camera?.takePicture(null, null, picture)
CameraX: CameraController
CameraController из CameraX сохраняет простоту Camera1 для захвата изображений, реализуя собственный метод takePicture() . Здесь определите функцию для настройки записи MediaStore и сделайте снимок, который будет сохранен там.
// CameraX: define a function that uses CameraController to take a photo. private val FILENAME_FORMAT = "yyyy-MM-dd-HH-mm-ss-SSS" private fun takePhoto() { // Create time stamped name and MediaStore entry. val name = SimpleDateFormat(FILENAME_FORMAT, Locale.US) .format(System.currentTimeMillis()) val contentValues = ContentValues().apply { put(MediaStore.MediaColumns.DISPLAY_NAME, name) put(MediaStore.MediaColumns.MIME_TYPE, "image/jpeg") if(Build.VERSION.SDK_INT > Build.VERSION_CODES.P) { put(MediaStore.Images.Media.RELATIVE_PATH, "Pictures/CameraX-Image") } } // Create output options object which contains file + metadata. val outputOptions = ImageCapture.OutputFileOptions .Builder(context.getContentResolver(), MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues) .build() // Set up image capture listener, which is triggered after photo has // been taken. cameraController.takePicture( outputOptions, ContextCompat.getMainExecutor(this), object : ImageCapture.OnImageSavedCallback { override fun onError(e: ImageCaptureException) { Log.e(TAG, "photo capture failed", e) } override fun onImageSaved( output: ImageCapture.OutputFileResults ) { val msg = "Photo capture succeeded: ${output.savedUri}" Toast.makeText(baseContext, msg, Toast.LENGTH_SHORT).show() Log.d(TAG, msg) } } ) }
CameraX: Поставщик камер
Фотографирование с помощью CameraProvider работает практически так же, как и с CameraController , но сначала необходимо создать и привязать объект ImageCapture UseCase , чтобы иметь возможность вызывать takePicture() :
// CameraX: create and bind an ImageCapture UseCase. // Make a reference to the ImageCapture UseCase at a scope that can be accessed // throughout the camera logic in your app. private var imageCapture: ImageCapture? = null ... // Create an ImageCapture instance (can be added with other // UseCase definitions). imageCapture = ImageCapture.Builder().build() ... // Bind UseCases to camera (adding imageCapture along with preview here, but // preview is not required to use imageCapture). This function returns a camera // object which can be used to perform operations like zoom, flash, and focus. var camera = cameraProvider.bindToLifecycle( this, cameraSelector, preview, imageCapture)
Затем, когда вам понадобится сделать снимок, вы можете вызвать метод ImageCapture.takePicture() . Полный пример использования функции takePhoto() см. в коде CameraController в этом разделе.
// CameraX: define a function that uses CameraController to take a photo. private fun takePhoto() { // Get a stable reference of the modifiable ImageCapture UseCase. val imageCapture = imageCapture ?: return ... // Call takePicture on imageCapture instance. imageCapture.takePicture( ... ) }
Запись видео
Запись видео — гораздо более сложная задача, чем рассмотренные ранее сценарии. Каждый этап процесса должен быть правильно настроен, как правило, в определенной последовательности. Кроме того, может потребоваться проверка синхронизации видео и звука или устранение дополнительных несоответствий в работе устройств.
Как вы увидите, CameraX снова берет на себя большую часть этой сложной работы.
Камера1
Для захвата видео с помощью Camera1 требуется тщательное управление Camera и MediaRecorder , а методы должны вызываться в определённом порядке. Для корректной работы приложения необходимо соблюдать этот порядок:
- Откройте камеру.
- Подготовьте и запустите предварительный просмотр (если ваше приложение показывает, что видео записывается, что обычно и происходит).
- Разблокируйте камеру для использования программой
MediaRecorder, вызвав методCamera.unlock(). - Настройте запись, вызвав следующие методы объекта
MediaRecorder:- Подключите экземпляр вашей
Cameraс помощьюsetCamera(camera). - Вызовите
setAudioSource(MediaRecorder.AudioSource.CAMCORDER). - Вызовите
setVideoSource(MediaRecorder.VideoSource.CAMERA). - Для установки качества изображения вызовите
setProfile(CamcorderProfile.get(CamcorderProfile.QUALITY_1080P)). Все параметры качества см. вCamcorderProfile. - Вызовите метод
setOutputFile(getOutputMediaFile(MEDIA_TYPE_VIDEO).toString()). - Если в вашем приложении есть предварительный просмотр видео, вызовите
setPreviewDisplay(preview?.holder?.surface). - Вызовите
setOutputFormat(MediaRecorder.OutputFormat.MPEG_4). - Вызовите
setAudioEncoder(MediaRecorder.AudioEncoder.DEFAULT). - Вызовите
setVideoEncoder(MediaRecorder.VideoEncoder.DEFAULT). - Вызовите метод
prepare()для завершения настройки вашегоMediaRecorder.
- Подключите экземпляр вашей
- Для начала записи вызовите
MediaRecorder.start(). - Чтобы остановить запись, вызовите следующие методы. Ещё раз, следуйте этому порядку:
- Вызовите метод
MediaRecorder.stop(). - При желании можно удалить текущую конфигурацию
MediaRecorder, вызвав методMediaRecorder.reset(). - Вызовите
MediaRecorder.release(). - Заблокируйте камеру, чтобы будущие сеансы
MediaRecorderмогли ее использовать, вызвав методCamera.lock().
- Вызовите метод
- Чтобы остановить предварительный просмотр, вызовите метод
Camera.stopPreview(). - Наконец, чтобы освободить
Cameraи позволить другим процессам использовать её, вызовите методCamera.release().
Вот все эти шаги в совокупности:
// Camera1: set up a MediaRecorder and a function to start and stop video // recording. // Make a reference to the MediaRecorder at a scope that can be accessed // throughout the camera logic in your app. private var mediaRecorder: MediaRecorder? = null private var isRecording = false ... private fun prepareMediaRecorder(): Boolean { mediaRecorder = MediaRecorder() // Unlock and set camera to MediaRecorder. camera?.unlock() mediaRecorder?.run { setCamera(camera) // Set the audio and video sources. setAudioSource(MediaRecorder.AudioSource.CAMCORDER) setVideoSource(MediaRecorder.VideoSource.CAMERA) // Set a CamcorderProfile (requires API Level 8 or higher). setProfile(CamcorderProfile.get(CamcorderProfile.QUALITY_HIGH)) // Set the output file. setOutputFile(getOutputMediaFile(MEDIA_TYPE_VIDEO).toString()) // Set the preview output. setPreviewDisplay(preview?.holder?.surface) setOutputFormat(MediaRecorder.OutputFormat.MPEG_4) setAudioEncoder(MediaRecorder.AudioEncoder.DEFAULT) setVideoEncoder(MediaRecorder.VideoEncoder.DEFAULT) // Prepare configured MediaRecorder. return try { prepare() true } catch (e: IllegalStateException) { Log.d(TAG, "preparing MediaRecorder failed", e) releaseMediaRecorder() false } catch (e: IOException) { Log.d(TAG, "setting MediaRecorder file failed", e) releaseMediaRecorder() false } } return false } private fun releaseMediaRecorder() { mediaRecorder?.reset() mediaRecorder?.release() mediaRecorder = null camera?.lock() } private fun startStopVideo() { if (isRecording) { // Stop recording and release camera. mediaRecorder?.stop() releaseMediaRecorder() camera?.lock() isRecording = false // This is a good place to inform user that video recording has stopped. } else { // Initialize video camera. if (prepareVideoRecorder()) { // Camera is available and unlocked, MediaRecorder is prepared, now // you can start recording. mediaRecorder?.start() isRecording = true // This is a good place to inform the user that recording has // started. } else { // Prepare didn't work, release the camera. releaseMediaRecorder() // Inform user here. } } }
CameraX: CameraController
С помощью CameraController из CameraX вы можете независимо переключать UseCase ImageCapture , VideoCapture и ImageAnalysis , при условии, что список сценариев использования может использоваться одновременно . UseCase ImageCapture и ImageAnalysis включены по умолчанию, поэтому вам не нужно вызывать setEnabledUseCases() для съемки фотографии.
Для использования CameraController для видеозаписи необходимо сначала использовать setEnabledUseCases() чтобы разрешить UseCase VideoCapture .
// CameraX: Enable VideoCapture UseCase on CameraController. cameraController.setEnabledUseCases(VIDEO_CAPTURE);
Чтобы начать запись видео, вызовите функцию CameraController.startRecording() . Эта функция может сохранить записанное видео в File , как показано в следующем примере. Кроме того, необходимо передать Executor и класс, реализующий интерфейс OnVideoSavedCallback для обработки коллбэков при успешном и ошибочном завершении записи. Когда запись должна завершиться, вызовите CameraController.stopRecording() .
Примечание: Если вы используете CameraX 1.3.0-alpha02 или более позднюю версию, существует дополнительный параметр AudioConfig , позволяющий включать или отключать запись звука в видео. Для включения записи звука необходимо убедиться, что у вас есть права доступа к микрофону. Кроме того, в версии 1.3.0-alpha02 удален метод stopRecording() , а метод startRecording() возвращает объект Recording , который можно использовать для приостановки, возобновления и остановки видеозаписи.
// CameraX: implement video capture with CameraController. private val FILENAME_FORMAT = "yyyy-MM-dd-HH-mm-ss-SSS" // Define a VideoSaveCallback class for handling success and error states. class VideoSaveCallback : OnVideoSavedCallback { override fun onVideoSaved(outputFileResults: OutputFileResults) { val msg = "Video capture succeeded: ${outputFileResults.savedUri}" Toast.makeText(baseContext, msg, Toast.LENGTH_SHORT).show() Log.d(TAG, msg) } override fun onError(videoCaptureError: Int, message: String, cause: Throwable?) { Log.d(TAG, "error saving video: $message", cause) } } private fun startStopVideo() { if (cameraController.isRecording()) { // Stop the current recording session. cameraController.stopRecording() return } // Define the File options for saving the video. val name = SimpleDateFormat(FILENAME_FORMAT, Locale.US) .format(System.currentTimeMillis()) val outputFileOptions = OutputFileOptions .Builder(File(this.filesDir, name)) .build() // Call startRecording on the CameraController. cameraController.startRecording( outputFileOptions, ContextCompat.getMainExecutor(this), VideoSaveCallback() ) }
CameraX: Поставщик камер
Если вы используете CameraProvider , вам необходимо создать UseCase VideoCapture и передать в него объект Recorder . В Recorder.Builder вы можете установить качество видео и, при необходимости, FallbackStrategy , которая обрабатывает случаи, когда устройство не может соответствовать желаемым параметрам качества. Затем свяжите экземпляр VideoCapture с CameraProvider с помощью других ваших UseCase .
// CameraX: create and bind a VideoCapture UseCase with CameraProvider. // Make a reference to the VideoCapture UseCase and Recording at a // scope that can be accessed throughout the camera logic in your app. private lateinit var videoCapture: VideoCaptureprivate var recording: Recording? = null ... // Create a Recorder instance to set on a VideoCapture instance (can be // added with other UseCase definitions). val recorder = Recorder.Builder() .setQualitySelector(QualitySelector.from(Quality.FHD)) .build() videoCapture = VideoCapture.withOutput(recorder) ... // Bind UseCases to camera (adding videoCapture along with preview here, but // preview is not required to use videoCapture). This function returns a camera // object which can be used to perform operations like zoom, flash, and focus. var camera = cameraProvider.bindToLifecycle( this, cameraSelector, preview, videoCapture)
На этом этапе доступ Recorder можно получить через свойство videoCapture.output . Recorder может запускать видеозаписи, которые сохраняются в File , ParcelFileDescriptor или MediaStore . В этом примере используется MediaStore .
Для подготовки Recorder можно вызвать несколько методов. Вызовите prepareRecording() , чтобы установить параметры вывода MediaStore . Если ваше приложение имеет разрешение на использование микрофона устройства, вызовите также withAudioEnabled() . Затем вызовите start() , чтобы начать запись, передав контекст и обработчик событий Consumer<VideoRecordEvent> для обработки событий записи видео. В случае успеха возвращенный Recording можно использовать для приостановки, возобновления или остановки записи.
// CameraX: implement video capture with CameraProvider. private val FILENAME_FORMAT = "yyyy-MM-dd-HH-mm-ss-SSS" private fun startStopVideo() { val videoCapture = this.videoCapture ?: return if (recording != null) { // Stop the current recording session. recording.stop() recording = null return } // Create and start a new recording session. val name = SimpleDateFormat(FILENAME_FORMAT, Locale.US) .format(System.currentTimeMillis()) val contentValues = ContentValues().apply { put(MediaStore.MediaColumns.DISPLAY_NAME, name) put(MediaStore.MediaColumns.MIME_TYPE, "video/mp4") if (Build.VERSION.SDK_INT > Build.VERSION_CODES.P) { put(MediaStore.Video.Media.RELATIVE_PATH, "Movies/CameraX-Video") } } val mediaStoreOutputOptions = MediaStoreOutputOptions .Builder(contentResolver, MediaStore.Video.Media.EXTERNAL_CONTENT_URI) .setContentValues(contentValues) .build() recording = videoCapture.output .prepareRecording(this, mediaStoreOutputOptions) .withAudioEnabled() .start(ContextCompat.getMainExecutor(this)) { recordEvent -> when(recordEvent) { is VideoRecordEvent.Start -> { viewBinding.videoCaptureButton.apply { text = getString(R.string.stop_capture) isEnabled = true } } is VideoRecordEvent.Finalize -> { if (!recordEvent.hasError()) { val msg = "Video capture succeeded: " + "${recordEvent.outputResults.outputUri}" Toast.makeText( baseContext, msg, Toast.LENGTH_SHORT ).show() Log.d(TAG, msg) } else { recording?.close() recording = null Log.e(TAG, "video capture ends with error", recordEvent.error) } viewBinding.videoCaptureButton.apply { text = getString(R.string.start_capture) isEnabled = true } } } } }
Дополнительные ресурсы
В нашем репозитории Camera Samples на GitHub есть несколько полноценных приложений CameraX. Эти примеры демонстрируют, как сценарии, описанные в этом руководстве, могут быть реализованы в полноценном Android-приложении.
Если вам нужна дополнительная поддержка при миграции на CameraX или у вас есть вопросы по набору API камеры Android, пожалуйста, свяжитесь с нами в группе обсуждений CameraX .