将 Camera1 迁移到 CameraX

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

  • 易用性: CameraX 处理低级细节,因此您可以将精力更多地放在应用差异化上,而不是从头构建相机体验。
  • CameraX 为您处理碎片化问题: CameraX 降低了长期维护成本和设备专用代码,为用户带来了更高质量的体验。有关此内容的更多信息,请参阅我们的提高 CameraX 设备兼容性博文。
  • 高级功能: CameraX 经过精心设计,可让高级功能轻松集成到您的应用中。例如,您可以使用 CameraX Extensions 轻松地将散景、人脸修饰、HDR(高动态范围)和弱光增亮夜间拍摄模式应用到照片中。
  • 可更新性: Android 全年都会向 CameraX 发布新功能和 bug 修复。通过迁移到 CameraX,您的应用可以随着每个 CameraX 版本获得最新的 Android 相机技术,而不仅仅是每年 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 为开发者处理低级细节的一个示例是活动 UseCase 之间共享的 ViewPort。这确保了所有 UseCase 都能看到完全相同的像素。在 Camera1 中,您必须自行管理这些细节,并且考虑到设备相机传感器和屏幕宽高比的可变性,要确保预览与捕获的照片和视频匹配可能很棘手。

另一个示例是,CameraX 会自动处理您传递给它的 Lifecycle 实例上的 Lifecycle 回调。这意味着 CameraX 在整个 Android Activity 生命周期期间处理您的应用与相机的连接,包括以下情况:当您的应用进入后台时关闭相机;当屏幕不再需要显示相机预览时移除相机预览;以及当另一个 Activity 获得前台优先级时(例如来电)暂停相机预览。

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

在深入了解细节之前,以下是 CameraX UseCase 的概览以及 Camera1 应用与它们的关系。(CameraX 概念为蓝色,Camera1 概念为绿色。)

CameraX

CameraController / CameraProvider 配置
预览 图像捕获 视频捕获 图像分析
管理预览 Surface 并在 Camera 上设置它 设置 PictureCallback 并在 Camera 上调用 takePicture() 按特定顺序管理 Camera 和 MediaRecorder 配置 基于预览 Surface 构建的自定义分析代码
设备专用代码
设备旋转和缩放管理
相机会话管理(相机选择、生命周期管理)

Camera1

CameraX 中的兼容性和性能

CameraX 支持运行 Android 5.0 (API 级别 21) 及更高版本的设备。这代表了现有 Android 设备中的 98% 以上。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 开发有大致了解。除了基础知识之外,在深入研究下面的代码之前,还有一些概念值得了解:

迁移常见场景

本节介绍了如何将常见场景从 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()
    }
}

如果您想控制选择哪个相机,这在 CameraX 中也是可能的,如果您使用 CameraProvider 并调用 getAvailableCameraInfos(),它会为您提供一个 CameraInfo 对象,用于检查某些相机属性,例如 isFocusMeteringSupported()。然后,您可以使用 CameraInfo.getCameraSelector() 方法将其转换为 CameraSelector,以便像上面示例中那样使用。

您可以使用 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()

显示预览

大多数相机应用都需要在某个时候在屏幕上显示相机Feed。使用 Camera1,您需要正确管理生命周期回调,并且还需要确定预览的旋转和缩放。

此外,在 Camera1 中,您需要决定是使用 TextureView 还是 SurfaceView 作为预览界面。两种选项都有权衡,并且在任何一种情况下,Camera1 都要求您正确处理旋转和缩放。另一方面,CameraX 的 PreviewView 具有 TextureViewSurfaceView 的底层实现。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 之间的值,用于在设置多个焦点 Area 时对其进行优先级排序。本示例只使用一个 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() 启用和禁用点击对焦,并使用相应的 getter isTapToFocusEnabled() 检查其值。

A 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(),以确保您的 Camera 上提供所需的缩放相关方法。

为了实现双指缩放,本示例使用 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() 检查其值。

A 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. 通过调用 Camera.lock() 锁定相机,以便将来的 MediaRecorder 会话可以使用它。
  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 参数,允许您启用或禁用视频的音频录制。要启用音频录制,您需要确保拥有麦克风权限。此外,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 实例与您的其他 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 示例 GitHub 仓库中有几个完整的 CameraX 应用。这些示例向您展示了本指南中的场景如何融入一个功能完备的 Android 应用。

如果您在迁移到 CameraX 方面需要额外支持,或者对 Android 相机 API 套件有疑问,请在 CameraX 讨论组上联系我们。