ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Android条码扫描Demo:集成CameraX与ML Kit实现

Android条码扫描Demo:集成CameraX与ML Kit实现 简介面向Android开发者的条码扫描集成示例基于ZXing开源库附带完整可运行的扫码项目。Demo覆盖从相机预览、条码检测到解码返回的完整链路支持QR码、Code 128、EAN-13等多种格式适合需要在App中快速落地扫码功能的初中级开发者参考。压缩包共262个文件以77个Java源码、61个XML布局与配置、91个HTML说明页面为主另含jar依赖与界面图片整体大小2.9MB目录结构清楚可直接导入工程研读。通过学习可重点掌握ZXing库的依赖配置、CaptureActivity的启动与自定义、相机运行时权限申请、扫描结果在onActivityResult中的解析以及按业务需求限制解码格式、调整扫描区域等优化手段并了解相机无法访问、解码失败时的处理思路。资源还涉及多格式支持、离线解码与性能调优配置便于读者掌握条码扫描的完整实现思路。目前已有873人学习下载对理解Android相机API、Intent交互和硬件权限管理均有直接帮助。1. 写这个扫码Demo之前先想清楚方案做 Android 条码扫描 demo 的场景我猜大多数人跟我一样不是要做一套完整的扫码支付级 SDK而是要在自己的 App 里先跑通一个“能扫、能出结果、能演示”的最小闭环。我这次接的需求也简单——内部工具里加一个扫码入口用来扫商品条码和二维码识别后把结果回填到表单。网上搜 android 条码扫描 demo很多教程还停留在 ZXing 时代要么给你一个老项目让你 clone要么源码里塞了一堆用不上的逻辑。我干脆用 CameraX 加 ML Kit 从零搭了一个干净版本这里把完整思路和踩过的坑一起写出来。1.1 需求其实很明确最小可用闭环这个 demo 不是要做一个扫码 App它的目标是验证三件事能不能打开相机预览、能不能对准条码自动识别、识别结果能不能回到业务层。只要这三点成立后续接业务逻辑就是半小时的事。所以我给自己定的验收标准很简单——打开页面自动出画面对准一张 EAN-13 商品码或者二维码1 秒内出结果TextView 显示原始内容页面不闪退、不卡死。1.2 为什么用 CameraX ML Kit而不是 ZXing当时我在三个方案里反复犹豫方案优点缺点ZXing Core CameraX体积小、文档多需要自行封装相机与解码流程CameraX ML Kit bundled离线可用、识别率高、接口干净APK 增加 23MBCameraX ML Kit unbundled包体积几乎不增依赖系统级服务部分设备无法使用最后我选了 bundled 版 ML Kit图一个省心。ML Kit 的 barcode-scanning 模型直接在本地跑适配 EAN-13、Code 128、QR Code 等常用格式不需要自己做图像预处理也不依赖网络。CameraX 则负责相机预览和帧回调它把 Camera2 那套复杂的状态机封装成了几个用例对象配合生命周期绑定页面切走自动释放相机省掉大量样板代码。1.3 Demo 的运行效果和适用范围跑通后的效果基本符合预期相机预览流畅条码对准画面中央后大约 500ms 以内出结果横向条码和竖屏二维码都能识别。适用范围很清晰——任何需要在 App 内快速集成扫码能力的 Android 项目无论是库存盘点、票据录入还是扫码登录都可以拿这个 demo 当起点。要接生产环境请继续看到最后一节。2. 环境准备与依赖配置2.1 Android Studio 与 SDK 版本选择我用的版本组合是 Android Studio Hedgehog2023.1.1Gradle 8.2AGP 8.2.2compileSdk 34minSdk 26。如果你用的是更新的 IDE 版本AGP 版本别乱升保持默认即可。这里有一个容易踩的坑CameraX 1.3.x 和 ML Kit 17.x 对 compileSdk 版本有要求低于 33 会直接编译报错。建议直接建一个新工程包名随意语言选 Kotlin空 Activity 模板就够了。2.2 引入 CameraX 和 ML Kit 依赖在 app 模块的 build.gradle 里加上这段依赖dependencies { implementation androidx.core:core-ktx:1.12.0 implementation androidx.appcompat:appcompat:1.6.1 implementation com.google.android.material:material:1.11.0 implementation androidx.constraintlayout:constraintlayout:2.1.4 def camerax_version 1.3.1 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:${camerax_version} implementation com.google.mlkit:barcode-scanning:17.3.0 }这里有几个细节需要说明。CameraX 的五个子模块各有分工camera-core 是核心接口camera-camera2 是底层实现camera-lifecycle 负责绑定生命周期camera-view 提供 PreviewView 控件。ML Kit 的 barcode-scanning 是 bundled 版本首次启动后不再依赖外部服务离线也能扫。2.3 权限声明与布局搭建AndroidManifest.xml 里加权限uses-permission android:nameandroid.permission.CAMERA / uses-feature android:nameandroid.hardware.camera android:requiredfalse /uses-feature 声明为 false是为了让没有摄像头的设备也能安装 APK虽然实际没什么用但算个好习惯。布局文件用最简单的方式全屏 PreviewView 作为相机预览层顶部一个 TextView 显示结果底部一个按钮控制闪光灯方便测试暗光环境androidx.constraintlayout.widget.ConstraintLayout xmlns:androidhttp://schemas.android.com/apk/res/android xmlns:apphttp://schemas.android.com/apk/res-auto android:layout_widthmatch_parent android:layout_heightmatch_parent androidx.camera.view.PreviewView android:idid/previewView android:layout_width0dp android:layout_height0dp app:layout_constraintTop_toTopOfparent app:layout_constraintBottom_toBottomOfparent app:layout_constraintStart_toStartOfparent app:layout_constraintEnd_toEndOfparent / TextView android:idid/resultText android:layout_widthmatch_parent android:layout_heightwrap_content android:background#80000000 android:padding12dp android:textColor#FFFFFF android:textSize16sp app:layout_constraintTop_toTopOfparent / Button android:idid/flashButton android:layout_widthwrap_content android:layout_heightwrap_content android:text手电筒 app:layout_constraintBottom_toBottomOfparent app:layout_constraintEnd_toEndOfparent app:layout_constraintStart_toStartOfparent / /androidx.constraintlayout.widget.ConstraintLayout布局里 TextView 背景用了半透明黑是为了在任何场景下都有可读性。实际项目中你可以换成扫码框动画、取景框之类的交互元素但 demo 阶段不需要花哨。3. 核心代码把扫码流程串起来3.1 预览与图像分析CameraX 的职责划分CameraX 的核心思想是把相机能力拆成三个用例Preview预览、ImageAnalysis帧分析、ImageCapture拍照。扫码场景只需要 Preview 和 ImageAnalysis 同时运行两者通过 bindToLifecycle 绑定到同一个生命周期持有者。这里要理解两个关键点第一Preview 只负责把相机画面渲染到 PreviewView 上它不参与数据分析。第二ImageAnalysis 会不断从相机流中取出图像帧回调给 Analyzer由你的代码决定如何处理。扫码识别的核心就在 Analyzer 里所以 Preview 和 ImageAnalysis 需要同时 bind缺一个都不行代码里如果只 bind 了 ImageAnalysis界面上是黑的别问我怎么知道的。3.2 条码解析ML Kit 的调用姿势ML Kit 的条码识别 API 调用非常简单核心就三个类BarcodeScannerOptions 用来指定识别格式BarcodeScanning 创建扫描器实例InputImage 把 CameraX 的 ImageProxy 包装成 ML Kit 能识别的图像对象。在 Activity 里绑定相机private fun startCamera() { val cameraProviderFuture ProcessCameraProvider.getInstance(this) cameraProviderFuture.addListener({ val cameraProvider cameraProviderFuture.get() cameraProvider.unbindAll() val preview Preview.Builder().build().also { it.setSurfaceProvider(binding.previewView.surfaceProvider) } val analysis ImageAnalysis.Builder() .setTargetResolution(Size(1280, 720)) .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST) .build() analysis.setAnalyzer(Executors.newSingleThreadExecutor()) { imageProxy - handleImageProxy(imageProxy) } val selector CameraSelector.DEFAULT_BACK_CAMERA cameraProvider.bindToLifecycle(this, selector, preview, analysis) }, ContextCompat.getMainExecutor(this)) }这段代码有几个值得解释的选项。setTargetResolution(Size(1280, 720)) 指定分析帧的分辨率720P 在识别精度和性能之间比较平衡分辨率太大徒增耗电太小条码容易糊。setBackpressureStrategy(STRATEGY_KEEP_ONLY_LATEST) 表示如果上一帧还没处理完直接丢掉最新帧保证 Analyzer 永远处理的是最新画面不会积压导致卡顿。3.3 结果回传与界面更新handleImageProxy 就是整个扫码 demo 的核心逻辑private val scanner BarcodeScanning.getClient( BarcodeScannerOptions.Builder() .setBarcodeFormats( Barcode.FORMAT_QR_CODE, Barcode.FORMAT_EAN_13, Barcode.FORMAT_EAN_8, Barcode.FORMAT_CODE_128 ) .build() ) private var canScan true private fun handleImageProxy(imageProxy: ImageProxy) { val mediaImage imageProxy.image if (mediaImage null) { imageProxy.close() return } val image InputImage.fromMediaImage(mediaImage, imageProxy.imageInfo.rotationDegrees) scanner.process(image) .addOnSuccessListener { barcodes - if (barcodes.isNotEmpty() canScan) { canScan false barcodes.firstOrNull()?.rawValue?.let { value - runOnUiThread { binding.resultText.text value // 这里就可以把 value 回传给你的业务层 } } } } .addOnCompleteListener { imageProxy.close() } }这里有两个细节必须注意。第一imageProxy.close() 一定要在回调里调用否则相机帧资源不释放连续扫码几分钟后画面会卡死。我一开始把 close() 写在 process 之后立刻调用结果 ML Kit 还在读帧就释放了崩溃概率极高。正确姿势是放在 addOnCompleteListener 里无论识别成功还是失败都关闭。第二canScan 标志用来控制扫码频率否则同一张条码在画面里停留两秒回调可能会触发十几次业务层会被轰炸。3.4 闪光灯与权限处理MainActivity 里的权限逻辑也比较常规。Android 6.0 以上需要运行时权限申请的结果回调里启动相机。闪光灯用 CameraControl 来切换binding.flashButton.setOnClickListener { val cameraControl cameraProviderFuture.get().bindToLifecycle( this, CameraSelector.DEFAULT_BACK_CAMERA, preview, analysis ).cameraControl cameraControl.enableTorch(torchOn) }我这里简化了逻辑实际项目建议把 cameraProvider 缓存成字段按钮点击时直接复用。一个小技巧如果相机被其他应用占用bindToLifecycle 会抛异常可以在调用前后用 try-catch 包住弹出 Toast 提示用户。4. 常见问题与排查实录4.1 画面模糊、对焦拉风箱模拟器上跑这个 demo画面糊是正常的真机上如果糊大概率是 CameraX 的对焦模式和扫描距离问题。CameraX 默认会自动对焦但条码识别对清晰度要求高尤其是细密的一维条码。我的处理是保持手机离条码 10~20cm让条码占据画面宽度的三分之一以上。如果暗光环境自动对焦跟不上就打开闪光灯实测在室内灯光不足时能把识别率从六成拉到九成以上。4.2 扫出来的中文是乱码二维码很多时候存的是中文文本ML Kit 返回的 rawValue 如果出现乱码大概率是编码格式问题。二维码内容可能是 GBK 编码而 SDK 默认按 UTF-8 解码。遇到这种情况可以尝试手动转码再展示private fun decodeRawValue(rawValue: String?): String? { if (rawValue null) return null return try { String(rawValue.toByteArray(Charsets.ISO_8859_1), Charsets.UTF_8) } catch (e: Exception) { rawValue } }注意这个方法不一定对每个乱码都有效因为不同二维码生成工具的编码策略不同。踩了坑之后我的建议是业务上约定好统一用 UTF-8 生成二维码从源头规避比在识别端做各种猜测省事得多。4.3 相机被占用或预览黑屏预览黑屏最常见原因是 Activity 启动时立刻绑定相机但权限还没申请成功或者 PreviewView 没设置 SurfaceProvider。另一个场景是已绑定相机后再次调用 bindToLifecycle不先 unbindAll() 就会报错画面停在黑屏状态。我建议在 startCamera 开头固定加一行 cameraProvider.unbindAll()无论首次还是后续调用都安全。还有一种情况是相机被系统相机应用或其他扫码 App 占用模拟器上尤其常见重启模拟器通常能解决。4.4 一次扫描频繁回调这是扫码场景最典型的业务问题。条码一直在画面里Analyzer 每帧都识别成功回调被无限触发。我前面用 canScan 做了简单拦截真实业务里还可以加个延时恢复机制——比如识别成功后 2 秒内忽略新结果2 秒后 allowScan 置回 trueprivate fun resetScan() { Handler(Looper.getMainLooper()).postDelayed({ canScan true }, 2000) }这个 2 秒延时不仅防抖还给了用户移动手机扫描下一条码的时间不会因为同一张条码反复触发而崩溃。4.5 ML Kit 初始化失败unbundled 版本如果在没有对应系统服务的设备上运行初始化时会直接崩日志里能看到“com.google.android.gms.common.internal”相关异常。我选择 bundled 依赖后这个坑就消失了代价是 APK 体积增加约 2.5MB在现在的应用体量下可以接受。如果你的项目对包体积极其敏感可以考虑用 ZXing Core 替代但识别精度和开发效率会有取舍。4.6 坐标与旋转偏离有同事问过我扫码框明明对准了条码结果区域却偏离。这涉及 ImageProxy 图像的旋转问题。CameraX 的 previewView 会正确适配屏幕旋转但 ImageAnalysis 的帧默认是传感器方向的必须通过 imageProxy.imageInfo.rotationDegrees 传给 ML KitML Kit 内部才会正确处理坐标系。如果你的代码里固定传 0竖屏扫码时 boundingBox 坐标就会错位。这属于比较容易忽视的细节代码里我已经做了正确处理。5. 从 Demo 到正式功能的几点经验5.1 把 Analyzer 做成独立模块demo 跑通后第一件事就是把扫码逻辑从 Activity 里抽出来。我建议定义一个 BarcodeAnalyzer 类构造参数传入识别结果回调class BarcodeAnalyzer( private val onBarcodeDetected: (String) - Unit ) : ImageAnalysis.Analyzer { private val scanner ... override fun analyze(imageProxy: ImageProxy) { ... } }这样 Activity 只负责生命周期和 UI扫码逻辑可以复用到任何一个页面甚至能做成独立的 library module。我做内部工具时销售端、仓库端、后台录入页面都要扫码每次都是复用同一个 Analyzer只换了回调里的业务处理改造成本极低。5.2 连续扫码模式与节流如果要盘点一整箱货每次扫码需要连续扫多个条码。这时 canScan 标志会挡住后续操作我改成 RestartableAnalyzer——每次识别成功后弹出一个确认对话框用户点击“确定”后才恢复扫码。有些场景需要自动连续扫码可以在列表底部显示新扫到的条码并添加去重逻辑避免同一箱货扫了两次。去重不能只比对 rawValue因为不同商品可能有相同的条码前缀建议比对“条码值 时间戳 用户 ID”的组合。5.3 相册选图识别有些用户拍照传图扫码的需求也很常见。做法是拿到相册图片的 Uri 后用 BitmapFactory 解码成 Bitmap再通过 InputImage.fromBitmap(bitmap, 0) 送入 ML Kit 识别。注意相册图片分辨率很高直接解码容易 OOM可以先做降采样val bounds BitmapFactory.decodeResource(resources, R.drawable.placeholder).bounds // 用 BitmapFactory.Options.inSampleSize 按需采样这个功能作为兜底很有用尤其是一些印刷质量差、相机对焦困难的条码用户直接拍一张清晰照片识别成功率很高。5.4 性能与包体积考虑我用真实场景对比过CameraX 默认的 720P 分析帧在骁龙 7 系处理器上CPU 占用约 15%识别耗时平均 200ms帧率稳定在 20FPS 以上日常使用没压力。如果目标设备性能较差可以把 targetResolution 降到 640x480识别速度更快但远距离小条码会吃力。ML Kit bundled 依赖对 start-up 时间的影响约 50ms基本无感知。如果 App 已经引入了 Firebase ML Kit 的其他模块可以直接复用同一套依赖避免重复初始化。这个 demo 我前后跑了一个下午最深的感受是CameraX 和 ML Kit 的组合已经把扫码的复杂度压到了最低剩下的功夫全在细节。比如 imageProxy.close() 的时机、旋转角度的传递、扫码节流的设计这些都是文档里不容易提示、但直接影响项目能不能上线的关键点。你照着这篇文章做应该能比我更快跑通跑通后再根据你的业务场景把 Analyzer 抽出来、接好回调、处理好重复扫码一个能用的扫码模块就成了。本文还有配套的精品资源点击获取
返回列表