ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Android Studio Kotlin CameraX实战:预览、拍照与分析全解析

Android Studio Kotlin CameraX实战:预览、拍照与分析全解析 简介面向需要在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-core和camera-camera2也是标配。版本号建议去Google Maven确认一下最新的稳定版我这里写1.3.4只是保证这篇文章里的代码能跑通。如果你后续要做一些更偏底层的功能比如手动曝光、传感器时间戳这些才需要考虑直接上Camera2如果只是取预览流、拍照、分析帧数据CameraX是性价比最高的选择。另外很多旧教程会让你额外加camerax-extensions这个库已经在1.3.0以后被合并和弱化了现在不需要主动添加。1.2 权限声明与运行时申请摄像头权限属于危险权限除了在AndroidManifest.xml里声明还必须处理运行时动态申请的逻辑这一步漏了后面所有功能都白搭。uses-permission android:nameandroid.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:idid/previewView android:layout_widthmatch_parent android:layout_heightmatch_parent /然后在代码里这样绑定val preview Preview.Builder().build().also { it.setSurfaceProvider(binding.previewView.surfaceProvider) }这一句是预览功能最核心的部分。setSurfaceProvider把Preview的输出流连到了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或VirtualScenePreviewView很可能一直黑屏但代码和执行日志完全没有任何报错。这是一个特别容易让人误判是代码问题的坑。检查方式是模拟器侧边栏Settings里的Camera设置或者启动模拟器时的-camera-back webcam0参数。日常开发如果只是调试预览和拍照逻辑用模拟器自带的VirtualScene一个假场景画面就够了。其次模拟器上的摄像头传感器数据是虚拟的ImageAnalysis拿到的YUV数据帧率不稳定某些帧的分辨率也和真机有差异。所以我一直建议凡是涉及图像算法、扫码、识别类的功能必须在真机上测试模拟器只能用来验证工程结构是否跑通。5.2 旋转、生命周期与并发拍照手机旋转时CameraX因为绑定了LifecycleOwner旋转后Activity重建会自动释放并重新创建相机实例一般不需要手动处理。但是如果你在Mainfest里对Activity设置了android:screenOrientationportrait锁死竖屏那就要额外注意保存照片的方向信息。CameraX在保存到文件时会自动写入EXIF旋转信息多数看图软件能自动纠正但某些自研图片加载库如果忽略了EXIF的Orientation字段照片看起来就是横的。我在项目里吃过这个亏后来统一在图片加载时强制旋转或者在布局中固定预览方向才把问题解决干净。生命周期的另一个坑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把整个摄像头调用流程封装得非常规整Preview、ImageCapture、ImageAnalysis三个用例的组合能覆盖绝大多数App场景。如果你真的需要非常底层的控制比如逐帧手动曝光、多摄同时采集再去碰Camera2也不迟。绝大多数时候拿CameraX先把核心流程跑通比一开始就陷进Camera2的状态回调里要高效得多。我自己做那个相机项目的过程中最大的体会就是相机相关的问题很多不是代码写错而是对生命周期、权限时机和机型差异的理解不够先把这三件事处理好项目就稳了一半。本文还有配套的精品资源点击获取
返回列表