将 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 发布新的功能和错误修复。通过迁移到 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 并将其设置在 Camera 上 设置 PictureCallback 并调用 Camera 上的 takePicture() 以特定顺序管理 Camera 和 MediaRecorder 配置 构建在预览 Surface 之上的自定义分析代码
设备特定代码
设备旋转和缩放管理
相机会话管理(相机选择、生命周期管理)

Camera1

CameraX 中的兼容性和性能

CameraX 支持运行 Android 5.0(API 级别 21) 和更高版本的设备。这代表了超过 98% 的现有 Android 设备。CameraX 旨在自动处理设备之间的差异,从而减少应用中对设备特定代码的需求。此外,我们在我们的 CameraX 测试实验室 中对自 5.0 以来的所有 Android 版本的 150 多台物理设备进行了测试。您可以查看 目前测试实验室中的设备完整列表

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

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

Android 开发概念

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

  • 视图绑定 为您的 XML 布局文件生成绑定类,允许您轻松地 在 Activity 中引用您的视图,如下面的几个代码片段中所示。与 findViewById()(以前引用视图的方法)相比,有一些 视图绑定和 findViewById() 之间的区别,但在下面的代码中,您应该能够用类似的 findViewById() 调用替换视图绑定行。
  • 异步协程 是在 Kotlin 1.3 中添加的一种并发设计模式,可用于处理返回 ListenableFuture 的 CameraX 方法。自 1.1.0 版本起,Jetpack 并发 库使这变得更容易。要向您的应用添加异步协程:
    1. implementation("androidx.concurrent:concurrent-futures-ktx:1.1.0") 添加到您的 Gradle 文件中。
    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,在CameraX中也可以做到这一点,方法是调用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的PreviewView同时具有TextureViewSurfaceView的底层实现。CameraX会根据设备类型和应用程序运行的Android版本等因素决定哪个实现最佳。如果任一实现兼容,您可以使用PreviewView.ImplementationMode声明您的首选项。COMPATIBLE选项使用TextureView进行预览,而PERFORMANCE值使用SurfaceView(如果可能)。

Camera1

要显示预览,您需要使用android.view.SurfaceHolder.Callback接口的实现编写您自己的Preview类,该接口用于将图像数据从相机硬件传递到应用程序。然后,在您可以启动实时图像预览之前,必须将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之间的值,用于优先考虑焦点Areas(如果设置了多个)。此示例仅使用一个Area,因此权重值无关紧要。矩形的坐标范围为-1000到1000。左上角点为(-1000, -1000)。右下角点为(1000, 1000)。方向相对于传感器方向,即传感器所看到的内容。方向不受Camera.setDisplayOrientation()的旋转或镜像影响,因此您需要将触摸事件坐标转换为传感器坐标。

// Camera1: implement tap-to-focus.

class TapToFocusHandler : Camera.AutoFocusCallback {
    private fun handleFocus(event: MotionEvent) {
        val camera = camera ?: return
        val parameters = try {
            camera.getParameters()
        } catch (e: RuntimeException) {
            return
        }

        // Cancel previous auto-focus function, if one was in progress.
        camera.cancelAutoFocus()

        // Create focus Area.
        val rect = calculateFocusAreaCoordinates(event.x, event.y)
        val weight = 1  // This value's not important since there's only 1 Area.
        val focusArea = Camera.Area(rect, weight)

        // Set the focus parameters.
        parameters.setFocusMode(Parameters.FOCUS_MODE_AUTO)
        parameters.setFocusAreas(listOf(focusArea))

        // Set the parameters back on the camera and initiate auto-focus.
        camera.setParameters(parameters)
        camera.autoFocus(this)
    }

    private fun calculateFocusAreaCoordinates(x: Int, y: Int) {
        // Define the size of the Area to be returned. This value
        // should be optimized for your app.
        val focusAreaSize = 100

        // You must define functions to rotate and scale the x and y values to
        // be values between 0 and 1, where (0, 0) is the upper left-hand side
        // of the preview, and (1, 1) is the lower right-hand side.
        val normalizedX = (rotateAndScaleX(x) - 0.5) * 2000
        val normalizedY = (rotateAndScaleY(y) - 0.5) * 2000

        // Calculate the values for left, top, right, and bottom of the Rect to
        // be returned. If the Rect would extend beyond the allowed values of
        // (-1000, -1000, 1000, 1000), then crop the values to fit inside of
        // that boundary.
        val left = max(normalizedX - (focusAreaSize / 2), -1000)
        val top = max(normalizedY - (focusAreaSize / 2), -1000)
        val right = min(left + focusAreaSize, 1000)
        val bottom = min(top + focusAreaSize, 1000)

        return Rect(left, top, left + focusAreaSize, top + focusAreaSize)
    }

    override fun onAutoFocus(focused: Boolean, camera: Camera) {
        if (!focused) {
            Log.d(TAG, "tap-to-focus failed")
        }
    }
}

CameraX:CameraController

CameraController侦听PreviewView的触摸事件以自动处理轻触对焦。您可以使用setTapToFocusEnabled()启用和禁用轻触对焦,并使用相应的getterisTapToFocusEnabled()检查该值。

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

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()。请参阅本节中的 CameraController 代码,了解 takePhoto() 函数的完整示例。

// 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 实例与 MediaRecorder
    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,它处理设备无法满足您所需质量规格的情况。然后使用其他 UseCaseVideoCapture 实例绑定到 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 讨论组 与我们联系。