Android 相机应用开发中 CameraX 的使用介绍
近期在禾苗通信面试中,他们的负责人介绍到,他们的相机开发都使用的是 CameraX 的API,暂不评估可行性,本文只介绍一下 CameraX 的API及相关使用。
什么是 CameraX?
CameraX 是 Android Jetpack 库套件中的一部分,旨在简化 Android 应用中的相机开发。它构建在复杂的 Camera2 API 之上,抽象了大量底层细节,为开发者提供了更易用、更一致的接口。CameraX 兼容性强,支持 Android 5.0 (API level 21) 及以上版本,并且会随着 Android 系统的演进而持续更新,确保应用在不同设备上的稳定性和性能。
为什么选择 CameraX?
- 易用性:CameraX 极大地降低了相机开发的复杂性。相较于直接使用 Camera2 API,它通过更简洁的 API 设计和生命周期管理,减少了大量样板代码。
- 一致性:解决了 Android 生态系统中设备碎片化导致的不同相机行为问题。CameraX 在内部处理了这些差异,确保应用在各种设备上都能提供一致的相机体验。
- 生命周期感知:CameraX 与 Android 生命周期(如
Activity
和LifecycleOwner
)紧密集成,自动处理相机的打开、关闭、释放等操作,避免了常见的内存泄漏和资源管理问题。 - 功能丰富:支持常见的相机用例,如拍照、录像、图像分析(条形码扫描、人脸识别等),并提供方便的配置选项。
- 扩展性:通过 Use Cases (用例) 的概念,开发者可以灵活组合不同功能,例如同时进行预览和拍照,或者预览和图像分析。
CameraX 的核心概念:用例 (Use Cases)
CameraX 的设计核心是“用例” (Use Cases),它将相机的功能划分为几个可独立或组合使用的模块。主要包括:
- Preview (预览):用于在屏幕上显示相机实时取景。这是所有相机应用的基础。
- Image Capture (拍照):用于捕获高分辨率的静态图像。
- Image Analysis (图像分析):用于实时访问相机帧进行图像处理,如计算机视觉任务(条形码扫描、文本识别、人脸检测等)。
- Video Capture (视频录制):用于录制视频。
开发者只需选择并配置所需的用例,然后将它们绑定到生命周期和相机选择器上。
CameraX 的基本使用步骤
下面是使用 CameraX 进行相机应用开发的基本流程:
- 添加依赖
在你的build.gradle (Module: app)
文件中添加 CameraX 库的依赖。通常需要camera-core
、camera-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}"
}
- 请求相机权限
在 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" />
并在运行时向用户请求这些权限。
- 设置预览视图
在布局文件中添加一个 PreviewView
(如果使用了 camera-view
库),它是一个自定义的 View
,用于显示相机预览流。
<androidx.camera.view.PreviewView
android:id="@+id/viewFinder"
android:layout_width="match_parent"
android:layout_height="match_parent" />
- 绑定用例到生命周期
在 Activity
或 Fragment
中,通常在 onCreate
或 onViewCreated
方法中初始化 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()
}
}
- 执行拍照操作
当你需要拍照时,调用 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 大大简化了相机开发,但在实际应用中,你仍可能遇到一些挑战和兼容性问题。了解这些“坑”能帮助你更好地规划和调试。
-
设备兼容性问题仍可能存在:
尽管 CameraX 致力于提供一致的体验,但 Android 生态系统的碎片化意味着并非所有功能在所有设备上都能完美运行。某些设备的 Camera2 实现可能存在缺陷,导致 CameraX 遇到意外行为,例如:- 某些 CaptureMode(如
CAPTURE_MODE_MAXIMIZE_QUALITY
)在特定设备上可能性能不佳或崩溃。 - 闪光灯模式(Flash Mode)在某些老旧设备或非主流设备上可能无法正常工作。
- 扩展功能 (Extensions):如 HDR、夜景模式等,这些功能依赖于设备厂商的实现,并非所有设备都支持或效果一致。在使用前最好进行能力查询。
- 某些 CaptureMode(如
-
预览与拍照的旋转、裁剪问题:
相机传感器通常是横向放置的,而手机可能在竖屏模式下使用。CameraX 会尝试处理预览和拍照的旋转,但有时可能会遇到以下情况:- 预览画面方向不正确:
PreviewView
的scaleType
和Prieview.setTargetRotation()
需要正确设置,以确保预览画面方向与设备方向一致且填充合理。 - 拍照图片方向不正确:捕获的图片可能会有不正确的 EXIF 旋转标记,需要手动在
ImageCapture.Builder
中设置setTargetRotation()
,或者在图片保存后处理旋转。 - 裁剪区域偏差:如果你在预览中绘制了一个裁剪框,拍照后需要根据预览分辨率和图片分辨率的比例,精确计算出实际图片上的裁剪区域。
- 预览画面方向不正确:
-
内存管理与性能:
ImageAnalysis
的帧率与内存消耗:如果ImageAnalysis
的分析器处理逻辑复杂或处理的帧率过高,可能会导致内存快速增长甚至 OOM。确保在ImageAnalysis
用例的回调中及时调用image.close()
释放图像缓冲区。- 多用例同时绑定:同时绑定
Preview
、ImageCapture
和ImageAnalysis
可能会对性能造成压力,尤其是在低端设备上。确保只绑定你实际需要的用例。 - 分辨率选择:通过
setTargetResolution()
或setTargetAspectRatio()
设置用例的分辨率时,要考虑到设备的硬件能力和内存限制。过高的分辨率可能导致性能下降。
-
相机启动速度:
在某些设备上,尤其是老旧或资源受限的设备,CameraX 绑定用例和启动预览可能需要几百毫秒甚至一秒以上。这可能导致短暂的黑屏。你可以在相机启动过程中显示一个加载指示器来改善用户体验。 -
设备权限和背景运行限制:
- 确保已正确处理运行时权限请求。
- 在 Android 10+(API 29+)中,后台访问相机有严格限制。你的应用在后台时无法直接访问相机。如果需要在后台进行相机相关操作,需要考虑使用前台服务和相应的权限。
-
与其他相机库或框架的冲突:
如果你的项目同时使用了其他相机相关的库(如 ZXing 扫描库自带的相机处理),可能会与 CameraX 产生冲突,导致相机资源无法正常打开或释放。确保同一时间只有一个相机管理框架在运行。 -
录像用例 (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 版权协议,转载请附上原文链接和本声明。
评论已关闭