本文首发地址 https://h89.cn/archives/415.html

近期在禾苗通信面试中,他们的负责人介绍到,他们的相机开发都使用的是 CameraX 的API,暂不评估可行性,本文只介绍一下 CameraX 的API及相关使用。


什么是 CameraX?

CameraX 是 Android Jetpack 库套件中的一部分,旨在简化 Android 应用中的相机开发。它构建在复杂的 Camera2 API 之上,抽象了大量底层细节,为开发者提供了更易用、更一致的接口。CameraX 兼容性强,支持 Android 5.0 (API level 21) 及以上版本,并且会随着 Android 系统的演进而持续更新,确保应用在不同设备上的稳定性和性能。


为什么选择 CameraX?

  1. 易用性:CameraX 极大地降低了相机开发的复杂性。相较于直接使用 Camera2 API,它通过更简洁的 API 设计和生命周期管理,减少了大量样板代码。
  2. 一致性:解决了 Android 生态系统中设备碎片化导致的不同相机行为问题。CameraX 在内部处理了这些差异,确保应用在各种设备上都能提供一致的相机体验。
  3. 生命周期感知:CameraX 与 Android 生命周期(如 ActivityLifecycleOwner)紧密集成,自动处理相机的打开、关闭、释放等操作,避免了常见的内存泄漏和资源管理问题。
  4. 功能丰富:支持常见的相机用例,如拍照、录像、图像分析(条形码扫描、人脸识别等),并提供方便的配置选项。
  5. 扩展性:通过 Use Cases (用例) 的概念,开发者可以灵活组合不同功能,例如同时进行预览和拍照,或者预览和图像分析。

CameraX 的核心概念:用例 (Use Cases)

CameraX 的设计核心是“用例” (Use Cases),它将相机的功能划分为几个可独立或组合使用的模块。主要包括:

  • Preview (预览):用于在屏幕上显示相机实时取景。这是所有相机应用的基础。
  • Image Capture (拍照):用于捕获高分辨率的静态图像。
  • Image Analysis (图像分析):用于实时访问相机帧进行图像处理,如计算机视觉任务(条形码扫描、文本识别、人脸检测等)。
  • Video Capture (视频录制):用于录制视频。

开发者只需选择并配置所需的用例,然后将它们绑定到生命周期和相机选择器上。


CameraX 的基本使用步骤

下面是使用 CameraX 进行相机应用开发的基本流程:

  1. 添加依赖
    在你的 build.gradle (Module: app) 文件中添加 CameraX 库的依赖。通常需要 camera-corecamera-camera2(底层实现)和 camera-lifecycle(生命周期集成)。
dependencies {
    def camerax_version = "1.3.3" // 替换为最新稳定版本

    // CameraX core library using camera2 implementation
    implementation "androidx.camera:camera-core:${camerax_version}"
    implementation "androidx.camera:camera-camera2:${camerax_version}"
    implementation "androidx.camera:camera-lifecycle:${camerax_version}"

    // CameraX Extensions (可选,提供更多高级功能,如HDR、夜景模式)
    implementation "androidx.camera:camera-extensions:${camerax_version}"

    // CameraX View (可选,简化预览视图管理)
    implementation "androidx.camera:camera-view:${camerax_version}"
}
  1. 请求相机权限

AndroidManifest.xml 中声明相机和存储权限(如果需要保存图片或视频):

<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.RECORD_AUDIO" /> 
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="28" />

并在运行时向用户请求这些权限。

  1. 设置预览视图

在布局文件中添加一个 PreviewView(如果使用了 camera-view 库),它是一个自定义的 View,用于显示相机预览流。

<androidx.camera.view.PreviewView
    android:id="@+id/viewFinder"
    android:layout_width="match_parent"
    android:layout_height="match_parent" />
  1. 绑定用例到生命周期

ActivityFragment 中,通常在 onCreateonViewCreated 方法中初始化 CameraX 并绑定用例。

import androidx.camera.core.*
import androidx.camera.lifecycle.ProcessCameraProvider
import androidx.camera.view.PreviewView
import androidx.core.content.ContextCompat
import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors

class CameraActivity : AppCompatActivity() {

    private lateinit var cameraExecutor: ExecutorService
    private lateinit var previewView: PreviewView
    private lateinit var imageCapture: ImageCapture

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

        previewView = findViewById(R.id.viewFinder)
        cameraExecutor = Executors.newSingleThreadExecutor()

        // 检查权限并启动相机
        if (allPermissionsGranted()) {
            startCamera()
        } else {
            // 请求权限
            ActivityCompat.requestPermissions(
                this, REQUIRED_PERMISSIONS, REQUEST_CODE_PERMISSIONS
            )
        }
    }

    private fun startCamera() {
        val cameraProviderFuture = ProcessCameraProvider.getInstance(this)

        cameraProviderFuture.addListener({
            val cameraProvider: ProcessCameraProvider = cameraProviderFuture.get()

            // 初始化 Preview 用例
            val preview = Preview.Builder()
                .build()
                .also {
                    it.setSurfaceProvider(previewView.surfaceProvider)
                }

            // 初始化 ImageCapture 用例
            imageCapture = ImageCapture.Builder()
                .setCaptureMode(ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY) // 优化捕获延迟
                .build()

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

            try {
                // 在重新绑定用例之前取消绑定所有用例
                cameraProvider.unbindAll()

                // 绑定用例到摄像头
                cameraProvider.bindToLifecycle(
                    this, cameraSelector, preview, imageCapture
                )

            } catch(exc: Exception) {
                Log.e(TAG, "Use case binding failed", exc)
            }

        }, ContextCompat.getMainExecutor(this))
    }

    // 处理权限请求结果
    override fun onRequestPermissionsResult(
        requestCode: Int, permissions: Array<String>, grantResults:
        IntArray) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults)
        if (requestCode == REQUEST_CODE_PERMISSIONS) {
            if (allPermissionsGranted()) {
                startCamera()
            } else {
                Toast.makeText(this,
                    "Permissions not granted by the user.",
                    Toast.LENGTH_SHORT).show()
                finish()
            }
        }
    }

    private fun allPermissionsGranted() = REQUIRED_PERMISSIONS.all {
        ContextCompat.checkSelfPermission(
            baseContext, it) == PackageManager.PERMISSION_GRANTED
    }

    companion object {
        private const val TAG = "CameraXApp"
        private const val REQUEST_CODE_PERMISSIONS = 10
        private val REQUIRED_PERMISSIONS =
            mutableListOf(
                Manifest.permission.CAMERA,
                Manifest.permission.RECORD_AUDIO
            ).apply {
                if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.P) {
                    add(Manifest.permission.WRITE_EXTERNAL_STORAGE)
                }
            }.toTypedArray()
    }

    override fun onDestroy() {
        super.onDestroy()
        cameraExecutor.shutdown()
    }
}
  1. 执行拍照操作

当你需要拍照时,调用 ImageCapture 用例的 takePicture 方法。

// 在某个按钮点击事件中
findViewById<Button>(R.id.image_capture_button).setOnClickListener {
    // 创建输出文件选项
    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")
        }
    }

    val outputOptions = ImageCapture.OutputFileOptions
        .Builder(contentResolver,
            MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
            contentValues)
        .build()

    // 拍照
    imageCapture.takePicture(
        outputOptions,
        ContextCompat.getMainExecutor(this),
        object : ImageCapture.OnImageSavedCallback {
            override fun onError(exc: ImageCaptureException) {
                Log.e(TAG, "Photo capture failed: ${exc.message}", exc)
                Toast.makeText(baseContext, "Photo capture failed: ${exc.message}", Toast.LENGTH_SHORT).show()
            }

            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 常见坑或兼容性注意事项

尽管 CameraX 大大简化了相机开发,但在实际应用中,你仍可能遇到一些挑战和兼容性问题。了解这些“坑”能帮助你更好地规划和调试。

  1. 设备兼容性问题仍可能存在:
    尽管 CameraX 致力于提供一致的体验,但 Android 生态系统的碎片化意味着并非所有功能在所有设备上都能完美运行。某些设备的 Camera2 实现可能存在缺陷,导致 CameraX 遇到意外行为,例如:

    • 某些 CaptureMode(如 CAPTURE_MODE_MAXIMIZE_QUALITY)在特定设备上可能性能不佳或崩溃。
    • 闪光灯模式(Flash Mode)在某些老旧设备或非主流设备上可能无法正常工作。
    • 扩展功能 (Extensions):如 HDR、夜景模式等,这些功能依赖于设备厂商的实现,并非所有设备都支持或效果一致。在使用前最好进行能力查询。
  2. 预览与拍照的旋转、裁剪问题:
    相机传感器通常是横向放置的,而手机可能在竖屏模式下使用。CameraX 会尝试处理预览和拍照的旋转,但有时可能会遇到以下情况:

    • 预览画面方向不正确PreviewViewscaleTypePrieview.setTargetRotation() 需要正确设置,以确保预览画面方向与设备方向一致且填充合理。
    • 拍照图片方向不正确:捕获的图片可能会有不正确的 EXIF 旋转标记,需要手动在 ImageCapture.Builder 中设置 setTargetRotation(),或者在图片保存后处理旋转。
    • 裁剪区域偏差:如果你在预览中绘制了一个裁剪框,拍照后需要根据预览分辨率和图片分辨率的比例,精确计算出实际图片上的裁剪区域。
  3. 内存管理与性能:

    • ImageAnalysis 的帧率与内存消耗:如果 ImageAnalysis 的分析器处理逻辑复杂或处理的帧率过高,可能会导致内存快速增长甚至 OOM。确保在 ImageAnalysis 用例的回调中及时调用 image.close() 释放图像缓冲区。
    • 多用例同时绑定:同时绑定 PreviewImageCaptureImageAnalysis 可能会对性能造成压力,尤其是在低端设备上。确保只绑定你实际需要的用例。
    • 分辨率选择:通过 setTargetResolution()setTargetAspectRatio() 设置用例的分辨率时,要考虑到设备的硬件能力和内存限制。过高的分辨率可能导致性能下降。
  4. 相机启动速度:
    在某些设备上,尤其是老旧或资源受限的设备,CameraX 绑定用例和启动预览可能需要几百毫秒甚至一秒以上。这可能导致短暂的黑屏。你可以在相机启动过程中显示一个加载指示器来改善用户体验。

  5. 设备权限和背景运行限制:

    • 确保已正确处理运行时权限请求。
    • 在 Android 10+(API 29+)中,后台访问相机有严格限制。你的应用在后台时无法直接访问相机。如果需要在后台进行相机相关操作,需要考虑使用前台服务和相应的权限。
  6. 与其他相机库或框架的冲突:
    如果你的项目同时使用了其他相机相关的库(如 ZXing 扫描库自带的相机处理),可能会与 CameraX 产生冲突,导致相机资源无法正常打开或释放。确保同一时间只有一个相机管理框架在运行。

  7. 录像用例 (VideoCapture) 的稳定性:
    视频录制相对于拍照和预览更为复杂,涉及到编码、音频同步等。在某些设备上,VideoCapture 可能会出现录制卡顿、音画不同步或录制文件损坏的问题。测试时要覆盖多款设备。


总结

CameraX 库为 Android 开发者提供了一个强大而易用的工具集,用于构建稳定、高性能的相机应用。它通过封装底层复杂的 Camera2 API,提供了一致的 API 接口和生命周期管理,极大地简化了开发流程。无论是简单的拍照应用还是复杂的图像分析工具,CameraX 都能助你一臂之力。如果你是 Android 相机开发新手,或者希望加速开发过程并提高应用兼容性,CameraX 绝对是你的首选。
如果使用 CameraX 的API开发系统相机,这是否合适呢,期待大家一起来讨论。


本文链接:Android 相机应用开发中 CameraX 的使用介绍 - https://h89.cn/archives/415.html

版权声明:原创文章 遵循 CC 4.0 BY-SA 版权协议,转载请附上原文链接和本声明。

标签: CameraX, Camera2, Androidx, PreviewView, ImageCapture

评论已关闭