ZXing实战:Android二维码与条形码同源识别技术解析

发布时间:2026/10/17 8:47:09
ZXing实战:Android二维码与条形码同源识别技术解析 简介面向 Android 开发者的二维码与条形码扫描项目实例核心基于 ZXing 库实现覆盖从 Gradle 依赖配置、自定义捕获界面、SurfaceView 相机预览、调用 startActivityForResult 发起扫描到在 onActivityResult 中解析 SCAN_RESULT 结果以及相机权限申请、闪光灯控制、扫描区域指示和异常提示等关键环节流程完整可直接复用。压缩包共 1874 个文件约 18.42MB主要包含 Java 源码、XML 布局与配置、编译生成的 dex/class 与 APK 安装包还含 jar 依赖、properties 配置、gradle 构建脚本和 docx/txt 说明文档基本保留 Android 工程原始结构与构建信息。项目内还包含配置文件、资源文件和构建产物可帮助排查依赖冲突并还原可运行工程。资源内提供 MyApplication 示例应用直观展示自定义扫码界面、结果回调和容错处理方式便于开发者对比验证。目前已有 2535 人学习下载适合需要快速集成扫码能力、理解 ZXing 底层工程结构的中高级移动开发者参考。1. 二维码和条形码一起扫一个基于ZXing的扫码项目能解决什么有一次我在做仓库盘点工具货架上的商品既有二维码也有条形码而且条形码还是EAN-13和CODE_128混用。最开始想的是接一个现成的扫码SDK后来发现它背后就是ZXing于是干脆把源码拆下来自己改。这个项目做的就是这件事一套基于ZXing的扫码实现同时支持二维码和条形码识别包含相机预览、自动对焦、闪光灯、取景框绘制、解码线程和结果回调可以直接跑成一个扫码Demo也可以把关键类搬进自己的App里。适合想在App内集成扫码能力、又想保留可控性的Android开发者。2. 技术选型与目录结构一套ZXing代码管两种码的底层设计2.1 从摄像头到结果ZXing把一帧图像变成字符串的链路扫码这件事表面上看是“拍一张照片再用OCR识别”实际上ZXing做得比这简单得多。摄像头预览出来的每一帧是NV21格式的YUV原始数据ZXing先从里面抽取亮度分量生成一个PlanarYUVLuminanceSource再用HybridBinarizer把亮度图转换成“黑/白两色”的BinaryBitmap最后交给解码器去解析。二维码和条形码在这个阶段没有区别都统一走MultiFormatReader.decodeWithState()进入哪种码对应的Reader分支完全由hints里的POSSIBLE_FORMATS决定。也就是说写“二维码扫描和条形码扫描”两个功能底层是同一套管线采集预览帧 → 转为二值图 → 按格式列表匹配解码器 → 返回Result。你需要做的只是把BarcodeFormat.QR_CODE和BarcodeFormat.EAN_13这些加进同一个格式集合。这样设计的好处是新增一种码的时候不用动相机逻辑只改格式配置。很多封装好的扫码SDK把这一套包成了黑匣子表面好看真到了识别率不行或者想要定制取景框的时候你会发现没法下手。自己基于ZXing core桥接相机最大的收益是每一步都可以控制自动对焦策略、预览帧裁剪、二值化方式、格式列表全都可以调。这也是这个项目最值得读的部分。2.2 目录结构扫码Demo的每个关键类负责什么一个典型的ZXing扫码Demo目录大概长这样app/src/main/java/com/example/qrcode/ ├── CaptureActivity.java # 扫码页主Activity ├── CaptureActivityHandler.java # 预览帧与解码结果的消息转发 ├── CameraManager.java # 相机的打开、关闭、预览帧回调 ├── CameraConfigurationManager.java # 相机参数对焦、分辨率、闪光灯 ├── DecodeThread.java # 解码线程持有Looper和MultiFormatReader ├── DecodeHandler.java # 在解码线程里真正执行decode ├── ViewfinderView.java # 自定义View画取景框和扫描线 ├── DecodeFormatManager.java # 集中管理支持的BarcodeFormat └── decode/ └── DecodeUtils.java # 对解码结果做二次整理的工具类CaptureActivity是入口负责把SurfaceView预览区、CameraManager和ViewfinderView串联起来CameraManager负责和摄像头驱动打交道DecodeThread跑在子线程里用Handler机制接收主线程发来的预览帧避免在主线程里做位图运算造成掉帧。这类拆分在扫码项目里几乎是标准答案照着这个目录去理解就行。2.3 解码参数POSSIBLE_FORMATS和CHARACTER_SET这样配多格式识别最关键的是DecodeHintType。我们看排在最前面的两个配置MapDecodeHintType, Object hints new EnumMap(DecodeHintType.class); SetBarcodeFormat formats DecodeFormatManager.QR_CODE_FORMATS; hints.put(DecodeHintType.POSSIBLE_FORMATS, formats); hints.put(DecodeHintType.CHARACTER_SET, UTF-8);POSSIBLE_FORMATS直接决定解码器尝试哪些Reader。如果只放QR_CODE解码线程就不会去看条形码如果塞进去十几种条形码格式每个预览帧都要把格式列表轮询一遍解码耗时自然变长。所以“同时支持二维码和条形码”不是无脑把格式全部堆上而是按场景取舍商品条码一般只要EAN_13、EAN_8、UPC_A、UPC_E工业场景再加CODE_128、CODE_39、ITF会议签到就只留QR_CODE。CHARACTER_SET影响字符串编码。默认不传时ZXing会按条码里的编码提示去猜绝大多数场景没问题。但碰到中文内容的二维码UTF-8没配好很容易读出一串乱码所以我会在项目里固定加上这个hint。参数调完就能看到这个Demo的复用方式换一套格式集合等于换了一个扫码场景。这也是为什么把格式配置单独放到DecodeFormatManager而不是写死在解码线程里的原因。后面做二次开发的时候你只需要在那一处调整。3. 核心模块拆解一次扫码从预览帧到结果回调的完整链路3.1 打开相机CameraManager的开机动作扫码页一启动CaptureActivity就会调用CameraManager的openDriver()。这个方法做的事情是拿到相机实例、设置预览尺寸、绑定SurfaceView、启动预览。一段接近实际代码的写法是这样public void openDriver() throws IOException { Camera theCamera camera; if (theCamera null) { theCamera Camera.open(); camera theCamera; } CameraParameters parameters theCamera.getParameters(); configManager.initFromCameraParameters(parameters); configManager.setDesiredCameraParameters(parameters); theCamera.setPreviewDisplay(previewHolder); theCamera.startPreview(); }相机是共享资源打开前要判断有没有已经被占用Camera.open()不带参数在Android 2.3之后默认打开后置摄像头如果设备有多个后摄部分机型需要选到主摄像头。configManager.setDesiredCameraParameters会按照屏幕比例去设置预览尺寸并把CONTINUOUS_PICTURE或AUTO对焦模式写进参数里。旧版Camera API虽然在新系统上有些机型兼容性问题但这个Demo保留它的原因很直接它的setOneShotPreviewCallback每次只回调一帧把回调数据交给解码线程后马上释放相机管线比起更上层的相机框架更贴近ZXing已有的解码逻辑。如果你想换新框架改动量比想象中大建议先用同一个CameraManager把流程跑通。3.2 预览帧进入子线程DecodeThread和Handler摄像头回调非常频繁不能直接在onPreviewFrame里执行解码。常见做法是启动一个带Looper的DecodeThread把解码任务排队执行final class DecodeThread extends Thread { private final CaptureActivity activity; private final MultiFormatReader multiFormatReader; private DecodeHandler handler; DecodeThread(CaptureActivity activity, int decodeMode) { this.activity activity; multiFormatReader new MultiFormatReader(); multiFormatReader.setHints(DecodeFormatManager.buildDecodeHints(decodeMode)); } Override public void run() { Looper.prepare(); handler new DecodeHandler(activity, multiFormatReader); Looper.loop(); } DecodeHandler getHandler() { return handler; } }Looper.prepare()和Looper.loop()让这个线程有了消息队列DecodeHandler就可以像主线程一样处理Message。CaptureActivityHandler负责接收主线程发来的“预览帧已就绪”消息再从DecodeThread.getHandler()取出解码Handler把帧数据post过去。这样主线程、预览回调、解码线程三者之间全是异步消息不会卡界面。这里有个容易翻车的点DecodeThread里创建的handler必须在线程的run方法执行完之后才能拿到如果你在Activity的onCreate里立刻调用getHandler()很可能拿到null。所以CaptureActivity通常是在onResume里把DecodeThread start起来再通过CaptureActivityHandler发一个预备消息等解码线程ready后再触发预览。3.3 真正解码PlanarYUVLuminanceSource加HybridBinarizer当预览帧数据到DecodeHandler后常见的处理是把YUV数据包装成亮度图public void handlePreviewFrame(byte[] data, Camera camera) { Point cameraResolution configManager.getCameraResolution(); PlanarYUVLuminanceSource source buildLuminanceSource(data, cameraResolution); if (source ! null) { BinaryBitmap bitmap new BinaryBitmap(new HybridBinarizer(source)); try { Result rawResult multiFormatReader.decodeWithState(bitmap); activity.handleDecode(rawResult, bitmap); } catch (ReaderException ignored) { // 这一帧没有码继续下一帧 activity.continuePreview(); } finally { multiFormatReader.reset(); } } }buildLuminanceSource不是简单把整个YUV数组传进去而是会按取景框在屏幕上的矩形范围做一次裁剪。很多初学者把整帧数据都交给解码器结果就是取景框外的干扰信息太多识别率反而下降。裁剪坐标通常来自ViewfinderView计算出来的Rect和用户在屏幕上看到的框必须一致。decodeWithState是解码入口为什么后面还要调reset()因为MultiFormatReader实例会被复用到下一个预览帧不清掉上一次尝试过程中产生的状态可能影响之后的匹配。这也是一个典型的全局状态清理问题。ReaderException在这里是预期异常扫码的本质就是“大部分帧都扫不出”把它当普通异常打日志反而会刷屏。3.4 结果回到界面防抖和防重复识别成功以后DecodeHandler会发一个成功消息给CaptureActivityHandlerCaptureActivity在handleDecode里处理public void handleDecode(Result rawResult, Bitmap barcode) { long current System.currentTimeMillis(); if (current - lastScanTime 500L) { continuePreview(); return; } lastScanTime current; String text rawResult.getText(); BarcodeFormat format rawResult.getBarcodeFormat(); // 更新界面提示“已识别到条码”或直接退出扫码页 }lastScanTime是Activity里维护的一个时间戳作用是防重复回调。扫码成功之后用户还没来得及移开手机下一帧解码又成功了如果不做时间窗拦截同一张码会连续回调两三次。500毫秒是一个比较稳的阈值追求响应速度可以压到300毫秒再低就会误伤连续扫码的场景。Result里除了getText()还有getBarcodeFormat()、getRawBytes()、getResultPoints()。二次开发时经常用到getResultPoints()可以拿到条码在图像里的四个角点用来做“对准提示”或者“码在画面中心才允许识别”。3.5 格式集合二维码和条形码在一个hints里共存为了让这个项目同时支持二维码和条形码DecodeFormatManager里会把它们组合到一起public static final SetBarcodeFormat ALL_FORMATS EnumSet.of( BarcodeFormat.QR_CODE, BarcodeFormat.DATA_MATRIX, BarcodeFormat.CODE_128, BarcodeFormat.CODE_39, BarcodeFormat.EAN_13, BarcodeFormat.EAN_8, BarcodeFormat.UPC_A, BarcodeFormat.UPC_E, BarcodeFormat.ITF ); public static MapDecodeHintType, Object buildDecodeHints(int decodeMode) { EnumMapDecodeHintType, Object hints new EnumMap(DecodeHintType.class); hints.put(DecodeHintType.POSSIBLE_FORMATS, ALL_FORMATS); hints.put(DecodeHintType.CHARACTER_SET, UTF-8); return hints; }EnumSet.of里格式的顺序会影响解码匹配顺序。ZXing内部会遍历这个集合尝试每个Reader所以把最常出现的QR_CODE放前面能减少一点无效尝试。这里面DATA_MATRIX属于工业码普通项目用不上ITF长得很奇怪是物流码一般只有快递面单才需要。保持一个最小可用集合比堆格式明智得多。4. 运行与集成指南把Demo编译成App再并进自己的项目4.1 项目依赖与最低版本我先说结论这个Demo用到的就是ZXing的core和android-core两个库。Gradle里通常这样加android { defaultConfig { minSdk 21 targetSdk 31 } } dependencies { implementation com.google.zxing:core:3. implementation com.google.zxing:android-core:3. }3.是为了在本地仓库里匹配到最新3.x小版本正式项目里我会锁成一个固定版本号避免哪天自动升级变了包结构。minSdk为什么建议21因为CameraManager的旧API在5.0以后稳定很多Camera.Parameters的连续对焦模式也普遍可用再往下兼容的话还要处理内置闪光灯和摄像头数量判断工作量会变大。4.2 清单文件和相机权限扫码必须声明相机权限这个权限属于运行时危险权限Android 6.0以上还需要在代码里动态申请。uses-permission android:nameandroid.permission.CAMERA/ uses-feature android:nameandroid.hardware.camera android:requiredfalse/ uses-feature android:nameandroid.hardware.camera.autofocus android:requiredfalse/ uses-feature android:nameandroid.hardware.camera.flash android:requiredfalse/requiredfalse的意思是没有摄像头的设备也能安装App只是装上了进不了扫码页。有些项目把required漏掉平板或电视没有摄像头就会被渠道市场过滤虽然我们的目标不是上架但养成这个习惯没坏处。权限方面录像是完全不需要的扫码只用setOneShotPreviewCallback拿到单帧不需要录制视频。别把相机权限和存储权限混在一起去申请扫码结果拿到的是字符串不需要存图片。动态申请的代码逻辑通常是检查checkSelfPermission没有授权就requestPermissions授权成功后再走openDriver。4.3 编译和真机验证直接用命令行编译安装./gradlew :app:assembleDebug adb install -r app/build/outputs/apk/debug/app-debug.apk adb shell am start -n com.example.qrcode/.CaptureActivity装上后先不要急着扫二维码从打印的条形码、药盒上的EAN-13、电脑屏幕上的二维码各扫一遍。模拟器基本不要指望相机预览在多数模拟器上是一张静态图解码线程拿到的帧是空的扫不出来不是代码问题。做扫码开发一定要用真机。如果构建时报core和android-core版本不一致一般是两个依赖都用了3.而实际解析到了不同的小版本把两个坐标都改成同一个明确版本就能解决。4.4 集成方式一Intent方式如果不想把整个CaptureActivity搬进自己的App可以像调用系统相机一样用Intent启动扫码页IntentIntegrator.forActivity(this) .setCaptureActivity(ScanActivity.class) .setDesiredBarcodeFormats(IntentIntegrator.ALL_CODE_TYPES) .setPrompt(请对准条码) .initiateScan();IntentIntegrator会帮你构造Intent并在扫码页返回后把结果写到onActivityResult里Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { IntentResult result IntentIntegrator.parseActivityResult(requestCode, resultCode, data); if (result ! null result.getContents() ! null) { String text result.getContents(); BarcodeFormat format BarcodeFormat.valueOf(result.getFormatName()); // 这里拿到扫码结果 } }这种方式适合“App里只有一个入口需要扫码”的场景。缺点是扫码页UI是写死的你想在取景框下面加个“手动输入条码”按钮就得改库里的布局。4.5 集成方式二源码模式“同时支持二维码和条形码”这个项目最有价值的地方就是源码可以直接搬。把CameraManager、DecodeThread、DecodeHandler、ViewfinderView、DecodeFormatManager复制到你自己的模块里然后把CaptureActivity改成你想要的样子。搬过去之后要改的地方主要是包名、资源引用以及CaptureActivity对外的结果封装。个人经验是先用Intent方式跑通全流程再逐步把源码替换进来避免一次替换引入太多问题。4.6 扫码页生命周期onResume启动、onPause释放CaptureActivity需要在onResume里打开相机和启动解码线程在onPause里关闭相机Override protected void onResume() { super.onResume(); cameraManager new CameraManager(getApplicationContext()); decodeThread new DecodeThread(this, decodeMode); decodeThread.start(); // 通过Handler触发一次预览帧请求 } Override protected void onPause() { if (decodeThread ! null) { decodeThread.getHandler().quitSafely(); } cameraManager.closeDriver(); super.onPause(); }如果不在onPause释放相机下次进入扫码页时摄像头会被占用出现黑屏或“相机被打开失败”的异常。这里面的顺序有讲究先退出解码线程再关相机保证不会有一帧数据在关闭过程中飞进已经销毁的Handler。5. 扫码避坑指南预览正常却扫不出来时的排查顺序扫码识别率这件事很多问题不是ZXing造成的而是相机数据和取景框流程没接对。这一章是血泪经验按“现象 → 原因 → 解决”整理五条最常见的。5.1 现象暗光环境下识别率骤降原因是光线太暗时黑、白区域的灰度值被压在一起HybridBinarizer二值化很容易把整块区域判成纯黑。解决在扫码页提供一个闪光灯开关在解码失败连续超过几帧后自动打开private void maybeSwitchFlash(boolean enabled) { CameraManager cameraManager cameraManager; if (cameraManager ! null) { cameraManager.setTorch(enabled); } }注意很多设备第一次开启闪光灯会有几百毫秒延迟打开后要等待下一次预览帧再解码。不要在主线程循环调用setTorch否则相机会卡住。我一般会让“自动补光”的触发条件宽松一点只要连续两秒没有识别结果就提示用户是否打开闪光灯而不是直接打开避免在强光环境误伤。5.2 现象二维码在手机或电脑屏幕上扫不出来原因是屏幕自发光导致摩尔纹加上屏幕玻璃反光二值化容易产生细碎噪点。还有一个原因是很多人生成的二维码没有留白边ZXing要求二维码周围有足够安静区。解决生成二维码时用QRCodeWriter输出前保留至少四个模块的padding。扫码时建议把手机离屏幕20厘米左右不要贴着屏。如果你的App需要扫自己页面上的二维码最好在生成时就把quietZone参数调大比用户反复调整距离省事得多。5.3 现象条形码竖着放扫不出来横着也时灵时不灵原因是ZXing对一维码的解码依赖整行像素的扫描码的左边和右边至少要有一段连续空间。如果条码被取景框的上下边缘切掉解码就会失败。解决在提示文案里告诉用户“请将条码横向对准取景框”并在代码里对预览帧做旋转把横竖屏的影响统一掉。很多扫码界面会在取景框两侧加白色半透明蒙层本质就是逼迫用户把码放在有效宽度内。5.4 现象手机离条码太近反而模糊原因是镜头最近对焦距离一般在7厘米以上靠近后已经超过镜头的微距能力。持续对焦模式在部分机型上近摄会来回拉风箱。解决在扫码开始前主动调用一次自动对焦并把对焦模式设置成MACRO扫完再切回去Camera.Parameters params camera.getParameters(); params.setFocusMode(Camera.Parameters.FOCUS_MODE_MACRO); camera.setParameters(params);这个参数不是所有机型都支持设置前最好用getSupportedFocusModes()判断一下。如果设备不支持MACRO就退回CONTINUOUS_PICTURE。5.5 现象画面正常但解码线程一直没有输出最常出现在把Demo改造进自己项目时没有处理预览帧循环。setOneShotPreviewCallback只回调一次如果你在解码失败的分支里没有调用requestPreviewFrame整个预览回调就断掉了。解决检查解码失败分支有没有activity.continuePreview()或者检查DecodeThread的handler是否为nullif (source null) { activity.continuePreview(); return; }排查顺序一定是先看预览是否在跑再看帧数据有没有进解码线程最后才怀疑算法参数。很多时候不是ZXing的问题而是我们自己的相机循环断在某个分支里了。上面这五条几乎覆盖了扫码项目里八成以上的“扫不出来”问题。6. 二次开发实战把单次扫码改成连续批量扫码当需求从“扫一下关页面”变成“连续扫一批条码”时改造点其实很小。最常见的做法是在handleDecode里不调用finish()而是把本次结果追加到列表后继续请求下一帧预览private void handleDecode(Result rawResult, Bitmap barcode) { String key rawResult.getBarcodeFormat().name() : rawResult.getText(); if (key.equals(lastResultKey)) { continuePreview(); return; } lastResultKey key; resultList.add(key); resultAdapter.notifyItemInserted(resultList.size() - 1); continuePreview(); } private void continuePreview() { cameraManager.requestPreviewFrame(previewCallback); }这里的lastResultKey做去除重复码如果不加用户在一个码前多停留一会列表里会塞满同一个结果。如果想允许同一个码被扫多次就把key改成加上时间戳的版本或者干脆去掉去重逻辑。连续模式还需要注意解码线程忙不忙如果上一帧还没解完下一帧就发过去Handler队列会越来越长。稳妥的做法是在发送前加一个isDecoding标志正在解码就暂时不请求新帧。验证这个方法很简单找一张打印纸贴上三个不同的二维码从画面左到右依次扫过。连续模式要求三个结果都进列表顺序不能乱重复扫同一个码不会重复添加。我从那以后每次做扫码项目都会强制走一遍“暗光、条纹、连续扫”三条路径确认这些分支没退化才敢收工。希望帮到你。本文还有配套的精品资源点击获取

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询