
简介基于 Google Zxing 的 Android 二维码/条形码扫码工程资源面向具备初步 Android 开发基础、希望快速实现扫码功能并模仿微信扫描交互的开发者。项目集成 Zxing 3.4.1 与 zxing-android-embedded 3.6.0完整覆盖相机预览、解码逻辑、扫描框动画、结果处理与分辨率适配等关键环节代码结构清晰可直接导入学习复用。压缩包共 177 个文件主要包含 Java 源码、XML 布局、PNG 图片资源、class 编译文件、jar 依赖以及一个可直接安装的 APK整体大小约 2.5MB文件分类明确便于按模块查阅。已有 437 人浏览学习。通过该资源读者可以掌握 Zxing 的集成方式、CameraManager 与 ViewfinderView 的协作机制理解仿微信扫描框的高亮跟随与异步解码流程同时获得针对 800×480 分辨率优化后的布局与摄像头参数设置经验适合用于学习研究或项目二次开发。 做Android开发这么久基本上只要涉及扫码功能十个人里有九个人第一反应都是ZXing。这个库确实是老牌经典但问题在于如果直接照搬官方Demo的CaptureActivity那个丑陋的扫描界面根本没法看产品经理那一关就过不去。所以后来我干脆花了一下午时间基于ZXing重新封装了一套仿微信二维码扫描效果把扫码界面完全自定义成微信那种竖条纹扫一扫的感觉二维码、条形码通吃工程结构也清晰很多。这篇就把我这套实现的核心思路、代码细节和踩坑记录整理出来给同样被ZXing原生界面折磨过的兄弟们一个参考。1. 技术方案选型为什么还是ZXing而不是其他扫码库先说结论现在做扫一扫功能可选的方案其实不少但我仍然推荐基于ZXing来做二次开发原因主要有三点。第一ZXing是开源界的老牌选手识别算法成熟对二维码和条形码的支持都极其稳定。虽然中间几年Google大力推广CameraX和ML Kit但在生产环境中跑一跑就会发现ZXing在低分辨率图像上的解码速度和容错率仍然很能打尤其是在条形码场景下ZXing对EAN-13、Code-128这些格式的支持非常全面。第二ZXing的代码结构清晰core模块和android模块分离。我们完全依赖core模块做核心解码逻辑界面和相机预览自己写这样自由度反而最高。很多封装库比如BGAQRCode-Android、QRCode-Android虽然开箱即用但一旦遇到定制需求就得去改别人的大量源码UML图翻半天也不知道改哪一行。自己做一套所有东西都掌握在自己手里。第三扫码的本质是光学识别最核心的性能瓶颈不在解码而在预览数据的获取和格式转换。ZXing的解码入口接收的是YUV格式的相机帧如果我们直接使用ZXing官方提供的CameraConfigurationUtils和CameraManager可以少走很多弯路而且它内部已经处理好了旋转、裁剪、对焦策略这些细枝末节。当然有人会问为什么不用Camera2 API或者CameraX重写相机预览我的答案是能用CameraX当然好但ZXing的解码包在设计时是基于Camera1的接口CameraX的Preview拿到的是ImageProxy格式转换还需要自己写YUV转NV21的操作反正都要写那不如直接用Camera1配合ZXing原生CameraManager代码量最少稳定性也够。选型确定之后整个项目结构就很清晰了相机预览用SurfaceView CameraManager解码逻辑走ZXing core界面自己画扫描框和动画完全自定义。接下来就是具体实现。2. 环境引入与基础配置2.1 项目依赖与仓库配置如果你是从旧项目升级或第一次引入ZXing一定要注意版本和依赖坐标的变化。老教程里经常写com.google.zxing:core:3.3.0这个坐标一直没问题但完整的扫码依赖如果直接引com.google.zxing:android-core:3.3.0和com.google.zxing:android-integration:3.3.0你会发现android-core这个额外库其实更像一个Google内置BarcodeScanner的封装并不适合我们自定义界面。所以我的做法是在build.gradle里只引入zxing的core模块相机和界面相关全部自己实现dependencies { // ZXing核心解码库 implementation com.google.zxing:core:3.4.1 }注意目前最新版3.4.1依赖的是JRE 1.8如果你的项目是Android SDK 29或以上默认支持没问题。如果你是在library模块里使用记得在module的build.gradle中声明compileOptions指向Java 8否则会报DexIndexOverflowException之类的异常。2.2 权限声明与Android 6.0动态权限扫码必须的权限有相机权限和震动权限震动用于扫到结果后的反馈。在AndroidManifest.xml中这样声明uses-permission android:nameandroid.permission.CAMERA / uses-permission android:nameandroid.permission.VIBRATE /注意Android 6.0及以上系统要求CAMERA属于危险权限必须在运行时动态申请。如果偷懒不申请直接打开相机在MainActivity里会直接抛出SecurityException而且有些国产ROM会在后台偷偷杀掉权限弹窗导致相机黑屏。我封装了一个简单的权限处理直接在onCreate里调用private void checkCameraPermission() { if (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA) ! PackageManager.PERMISSION_GRANTED) { ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.CAMERA}, REQUEST_CODE_CAMERA); } else { initCamera(); } }拿到权限后再去初始化相机这是扫码类应用最基础的清单流程。2.3 混淆规则如果你的项目开启混淆千万别忘了在proguard-rules.pro里加上ZXing的keep规则否则Release包扫码时直接崩溃或者识别率骤降-keep class com.google.zxing.** { *; } -dontwarn com.google.zxing.**这行配置的意义在于ZXing内部使用了大量的反射机制和新版本的Java特性混淆后如果把类的成员变量名改掉解码器就找不到对应的BitMatrix和DecodeHintType识别会变得极不稳定。我在发布第一个正式版本时就被这个坑过当时扫码只有第一次有效后面再扫就什么都识别不出来了坑了一下午才发现是混淆把解码算法的内部状态给搞乱了。3. 仿微信扫码界面实现的核心流程微信扫码界面的特点我们都很熟悉全屏相机预览中间一个正方形或长条形的扫描框四角有白色高亮角标中间有水平移动的扫描激光线整体视觉简洁、干净。要复刻这个效果关键不在UI画得多像而在于相机预览数据和扫描框UI的配合是否精准。3.1 整体架构预览、解码、回调三层分离我建议把扫码逻辑封装成一个独立的Activity叫ScanActivity然后在这个Activity里完成三件事初始化相机预览SurfaceView。SurfaceView的好处是独立窗口刷新率高扫码预览流畅。覆盖一个自定义View用来画扫描框和扫描线。在线程中循环获取相机帧交给ZXing的MultiFormatReader去解码解码成功后回调onResult。按层次拆开UI层代码不会堆在一起后续要改扫描框样式或者加识别结果动画都只需要修改一小块区域。3.2 相机预览与ZXing联动ZXing官方提供的CameraManager类非常关键它封装了相机的打开、参数设置、帧回调等逻辑。我直接复用了它在core模块外的包里的代码不过需要注意官方CameraManager在初始化时会读取R.id.preview_view而我在自定义布局中可能不叫这个名字所以需要保留或修改资源ID对应关系。实际使用中CameraManager提供了两个关键方法// 开始预览传入SurfaceHolder.AutoFitSurfaceView cameraManager.startPreview(); // 请求一次帧数据解码成功后回调 cameraManager.requestPreviewFrame(frame - { // 这里的data就是YUV格式的预览帧数据 decode(data, width, height); });每次回调拿到原始帧数据后会把它包装成PlanarYUVLuminanceSource再交给HybridBinarizer做二值化最后通过MultiFormatReader.decodeWithState()产出结果。这里要特别说明一下旋转角度的问题。Android相机的默认数据方向是横向的即使你把Activity锁定为竖屏传感器输出的数据仍然是横向的。所以ZXing的解码逻辑里增加了setDesiredCameraRotation的判断。如果你发现二维码明明是对的但识别不出来十有八九就是旋转角度没处理好。我封装时固定设成了90度这个值可以根据设备的前置/后置摄像头动态计算我后面在问题排查部分再展开。3.3 自定义扫描框View扫描框我直接用自定义View实现核心思路是在onDraw里先绘制半透明遮罩然后在扫描框范围内挖孔用PorterDuff.Mode.CLEAR清空像素最后画出四角和扫描线动画。画遮罩和角标的代码大致是这样的Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); int width getWidth(); int height getHeight(); // 扫描框大小按屏幕宽度计算 int frameWidth (int) (width * 0.7f); int frameHeight (int) (height * 0.45f); int left (width - frameWidth) / 2; int top (height - frameHeight) / 2; // 半透明遮罩 canvas.drawRect(0, 0, width, top, maskPaint); canvas.drawRect(0, top, left, top frameHeight, maskPaint); canvas.drawRect(left frameWidth, top, width, top frameHeight, maskPaint); canvas.drawRect(0, top frameHeight, width, height, maskPaint); // 四角白色角标 canvas.drawRect(left, top, left CORNER_SIZE, top CORNER_WIDTH, cornerPaint); canvas.drawRect(left, top, left CORNER_WIDTH, top CORNER_SIZE, cornerPaint); // ... 依次画右上、左下、右下 // 扫描线 lineTop (int) ((top scanLineOffset) % frameHeight); canvas.drawRect(left, top lineTop, left frameWidth, top lineTop LINE_HEIGHT, linePaint); // 用ValueAnimator驱动重绘 }画扫描线时注意想要微信那种“上下来回移动”的感觉动画插值器最好选择Linear或者说慢-快-慢的CycleInterpolator单纯用LinearInterpolator从顶部线性移动到底部视觉上会显得很僵硬。我用的是ValueAnimator animator ValueAnimator.ofInt(0, frameHeight); animator.setDuration(2500); animator.setRepeatMode(ValueAnimator.REVERSE); animator.addUpdateListener(animation - { scanLineOffset (int) animation.getAnimatedValue(); postInvalidateDelayed(16); }); animator.start();使用REVERSE模式让扫描线在扫描框内反弹模拟微信那种往返扫动效果实际体验比无限循环更自然。3.4 支持条形码的界面切换条形码和二维码的扫描框长宽比完全不同微信在处理条形码时会在二维码扫描框的下方临时弹出一个窄长的确认框用来引导用户横向对齐。仿微信效果如果要同时支持条形码最好的方案是根据用户选择切换扫描框模式。我在自定义View里增加了一个scanMode变量扫码页面通过按钮让用户在“二维码模式”和“条码模式”之间切换public enum ScanMode { QR_CODE, // 正方形扫描框 BAR_CODE // 窄长条形框 }切换ScanMode时onDraw里的frameWidth和frameHeight会跟着调整扫描动画的方向也要跟着改。二维码模式保持70%屏幕宽度的正方形条码模式则采用屏幕宽度90%、高度只有宽度的35%左右的长条形以便用户更轻松地将条码全部纳入取景范围内。4. 关键细节如何让扫码又快又准仿微信扫码界面只是表面功夫真正决定用户体验的是扫码速度和准确率。如果扫一个常规二维码要转半天圈界面再好看也没用。想提升扫码性能必须在“相机参数调优”和“解码策略调优”两个方向下功夫。4.1 相机参数调优分辨率与对焦ZXing的CameraManager在打开相机时会自动设置一个合理的预览分辨率但如果你发现默认分辨率太低导致扫码困难最好在自定义CameraManager里手动设置。我的经验是预览分辨率在1280*720附近比较合适更高分辨率不仅会降低帧率而且对解码算法的位矩阵计算帮助不大反而会让CPU持续高负荷运转造成发热。对焦策略更是直接影响扫码体验。我强烈建议在相机启动后自动对焦模式设为FOCUS_MODE_CONTINUOUS_PICTURE并且每500ms执行一次自动对焦动作private void initCameraParameters(Camera.Parameters parameters) { ListString focusModes parameters.getSupportedFocusModes(); if (focusModes.contains(Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE)) { parameters.setFocusMode(Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE); } else { // 老设备不支持连续对焦就手动每隔一段时间触发autoFocus mAutoFocusHandler.postDelayed(new Runnable() { Override public void run() { if (camera ! null) { camera.autoFocus(null); } } }, 1000); } }还应该开启FLASH_MODE_AUTO这样在暗光环境下扫不到码的时候用户点击屏幕或按下闪光灯按钮可以强制开启闪光灯。微信在低亮度环境下会自动提示“轻点照亮”这也是仿微信体验的一部分。4.2 解码策略设置解码格式与区域裁剪解码格式建议根据业务场景收缩范围。如果产品只需要二维码那就只设置QR_CODE和DATA_MATRIX如果需要条形码那再加上EAN_13、EAN_8、CODE_128、CODE_39、UPC_A、UPC_E等。减少解码格式可以减少MultiFormatReader内部尝试的解析次数速度会有秒级提升MapDecodeHintType, Object hints new HashMap(); hints.put(DecodeHintType.POSSIBLE_FORMATS, formats); hints.put(DecodeHintType.CHARACTER_SET, UTF-8); hints.put(DecodeHintType.TRY_HARDER, Boolean.TRUE); reader.setHints(hints);TRY_HARDER这个Hint尤其重要。它会让ZXing在同一个位矩阵上尝试更多可能的二值化扫描轨迹很多情况下能解决二维码边缘被背景色干扰导致的识别失败问题。代价是解码时间会略微上升但实测下来涨幅可以接受。另外解码前最好先把相机帧裁剪到扫描框对应的区域再交给ZXing而不是把整帧预览数据传进去。这样可以明显减少计算量。具体做法是在拿到YUV数据后根据扫描框在UI中的矩形区域和预览分辨率之间的比例关系手动裁剪出一个新的YUV缓冲区。我在实际操作里是这么处理的先拿到PlanarYUVLuminanceSource然后通过crop()方法裁剪出指定区域。裁剪区域的选择要和扫描框在屏幕中的位置保持比例一致否则就会出现“你把二维码放在扫描框里但解码器看得却是屏幕中央”的错位问题。这一步很多人不重视但我发现至少能降低30%的无效解码计算量。4.3 扫码结果的去重与回调微信扫码后二维码会在短时间内被连续识别到多次。如果我们不做去重处理系统会疯狂弹出结果页面。最简单的方案是全局设置一个解码成功的原子布尔值或者记录上一次识别的内容和间隔时间private static final long MIN_SCAN_INTERVAL 1500; private long lastScanTime 0L; private String lastResultText ; private void handleResult(String resultText) { long now System.currentTimeMillis(); if (now - lastScanTime MIN_SCAN_INTERVAL lastResultText.equals(resultText)) { return; } lastScanTime now; lastResultText resultText; // 回调结果 }同时解码成功后最好调用cameraManager.stopPreview()把相机预览关掉防止底层继续回调帧数据。结束页面后回到当前Activity时再重新初始化相机和扫描状态。5. 实现过程中的常见问题与排查技巧做扫码功能大部分时间不是在写逻辑而是在排查各种相机适配问题。这里把我实际工作中踩过的坑列出来基本覆盖了扫码功能最常见的一线问题。问题现象可能原因解决方法扫码画面黑屏只有扫描框UI正常相机权限没申请成功或SurfaceView生命周期未绑定Camera检查动态权限回调把开相机逻辑放到surfaceCreated之后二维码明明很清晰但就是识别失败旋转角度不对解码区域和扫描框对不上调整setDesiredCameraRotation用PlanarYUVLuminanceSource.crop()时按比例换算扫码特别慢经常要两三秒才出结果预览分辨率太高解码格式设置得太宽泛TRY_HARDER没开分辨率锁在720P收紧解码格式范围开启TRY_HARDER屏幕旋转或从后台恢复时相机报错没有正确释放Camera或者SurfaceView被销毁了在onPause/onDestroy中调用cameraManager.stopPreview()和closeDriver()扫条码死活扫不出来扫描框太宽条码占比太小条形码和二维码格式混淆切换到条码模式缩小扫描框宽度解码格式只保留BAR_CODE格式Release包扫码崩溃混淆规则缺失加上-keep class com.google.zxing.** { *; }手机贴得近或远扫码效果差异大对焦模式不正确使用CONTINUOUS_PICTURE对焦低端机启用手动autoFocus提示排查扫码问题有一个最有效的工具就是ZXing官方提供的CaptureActivity源码网上大量开源项目基于它改造。如果自己写的解码链路怎么都不通果断下载官方Demo一步步对比CameraManager和DecodeThread的差异往往很快就能定位到问题。另一个容易被忽略的问题是不同手机厂商对Camera1 API的实现并不完全一致。比如有些华为、荣耀机型在关闭相机时会抛异常导致crash有些小米机型在后台恢复后焦距参数会丢失。如果项目对稳定性要求高建议在CameraManager的closeDriver()方法里包一层try-catch把所有相机相关调用都用防护性代码包起来。扫描功能不是核心业务绝不因为相机偶发异常导致整个App崩溃退出。从工程复用角度我还建议把扫码页设计成一个独立的模块不要和具体业务耦合。回调接口只传递两个东西扫码文本内容和原始BarcodeFormat。这样未来产品要做扫一扫跳转、登录、票据核销等场景都可以直接在回调里做判断一个Activity服务多个入口代码复用性拉满。最后再分享一个小技巧在调试扫码界面时可以在ScanActivity的入口额外传入一个“Debug模式”参数开启后把每一帧解码的耗时和识别到的二维码信息实时显示在屏幕上。这样在真机适配测试时你可以直接看到不同机型上解码速度的差异为后续做性能优化提供数据参考而不必每次猜测是不是机型太老的问题。我在适配低端Android设备时靠这个Debug模式省了非常多时间。本文还有配套的精品资源点击获取