从零手搓扫码模块:ZXing二维码与条形码扫描实现全解析 简介这份资源面向Android开发者与移动应用初学者聚焦在项目中快速落地二维码与条形码扫描功能解决相机调用、条码解码与权限处理等常见集成难题。资源以ZXing开源库为核心涵盖Gradle依赖配置、扫描界面搭建、Intent启动扫描、结果回调解析以及闪光灯与扫描区域提示等完整实现思路。压缩包共1874个文件约18.42MB包含java源码、xml布局、gradle构建脚本、jar与dex库文件、json配置及png图片等其中java与xml构成示例工程主体jar与dex体现已编译依赖docx说明文档则补充集成步骤与常见问题。目前已有2535人学习下载。通过附带的说明文档与MyApplication示例工程读者可对照源码理解ZXing的接入流程掌握自定义扫描界面、相机权限检查与错误处理等关键环节适合作为Android条码扫描功能的参考实现与排错依据。1. 从零手搓一个扫码模块为什么我最后选了 ZXing去年接了个仓储盘点的小工具需求很朴素用手机摄像头扫商品条码扫完直接入库。我一开始想得很简单调个系统相机不就完了结果真上手才发现系统相机只负责拍照识别条码得自己接识别库。试了两个方案要么识别率玄学光线一暗就罢工要么依赖太重打包出来体积直接起飞。折腾一圈最后还是回到 ZXing 这个老牌开源库上。这份资源就是围绕 ZXing 做的一套二维码扫描和条形码扫描实现覆盖了从相机预览、图像帧解析到结果回调的完整链路。它适合两类人一类是刚接触移动端扫码、想找个能跑通的参考实现的新手另一类是已经在用扫码功能、但想搞清楚底层识别流程、方便后续做定制优化的开发者。下面我按「这项目能干什么 → 怎么跑起来 → 核心代码怎么改」的顺序把这份资源拆开讲。2. 项目定位与技术选型ZXing 到底解决了什么问题2.1 扫码功能的本质是一条图像处理流水线很多人以为扫码就是「摄像头对准 → 出结果」中间是黑匣子。实际上它是一条挺长的流水线相机采集到的是 YUV 格式的原始帧得先转成灰度图再做二值化处理然后定位条码的四个角点最后才是解码。ZXing 的价值就在于它把这套流水线封装好了你只要把图像帧喂进去它吐给你一个 Result 对象。这份资源的技术栈是典型的移动端扫码组合相机层负责取流ZXing 负责识别中间用一个帧调度器控制识别频率。为什么不用系统自带的扫码 API因为系统 API 通常只给你一个「扫到了」的结果中间过程你碰不到想调识别区域、想换解码格式、想加自定义预处理都没地方下手。ZXing 是开源的源码就在那改起来心里有底。2.2 为什么是 ZXing 而不是别的方案选型这事我踩过坑。最早用的是一个封装得很漂亮的商业 SDK识别率确实高但按调用次数收费盘点这种高频场景成本扛不住。后来试过纯前端方案用 JavaScript 调摄像头在低端安卓机上帧率掉得厉害扫一个码要等两三秒。ZXing 的优势在于第一它是纯 Java 实现安卓端集成没有 native 依赖打包体积可控第二它支持的码制全QR Code、Data Matrix、Code 128、EAN-13 这些常见格式都覆盖了第三社区资料多遇到问题搜一下基本能找到方向。代价是它的默认配置偏保守识别速度不是最快的需要自己调参数。提示ZXing 的识别速度和准确率是一对矛盾。把TRY_HARDER打开能提升识别率但每帧耗时明显增加低端机上会卡。我一般是在预览阶段用快速模式用户点击「精确扫描」时再切到 TRY_HARDER。2.3 这份资源的模块划分资源里的代码按职责分成了几块相机管理模块负责打开摄像头、配置预览尺寸、处理生命周期帧分析模块负责从预览流里抽帧、控制识别频率解码模块封装了 ZXing 的 MultiFormatReader负责把图像数据转成识别结果UI 层负责画扫描框、处理权限申请和结果展示。这种分层的好处是你想换识别库只需要动解码模块想换相机实现只动相机管理模块。耦合度低改起来不牵一发动全身。3. 跑起来环境准备与最小可运行配置3.1 依赖引入与权限声明先把依赖加上。ZXing 的核心库通过 Gradle 引入安卓端还需要一个 camera 的封装库来简化相机操作。常见做法是引zxing-android-embedded它把相机预览和 ZXing 解码串好了开箱即用。// build.gradle 模块级 dependencies { // ZXing 核心解码库 implementation com.google.zxing:core:3.5.2 // 安卓端封装处理相机预览和解码调度 implementation com.journeyapps:zxing-android-embedded:4.3.0 }参数说明core是纯 Java 的解码核心不依赖安卓zxing-android-embedded是安卓封装层版本号跟着 core 走。如果你的项目已经用了 CameraX也可以只引 core自己写相机逻辑但工作量会大不少。权限这块安卓 6.0 以上要动态申请相机权限安卓 10 以上如果要把扫描结果存成图片还得加存储权限。清单文件里先声明uses-permission android:nameandroid.permission.CAMERA / uses-feature android:nameandroid.hardware.camera android:requiredtrue /requiredtrue表示没有相机的设备不安装这个应用避免装上去闪退。3.2 启动扫描页面的最小代码封装库提供了一个CaptureActivity直接拿来用就能扫。但实际项目里一般要自定义界面所以更常见的做法是继承CaptureActivity或者用DecoratedBarcodeView自己搭。// 自定义扫描 Activity public class ScanActivity extends AppCompatActivity { private DecoratedBarcodeView barcodeView; Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_scan); barcodeView findViewById(R.id.barcode_scanner); // 设置解码格式这里同时支持二维码和条形码 barcodeView.getBarcodeView().setDecoderFactory( new DefaultDecoderFactory(Arrays.asList( BarcodeFormat.QR_CODE, BarcodeFormat.CODE_128, BarcodeFormat.EAN_13 )) ); barcodeView.decodeContinuous(new BarcodeCallback() { Override public void barcodeResult(BarcodeResult result) { // 识别成功回调result.getText() 就是内容 handleResult(result.getText()); } Override public void possibleResultPoints(ListResultPoint resultPoints) { // 可选拿到条码在画面中的位置点用来画定位框 } }); } Override protected void onResume() { super.onResume(); barcodeView.resume(); // 恢复相机预览 } Override protected void onPause() { super.onPause(); barcodeView.pause(); // 暂停预览释放相机 } }逻辑说明setDecoderFactory决定了识别哪些码制不设的话默认只认二维码条形码扫不出来。decodeContinuous是持续解码模式相机每出一帧就尝试识别一次适合扫码场景。onResume和onPause里必须调resume和pause否则切到后台相机不释放再回来会黑屏。参数说明BarcodeFormat是个枚举按需添加。加得越多每帧尝试的解码器越多速度越慢。我一般只加业务需要的两三种。3.3 布局文件与扫描框绘制布局里放DecoratedBarcodeView它自带了一个扫描框的绘制逻辑也可以自己覆盖。LinearLayout xmlns:androidhttp://schemas.android.com/apk/res/android android:layout_widthmatch_parent android:layout_heightmatch_parent android:orientationvertical com.journeyapps.barcodescanner.DecoratedBarcodeView android:idid/barcode_scanner android:layout_widthmatch_parent android:layout_heightmatch_parent app:zxing_scanner_layoutlayout/custom_scanner_overlay / /LinearLayoutcustom_scanner_overlay是你自己画的扫描框布局中间一个透明区域四周半透明遮罩。这个文件不复杂但决定了用户体验——扫描框大小要跟相机预览的识别区域匹配画太大用户以为整个屏幕都能扫画太小又难对准。注意扫描框的宽高比最好跟相机预览的宽高比一致否则会出现「框内明明有码却扫不出来」的情况。我一般把扫描框设成正方形因为二维码是方的条形码横向扫的时候稍微偏一点也能识别。4. 核心模块拆解一帧图像从相机到识别结果的完整链路4.1 相机预览与帧数据获取相机预览这块封装库底层用的是CameraAPI不是 Camera2兼容性好但配置项少。预览启动后相机会持续输出帧数据封装库通过PreviewCallback拿到 NV21 格式的字节数组。// 封装库内部简化逻辑理解原理用 private final Camera.PreviewCallback previewCallback new Camera.PreviewCallback() { Override public void onPreviewFrame(byte[] data, Camera camera) { // data 是 NV21 格式的原始帧 Camera.Size size camera.getParameters().getPreviewSize(); // 把帧数据交给解码线程注意这里不能做耗时操作 decodeHandler.post(() - decodeFrame(data, size.width, size.height)); } };逻辑说明onPreviewFrame是在相机线程回调的不能在这里做解码否则会阻塞相机导致预览卡顿。常见做法是丢到一个 HandlerThread 或者线程池里异步处理。data是 NV21 格式ZXing 需要的是灰度图中间要做一次转换。参数说明size.width和size.height是预览分辨率。分辨率越高识别越准但转换和解码耗时越长。我一般选 1280x720兼顾速度和精度。4.2 NV21 转灰度图与识别区域裁剪ZXing 的PlanarYUVLuminanceSource可以直接吃 NV21 数据但如果你只想识别扫描框内的区域得先裁剪。// 把 NV21 帧转成 ZXing 能吃的 LuminanceSource private PlanarYUVLuminanceSource buildLuminanceSource(byte[] data, int width, int height) { // 计算扫描框在预览画面中的矩形区域 Rect rect getScanRect(width, height); // 裁剪并转换只保留框内数据 return new PlanarYUVLuminanceSource( data, width, height, rect.left, rect.top, rect.width(), rect.height(), false // 不旋转 ); }逻辑说明PlanarYUVLuminanceSource是 ZXing 提供的类它只取 Y 通道亮度忽略 UV 色度通道因为条码识别只需要亮度信息。裁剪的好处是减少计算量同时避免框外的干扰图案被误识别。参数说明rect的计算要跟 UI 上画的扫描框对应。如果预览画面和屏幕显示有缩放得做坐标映射。这块容易出错我一般会打日志把 rect 的四个值打出来跟 UI 上的框对比。4.3 MultiFormatReader 解码与结果封装拿到 LuminanceSource 后交给MultiFormatReader解码。// 解码核心逻辑 private void decodeFrame(byte[] data, int width, int height) { try { PlanarYUVLuminanceSource source buildLuminanceSource(data, width, height); BinaryBitmap bitmap new BinaryBitmap(new HybridBinarizer(source)); // 配置解码提示 MapDecodeHintType, Object hints new EnumMap(DecodeHintType.class); hints.put(DecodeHintType.POSSIBLE_FORMATS, Arrays.asList( BarcodeFormat.QR_CODE, BarcodeFormat.CODE_128)); hints.put(DecodeHintType.TRY_HARDER, Boolean.TRUE); // 执行解码 Result result multiFormatReader.decode(bitmap, hints); // 回到主线程更新 UI mainHandler.post(() - handleResult(result.getText())); } catch (NotFoundException e) { // 这一帧没识别到正常现象忽略继续下一帧 } catch (ChecksumException | FormatException e) { // 识别到了但校验失败可能是图像模糊记录日志 } }逻辑说明HybridBinarizer是二值化策略它比GlobalHistogramBinarizer更适合光照不均的场景。DecodeHintType是解码提示POSSIBLE_FORMATS限定码制能提速TRY_HARDER让解码器多试几种策略。NotFoundException是最高频的异常每帧扫不到都会抛不要当错误处理。参数说明multiFormatReader要复用不要每次 new它内部有缓存。hints里的TRY_HARDER在预览阶段建议关掉用户点「精确扫描」时再开。4.4 识别频率控制与性能平衡相机一秒出 30 帧如果每帧都解码CPU 扛不住。常见做法是隔帧处理或者加时间间隔。// 用时间戳控制解码频率最快 100ms 一次 private long lastDecodeTime 0; private static final long DECODE_INTERVAL 100; private void onFrameAvailable(byte[] data, int width, int height) { long now System.currentTimeMillis(); if (now - lastDecodeTime DECODE_INTERVAL) { return; // 间隔太短跳过这一帧 } lastDecodeTime now; decodeFrame(data, width, height); }逻辑说明DECODE_INTERVAL设成 100ms相当于每秒最多解码 10 次肉眼感觉不到延迟但 CPU 占用降了一半以上。这个值可以根据设备性能调低端机设 150ms旗舰机设 50ms。参数说明不要用Thread.sleep来控制频率会阻塞线程。用时间戳判断跳过的帧直接丢弃不占资源。5. 避坑与常见问题排查5.1 扫不出来先看权限再看对焦现象相机预览正常但怎么扫都没反应。原因最常见的是相机没对焦。手机离条码太近超过了最近对焦距离画面是糊的ZXing 自然识别不了。其次是权限只申请了相机但没处理用户拒绝后的逻辑导致预览是黑的但代码不报错。解决在相机配置里打开连续对焦模式Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE。权限申请用ActivityCompat.requestPermissions在onRequestPermissionsResult里判断结果拒绝了要给用户提示并引导去设置页。5.2 识别慢分辨率太高或 TRY_HARDER 常开现象扫一个码要等一两秒才出结果。原因预览分辨率设成了 1920x1080每帧转换和裁剪耗时翻倍。或者TRY_HARDER一直开着解码器每帧都在做多策略尝试。解决预览分辨率降到 1280x720TRY_HARDER只在用户主动触发精确扫描时开。另外检查POSSIBLE_FORMATS是不是加了一堆用不上的码制砍到只剩业务需要的。5.3 条形码扫不出码制没加或方向不对现象二维码能扫条形码死活扫不出来。原因DefaultDecoderFactory里没加条形码格式默认只认二维码。或者条形码是横向的但相机预览是竖屏ZXing 默认不旋转图像导致条码方向跟解码器预期不符。解决在setDecoderFactory里显式加上BarcodeFormat.CODE_128、EAN_13等。方向问题可以在PlanarYUVLuminanceSource构造时把最后一个参数设成true尝试旋转或者引导用户横屏扫描。5.4 切后台回来黑屏生命周期没管好现象按 Home 键再回来相机预览黑了要杀掉重进才恢复。原因onPause里没调barcodeView.pause()相机没释放再resume时资源冲突。解决严格在onResume调resume()onPause调pause()。如果用了 Fragment对应onResume和onPause也要处理。另外有些设备在onStop后相机会被系统回收onStart里要重新初始化。5.5 识别结果重复回调没有去重现象同一个码连续回调好几次导致重复入库。原因decodeContinuous模式下只要码还在画面里每帧都会识别成功并回调。解决加一个标志位识别成功后先暂停解码处理完业务再恢复。或者记录上次识别的内容和时间短时间内相同内容直接忽略。private String lastResult; private long lastResultTime; private void handleResult(String text) { long now System.currentTimeMillis(); if (text.equals(lastResult) now - lastResultTime 2000) { return; // 2 秒内相同结果忽略 } lastResult text; lastResultTime now; // 处理业务逻辑 }6. 二次开发自定义解码格式与识别区域的高级玩法6.1 自定义解码器处理非标准码制ZXing 内置的码制覆盖了主流标准但有些行业场景用的是私有格式比如某些仓储标签在 Code 128 基础上加了自定义校验位。这时候可以继承MultiFormatReader或者实现Reader接口在decode方法里先调父类解码拿到原始字节后再做二次校验。// 自定义 Reader在标准解码后追加业务校验 public class CustomCode128Reader implements Reader { private final MultiFormatReader delegate new MultiFormatReader(); Override public Result decode(BinaryBitmap image, MapDecodeHintType, ? hints) throws NotFoundException, ChecksumException, FormatException { Result result delegate.decode(image, hints); String text result.getText(); // 追加自定义校验不通过就抛 NotFoundException if (!verifyCustomChecksum(text)) { throw NotFoundException.getNotFoundInstance(); } return result; } private boolean verifyCustomChecksum(String text) { // 业务校验逻辑比如长度、前缀、校验位计算 return text.length() 16 text.startsWith(WH); } }逻辑说明delegate负责标准解码拿到结果后做业务层校验。校验不通过抛NotFoundException上层会当作「这一帧没扫到」处理继续下一帧。这样不会因为一个非法码中断整个扫描流程。参数说明verifyCustomChecksum里不要做耗时操作它每帧都会被调用。复杂校验建议放到结果回调之后异步做。6.2 多码同时识别一次扫多个条码盘点场景经常一箱货有好几个条码一个个扫效率太低。ZXing 支持GenericMultipleBarcodeReader可以在一次解码中找出多个条码。// 多码识别 private void decodeMultiple(byte[] data, int width, int height) { try { PlanarYUVLuminanceSource source buildLuminanceSource(data, width, height); BinaryBitmap bitmap new BinaryBitmap(new HybridBinarizer(source)); MultipleBarcodeReader reader new GenericMultipleBarcodeReader(multiFormatReader); Result[] results reader.decodeMultiple(bitmap); for (Result r : results) { // 逐个处理识别结果 handleResult(r.getText()); } } catch (NotFoundException e) { // 一个都没扫到 } }逻辑说明GenericMultipleBarcodeReader会先找一个码然后在剩余区域继续找直到找不到为止。它比单码识别慢但一次能出多个结果。参数说明多码识别对图像质量要求高建议在TRY_HARDER模式下用并且限制最大识别数量避免一帧里误识别出太多噪声。6.3 识别区域动态调整根据码的位置自动缩放固定扫描框有个问题用户离得远码在框里太小离得近码又超出框。可以根据上一帧识别到的ResultPoint动态调整裁剪区域。// 根据上一帧结果点动态调整扫描区域 private Rect dynamicScanRect null; private PlanarYUVLuminanceSource buildDynamicSource(byte[] data, int width, int height) { if (dynamicScanRect null) { // 首次使用默认扫描框 dynamicScanRect getDefaultScanRect(width, height); } return new PlanarYUVLuminanceSource( data, width, height, dynamicScanRect.left, dynamicScanRect.top, dynamicScanRect.width(), dynamicScanRect.height(), false ); } // 在 possibleResultPoints 回调里更新区域 Override public void possibleResultPoints(ListResultPoint points) { if (points.size() 3) { // 根据三个定位点估算条码区域适当外扩 dynamicScanRect calculateRectFromPoints(points, 1.2f); } }逻辑说明possibleResultPoints是 ZXing 在解码过程中找到的定位点即使最终没解码成功这些点也能告诉你「这里可能有码」。根据这些点动态调整下一帧的裁剪区域能提升连续扫描的体验。参数说明外扩系数1.2f是经验值太小容易切掉码的边缘太大又失去了裁剪的意义。可以根据实际场景调。6.4 验证改造效果的方法改完代码怎么验证我一般分三步第一步用固定的测试码图片在单元测试里跑解码逻辑确保功能正确第二步在真机上用不同光照、不同距离、不同角度的码做压力测试记录识别率和平均耗时第三步用 Android Profiler 看 CPU 和内存占用确保没有泄漏。提示ZXing 的识别率对图像质量很敏感。测试时不要只用打印清晰的二维码要覆盖屏幕反光、纸张褶皱、部分遮挡这些真实场景。我吃过亏实验室里跑得好好的到仓库里一用就翻车。从那以后我每次改扫码逻辑都强制走一遍「单元测试 → 真机多场景 → 性能分析」这三步再急也不跳过。希望这些经验能帮到你少走点弯路。本文还有配套的精品资源点击获取