将 Camera1 迁移到 CameraX

如果您的应用使用自 Android 5.0(API 级别 21)起已废弃的原始 Camera 类(“Camera1”),我们强烈建议您更新到新型 Android Camera API。Android 提供 CameraXCamera2。CameraX 是一种强大的标准化 Jetpack Camera API,Camera2 是一种低层级的框架 API。对于绝大多数情况,我们建议您将应用迁移到 CameraX。原因如下:

  • 易于使用:CameraX 能够处理低层级细节,因此您可以减少在从零开始构建相机体验方面所花的精力,从而将更多精力用于完善自己的应用,使其能够脱颖而出。
  • CameraX 可为您处理碎片化问题:CameraX 可以减少长期维护费用和设备专属代码,为用户带来更优质的体验。如需了解更多详情,请参阅我们的使用 CameraX 提升设备兼容性博文。
  • 高级功能:CameraX 经过精心设计,可让您轻松地将高级功能整合到自己的应用中。例如,您可以通过 CameraX 扩展程序轻松地为照片应用焦外成像、脸部照片修复、HDR(高动态范围)和低光照夜间拍摄模式。
  • 可更新性:Android 会持续不断地针对 CameraX 发布新功能和 bug 修复程序。迁移到 CameraX 后,每当有 CameraX 内容发布时,您的应用都会获得最新的 Android 相机技术,而不仅仅是在每年 Android 版本发布时才会获得此类技术。

在本指南中,我们将介绍相机应用的常见场景。每个场景都包含 Camera1 实现和 CameraX 实现,以进行比较。

在进行迁移时,有时您需要更高的灵活性来与现有代码库集成。本指南中的所有 CameraX 代码都有 CameraController 实现和 CameraProvider 实现。如果您想以最简单的方式使用 CameraX,前者是不错的选择;如果您需要更高的灵活性,后者则非常适合您。为了帮助您确定哪种实现适合您,下面列出了每种实现的优势:

CameraController

CameraProvider

需要很少的设置代码 允许有更大的控制权
允许 CameraX 处理更多设置流程,这意味着点按对焦和双指张合缩放等功能可自动工作 由于应用开发者负责处理设置,因此有更多机会自定义配置,例如在 ImageAnalysis 中启用输出图片旋转功能或设置输出图片格式
必须使用 PreviewView 进行摄像头预览,从而可使 CameraX 提供顺畅的端到端集成,就像在我们的机器学习套件集成中那样,后者可将机器学习模型结果坐标(例如人脸边界框)直接映射到预览坐标 能够使用自定义“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 配置
Preview ImageCapture VideoCapture ImageAnalysis
管理预览 Surface,并在 Camera 上设置它 设置 PictureCallback,并在 Camera 上调用 takePicture() 按特定顺序管理 Camera 和 MediaRecorder 配置 基于预览 Surface 构建的自定义分析代码
设备专属代码
设备旋转和缩放管理
摄像头会话管理(摄像头选择、生命周期管理)

Camera1

CameraX 中的兼容性和性能

CameraX 支持搭载 Android 5.0(API 级别 21)及更高版本的设备。这涵盖了超过 98% 的现有 Android 设备。CameraX 经过精心设计,能够自动处理设备之间的差异,从而减少应用中对设备专属代码的需求。此外,我们正在 CameraX Test Lab 中测试超过 150 种实体设备,涵盖了自 5.0 以来的所有 Android 版本。您可以查看正在 Test Lab 中接受测试的设备的完整列表。

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

如果您的应用的目标市场包括低端设备,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()。然后,您可以将其转换为 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 作为预览 surface。无论您选择哪一项,都需要进行权衡取舍,并且 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 之间的值,如果设置了多个权重,则用于确定对焦 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() 来启用和停用点按对焦功能,并通过相应的 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(),以确保您需要的相关缩放方法在您的 Camera 上可用。

为了实现双指张合缩放功能,此示例使用了 setZoom(),因为预览 surface 上的触摸监听器会在双指张合手势发生时持续触发事件,这样一来,每次都会立即更新缩放级别。ZoomTouchListener 类定义如下,且应设置为对预览 surface 的触摸监听器的回调。

// 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 对象,用于跟踪 CameraController 上的 ZoomState 更改。

// 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,当您调用 bindToLifecycle() 时,系统会返回 Camera 实例。
  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,以便在收到照片数据请求时管理照片数据。下面是一个简单的 PictureCallback 示例,用于处理 JPEG 图片数据:

// Camera1: define a Camera.PictureCallback to handle JPEG data.

private val picture = Camera.PictureCallback { data, _ ->
    val pictureFile: File = getOutputMediaFile(MEDIA_TYPE_IMAGE) ?: run {
        Log.d(TAG,
              "error creating media file, check storage permissions")
        return@PictureCallback
    }

    try {
        val fos = FileOutputStream(pictureFile)
        fos.write(data)
        fos.close()
    } catch (e: FileNotFoundException) {
        Log.d(TAG, "file not found", e)
    } catch (e: IOException) {
        Log.d(TAG, "error accessing file", e)
    }
}

然后,无论您何时想要拍照,都可以对 Camera 实例调用 takePicture() 方法。takePicture() 方法有三个不同的参数,分别对应于不同的数据类型。第一个参数对应于 ShutterCallback(此示例中未定义)。第二个参数对应于 PictureCallback,用于处理原始(未压缩)的摄像头数据。本示例中使用了第三个参数,因为它是 PictureCallback,用于处理 JPEG 图片数据。

// 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 参数来启用或停用视频录音功能。如需启用录音功能,您需要确保自己拥有麦克风使用权限。此外,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
                   }
               }
           }
       }
}

其他资源

我们的相机示例 GitHub 代码库中提供了一些完整的 CameraX 应用。这些示例展示了如何将本指南中介绍的场景纳入到完善的 Android 应用中。

如果您在迁移到 CameraX 方面需要更多支持,或有 Android 相机 API 套件方面的问题,请通过 CameraX 论坛与我们联系。