Перенос Camera1 в CameraX

Если ваше приложение использует оригинальный класс 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 . Чтобы добавить асинхронную сопрограмму в ваше приложение:
    1. Добавьте в свой файл Gradle строку implementation("androidx.concurrent:concurrent-futures-ktx:1.1.0") .
    2. Поместите любой код CameraX, возвращающий ListenableFuture , в блок launch или приостанавливающую функцию .
    3. Добавьте вызов await() к вызову функции, возвращающей ListenableFuture .
    4. Для более глубокого понимания принципа работы сопрограмм см. руководство «Запуск сопрограммы» .

Миграция распространенных сценариев

В этом разделе объясняется, как перенести распространенные сценарии с 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 :

  1. Настройте детектор жестов для обработки событий касания.
  2. При возникновении события касания создайте объект MeteringPoint , используя MeteringPointFactory.createPoint() .
  3. Для объекта MeteringPoint создайте объект FocusMeteringAction .
  4. Имея объект CameraControl на вашей Camera (возвращаемый функцией bindToLifecycle() ), вызовите startFocusAndMetering() , передав в него FocusMeteringAction .
  5. (Необязательно) Ответьте на запрос FocusMeteringResult .
  6. Настройте детектор жестов для реагирования на события касания в 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 :

  1. Настройте детектор жестов масштабирования для обработки событий сжатия.
  2. Получите значение ZoomState из объекта Camera.CameraInfo , который возвращается при вызове Camera bindToLifecycle() .
  3. Если в ZoomState указано значение zoomRatio , сохраните его как текущее соотношение масштабирования. Если zoomRatio в ZoomState отсутствует, используйте стандартное соотношение масштабирования камеры (1.0).
  4. Для определения нового коэффициента масштабирования возьмите произведение текущего коэффициента масштабирования на коэффициент scaleFactor и передайте это значение в метод CameraControl.setZoomRatio() .
  5. Настройте детектор жестов для реагирования на события касания в 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 , а методы должны вызываться в определённом порядке. Для корректной работы приложения необходимо соблюдать этот порядок:

  1. Откройте камеру.
  2. Подготовьте и запустите предварительный просмотр (если ваше приложение показывает, что видео записывается, что обычно и происходит).
  3. Разблокируйте камеру для использования программой MediaRecorder , вызвав метод Camera.unlock() .
  4. Настройте запись, вызвав следующие методы объекта MediaRecorder :
    1. Подключите экземпляр вашей Camera с помощью setCamera(camera) .
    2. Вызовите setAudioSource(MediaRecorder.AudioSource.CAMCORDER) .
    3. Вызовите setVideoSource(MediaRecorder.VideoSource.CAMERA) .
    4. Для установки качества изображения вызовите setProfile(CamcorderProfile.get(CamcorderProfile.QUALITY_1080P)) . Все параметры качества см. в CamcorderProfile .
    5. Вызовите метод setOutputFile(getOutputMediaFile(MEDIA_TYPE_VIDEO).toString()) .
    6. Если в вашем приложении есть предварительный просмотр видео, вызовите setPreviewDisplay(preview?.holder?.surface) .
    7. Вызовите setOutputFormat(MediaRecorder.OutputFormat.MPEG_4) .
    8. Вызовите setAudioEncoder(MediaRecorder.AudioEncoder.DEFAULT) .
    9. Вызовите setVideoEncoder(MediaRecorder.VideoEncoder.DEFAULT) .
    10. Вызовите метод prepare() для завершения настройки вашего MediaRecorder .
  5. Для начала записи вызовите MediaRecorder.start() .
  6. Чтобы остановить запись, вызовите следующие методы. Ещё раз, следуйте этому порядку:
    1. Вызовите метод MediaRecorder.stop() .
    2. При желании можно удалить текущую конфигурацию MediaRecorder , вызвав метод MediaRecorder.reset() .
    3. Вызовите MediaRecorder.release() .
    4. Заблокируйте камеру, чтобы будущие сеансы MediaRecorder могли ее использовать, вызвав метод Camera.lock() .
  7. Чтобы остановить предварительный просмотр, вызовите метод Camera.stopPreview() .
  8. Наконец, чтобы освободить 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: VideoCapture
private 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 .