Перенос Camera1 в CameraX

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

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

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

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

КамераКонтроллер

Поставщик камеры

Требуется небольшой установочный код Позволяет больше контроля
Разрешение CameraX выполнять большую часть процесса настройки означает, что такие функции, как фокусировка касанием и масштабирование, работают автоматически. Поскольку настройкой занимается разработчик приложения, существует больше возможностей для настройки конфигурации, например включение поворота выходного изображения или установка формата выходного изображения в ImageAnalysis
Требование PreviewView для предварительного просмотра камеры позволяет CameraX предлагать плавную сквозную интеграцию, как в случае с нашей интеграцией ML Kit, которая может сопоставлять координаты результатов модели ML (например, ограничивающие рамки лица) непосредственно с координатами предварительного просмотра. Возможность использовать собственную «Поверхность» для предварительного просмотра камеры обеспечивает большую гибкость, например, использование существующего кода «Поверхности», который может быть входными данными для других частей вашего приложения.

Если вы застряли при попытке миграции, свяжитесь с нами в дискуссионной группе 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
Предварительный просмотр Захват изображения Видеозахват Анализ изображений
Управляйте поверхностью предварительного просмотра и устанавливайте ее на камере. Установите PictureCallback и вызовите takePicture() на камере. Управляйте конфигурацией камеры и MediaRecorder в определенном порядке. Пользовательский код анализа, созданный поверх предварительной версии 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 , как это сделано в нескольких фрагментах кода ниже. Существуют некоторые различия между привязкой представления и 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 .

Выбор камеры

В приложении камеры первое, что вы можете предложить, — это возможность выбирать разные камеры.

Камера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: Контроллер камеры

В 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 "Android development concepts"
// section above.
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: Контроллер камеры

В 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 CameraProvider вам не обязательно использовать 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 "Android development concepts"
// section above.
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

Чтобы реализовать фокусировку касанием в «Камере1», необходимо рассчитать оптимальную 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 прослушивает события касания 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
// "Android development concepts" section above.
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-zooom 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 слушает события касания 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 , сохраните его как текущий коэффициент масштабирования. Если в 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-zooom 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 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 , но сначала вам нужно создать и связать UseCase ImageCapture , чтобы иметь объект для вызова 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 от CameraX вы можете независимо переключать UseCase ImageCapture , VideoCapture и ImageAnalysis , при условии, что список UseCase можно использовать одновременно . UseCase ImageCapture и ImageAnalysis включены по умолчанию, поэтому вам не нужно вызывать setEnabledUseCases() для съемки фотографии.

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

// 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 , вам необходимо создать 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
                   }
               }
           }
       }
}

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

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

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