一、为什么我们需要CameraX?

如果你曾经尝试过在Android上开发相机功能,可能会觉得有点头疼。原生的Camera API虽然强大,但代码复杂,不同厂商的设备表现还不一致,适配起来就像在走钢丝。后来有了Camera2 API,功能更精细,但学习曲线也更陡峭,写个简单的预览都要一大堆代码。

这时候,CameraX就像一位救星出现了。它是Google推出的Jetpack组件库的一员,专门为了简化相机开发而生。你可以把它理解为一个“相机管家”。你只需要告诉它:“我想预览”、“我想拍照”或者“我想分析画面”,它就会帮你处理好背后所有繁琐的事情,比如生命周期管理、设备兼容性、相机操作线程等等。它基于Camera2,但提供了更简单、更一致的API,让我们能更专注于业务逻辑,而不是和设备碎片化做斗争。

简单来说,如果你想快速、稳定地为应用添加相机功能,CameraX是目前最值得推荐的选择。

二、搭建环境与基础准备

在开始写代码之前,我们得先把“厨房”准备好。首先,确保你的build.gradle文件里已经添加了必要的依赖。CameraX的版本更新较快,建议使用最新的稳定版。

技术栈:Android (Kotlin + CameraX)

// 在app模块的build.gradle文件中添加依赖
dependencies {
    // CameraX 核心库
    def camerax_version = "1.3.0"
    implementation "androidx.camera:camera-core:${camerax_version}"
    implementation "androidx.camera:camera-camera2:${camerax_version}"
    // 如果你需要使用生命周期自动管理
    implementation "androidx.camera:camera-lifecycle:${camerax_version}"
    // 预览视图,这是实现预览的关键
    implementation "androidx.camera:camera-view:1.3.0"
    // 其他可能用到的扩展,比如视频拍摄
    // implementation "androidx.camera:camera-video:${camerax_version}"
}

然后,别忘了在AndroidManifest.xml文件中声明相机权限。一个完整的相机应用通常需要这些权限:

<manifest ...>
    <!-- 使用相机所需的权限 -->
    <uses-permission android:name="android.permission.CAMERA" />
    <!-- 如果需要将照片保存到外部存储,还需要这个 -->
    <uses-feature android:name="android.hardware.camera" android:required="true" />

    <application ...>
        ...
    </application>
</manifest>

权限的动态申请是Android开发的基础课,这里我们假设你已经处理好运行时权限请求的逻辑。准备好这些,我们的舞台就搭好了。

三、实现相机预览:让画面动起来

相机预览是用户最直观的感受,也是所有相机操作的基础。在CameraX中,我们使用Preview用例来实现它。这个过程就像搭积木:配置预览用例 -> 绑定到相机生命周期 -> 将画面显示到屏幕上。

技术栈:Android (Kotlin + CameraX)

class CameraPreviewActivity : AppCompatActivity() {
    // 1. 声明关键组件
    private lateinit var previewView: PreviewView // 用于显示预览画面的视图
    private var cameraProviderFuture: ListenableFuture<ProcessCameraProvider>? = null

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_camera_preview)

        // 2. 初始化预览视图
        previewView = findViewById(R.id.preview_view)

        // 3. 获取相机提供者的未来对象(异步操作)
        cameraProviderFuture = ProcessCameraProvider.getInstance(this)

        // 4. 添加监听器,当相机提供者准备就绪后执行绑定
        cameraProviderFuture?.addListener(Runnable {
            // 获取相机提供者实例
            val cameraProvider = cameraProviderFuture?.get() ?: return@Runnable

            // 5. 创建并配置预览用例
            val preview = Preview.Builder()
                .build()
                .also {
                    // 设置SurfaceProvider,将预览画面输出到previewView
                    it.setSurfaceProvider(previewView.surfaceProvider)
                }

            // 6. 选择后置摄像头作为默认摄像头
            val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA

            try {
                // 7. 解除所有已绑定用例,准备重新绑定
                cameraProvider.unbindAll()

                // 8. 将用例绑定到当前Activity的生命周期
                // 这行代码是核心:相机生命周期将自动与Activity同步(启动、停止、销毁)
                cameraProvider.bindToLifecycle(
                    this, // 生命周期所有者
                    cameraSelector, // 摄像头选择器
                    preview // 要绑定的用例(这里只有预览)
                )
            } catch (exc: Exception) {
                Log.e("CameraX", "用例绑定失败", exc)
            }
        }, ContextCompat.getMainExecutor(this)) // 在主线程执行监听器
    }
}

对应的布局文件activity_camera_preview.xml非常简单:

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <!-- CameraX提供的专用预览视图,自动处理画面拉伸和旋转 -->
    <androidx.camera.view.PreviewView
        android:id="@+id/preview_view"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

运行这段代码,你应该就能看到后置摄像头的实时画面了。CameraX的PreviewView会自动处理屏幕旋转和不同尺寸的适配,省去了我们很多计算工作。这里的关键是bindToLifecycle()方法,它建立了相机与UI生命周期的关联,当界面不可见时,相机会自动释放,避免资源浪费和潜在错误。

四、实现拍照功能:捕捉精彩瞬间

有了预览,接下来就是拍照了。在CameraX中,拍照功能由ImageCapture用例负责。我们可以把它和预览用例一起绑定到相机上,让相机同时服务于预览和拍照。

技术栈:Android (Kotlin + CameraX)

class CameraCaptureActivity : AppCompatActivity() {
    private lateinit var previewView: PreviewView
    private lateinit var btnCapture: Button
    private var cameraProviderFuture: ListenableFuture<ProcessCameraProvider>? = null
    private var imageCapture: ImageCapture? = null // 新增:拍照用例

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_camera_capture)

        previewView = findViewById(R.id.preview_view)
        btnCapture = findViewById(R.id.btn_capture)
        cameraProviderFuture = ProcessCameraProvider.getInstance(this)

        cameraProviderFuture?.addListener(Runnable {
            val cameraProvider = cameraProviderFuture?.get() ?: return@Runnable

            // 1. 创建预览用例(和之前一样)
            val preview = Preview.Builder()
                .build()
                .also { it.setSurfaceProvider(previewView.surfaceProvider) }

            // 2. 创建拍照用例,并配置高质量输出
            imageCapture = ImageCapture.Builder()
                .setCaptureMode(ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY) // 模式:最小化延迟
                .setTargetRotation(previewView.display.rotation) // 设置目标旋转,与预览一致
                .build()

            val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA

            try {
                cameraProvider.unbindAll()
                // 3. 将预览和拍照两个用例同时绑定到生命周期
                cameraProvider.bindToLifecycle(
                    this,
                    cameraSelector,
                    preview,
                    imageCapture // 绑定拍照用例
                )
            } catch (exc: Exception) {
                Log.e("CameraX", "用例绑定失败", exc)
            }
        }, ContextCompat.getMainExecutor(this))

        // 4. 为拍照按钮设置点击事件
        btnCapture.setOnClickListener {
            takePhoto()
        }
    }

    private fun takePhoto() {
        // 获取拍照用例实例,确保已初始化
        val imageCapture = this.imageCapture ?: return

        // 5. 创建存储照片的元数据
        val name = "JPEG_${System.currentTimeMillis()}.jpg"
        val contentValues = ContentValues().apply {
            put(MediaStore.MediaColumns.DISPLAY_NAME, name)
            put(MediaStore.MediaColumns.MIME_TYPE, "image/jpeg")
            // 如果针对Android 10及以上,可以指定相对路径
            if (Build.VERSION.SDK_INT > Build.VERSION_CODES.P) {
                put(MediaStore.Images.Media.RELATIVE_PATH, "Pictures/CameraX-Demo")
            }
        }

        // 6. 创建输出选项,指定将照片保存到MediaStore
        val outputOptions = ImageCapture.OutputFileOptions.Builder(
            contentResolver,
            MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
            contentValues
        ).build()

        // 7. 执行拍照操作(这是一个异步操作)
        imageCapture.takePicture(
            outputOptions,
            ContextCompat.getMainExecutor(this),
            object : ImageCapture.OnImageSavedCallback {
                // 拍照成功回调
                override fun onImageSaved(outputFileResults: ImageCapture.OutputFileResults) {
                    val savedUri = outputFileResults.savedUri
                    val msg = "照片保存成功: ${savedUri ?: "未知URI"}"
                    Toast.makeText(this@CameraCaptureActivity, msg, Toast.LENGTH_SHORT).show()
                    Log.d("CameraX", msg)
                }

                // 拍照失败回调
                override fun onError(exception: ImageCaptureException) {
                    val msg = "拍照失败: ${exception.message}"
                    Toast.makeText(this@CameraPreviewActivity, msg, Toast.LENGTH_SHORT).show()
                    Log.e("CameraX", msg, exception)
                }
            }
        )
    }
}

在这个例子中,我们创建了一个ImageCapture用例,并通过bindToLifecycle将它和Preview用例一起绑定。这意味着相机可以同时处理预览流和拍照请求。当用户点击按钮时,takePhoto()方法被调用,它配置了照片的输出位置(这里是系统的相册目录),然后异步执行拍照。拍照完成后,无论成功或失败,都会在对应的回调中通知我们。

你可以根据需要修改输出选项,比如保存到应用私有目录、自定义文件名等。ImageCapture还支持内存中回调(takePicture(Executor, OnImageCapturedCallback)),让你直接获取到图像字节数据进行处理,这在需要上传或即时识别的场景非常有用。

五、应对常见核心问题与进阶技巧

在实际开发中,你可能会遇到一些典型问题。这里分享几个常见场景的解决方案。

1. 预览画面拉伸或变形 这通常是因为预览视图的宽高比与相机传感器输出不匹配。PreviewView提供了几种缩放类型来应对:

previewView.scaleType = PreviewView.ScaleType.FILL_CENTER // 填充视图,可能裁剪画面
// 或者
previewView.scaleType = PreviewView.ScaleType.FIT_CENTER // 适应视图,可能留黑边

通常,FIT_CENTER能保证画面完整,而FILL_CENTER能保证视图被填满。你可以根据UI设计来选择。

2. 前后摄像头切换 切换摄像头本质上是更换CameraSelector并重新绑定。

技术栈:Android (Kotlin + CameraX)

// 假设有一个切换按钮 btnSwitch
private var isBackCamera = true // 标记当前是否为后置摄像头

btnSwitch.setOnClickListener {
    isBackCamera = !isBackCamera
    switchCamera()
}

private fun switchCamera() {
    val cameraProvider = cameraProviderFuture?.get() ?: return
    val newSelector = if (isBackCamera) {
        CameraSelector.DEFAULT_BACK_CAMERA
    } else {
        CameraSelector.DEFAULT_FRONT_CAMERA
    }

    // 重新配置并绑定用例
    val preview = Preview.Builder().build().apply {
        setSurfaceProvider(previewView.surfaceProvider)
    }
    imageCapture = ImageCapture.Builder().build() // 重建拍照用例以应用新的摄像头参数

    try {
        cameraProvider.unbindAll()
        cameraProvider.bindToLifecycle(this, newSelector, preview, imageCapture)
    } catch (e: Exception) {
        Log.e("CameraX", "摄像头切换失败", e)
    }
}

3. 对焦与变焦 CameraX提供了简洁的API来控制这些功能。你可以通过CameraControl对象来操作。

// 在成功绑定相机后,可以通过camera对象获取CameraControl
// val camera = cameraProvider.bindToLifecycle(...) // bindToLifecycle会返回Camera对象
// val cameraControl = camera.cameraControl

// 设置线性对焦区域(例如,点击屏幕某点对焦)
// cameraControl.startFocusAndMetering(FocusMeteringAction.Builder(point, MeteringMode.AF).build())

// 设置变焦比例(1.0f为原始大小)
// cameraControl.setZoomRatio(2.0f)

在实际应用中,你可以在PreviewView上添加触摸监听,将触摸点坐标转换为对焦点。

六、应用场景、优缺点与注意事项

应用场景 CameraX非常适合需要快速集成稳定相机功能的应用,例如:

  • 社交应用:用于拍摄并分享照片、短视频。
  • 工具类应用:扫码、文档扫描、测量工具等。
  • 教育类应用:在线作业拍照上传、实验记录等。
  • 企业应用:工单拍照、现场巡检记录等。

在这些场景中,开发者更关注业务逻辑而非底层相机控制,CameraX的高层抽象能极大提升开发效率。

技术优缺点 优点

  1. 简单易用:API设计直观,大幅减少了样板代码。
  2. 生命周期感知:自动管理相机开启和关闭,避免内存泄漏和错误。
  3. 设备兼容性:通过供应商扩展库,在不同厂商设备上提供一致体验,解决了碎片化难题。
  4. 用例组合:可以轻松组合预览、拍照、分析等多个用例,功能强大。

缺点

  1. 灵活性相对受限:对于需要极精细控制相机参数(如手动对焦、长时间曝光、RAW拍摄)的高级专业应用,原生Camera2 API可能更合适。
  2. 新特性支持有延迟:CameraX需要时间适配Android和硬件厂商的最新相机特性。

注意事项

  1. 权限是前提:务必处理好运行时权限申请,并在onRequestPermissionsResult中根据授权结果初始化或关闭相机。
  2. 后台限制:当应用退到后台,CameraX会自动暂停相机。从后台返回时,PreviewView可能需要一点时间重新渲染画面,这是正常现象。
  3. 测试多设备:虽然CameraX解决了大部分兼容性问题,但仍建议在几种不同品牌、不同系统的真机上进行测试,特别是前后摄像头切换和闪光灯功能。
  4. 资源清理:虽然bindToLifecycle能自动管理,但在Activity/Fragment销毁时,确保不再持有对PreviewViewImageCapture等对象的引用是一个好习惯。

七、总结

通过本文的探索,我们可以看到CameraX将Android相机开发从一项复杂任务变成了一个相对愉快的过程。它通过“用例”这一核心概念,将预览、拍照、分析等常见需求模块化,让我们能够像搭积木一样构建相机功能。其强大的生命周期管理和设备兼容性处理,更是为我们扫清了开发路上的主要障碍。

从简单的预览到完整的拍照流程,再到摄像头切换等进阶控制,CameraX都提供了清晰而稳健的API。尽管它在超高级控制上有所取舍,但对于绝大多数应用场景来说,它提供的功能、稳定性和开发效率的提升是无可比拟的。

建议你在下一个需要相机功能的项目中尝试CameraX,从实现一个简单的预览开始,逐步添加拍照、闪光灯控制等功能,亲身体验它带来的便捷。相信它会成为你Android开发工具箱中得力的一员。