将 Camera1 迁移到 CameraX

如果您的应用程序使用原始的 Camera 类(“Camera1”),该类已从 Android 5.0(API 级别 21) 开始弃用,我们强烈建议您更新到现代的 Android 相机 API。Android 提供了 CameraX(一个标准化、健壮的 Jetpack 相机 API)和 Camera2(一个低级框架 API)。对于绝大多数情况,我们建议将您的应用程序迁移到 CameraX。以下是原因:

  • 易于使用:CameraX 处理低级细节,这样您就可以减少从头开始构建相机体验的时间,而将更多精力放在使您的应用程序与众不同方面。
  • CameraX 为您处理碎片化:CameraX 降低了长期维护成本和设备特定代码,为用户带来更高质量的体验。有关更多信息,请查看我们的 通过 CameraX 提升设备兼容性 博客文章。
  • 高级功能:CameraX 经过精心设计,可将高级功能轻松地集成到您的应用程序中。例如,您可以使用 CameraX 扩展 轻松地将散景、人脸修饰、HDR(高动态范围)和弱光亮化夜间拍摄模式应用于您的照片。
  • 可更新性:Android 每年都会向 CameraX 发布新功能和错误修复。通过迁移到 CameraX,您的应用程序将获得最新的 Android 相机技术 随着每次 CameraX 版本发布,而不仅仅是在每年的 Android 版本发布时。

在本指南中,您将找到相机应用程序的常见场景。每个场景都包含 Camera1 实现和 CameraX 实现,以供比较。

在迁移时,有时您需要额外的灵活性才能与现有代码库集成。本指南中的所有 CameraX 代码都有 CameraController 实现(如果您希望使用 CameraX 的最简单方法,则非常适合)以及 CameraProvider 实现(如果您需要更多灵活性,则非常适合)。为了帮助您确定哪种方式最适合您,以下列出了每种方式的优点

CameraController

CameraProvider

需要很少的设置代码 允许更多控制
允许 CameraX 处理更多设置过程,这意味着点触对焦和捏合缩放等功能会自动运行 由于应用程序开发人员处理设置,因此有更多机会自定义配置,例如启用输出图像旋转或在 ImageAnalysis 中设置输出图像格式
要求 PreviewView 用于相机预览,允许 CameraX 提供无缝的端到端集成,就像我们的 ML Kit 集成一样,它可以将 ML 模型结果坐标(如人脸边界框)直接映射到预览坐标 能够使用自定义 `Surface` 用于相机预览,允许更大的灵活性,例如使用您现有的 `Surface` 代码,这可能是您应用程序其他部分的输入

如果您在尝试迁移时遇到问题,请在 CameraX 讨论组 中与我们联系。

迁移之前

将 CameraX 与 Camera1 使用情况进行比较

尽管代码可能看起来不同,但 Camera1 和 CameraX 中的基本概念非常相似。CameraX 将常见的相机功能抽象为用例,因此,在 Camera1 中留给开发人员的许多任务会由 CameraX 自动处理。CameraX 中有四个 UseCase,您可以将其用于各种相机任务:PreviewImageCaptureVideoCaptureImageAnalysis

CameraX 为开发人员处理低级细节的一个示例是 ViewPort,它在活动 UseCase 之间共享。这确保所有 UseCase 都看到完全相同的像素。在 Camera1 中,您必须自己管理这些细节,并且考虑到设备相机传感器和屏幕纵横比的变化,确保预览与拍摄的照片和视频匹配可能很棘手。

另一个示例是 CameraX 自动处理 Lifecycle 回调,方法是将回调传递给它时执行 Lifecycle 实例。这意味着 CameraX 在整个 Android 活动生命周期 中处理您的应用程序与相机的连接,包括以下情况:当您的应用程序进入后台时关闭相机;当屏幕不再需要显示时移除相机预览;以及当另一个活动(如传入视频通话)优先获取前台时暂停相机预览。

最后,CameraX 处理旋转和缩放,而无需您额外编写任何代码。在方向解锁的 Activity 的情况下,每次设备旋转时都会完成 UseCase 设置,因为系统会在方向更改时销毁并重新创建 Activity。这会导致 UseCases 默认情况下每次将其目标旋转设置为与显示器的方向匹配。详细了解 CameraX 中的旋转

在深入了解细节之前,以下是 CameraX 的 UseCase 的高级概述以及 Camera1 应用程序如何相关。(CameraX 概念以 蓝色 表示,而 Camera1 概念以 绿色 表示。)

CameraX

CameraController / CameraProvider 配置
预览 ImageCapture VideoCapture ImageAnalysis
管理预览 Surface 并将其设置在相机上 设置 PictureCallback 并调用相机上的 takePicture() 以特定顺序管理相机和 MediaRecorder 配置 基于预览 Surface 构建的自定义分析代码
设备特定代码
设备旋转和缩放管理
相机会话管理(相机选择、生命周期管理)

Camera1

CameraX 中的兼容性和性能

CameraX 支持运行 Android 5.0(API 级别 21) 及更高版本的设备。这代表了超过 98% 的现有 Android 设备。CameraX 旨在自动处理设备之间的差异,减少应用程序中需要编写设备特定代码的数量。此外,我们在 CameraX 测试实验室 中对 Android 5.0 之后的每个 Android 版本上的 150 多台物理设备进行了测试。您可以查看 当前在测试实验室中的设备 的完整列表。

CameraX 使用 Executor 来驱动相机堆栈。如果您应用程序有特定的线程要求,您可以 在 CameraX 上设置您自己的执行器。如果未设置,CameraX 会创建和使用优化的默认内部 Executor。CameraX 所基于的许多平台 API 需要与硬件进行阻塞的进程间通信(IPC),而硬件有时可能需要数百毫秒才能响应。因此,CameraX 仅从后台线程调用这些 API,这可以确保主线程不被阻塞,并且 UI 保持流畅。详细了解线程

如果您的应用程序的目标市场包括低端设备,CameraX 提供了一种使用 相机限制器 来缩短设置时间的方法。由于连接到硬件组件的过程可能需要相当长的时间,尤其是在低端设备上,因此您可以指定应用程序所需的相机集。CameraX 只会在设置过程中连接到这些相机。例如,如果应用程序只使用后置摄像头,它可以使用 DEFAULT_BACK_CAMERA 设置此配置,然后 CameraX 避免初始化前置摄像头,以减少延迟。

Android 开发概念

本指南假设您对 Android 开发有一定的了解。除了基础知识之外,以下是一些在深入了解以下代码之前需要了解的概念

  • 视图绑定 为您的 XML 布局文件生成绑定类,允许您轻松地 在活动中引用视图,就像以下几个代码段中所做的那样。在 视图绑定和 findViewById()(以前引用视图的方式)之间存在一些差异,但在下面的代码中,您可以将视图绑定行替换为类似的 findViewById() 调用。
  • 异步协程 是在 Kotlin 1.3 中添加的一种并发设计模式,可用于处理返回 ListenableFuture 的 CameraX 方法。从 1.1.0 版本开始,Jetpack 并发 库使这变得更加容易。要向您的应用程序添加异步协程
    1. 在您的 Gradle 文件中添加 implementation("androidx.concurrent:concurrent-futures-ktx:1.1.0")
    2. 将返回 ListenableFuture 的任何 CameraX 代码放入 launch 块或 挂起函数 中。
    3. 在返回 ListenableFuture 的函数调用中添加一个 await() 调用。
    4. 要更深入地了解协程的工作原理,请查看 启动协程 指南。

迁移常见场景

本节说明如何将常见场景从 Camera1 迁移到 CameraX。每个场景都包含 Camera1 实现、CameraX CameraProvider 实现和 CameraX CameraController 实现。

选择相机

在您的相机应用程序中,您可能想要提供的第一个功能之一就是选择不同的相机。

Camera1

在 Camera1 中,您可以使用 Camera.open() 并无参数来打开第一个后置摄像头,或者可以传入您要打开的相机的整数 ID。以下是如何实现此功能的示例

// 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 使使用默认相机变得容易。您可以指定需要默认前置摄像头还是默认后置摄像头。此外,CameraX 的 CameraControl 对象让您可以轻松地 设置应用程序的缩放级别,因此,如果您的应用程序在支持 逻辑相机 的设备上运行,那么它将切换到正确的镜头。

以下是使用 CameraController 的默认后置相机的 CameraX 代码

// 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 选择默认前置相机的示例(前置或后置摄像头都可以与 CameraControllerCameraProvider 一起使用)

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

如果您希望控制选择哪个相机,这也是可能的。如果您使用 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 都要求您正确处理旋转和缩放。另一方面,CameraX 的 PreviewViewTextureViewSurfaceView 都有底层实现。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

使用 CameraX 的 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()
    }
}

点触对焦

当相机预览出现在屏幕上时,常见的控制方法是在用户点击预览时设置焦点点。

Camera1

要实现 Camera1 中的点触对焦,您必须计算最佳焦点 Area 以指示 Camera 应该尝试对焦的位置。此 Area 将传递给 setFocusAreas()。此外,您必须在 Camera 上设置兼容的对焦模式。只有当当前对焦模式为 FOCUS_MODE_AUTOFOCUS_MODE_MACROFOCUS_MODE_CONTINUOUS_VIDEOFOCUS_MODE_CONTINUOUS_PICTURE 时,焦点区域才会起作用。

每个 Area 都是一个具有指定权重的矩形。权重是 1 到 1000 之间的数值,它用于在设置多个焦点区域时优先考虑焦点区域。此示例仅使用一个区域,因此权重值无关紧要。矩形的坐标范围为 -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() 启用和禁用点触对焦,并使用相应的 getter 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. 使用点击事件,使用 MeteringPointFactory.createPoint() 创建一个 MeteringPoint
  3. 使用 MeteringPoint,创建一个 FocusMeteringAction
  4. 使用 Camera 上的 CameraControl 对象(从 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
}

捏合缩放

放大和缩小预览是另一种常见的相机预览直接操作。随着设备上相机数量的增加,用户还期望在缩放时自动选择具有最佳焦距的镜头。

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() 启用和禁用捏合缩放,并使用相应的 getter isPinchToZoomEnabled() 检查值。

getZoomState() 方法返回一个 LiveData 对象,用于跟踪 CameraControllerZoomState 的变化。

// 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. Camera.CameraInfo 对象中获取 ZoomState,其中 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
}

拍照

本节介绍如何触发拍照,无论您是在快门按钮按下、计时器到期还是您选择的任何其他事件后进行拍照。

Camera1

在 Camera1 中,您首先定义一个 Camera.PictureCallback 来在请求时管理图片数据。以下是一个处理 JPEG 图像数据的简单 PictureCallback 示例。

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

然后,无论何时您想拍照,您都会在您的 Camera 实例上调用 takePicture() 方法。此 takePicture() 方法有三个不同的参数用于不同的数据类型。第一个参数用于 ShutterCallback(在此示例中未定义)。第二个参数用于 PictureCallback 处理原始(未压缩)相机数据。第三个参数是此示例使用的参数,因为它是一个用于处理 JPEG 图像数据的 PictureCallback

// Camera1: call takePicture on Camera instance, passing our PictureCallback.

camera?.takePicture(null, null, picture)

CameraX:CameraController

CameraX 的 CameraController 通过实现自己的 takePicture() 方法保持 Camera1 的简单性。在这里,定义一个函数来配置 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 录制视频需要仔细管理 CameraMediaRecorder,并且必须按特定顺序调用方法。您 **必须** 遵循此顺序才能使您的应用程序正常工作。

  1. 打开相机。
  2. 准备并启动预览(如果您的应用程序显示正在录制的视频,这通常是这种情况)。
  3. 通过调用 Camera.unlock() 解锁 MediaRecorder 使用的相机。
  4. 通过在 MediaRecorder 上调用以下方法来配置录制
    1. 使用 setCamera(camera) 连接您的 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.reset() 删除当前的 MediaRecorder 配置。
    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 的 CameraController,您可以独立切换 ImageCaptureVideoCaptureImageAnalysis UseCase只要 UseCase 列表可以同时使用。默认情况下,ImageCaptureImageAnalysis UseCase 已启用,这就是您不需要为拍照调用 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 参数,允许您在视频中启用或禁用音频录制。要启用音频录制,您需要确保已获得麦克风权限。此外,在 1.3.0-alpha02 中,stopRecording() 方法已删除,startRecording() 返回一个 Recording 对象,可用于暂停、恢复和停止视频录制。

// CameraX: implement video capture with CameraController.

private val FILENAME_FORMAT = "yyyy-MM-dd-HH-mm-ss-SSS"

// Define a VideoSaveCallback class for handling success and error states.
class VideoSaveCallback : OnVideoSavedCallback {
    override fun onVideoSaved(outputFileResults: OutputFileResults) {
        val msg = "Video capture succeeded: ${outputFileResults.savedUri}"
        Toast.makeText(baseContext, msg, Toast.LENGTH_SHORT).show()
        Log.d(TAG, msg)
    }

    override fun onError(videoCaptureError: Int, message: String,
                         cause: Throwable?) {
        Log.d(TAG, "error saving video: $message", cause)
    }
}

private fun startStopVideo() {
    if (cameraController.isRecording()) {
        // Stop the current recording session.
        cameraController.stopRecording()
        return
    }

    // Define the File options for saving the video.
    val name = SimpleDateFormat(FILENAME_FORMAT, Locale.US)
        .format(System.currentTimeMillis())

    val outputFileOptions = OutputFileOptions
        .Builder(File(this.filesDir, name))
        .build()

    // Call startRecording on the CameraController.
    cameraController.startRecording(
        outputFileOptions,
        ContextCompat.getMainExecutor(this),
        VideoSaveCallback()
    )
}

CameraX:CameraProvider

如果您使用的是 CameraProvider,则需要创建一个 VideoCapture UseCase 并传入一个 Recorder 对象。在 Recorder.Builder 上,您可以设置视频质量,并且可以选择一个 FallbackStrategy,该策略处理设备无法满足您所需质量规格的情况。然后将 VideoCapture 实例与其他 UseCase 绑定到 CameraProvider

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

此时,可以在 videoCapture.output 属性上访问 RecorderRecorder 可以启动保存到 FileParcelFileDescriptorMediaStore 的视频录制。此示例使用 MediaStore

Recorder 上,有几种方法可以调用来准备它。调用 prepareRecording() 来设置 MediaStore 输出选项。如果您的应用有权使用设备的麦克风,请也调用 withAudioEnabled()。然后,调用 start() 开始录制,传入上下文和 Consumer<VideoRecordEvent> 事件监听器来处理视频录制事件。如果成功,返回的 Recording 可用于暂停、恢复或停止录制。

// CameraX: implement video capture with CameraProvider.

private val FILENAME_FORMAT = "yyyy-MM-dd-HH-mm-ss-SSS"

private fun startStopVideo() {
   val videoCapture = this.videoCapture ?: return

   if (recording != null) {
       // Stop the current recording session.
       recording.stop()
       recording = null
       return
   }

   // Create and start a new recording session.
   val name = SimpleDateFormat(FILENAME_FORMAT, Locale.US)
       .format(System.currentTimeMillis())
   val contentValues = ContentValues().apply {
       put(MediaStore.MediaColumns.DISPLAY_NAME, name)
       put(MediaStore.MediaColumns.MIME_TYPE, "video/mp4")
       if (Build.VERSION.SDK_INT > Build.VERSION_CODES.P) {
           put(MediaStore.Video.Media.RELATIVE_PATH, "Movies/CameraX-Video")
       }
   }

   val mediaStoreOutputOptions = MediaStoreOutputOptions
       .Builder(contentResolver, MediaStore.Video.Media.EXTERNAL_CONTENT_URI)
       .setContentValues(contentValues)
       .build()

   recording = videoCapture.output
       .prepareRecording(this, mediaStoreOutputOptions)
       .withAudioEnabled()
       .start(ContextCompat.getMainExecutor(this)) { recordEvent ->
           when(recordEvent) {
               is VideoRecordEvent.Start -> {
                   viewBinding.videoCaptureButton.apply {
                       text = getString(R.string.stop_capture)
                       isEnabled = true
                   }
               }
               is VideoRecordEvent.Finalize -> {
                   if (!recordEvent.hasError()) {
                       val msg = "Video capture succeeded: " +
                           "${recordEvent.outputResults.outputUri}"
                       Toast.makeText(
                           baseContext, msg, Toast.LENGTH_SHORT
                       ).show()
                       Log.d(TAG, msg)
                   } else {
                       recording?.close()
                       recording = null
                       Log.e(TAG, "video capture ends with error",
                             recordEvent.error)
                   }
                   viewBinding.videoCaptureButton.apply {
                       text = getString(R.string.start_capture)
                       isEnabled = true
                   }
               }
           }
       }
}

其他资源

我们在 Camera Samples GitHub 仓库 中提供了一些完整的 CameraX 应用。这些示例向您展示了本指南中的场景如何在完整的 Android 应用中发挥作用。

如果您在迁移到 CameraX 时需要更多支持,或者对 Android 相机 API 套件有任何疑问,请在 CameraX 讨论组 中与我们联系。