简介:面向安卓开发者的二维码与条形码扫描资源,基于 Google 的 ZXing 开源库,整体交互参考微信扫码界面。资料重点解决相机预览、图像帧解码、取景框动画、闪光灯控制、相机切换和扫描结果处理等核心问题,适合有一定安卓基础、希望快速集成扫码功能或自研仿微信扫码器的开发者。压缩包共 177 个文件,涵盖 Java 源码、XML 布局、PNG 图片资源、相关 jar 依赖,以及可直接安装运行的 APK 示例,整体大小仅 2.5MB。工程内包含相机配置管理、异步解码、动态取景框绘制等关键模块,并针对 800×480 分辨率完成屏幕适配;通过研究这份代码,可以理解扫码流程中预览、解码、结果回传的完整链路,并方便替换或扩展自己的业务逻辑。目前已有 439 人学习或下载,配套的工程结构和注释有助于开发者直接对照调试,缩短功能开发周期。
1. 一个扫码需求,为什么不是直接引入 Zxing 完事
做 Android 的人迟早会遇到扫码需求。产品经理一句「就做成微信扫一扫那样的」,听起来不复杂,但把 google Zxing 官方 demo 跑出来,画风完全不对:黑底白线的取景框、没有扫描线动画、强光下条码识别不稳定、屏幕尺寸一变预览就畸变。这个项目解决的就是这件事——保留 Zxing 的二维码、条形码识别内核,把相机预览、取景框、扫描线、补光逻辑整体替换成微信扫码的交互习惯。适合正在做扫码登录、扫码点餐、自助机具扫码,又不想从零调相机参数的开发者。你有多少条码识别需求,它就能当多少种不同场景的底座。
2. 技术选型与工程结构:为什么 Zxing 仍是扫码集成的最优解
2.1 扫码方案横向对比,Zxing 的取舍在哪里
Android 上能选的扫码开源方案,数得上的就是 Zxing、ZBar 和 ML Kit。ZBar 从 2012 年后就不再更新,识别率虽然还在,但工程上引入它的兼容性链路问题越来越多——armeabi 库在新 NDK 下要单独配置,真机 arm64 上还得打包 so 文件,稍微一动就出玄学问题。ML Kit 的识别速度和质量都很强,但这套方案依赖 Google Play 服务和移动端服务框架,国内很多机器上还得先判断服务是否存在,而且把自定义扫码框的细节封装得比较死,想做到和微信完全一致的取景口动画反而费劲。
Zxing 的优势很特别:它是纯 Java 实现的解码库,没有原生 so 依赖,core 模块实际打包后几百 KB,从 Android 4 到 Android 13 都能直接跑。MultiFormatReader 里集成了 QR_CODE、EAN_13、CODE_128、CODE_39、UPC_A、ITF 等十几种格式的识别,扫码枪能读的它基本都能碰一碰。这意味着不需要为不同条码引入不同 SDK,一个库全吃掉。
Zxing 的官方 demo 缺陷也明显。它默认那个取景界面几乎没有任何现代 App 感:没有扫描线、没有补光入口、相机预览尺寸不根据屏幕自适应、界面横竖屏切换逻辑也很粗糙。所以本项目把 Zxing 定位成「解码引擎」而不是「完整扫码 App」,相机和图层全部自己接管。这样一个老库反而成了最灵活的组件。
2.2 依赖集成与源码移植,我为什么选用源码移植
Zxing 现在有两种常见集成路线。第一种是直接依赖 core 库,核心解码能力由它提供,相机和 UI 自己写:
implementation 'com.google.zxing:core:3.5.3'这种做法的优点是干净,你拿到的就是一个纯粹的识别工具,不掺任何相机逻辑。缺点是相机参数、预览尺寸、解码线程这些全要从零搭,工作量大不少。第二种是先把 Zxing 工程里的 camera、decode 相关类拷贝进项目再改,俗称源码移植。本项目选择的就是源码移植。
源码移植带来的收益是实打实的:摄像头参数完全可控,对焦模式、曝光补偿、缩放比例都能自己调整;解码线程调度握在自己手里,不会因为一个 Handler 的时序问题干等;自定义 View 层可以随便画,想仿微信的遮蔽框、渐变扫描线、四角高亮都行。后面要是想迁移到 CameraX,也只需要把 camera 文件夹替换掉,decode 层完全不动。
2.3 项目目录结构逐层拆解
工程按功能拆包,不是按层级拆包,这一点对二次开发很友好:
app/src/main/java/com/example/qrscan/ ├── capture/ │ ├── ScannerActivity.java // 扫码入口界面,持相机和解码器 │ ├── ViewfinderView.java // 自绘取景框、遮罩、扫描线 │ └── CaptureActivityHandler.java // 相机线程与 UI 线程的消息桥 ├── camera/ │ ├── CameraManager.java // 相机打开、参数设置、预览回调 │ ├── CameraConfigurationManager.java // 对焦、曝光、白平衡 │ └── PreviewCallback.java // 帧数据回调 ├── decode/ │ ├── DecodeThread.java // 解码线程入口 │ ├── DecodeHandler.java // 真正调用 MultiFormatReader 的地方 │ └── BarcodeResult.java // 统一存放解析结果和原始信息 └── util/ ├── BeepManager.java // 提示音与震动 └── FlashlightManager.java // 补光控制decode 包是整个项目的地基,它不依赖任何 Android 相机 API,输入是 byte[] 的 YUV 帧,输出是Result对象。camera 包负责把相机帧喂给 decode,capture 包负责把解码结果转成用户能看到的交互。这样拆完以后,你换机型、换相机实现、换 UI 风格,影响的都是各自包内的事,改起来不会互相踩脚。
3. 核心扫描链路:从相机帧到 Result 回调解码全过程
3.1 相机打开与预览初始化的关键参数
相机的启动是整个扫码流程的第一个瓶颈。官方 Zxing 源码里这段openDriver很经典,我做了一层精简但保留了核心:
public void openDriver(int screenWidth, int screenHeight) throws IOException { if (camera != null) return; camera = Camera.open(); Camera.Parameters params = camera.getParameters(); // 从所有支持的预览尺寸里挑一个最接近屏幕比例的,而不是挑最大的 Camera.Size previewSize = findBestPreviewSize(params, screenWidth, screenHeight); params.setPreviewSize(previewSize.width, previewSize.height); // 这个连续对焦模式是扫码识别快不快的关键 if (params.getSupportedFocusModes().contains( Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE)) { params.setFocusMode(Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE); } camera.setParameters(params); camera.setDisplayOrientation(90); // 竖屏预览必须旋转 }这段代码藏着两个资深开发者才在意的问题。第一,预览尺寸不是越大越好,选最大尺寸在低端机上会掉帧,解码速度反而变慢。findBestPreviewSize我会从支持尺寸列表里找宽高比最接近屏幕比例的候选,再取其中分辨率最大的那一个,这样既保证画面不变形,又不会把 GPU 和 ISP 压满。第二,setDisplayOrientation(90)在竖屏扫码里是必须的,不设置的话预览画面是横的,取景框和画面完全对不上,新手在这个点上最容易翻车。
3.2 预览帧数据与解码线程的交接
相机预览回调拿到的是 YUV420 格式的原始数据,格式是 NV21,数据量大而且不能在 UI 线程里做处理。这里我使用带缓冲区的回调,让相机把数据写进我们提供的 buffer,避免底层重复分配内存:
private byte[] previewBuffer; public void start() { // 一帧 NV21 的数据量 = 宽 * 高 * 3 / 2 previewBuffer = new byte[previewWidth * previewHeight * 3 / 2]; camera.addCallbackBuffer(previewBuffer); camera.setPreviewCallbackWithBuffer(previewCallback); } private PreviewCallback previewCallback = new PreviewCallback() { @Override public void onPreviewFrame(byte[] data, Camera camera) { // 把帧交给解码线程,不在相机回调里占用时间 decodeWorker.postFrame(data); // 重新把 buffer 还给相机,否则预览会卡在某一帧 camera.addCallbackBuffer(previewBuffer); } };addCallbackBuffer是整个链路的节拍器。如果不重新 add,相机只会回调一次就停住,界面看起来就像卡死的黑屏,这也是刚接触 Zxing 的开发者最容易遇到的「相机回调只触发一次」问题。注意data数组和previewBuffer是同一个对象,解码线程读取data时不能直接持有,因为下一帧会重写这个数组,必须复制一份再交给解码端。
3.3 解码线程里的核心调用
解码线程拿到 YUV 数据后,先裁剪出取景框对应区域,再做二值化,最后交给MultiFormatReader:
public void run() { MultiFormatReader reader = new MultiFormatReader(); Map<DecodeHintType, Object> hints = new HashMap<>(); hints.put(DecodeHintType.POSSIBLE_FORMATS, EnumSet.of(BarcodeFormat.QR_CODE, BarcodeFormat.EAN_13, BarcodeFormat.CODE_128, BarcodeFormat.CODE_39, BarcodeFormat.UPC_A, BarcodeFormat.ITF)); reader.setHints(hints); PlanarYUVLuminanceSource source = new PlanarYUVLuminanceSource( yuvData, previewWidth, previewHeight, cropLeft, cropTop, cropWidth, cropHeight, false); BinaryBitmap bitmap = new BinaryBitmap(new HybridBinarizer(source)); Result result = reader.decodeWithState(bitmap); // 成功后通过 Handler 发回主线程 }POSSIBLE_FORMATS限定了解码范围,是很大的提速点。项目里把这个限制做成可配置的数组,扫码场景只识别二维码时就把 EAN 和 CODE_128 关掉。cropLeft、cropTop、cropWidth、cropHeight需要和上层ViewfinderView的取景框坐标换算一致。有一个细节必须提醒:decodeWithState会在失败后保留上次解码的中间状态,连续失败时必须调用reader.reset(),不 reset 的话第二次以后解码同一二维码会莫名失败。我在解码线程里通过捕获NotFoundException来调 reset,这行代码救了不少真实扫码场景。
4. 仿微信界面:取景框、扫描线、补光逻辑的实现与调参
4.1 自定义取景框的绘制
微信扫码的视觉记忆点集中在「暗色遮罩 + 亮色取景框 + 四角装饰」。这里用自定义 View 的onDraw完成,不依赖任何图片资源:
@Override protected void onDraw(Canvas canvas) { Rect frame = getScanFrame(); // 取景框实时位置 // 取景框外铺一层半透明黑色遮罩 canvas.drawRect(0, 0, getWidth(), frame.top, maskPaint); canvas.drawRect(0, frame.bottom, getWidth(), getHeight(), maskPaint); canvas.drawRect(0, frame.top, frame.left, frame.bottom, maskPaint); canvas.drawRect(frame.right, frame.top, getWidth(), frame.bottom, maskPaint); // 绘制取景框四角的白色高亮线,微信效果的一根手指 canvas.drawRect(frame.left, frame.top, frame.left + CORNER_WIDTH, frame.top + CORNER_HEIGHT, cornerPaint); canvas.drawRect(frame.left, frame.top, frame.left + CORNER_HEIGHT, frame.top + CORNER_WIDTH, cornerPaint); // 另外三个角逻辑对称 }getScanFrame()有必要解释:它的位置是动态的,要考虑状态栏高度、标题栏高度和 SurfaceView 的位置偏移。直接把 View 的宽高按比例算出来的框,在相机预览区域和 View 不对齐的机型上会出现「框住的位置和解码区域错位」的怪现象。这个项目里我把getScanFrame()的结果同时给解码端做 crop 坐标,两边用同一个方法保证同步。
4.2 扫描线的动画与扫码反馈
微信的扫描线是一条从上往下循环移动的渐变亮线。这里用 ValueAnimator 驱动:
private ValueAnimator animator; public void startScanLine() { animator = ValueAnimator.ofFloat(0f, 1f); animator.setDuration(3000); animator.setRepeatMode(ValueAnimator.RESTART); animator.setRepeatCount(ValueAnimator.INFINITE); animator.addUpdateListener(animation -> { float progress = (float) animation.getAnimatedValue(); scanLineY = frame.top + (frame.bottom - frame.top) * progress; postInvalidate(); }); animator.start(); }扫描线时长用 3000ms,比微信默认节奏稍快一点,用户心理上会觉得「响应更快」。扫码成功时我会调用stopScanLine(),并用一个短暂的「线闪光」替代线移动,给用户一个明确的命中反馈。如果在解码连续失败的情况下重启动画,注意先把animator.cancel()再重新start(),否则多条动画叠在一起,画面会闪得让人怀疑人生。
4.3 自动补光与提示音效
弱光场景是官方 Zxing 的一个重灾区,几乎不做处理。补光逻辑其实很简单,调用相机参数的开闪关闪:
public void setTorch(boolean enabled) { if (camera == null) return; Camera.Parameters params = camera.getParameters(); if (params.getSupportedFlashModes() != null && params.getSupportedFlashModes().contains(Camera.Parameters.FLASH_MODE_TORCH)) { params.setFlashMode(enabled ? Camera.Parameters.FLASH_MODE_TORCH : Camera.Parameters.FLASH_MODE_OFF); camera.setParameters(params); } }补光的关键不在设置闪关灯,而在「什么时候自动开启」。我读取预览帧的平均亮度,当亮度值低于某个阈值时在扫描界面上显示一个补光按钮,并对边框加亮提示。这比一直开着闪光灯省电,也比完全依赖用户按按钮更贴近微信的习惯。提示音我用ToneGenerator生成一段短音,再用Vibrator加一次 20ms 的震动,别用 MediaPlayer,因为 MediaPlayer 加载资源的延迟会让扫码成功后的反馈硬生生慢半拍。
5. 避坑与排查:从黑屏、卡帧到内存泄漏的实战记录
5.1 相机权限申请后仍然黑屏,回调不触发
现象:在 Android 6 以上机型运行,权限弹窗点「允许」之后,预览区域黑屏,相机回调完全不触发。
原因:权限申请是异步的,允许后相机并没有立刻拿到权限,Activity 的onResume已经提前执行了Camera.open(),这时底层会直接抛异常或返回 null。
解决:把相机启动逻辑放到权限回调之后。在onRequestPermissionsResult里确认权限 GRANTED 再调openDriver(),同时做一个标记位,避免onResume里重复启动导致「相机已被占用」。从那以后我所有涉及相机权限的代码都强制走一遍这个标记位。
5.2 识别速度慢,扫码要等好几秒
现象:同一个二维码用微信扫是秒识别,自己的项目要扫 3 秒以上,取景框内画面看起来也不流畅。
原因:预览尺寸选得太高,中低端机 ISP 处理不过来导致帧率掉到 15FPS 以下;另一个原因是解码时把整帧预览数据都交给解码器,没有裁剪到取景框区域。
解决:预览尺寸改成 1280x720 级别,而不是直接选最大 1920x1080;PlanarYUVLuminanceSource的 crop 参数严格对齐取景框。这两项做完,识别速度基本能有肉眼可见的翻倍改善。
5.3 横竖屏切换导致预览方向错乱
现象:手机横屏后再竖屏,预览画面是倒的,取景框还停留在原位置。
原因:没有锁定屏幕方向。扫码场景在绝大多数产品里应该固定竖屏,但项目配置文件里 Activity 没有声明android:screenOrientation="portrait",导致系统在切换方向时重建 Activity,相机链路被打断。
解决:在清单文件里给扫码页加android:screenOrientation="portrait",并在onConfigurationChanged里处理方向变化。这个操作一分钟改完,能省掉一整天的兼容性调试。
5.4 条形码扫不出来,二维码却秒识别
现象:EAN_13 和 CODE_128 条形码在真机上一直出不来结果,二维码完全正常。
原因:条形码识别对焦距离比二维码更敏感。二维码可以接受较大范围的模糊,条形码一旦焦距虚了,二值化出来的黑白条条就已经糊成一片。另一个隐藏问题是POSSIBLE_FORMATS里没加对应格式。
解决:对焦模式改用FOCUS_MODE_MACRO试试,扫码页提供一个“近距离模式”的切换按钮;同时确认DecodeHintType.POSSIBLE_FORMATS里包含BarcodeFormat.EAN_13和BarcodeFormat.CODE_128。这两个点经常是同时出问题,只改一个都会给人“扫码看运气”的错觉。
5.5 扫码成功后再次进入预览,相机打不开
现象:第一次扫码成功返回,第二次进入扫码页相机无法打开,报Camera.open() failed。
原因:onPause里释放相机的代码没兜住异常,如果 Release 前相机已经被系统回收,状态就卡死;另一个常见原因是解码线程还在跑,Handler 里继续回调容器导致 Activity 无法销毁。
解决:在onPause里camera.release()后置空引用,并在onDestroy里移除 Handler 的 Callback;解码线程使用isInterrupted()判断退出循环。这套流程做好后,重复进出的状态就稳定了。
6. 二次开发:把扫码能力改造成业务功能模块
6.1 解耦设计:把解码结果抽象成通用回调
对接业务时,扫码页不应该把Result对象直接抛给业务层。项目里我加了一层ScanResultListener:监听器拿到的是BarcodeResult,里面包含解码原文、原始字节、格式类型、字符编码。这样一来,扫码登录、扫码点餐、巡检扫码可以共用同一个扫描界面,每个业务只要实现自己的回调逻辑就行。
6.2 验证方法:改造完怎么确认没改坏
扫描识别这类功能最怕改完 UI 后把解码链路搞坏。我的习惯是写一个自动化验证入口:开发模式下把一张二维码图片的 byte[] 直接喂给DecodeHandler,判断是否能解出预期字符串。这个入口不进 UI 流程,但能快速帮你判断问题是出在相机链路、解码线程还是 UI 层。改完取景框我第一件事就是跑这个入口,能过就说明解码层没动到。
6.3 扩展思路:从「扫一扫」到多码共存
如果未来要支持相册识别二维码,只需把图片转成RGBLuminanceSource,再走BinaryBitmap和MultiFormatReader,复用现有 decode 包就能实现。做扫码登录时,可以在DecodeFormatSet里只留QR_CODE,把速度再往上提;做门店自助机时,再加一个FlashlightManager的自动关闭逻辑,避免补光一直亮着导致发热。核心思路是一个坑一个坑踩稳,把 decode 包保持纯净,上层随便折腾都翻不了车。
Zxing 这套方案我已经在好几个项目里落过地,从最早照着官方 demo 改得一团糟,到现在能控制每一帧数据、每一条动画曲线,中间踩过的坑都在前面几章里。希望帮到你,让你在扫码这个高频需求上少走些弯路。
本文还有配套的精品资源,点击获取