Переход с Camera1 на CameraX

Если в вашем приложении используется исходный класс Camera (Camera1), который считается устаревшим с момента выхода Android 5.0 (уровень API 21), настоятельно рекомендуем перейти на современный Android Camera API. В Android есть CameraX (стандартизированный и надежный API камеры Jetpack) и Camera2 (низкоуровневый API фреймворка). В большинстве случаев мы рекомендуем перевести приложение на CameraX. И вот почему.

  • Простота использования. CameraX обрабатывает низкоуровневые детали, поэтому вы можете меньше сосредоточиться на создании камеры с нуля и больше на дифференциации своего приложения.
  • CameraX решает проблему фрагментации. CameraX снижает долгосрочные затраты на обслуживание и позволяет не писать код для конкретных устройств, что повышает качество работы пользователей. Подробнее об этом читайте в записи блога Улучшенная совместимость устройств с CameraX.
  • Расширенные возможности. CameraX позволяет легко добавлять в приложение расширенные функции. Например, с помощью расширений CameraX вы можете применять к фотографиям эффект боке, ретушь лица, HDR (расширенный динамический диапазон) и ночной режим для съемки при слабом освещении.
  • Возможность обновления. В течение года в CameraX добавляются новые функции и исправляются ошибки. Перейдя на CameraX, вы сможете использовать новейшие технологии Android для работы с камерой, которые появляются с каждым выпуском CameraX, а не только с ежегодными обновлениями Android.

В этом руководстве описаны распространенные сценарии для приложений камеры. Для каждого сценария приводится реализация Camera1 и CameraX.

При переносе иногда требуется дополнительная гибкость для интеграции с существующей базой кода. Весь код CameraX в этом руководстве имеет реализацию CameraController, которая отлично подходит, если вы хотите использовать CameraX более простым способом, а также реализацию CameraProvider, которая отлично подходит, если вам нужна большая гибкость. Чтобы вам было проще выбрать подходящий вариант, мы перечислили преимущества каждого из них.

CameraController

CameraProvider

Требуется минимум кода настройки. Больше контроля
Если вы разрешите 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 управляет подключением приложения к камере на протяжении всего жизненного цикла объекта activity, в том числе когда приложение переходит в фоновый режим, когда предварительный просмотр больше не требуется или когда его нужно приостановить, например при входящем видеовызове.

Наконец, CameraX обрабатывает поворот и масштабирование без какого-либо дополнительного кода с вашей стороны. Если у Activity не заблокирована ориентация, то UseCase настраивается каждый раз, когда устройство поворачивают, поскольку система удаляет и заново создает Activity при изменении ориентации. В результате UseCases каждый раз по умолчанию устанавливает целевой поворот в соответствии с ориентацией экрана. Подробнее о поворотах в CameraX…

Прежде чем перейти к деталям, давайте посмотрим, как устроена библиотека CameraX UseCase и как она связана с Camera1. (Концепции CameraX выделены синим, а Camera1 – зеленым.)

CameraX

Конфигурация CameraController/CameraProvider
↓ ↓ ↓ ↓
Предпросмотр ImageCapture VideoCapture ImageAnalysis
⁞ ⁞ ⁞ ⁞
Управление предварительным просмотром Surface и его настройка на камере Настройте PictureCallback и вызовите takePicture() в Camera. Управляйте конфигурацией Camera и MediaRecorder в определенном порядке Специальный код для анализа, созданный на основе предварительной версии Surface
↑ ↑ ↑ ↑
Код для конкретного устройства
↑
Управление поворотом и масштабированием устройства
↑
Управление сеансом камеры (выбор камеры, управление жизненным циклом)

Camera1

Совместимость и производительность в CameraX

CameraX поддерживает устройства с Android 5.0 (уровень API 21) и более поздними версиями. Это более 98 % существующих устройств Android. CameraX автоматически учитывает различия между устройствами, поэтому вам не нужно добавлять в приложение код для каждого из них. Кроме того, мы тестируем более 150 физических устройств со всеми версиями Android, начиная с 5.0, в нашей лаборатории тестирования CameraX. Полный список устройств можно посмотреть в Test Lab.

CameraX использует Executor для управления стеком камеры. Если у вашего приложения есть особые требования к потокам, вы можете настроить собственный исполнитель в CameraX. Если он не задан, CameraX создает и использует оптимизированный внутренний Executor по умолчанию. Многие API платформы, на которых основана библиотека CameraX, требуют блокировки межпроцессного взаимодействия (IPC) с аппаратным обеспечением, ответ от которого иногда может занимать сотни миллисекунд. Поэтому CameraX вызывает эти API только из фоновых потоков, что гарантирует, что основной поток не заблокирован и пользовательский интерфейс остается плавным. Подробнее о цепочках…

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

Концепции разработки для Android

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

  • Привязка представлений генерирует класс привязки для XML-файлов макета, позволяя ссылаться на представления в объектах Activity, как показано в нескольких фрагментах кода ниже. Между привязкой представлений и findViewById() (прежним способом ссылки на представления) есть некоторые различия, но в приведенном ниже коде вы можете заменить строки привязки представлений аналогичным вызовом findViewById().
  • Асинхронные сопрограммы – это шаблон проектирования параллелизма, добавленный в Kotlin 1.3. Его можно использовать для обработки методов CameraX, которые возвращают ListenableFuture. Это стало проще благодаря библиотеке Jetpack Concurrent версии 1.1.0. Чтобы добавить в приложение асинхронную сопрограмму:
    1. Добавьте implementation("androidx.concurrent:concurrent-futures-ktx:1.1.0") в файл Gradle.
    2. Поместите любой код CameraX, который возвращает ListenableFuture, в блок launch или приостанавливающую функцию.
    3. Добавьте вызов await() к вызову функции, которая возвращает ListenableFuture.
    4. Чтобы лучше понять, как работают сопрограммы, ознакомьтесь с руководством Запуск сопрограммы.

Перенос данных в распространенных сценариях

В этом разделе объясняется, как перенести распространенные сценарии из Camera1 в CameraX. Каждый сценарий охватывает реализацию Camera1, реализацию CameraX CameraProvider и реализацию CameraX CameraController.

Выбор камеры

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

Camera1

В 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

Вот пример выбора фронтальной камеры по умолчанию с помощью 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 (если возможно).

Camera1

Чтобы показать предварительный просмотр, вам нужно написать собственный класс 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. Это означает, что Preview UseCase подразумевается, что значительно упрощает настройку:

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

При использовании CameraProvider в CameraX вам не нужно использовать PreviewView, но это все равно значительно упрощает настройку предварительного просмотра по сравнению с Camera1. В этом примере используется PreviewView, но вы можете написать собственный SurfaceProvider и передать его в setSurfaceProvider(), если у вас более сложные требования.

В этом случае Preview UseCase не подразумевается, как в случае с CameraController, поэтому его нужно настроить:

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

Фокусировка по нажатию

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

Camera1

Чтобы реализовать фокусировку по нажатию в 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

Чтобы использовать фокусировку по нажатию на 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
}

Сведите два пальца для масштабирования

Увеличение и уменьшение масштаба предварительного просмотра – ещё одна распространенная прямая манипуляция с предварительным просмотром камеры. Поскольку на устройствах появляется все больше камер, пользователи ожидают, что при масштабировании будет автоматически выбираться объектив с наилучшим фокусным расстоянием.

Camera1

В 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

Чтобы использовать масштабирование в CameraProvider, нужно выполнить настройку. Если вы не используете PreviewView, вам нужно адаптировать логику, чтобы применить ее к вашему собственному Surface.

Вот как это сделать в PreviewView:

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

Как сделать фото

В этом разделе показано, как запустить фотосъемку, независимо от того, нужно ли вам сделать это при нажатии кнопки спуска затвора, по истечении таймера или при любом другом событии по вашему выбору.

Camera1

В 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

Съемка фотографий с помощью 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 снова берет на себя большую часть этой сложности.

Camera1

При записи видео с помощью 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 вы можете независимо переключать ImageCapture, VideoCapture и ImageAnalysis UseCase, если список UseCase можно использовать одновременно. ImageCapture и ImageAnalysis UseCase включены по умолчанию, поэтому вам не нужно было вызывать setEnabledUseCases(), чтобы сделать снимок.

Чтобы использовать CameraController для записи видео, сначала нужно разрешить VideoCapture UseCase в setEnabledUseCases().

// CameraX: Enable VideoCapture UseCase on CameraController.

cameraController.setEnabledUseCases(VIDEO_CAPTURE);

Чтобы начать запись видео, вызовите функцию CameraController.startRecording(). Эта функция может сохранить записанное видео в File, как показано в следующем примере. Кроме того, необходимо передать Executor и класс, реализующий OnVideoSavedCallback, для обработки обратных вызовов в случае успеха и ошибки. Когда запись нужно остановить, вызовите CameraController.stopRecording().

Примечание. Если вы используете CameraX 1.3.0-alpha02 или более поздней версии, то можете включить или отключить запись звука в видео с помощью дополнительного параметра AudioConfig. Чтобы включить запись аудио, необходимо предоставить разрешение на доступ к микрофону. Кроме того, метод stopRecording() удален в версии 1.3.0-alpha02, а 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

Если вы используете CameraProvider, вам нужно создать VideoCapture UseCase и передать объект 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
                   }
               }
           }
       }
}

Дополнительные ресурсы

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

Если вам нужна дополнительная поддержка при переходе на CameraX или у вас есть вопросы о наборе Android Camera API, свяжитесь с нами в группе обсуждения CameraX.