Android Studio Kotlin CameraX实战:预览、拍照与分析全解析
2026/9/2 2:56:48 网站建设 项目流程

简介:面向需要在Android Studio中快速实现相机功能的Kotlin开发者,这份工程示例演示了如何基于CameraX完成相机预览、拍照与图片保存。项目清晰展示了AndroidManifest权限声明、Gradle依赖配置、CameraActivity中Preview与ImageCapture的绑定,以及配套的XML布局文件,适合初学者入门或作为功能参考。资源包为ZIP格式,共41个文件,以Gradle构建脚本、XML配置、Kotlin源码和PNG图标资源为主,整体仅148KB,便于快速下载与导入。已有901人学习下载。借助这份工程,可直观理解CameraX的核心调用流程,省去从零搭建环境的时间,直接对照源码修改并集成到自己的应用中,是学习Android相机开发不可多得的实用样例。 前阵子在Android Studio里用Kotlin写一个相机类的小项目,从CameraX的依赖配置、权限申请,到预览、拍照、实时帧处理,再到真机和模拟器上那些奇奇怪怪的表现,一路踩下来其实花了不少时间。很多人一提起"在Android Studio上使用摄像头",第一反应就是Camera2那套复杂繁琐的模板代码,其实如果你只做常规的拍照、扫码、画面预览这些需求,CameraX完全够用,而且上手成本低得多。这篇文章我就按自己实际跑的Kotlin项目来写,把这个过程里真正关键的步骤和坑都摊开讲清楚,希望对准备做类似项目的朋友有帮助。

1. 动手前先把环境这块理顺

1.1 新建项目与依赖配置

摄像头相关的开发,建议直接用Android Studio里带Empty Views Activity模板的新项目,语言选Kotlin,包名和应用名随意。工程项目建立起来以后,第一件事就是去build.gradle.kts(模块级)里把CameraX的依赖加进去。

dependencies { val cameraxVersion = "1.3.4" implementation("androidx.camera:camera-core:$cameraxVersion") implementation("androidx.camera:camera-camera2:$cameraxVersion") implementation("androidx.camera:camera-lifecycle:$cameraxVersion") implementation("androidx.camera:camera-view:$cameraxVersion") }

这四个库的作用分别是:核心API、基于Camera2的实现、与Lifecycle绑定、以及PreviewView预览控件。后面两个几乎一定会用到,camera-corecamera-camera2也是标配。版本号建议去Google Maven确认一下最新的稳定版,我这里写1.3.4只是保证这篇文章里的代码能跑通。

如果你后续要做一些更偏底层的功能,比如手动曝光、传感器时间戳这些,才需要考虑直接上Camera2;如果只是取预览流、拍照、分析帧数据,CameraX是性价比最高的选择。另外很多旧教程会让你额外加camerax-extensions,这个库已经在1.3.0以后被合并和弱化了,现在不需要主动添加。

1.2 权限声明与运行时申请

摄像头权限属于危险权限,除了在AndroidManifest.xml里声明,还必须处理运行时动态申请的逻辑,这一步漏了,后面所有功能都白搭。

<uses-permission android:name="android.permission.CAMERA" />

运行时申请的代码,用registerForActivityResult这套API写起来最干净:

private val cameraPermissionLauncher = registerForActivityResult(ActivityResultContracts.RequestPermission()) { granted -> if (granted) { startCamera() } else { Toast.makeText(this, "没有摄像头权限,无法启动相机", Toast.LENGTH_SHORT).show() } } private fun checkCameraPermission() { val permission = Manifest.permission.CAMERA if (ContextCompat.checkSelfPermission(this, permission) == PackageManager.PERMISSION_GRANTED) { startCamera() } else { cameraPermissionLauncher.launch(permission) } }

需要提醒一点:不要一进页面就弹权限框,最好放在用户主动点击某个按钮之后再去申请。很多安全检查工具会关注权限申请的时机是否合理,我自己提交应用审核时也因为"一启动就弹权限"被打回过一次,改成用户点击后再弹就通过了。

2. 三行代码跑通相机预览的核心逻辑

2.1 PreviewView 与控制它的用例

CameraX的设计思路是把摄像头操作拆成一个个用例(UseCase),预览是Preview,拍照是ImageCapture,帧分析是ImageAnalysis。这三个用例可以单独使用,也可以同时绑定到你页面的生命周期上,绑定之后它会自动感知页面销毁和重建,不需要你手动释放摄像头。

PreviewView是一个View控件,直接在布局里声明:

<androidx.camera.view.PreviewView android:id="@+id/previewView" android:layout_width="match_parent" android:layout_height="match_parent" />

然后在代码里这样绑定:

val preview = Preview.Builder().build().also { it.setSurfaceProvider(binding.previewView.surfaceProvider) }

这一句是预览功能最核心的部分。setSurfaceProviderPreview的输出流连到了PreviewView上,摄像头捕获的画面会实时渲染到这个View上。

2.2 绑定生命周期与摄像头选择

startCamera()里,完整流程是:获取ProcessCameraProvider,构建预览用例,选择前置/后置摄像头,最后用bindToLifecycle把用例绑定到当前界面的生命周期。

private fun startCamera() { val cameraProviderFuture = ProcessCameraProvider.getInstance(this) cameraProviderFuture.addListener({ val cameraProvider = cameraProviderFuture.get() val preview = Preview.Builder().build().also { it.setSurfaceProvider(binding.previewView.surfaceProvider) } val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA try { cameraProvider.unbindAll() cameraProvider.bindToLifecycle( this, cameraSelector, preview ) } catch (e: Exception) { Log.e(TAG, "相机启动失败", e) } }, ContextCompat.getMainExecutor(this)) }

bindToLifecycle第三个参数后续准备加什么用例就继续往里传,比如preview, imageCapture, imageAnalysis。我在第一次写的时候碰到一个很疑惑的情况:明明代码没报错,预览就是黑屏,后来发现是因为ProcessCameraProvider.getInstance(this)get()调用不能直接放在主线程里尝试获取结果,必须放在addListener回调里执行。这个顺序是CameraX的固定用法,照着写就行。

切换前摄后摄也很简单,把CameraSelector.DEFAULT_BACK_CAMERA换成CameraSelector.DEFAULT_FRONT_CAMERA,然后unbindAll()再重新bindToLifecycle一次,就可以在前后摄之间切换了。

3. 拍照落地:从预览画面到本地文件的那一步

3.1 拍一张照片并保存

要给相机加上拍照功能,需要在绑定用例时把ImageCapture加进去:

val imageCapture = ImageCapture.Builder() .setCaptureMode(ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY) .build() cameraProvider.bindToLifecycle(this, cameraSelector, preview, imageCapture)

CAPTURE_MODE_MINIMIZE_LATENCY表示快门优先,适合普通拍照场景;如果照片质量优先,可以换CAPTURE_MODE_MAXIMIZE_QUALITY,但连拍和响应速度会慢一点。

实际拍照时,需要把图片保存到某个文件或者MediaStore。最省事的做法是保存到应用专属目录,不和系统相册打交道:

fun takePhoto() { val file = File(filesDir, "IMG_${System.currentTimeMillis()}.jpg") val outputOptions = ImageCapture.OutputFileOptions.Builder(file).build() imageCapture.takePicture( outputOptions, ContextCompat.getMainExecutor(this), object : ImageCapture.OnImageSavedCallback { override fun onImageSaved(outputFileResults: ImageCapture.OutputFileResults) { Log.d(TAG, "照片已保存: ${file.absolutePath}") } override fun onError(exception: ImageCaptureException) { Log.e(TAG, "拍照失败", exception) } } ) }

如果希望照片出现在系统相册里,OutputFileOptions就要换成基于ContentResolver的写法:

val contentValues = ContentValues().apply { put(MediaStore.Images.Media.DISPLAY_NAME, "IMG_${System.currentTimeMillis()}.jpg") put(MediaStore.Images.Media.MIME_TYPE, "image/jpeg") } val outputOptions = ImageCapture.OutputFileOptions.Builder( contentResolver, MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues ).build()

媒体库会在系统相册中生成一条记录,文件会被写入公有存储目录。这里需要注意:如果走MediaStore方案,Android 10以下需要存储权限,Android 10以上使用分区存储则不需要额外权限,但如果要往EXTERNAL_CONTENT_URI写文件,部分机型仍然可能出现权限异常,稳妥做法是先声明WRITE_EXTERNAL_STORAGE(只在旧版本上生效),再配合maxSdkVersion限制。

3.2 拍照与协程怎么配合

CameraX的takePicture是回调式的,写成Kotlin协程可以避免层层回调,代码看起来更干净。我做项目时习惯封装一个suspendCancellableCoroutine版本的拍照函数:

suspend fun takePhotoSuspend(imageCapture: ImageCapture): File = suspendCancellableCoroutine { continuation -> val file = File(filesDir, "IMG_${System.currentTimeMillis()}.jpg") val outputOptions = ImageCapture.OutputFileOptions.Builder(file).build() imageCapture.takePicture( outputOptions, ContextCompat.getMainExecutor(this), object : ImageCapture.OnImageSavedCallback { override fun onImageSaved(outputFileResults: ImageCapture.OutputFileResults) { continuation.resume(file) } override fun onError(exception: ImageCaptureException) { continuation.resumeWithException(exception) } } ) }

配合ViewModel或者协程作用域里调用,逻辑会清晰很多。我自己的经验是:CameraX本身的回调已经不算复杂,但项目里拍照前后往往要接其他业务逻辑,比如上传、缩略图预览、埋点,用协程包装之后整条链路才真正好维护

拍照还有一个容易踩的点:takePicture在上一张还没处理完时再次调用,会直接抛IllegalStateException。我建议在回调成功或失败前给快门按钮加一个isEnabled = false的状态锁,防止用户连续点按导致崩溃。

4. 实时帧处理:把摄像头变成数据源

4.1 ImageAnalysis用例怎么接

很多场景不只是拍照,而是需要对每一帧画面做实时分析——二维码扫码、人脸检测、颜色识别这些都用ImageAnalysis。它的用法和Preview类似,也是绑定到生命周期上:

val analysis = ImageAnalysis.Builder() .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST) .build() analysis.setAnalyzer(executor) { imageProxy -> val rotationDegrees = imageProxy.imageInfo.rotationDegrees imageProxy.planes.forEach { plane -> val buffer = plane.buffer val bytes = ByteArray(buffer.remaining()) buffer.get(bytes) // 这里拿到的是YUV原始数据,按需解析 } imageProxy.close() } cameraProvider.bindToLifecycle(this, cameraSelector, preview, imageCapture, analysis)

setBackpressureStrategy有两个选项:STRATEGY_KEEP_ONLY_LATEST表示如果分析器处理不过来,就丢弃中间帧,只保留最新一帧,适合扫码这类实时性要求高的场景;STRATEGY_BLOCK_PRODUCER表示当分析器忙时阻塞新帧的产生,适合需要逐帧分析的场景。我用下来的结论是无脑选KEEP_ONLY_LATEST,因为阻塞生产者会直接影响预览流畅度,手机发热和掉帧都容易从这来。

4.2 帧率控制与性能取舍

ImageAnalysis默认会把所有帧都交给分析器,在真机上几乎每秒钟就有30帧左右的数据。如果每一帧都做重量级计算,CPU会被占满。我实测过一个简单的二维码识别逻辑,不做任何限制时手机温度明显上升,掉帧卡顿肉眼可见。

合理的处理方式是控制分析频率:

private var lastAnalyzedTime = 0L analysis.setAnalyzer(executor) { imageProxy -> val currentTime = System.currentTimeMillis() if (currentTime - lastAnalyzedTime >= 300) { lastAnalyzedTime = currentTime // 实际分析逻辑 } imageProxy.close() }

把分析频率控制在每秒3~5次,对纯识别类需求完全够用,同时可以大幅降低CPU占用。还有一个细节:imageProxy在每次分析回调里必须调用close(),否则底层buffer会被持续占用,跑不了多久就会出现"ImageProxy closed unexpectedly"或者相机直接黑屏的异常。这一点在CameraX文档里反复强调,也是新手最容易忽略的。

如果要做更复杂的目标检测,建议在拿到YUV数据后先转成Bitmap或者直接使用CameraX配合ML Kit的官方扩展,在ImageAnalysis的回调里把ImageProxy转成InputImage,再接ML Kit的处理。这个链路我用下来很稳定,比自己用OpenCV解析YUV数据省事太多。

5. 真机、模拟器和屏幕旋转:绕过那些实打实的坑

5.1 模拟器摄像头和真机行为差异

我在Android Studio自带模拟器上调试过很多次,几个常见的怪现象值得提前说清楚。

首先,模拟器的相机来源是宿主机的摄像头。如果你启动模拟器时没有把摄像头设置为"Webcam"或"VirtualScene",PreviewView很可能一直黑屏,但代码和执行日志完全没有任何报错。这是一个特别容易让人误判是代码问题的坑。检查方式是:模拟器侧边栏"Settings"里的Camera设置,或者启动模拟器时的-camera-back webcam0参数。日常开发,如果只是调试预览和拍照逻辑,用模拟器自带的"VirtualScene"(一个假场景画面)就够了。

其次,模拟器上的摄像头传感器数据是虚拟的,ImageAnalysis拿到的YUV数据帧率不稳定,某些帧的分辨率也和真机有差异。所以我一直建议:凡是涉及图像算法、扫码、识别类的功能,必须在真机上测试,模拟器只能用来验证工程结构是否跑通。

5.2 旋转、生命周期与并发拍照

手机旋转时,CameraX因为绑定了LifecycleOwner,旋转后Activity重建会自动释放并重新创建相机实例,一般不需要手动处理。但是如果你在Mainfest里对Activity设置了android:screenOrientation="portrait"锁死竖屏,那就要额外注意保存照片的方向信息。CameraX在保存到文件时会自动写入EXIF旋转信息,多数看图软件能自动纠正,但某些自研图片加载库如果忽略了EXIFOrientation字段,照片看起来就是横的。我在项目里吃过这个亏,后来统一在图片加载时强制旋转,或者在布局中固定预览方向,才把问题解决干净。

生命周期的另一个坑:bindToLifecycle必须在LifecycleOwner处于STARTED状态之后调用。如果你在onCreate里直接调,虽然大多数情况下能跑,但个别版本或特殊场景会抛出"Lifecycle is not STARTED yet"。稳妥做法是在onStart里启动相机,或者至少保证调用在界面可见之后。

5.3 一些从实践中养成的好习惯

这段是连续做了几个相机项目之后总结出来的经验,不一定在官方文档里能找到,但能省掉很多排查时间:

  • 权限判断不要太早。有些国产ROM在应用启动阶段弹权限框会触发警告,甚至直接判定为"频繁弹窗",最好是用户进入页面后由一个明确的交互动作触发申请。
  • PreviewView的缩放模式要想好再定。默认的FILL_CENTER会裁剪画面,适配不同屏幕比例时预览是满屏的但是边缘会被切掉;FIT_CENTER会保留完整画面但左右或上下可能出现黑边。拍照App一般选FILL_CENTER,扫码类App我更推荐FIT_CENTER,避免画面边缘的二维码被切掉导致识别不到。
  • 不需要的用例不要绑定。如果只做预览,就不要把ImageAnalysis加进去,多一个用例就多一块buffer的占用,内存和耗电都会上升。
  • 自定义分辨率时谨慎。CameraX 1.3之后推荐用ResolutionSelector来控制分辨率,直接在ImageAnalysis.Builder()里传Size(1280, 720)这类旧API已经在后续版本标记为deprecated,不要学网上老代码照搬。真机上过高的分辨率并不会让预览更清晰,反而会带来性能负担。

最后再分享一个我自己的使用习惯。CameraX把整个摄像头调用流程封装得非常规整,PreviewImageCaptureImageAnalysis三个用例的组合能覆盖绝大多数App场景。如果你真的需要非常底层的控制,比如逐帧手动曝光、多摄同时采集,再去碰Camera2也不迟。绝大多数时候,拿CameraX先把核心流程跑通,比一开始就陷进Camera2的状态回调里要高效得多。我自己做那个相机项目的过程中,最大的体会就是:相机相关的问题,很多不是代码写错,而是对生命周期、权限时机和机型差异的理解不够,先把这三件事处理好,项目就稳了一半

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询