简介:这份资源面向 Android 开发初学者与需要快速集成扫码能力的开发者,聚焦在 Android Studio 中实现手机扫描二维码这一常见需求。内容围绕相机权限申请、ZXingLibrary 依赖引入、布局与 Activity 跳转等关键环节展开,帮助读者理解从点击按钮到调起扫描界面的完整流程,并兼顾 Android 6.0 以上动态权限与低版本兼容处理。资源包共 1 个文件,为 PDF 格式,大小约 44KB,篇幅精炼,适合作为随查随用的实现参考。目前已有 2819 人学习下载,说明该主题在移动开发入门与功能集成中具有较高关注度。通过阅读,读者可掌握扫码功能的基本接入思路、权限判断逻辑以及第三方库的调用方式,为后续扩展扫码支付、扫码登录等场景打下基础。
1. 扫码这件事,在 Android Studio 里到底难在哪
打开 Android Studio 新建一个空项目,想加个「扫一扫」按钮,很多人第一反应是找个库、贴几行代码就完事。真动手才发现,摄像头预览、权限申请、对焦、光线、识别速度、横竖屏切换,每一项都能让一个看似简单的功能卡上半天。手机扫描二维码这个需求,在 Android Studio 里从来不是「调个 API」那么轻,它是一条从相机硬件到图像解码的完整链路,任何一环没接好,用户看到的就是黑屏、糊图或者干脆没反应。
这篇笔记面向的是手里已经有 Android Studio 工程、想真正把扫码功能跑通的人。不管你是刚学完 Activity 生命周期的新手,还是做过几个 App 想补上扫码模块的老手,下面这套路径都能直接照着走:先讲清楚扫码在 Android 上靠什么实现,再落到依赖、权限、预览、解码、结果处理的具体代码,最后把那些让人抓狂的坑一条条拆开。核心词就三个——Android Studio、二维码、扫描,全文围绕它们展开,不绕弯子。
2. 选型先定死:用 ZXing 还是 ML Kit,差别比你想的大
扫码功能的第一步不是写代码,是决定用哪套识别引擎。Android 上做二维码扫描,主流就两条路:ZXing 和 Google 的 ML Kit。选错了,后面要么包体积爆炸,要么识别率上不去,返工成本很高。
2.1 ZXing 与 ML Kit 的能力边界对比
ZXing(Zebra Crossing)是纯 Java 实现的老牌开源库,核心只做图像解码,相机部分要自己接。ML Kit 是 Google 的机器学习套件,扫码只是它众多能力之一,底层用模型推理,识别率和弱光表现更好,但依赖 Google Play 服务。
| 维度 | ZXing | ML Kit |
|---|---|---|
| 依赖方式 | 本地 aar/jar,可离线 | 依赖 Play 服务,部分设备需下载模型 |
| 包体积增量 | 约 500KB 以内 | 约 2-3MB 起 |
| 识别速度 | 快,纯算法 | 快,模型推理 |
| 弱光/模糊 | 一般,依赖对焦 | 较好 |
| 自定义相机 | 完全可控 | 需配合 CameraX |
| 国内设备兼容 | 好,无 Google 依赖 | 部分机型缺 Play 服务会失败 |
我一般会这样判断:如果 App 面向国内、要控制包体积、相机 UI 要深度定制,选 ZXing;如果已经在用 CameraX、能接受 Play 服务依赖、想要更省心的识别,选 ML Kit。热搜里常出现的「条码扫描」「二维码生成器」这类需求,ZXing 生态更成熟,生成和解码能共用一套库。
2.2 在 build.gradle 里把依赖配到位
确定用 ZXing 后,第一步是加依赖。ZXing 官方核心库只含编解码,相机预览要自己写,所以通常再引入一个封装好的journeyapps:zxing-android-embedded,它把 CaptureActivity 都封装好了,适合快速起步。
// app/build.gradle android { defaultConfig { minSdk 21 // ZXing 嵌入式库最低支持 21 targetSdk 34 } } dependencies { // ZXing 核心编解码库 implementation 'com.google.zxing:core:3.5.3' // 封装好的扫码 Activity,省去自己写相机预览 implementation 'com.journeyapps:zxing-android-embedded:4.3.0' }这里core负责把图像数据转成二维码内容,zxing-android-embedded负责相机预览和扫描界面。minSdk设 21 是因为嵌入式库的 CaptureActivity 用到了较新的 Camera API。如果你的项目要支持更低版本,就得自己基于 Camera1 写预览,工作量翻倍,不建议新手这么干。
提示:
zxing-android-embedded4.x 版本默认用 Camera2,部分老机型对焦会异常,遇到问题可在CaptureActivity的 intent 里强制指定使用 Camera1。
2.3 权限声明与运行时申请
相机权限是扫码的硬门槛。Android 6.0 以后必须运行时申请,只在 Manifest 里声明是不够的。
<!-- AndroidManifest.xml --> <uses-permission android:name="android.permission.CAMERA" /> <uses-feature android:name="android.hardware.camera" android:required="true" />// 在启动扫码前检查并申请权限 private val requestCamera = registerForActivityResult( ActivityResultContracts.RequestPermission() ) { granted -> if (granted) startScan() else showToast("没有相机权限无法扫码") } fun checkAndScan() { if (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED) { startScan() } else { requestCamera.launch(Manifest.permission.CAMERA) } }registerForActivityResult是现在推荐的权限申请方式,替代了旧的onRequestPermissionsResult。uses-feature里required="true"会让没有摄像头的设备无法安装,如果你的 App 扫码只是可选功能,改成false更稳妥。权限被拒后要给明确提示,别让用户对着黑屏发呆。
3. 从相机预览到解码:把扫描链路一段段接起来
依赖和权限搞定,接下来是真正的核心:相机画面怎么变成二维码字符串。这条链路是「相机预览 → 帧数据 → 图像解码 → 结果回调」,任何一段断了,功能就是废的。
3.1 用 CaptureActivity 快速跑通最小可用版本
如果不想自己写相机,zxing-android-embedded提供的CaptureActivity能让你十分钟内看到效果。它内部已经处理好预览、对焦、解码。
// 启动扫码界面 fun startScan() { val options = ScanOptions().apply { setDesiredBarcodeFormats(ScanOptions.QR_CODE) // 只识别二维码 setPrompt("将二维码放入框内") // 扫描框下方提示 setCameraId(0) // 0 为后置摄像头 setBeepEnabled(false) // 关闭提示音 setOrientationLocked(true) // 锁定竖屏,避免旋转错乱 } val integrator = IntentIntegrator(this) integrator.setCaptureActivity(CaptureActivity::class.java) integrator.setScanningOptions(options) integrator.initiateScan() } // 接收扫描结果 override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { val result = IntentIntegrator.parseActivityResult(requestCode, resultCode, data) if (result != null && result.contents != null) { val qrText = result.contents // 二维码里的字符串 handleQrResult(qrText) } else { showToast("已取消扫描") } }setDesiredBarcodeFormats只留QR_CODE能提升识别速度,因为不用去尝试解析条形码。setOrientationLocked(true)很关键,扫码界面旋转会导致预览变形、解码失败,锁竖屏是最省事的做法。parseActivityResult会把返回的 Intent 解析成IntentResult,contents就是二维码内容,为空说明用户按了返回。
3.2 自定义扫描界面:把 ViewfinderView 换成自己的
CaptureActivity自带的扫描框样式比较朴素,实际项目里基本都要换。做法是继承CaptureActivity,重写布局。
// 自定义扫描 Activity class MyScanActivity : CaptureActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) // 替换默认布局为自定义布局 setContentView(R.layout.activity_my_scan) // 拿到 barcodeView 控制预览 val barcodeView = findViewById<BarcodeView>(R.id.barcode_view) barcodeView.resume() } }<!-- activity_my_scan.xml --> <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent"> <com.journeyapps.barcodescanner.BarcodeView android:id="@+id/barcode_view" android:layout_width="match_parent" android:layout_height="match_parent" /> <!-- 自定义扫描框,用半透明遮罩 + 中间镂空 --> <View android:layout_width="240dp" android:layout_height="240dp" android:layout_gravity="center" android:background="@drawable/scan_frame_border" /> </FrameLayout>BarcodeView是预览和解码的核心控件,resume()在onResume里调用才能启动相机。自定义扫描框用一张带边框的 drawable 叠在中间即可,遮罩部分可以用四个半透明 View 拼,或者直接画一张带镂空的图。注意别把BarcodeView盖住,否则预览被遮挡,解码区域也会受影响。
3.3 解码参数怎么调:识别率和速度的平衡
ZXing 的解码行为由DecodeHintType控制,调对了识别快,调错了要么慢要么漏。
// 构建解码提示参数 val hints = mapOf( DecodeHintType.POSSIBLE_FORMATS to listOf(BarcodeFormat.QR_CODE), DecodeHintType.TRY_HARDER to true, // 尝试更复杂的图像 DecodeHintType.CHARACTER_SET to "UTF-8" // 中文内容必须指定 ) // 用 MultiFormatReader 解码 val reader = MultiFormatReader() reader.setHints(hints) try { val result = reader.decode(bitmap) // bitmap 为相机帧转成的位图 val text = result.text } catch (e: NotFoundException) { // 当前帧没识别到,继续下一帧 }TRY_HARDER开启后会花更多时间分析图像,弱光或模糊时有用,但会拖慢速度,光线好的场景可以关掉。CHARACTER_SET设成 UTF-8 是血泪经验,不设的话中文二维码解出来是乱码。POSSIBLE_FORMATS限定成QR_CODE能减少无谓的格式尝试,识别更快。
注意:
decode是同步方法,别在主线程调用,否则预览会卡顿。放在子线程或 Camera 的回调线程里执行。
3.4 扫描结果的处理与安全校验
拿到二维码字符串只是开始,直接拿去做跳转或请求是危险的。二维码内容可能是 URL、JSON、纯文本,甚至是恶意构造的。
fun handleQrResult(text: String) { when { // 纯 URL,先校验协议 text.startsWith("http://") || text.startsWith("https://") -> { val uri = Uri.parse(text) // 只允许白名单域名跳转 if (isTrustedHost(uri.host)) openBrowser(uri) else showToast("链接来源不可信") } // JSON 格式,解析前做长度限制 text.startsWith("{") -> { if (text.length > 4096) { showToast("内容过长"); return } parseJsonSafely(text) } else -> showToast("识别结果:$text") } }isTrustedHost做域名白名单,防止二维码指向钓鱼站。JSON 解析前限制长度,避免超大内容导致 OOM。这些校验看着啰嗦,但线上出过一次问题就知道值。热搜里「可以看到对方名字的最后一个字」这类场景,本质也是二维码内容被滥用,校验环节不能省。
4. 避坑排查:扫码功能最常见的 5 个翻车现场
扫码功能代码不多,但坑特别密。下面这 5 条是我和身边人真实踩过的,按「现象 → 原因 → 解决」写清楚。
4.1 预览黑屏,相机没画面
现象:进入扫码界面,扫描框在,但背景全黑,没有任何画面。
原因:相机权限没拿到,或者BarcodeView的resume()没在onResume里调用,相机没启动。还有一种情况是setCameraId设了前置但设备没有前置。
解决:先确认权限已授予,再检查onResume里是否调了barcodeView.resume(),onPause里对应调pause()。相机 ID 用 0(后置)最稳,别乱设。
4.2 能识别但一直识别不出,框里明明有码
现象:二维码就在框里,画面也清晰,就是没反应。
原因:解码区域和预览区域不匹配,或者TRY_HARDER没开导致模糊时放弃。也可能是二维码内容字符集不是 UTF-8,解码抛异常被吞了。
解决:确认BarcodeView的setDecoderFactory用的解码器支持当前格式;开启TRY_HARDER;指定CHARACTER_SET为 UTF-8。如果还不行,打印NotFoundException的堆栈,看是不是格式不匹配。
4.3 扫描界面旋转后错乱
现象:手机横过来,预览拉伸变形,扫描框位置也偏了。
原因:Activity 没锁方向,相机预览的宽高比和屏幕宽高比不一致。
解决:在 Manifest 里给扫码 Activity 加android:screenOrientation="portrait",或者用setOrientationLocked(true)。如果必须支持横屏,得自己根据屏幕方向调整预览尺寸,工作量大,新手先锁竖屏。
4.4 连续扫描时重复回调
现象:扫一次码,onActivityResult被调了好几次,或者同一个码连续触发。
原因:CaptureActivity默认扫到就返回,但如果自己写的解码循环没做去重,同一帧会被反复解析。
解决:在解码回调里加一个标志位,识别成功后立即停止解码并返回。用CaptureActivity的话它内部已处理,自己写循环就要手动reader.reset()并跳出。
4.5 部分机型对焦失败,画面糊
现象:某些手机(尤其老机型)预览一直模糊,对不上焦。
原因:Camera2 的自动对焦在这些设备上兼容性差,或者对焦模式设成了固定焦距。
解决:在BarcodeView初始化时设置对焦模式为连续自动对焦,或强制使用 Camera1。zxing-android-embedded可以通过CameraSettings调整,实在不行就换 ML Kit 试试,它的对焦策略更宽容。
5. 进阶技巧:把扫码做成可复用模块的几个习惯
功能跑通只是及格线,真正让扫码模块好用的,是一些不起眼的习惯。我做了几个项目后,慢慢固定下来几件事。
第一件是封装一个ScanManager,把权限检查、启动扫码、结果解析、错误提示全收进去。业务层只调ScanManager.scan(this) { result -> ... },换库或改参数时只动一个文件。这样即使以后从 ZXing 换到 ML Kit,业务代码一行不用改。
第二件是给扫描结果加一层「内容类型」判断。二维码里可能是 URL、WiFi 配置、名片、支付码,不同类型走不同处理。我一般写一个QrContentParser,用正则先粗判类型,再交给对应的处理器。这样新增一种码类型时,不用改主流程。
第三件是埋点。扫码成功率、平均耗时、失败原因,这些数据上线后能告诉你很多事。比如发现某机型失败率特别高,就能针对性优化。埋点别贪多,就记「开始扫描」「识别成功」「识别失败」「用户取消」四个事件,够用了。
第四件是测试用例。二维码有各种版本(Version 1 到 40)、各种纠错等级(L/M/Q/H)、各种内容编码。我习惯准备一组测试码:纯英文、中文、URL、超长文本、低对比度、破损码,每次改完解码参数都跑一遍。这比上线后靠用户反馈靠谱得多。
// 一个简单的扫码结果封装 data class ScanResult( val rawText: String, val type: QrType, // URL / TEXT / JSON / WIFI ... val timestamp: Long ) enum class QrType { URL, TEXT, JSON, WIFI, UNKNOWN } // 解析入口,业务层只关心这个 fun parseQrContent(text: String): ScanResult { val type = when { text.startsWith("http") -> QrType.URL text.startsWith("WIFI:") -> QrType.WIFI text.startsWith("{") -> QrType.JSON else -> QrType.TEXT } return ScanResult(text, type, System.currentTimeMillis()) }这套封装不复杂,但能让扫码从「一次性代码」变成「可维护模块」。我吃过亏,第一个项目扫码逻辑散在三个 Activity 里,后来改个提示文案要翻半天。现在不管多小的项目,扫码都单独成包,省下的时间远超封装成本。
最后说个习惯:每次调完解码参数,一定在真机上用弱光、反光、倾斜三种场景各扫十次。模拟器永远测不出真实相机的脾气,这一步偷懒,上线就等着收投诉。希望帮到你。
本文还有配套的精品资源,点击获取