優(yōu)實(shí)戰(zhàn))
簡介本資源面向Android開發(fā)者與移動(dòng)應(yīng)用學(xué)習(xí)者聚焦在項(xiàng)目中集成ZXing實(shí)現(xiàn)二維碼與條形碼掃描功能解決條碼識(shí)別、相機(jī)調(diào)用與結(jié)果解析等常見開發(fā)需求。壓縮包共1874個(gè)文件以flat、json、xml、dex、class等構(gòu)建與配置產(chǎn)物為主另含java源碼、jar庫、png資源及gradle腳本整體約18.42MB結(jié)構(gòu)接近一個(gè)可直接參考的Android工程。資源附帶說明使用文檔.docx并包含MyApplication示例應(yīng)用便于對(duì)照學(xué)習(xí)依賴配置、掃描界面搭建、Intent啟動(dòng)掃描、onActivityResult結(jié)果處理、相機(jī)權(quán)限檢查、閃光燈支持與掃描區(qū)域提示等關(guān)鍵環(huán)節(jié)同時(shí)提供錯(cuò)誤處理與用戶體驗(yàn)優(yōu)化思路。目前已有2535人學(xué)習(xí)下載適合希望快速掌握ZXing集成流程、理解掃描邏輯并落地到實(shí)際項(xiàng)目中的開發(fā)者參考。1. 掃碼功能從零落地為什么我建議你從 ZXing 這套源碼開始拆上個(gè)月幫一個(gè)做倉儲(chǔ)盤點(diǎn)的小團(tuán)隊(duì)看代碼他們 App 里掃碼模塊用的是某商業(yè) SDK授權(quán)到期后整個(gè)入庫流程直接卡死臨時(shí)換庫又發(fā)現(xiàn)接口封裝得太深連個(gè)回調(diào)都改不動(dòng)。這種被第三方綁定卡脖子的場(chǎng)景我在項(xiàng)目里見過太多次。所以當(dāng)我拿到這份「實(shí)現(xiàn)二維碼掃描和條形碼掃描」的源碼包時(shí)第一反應(yīng)就是它把 ZXing 這頭老黃牛直接攤開給你看從相機(jī)預(yù)覽、圖像裁剪、解碼回調(diào)到結(jié)果處理整條鏈路都在你手里。這份資源適合兩類人一類是剛接觸 Android 掃碼、想搞明白一幀畫面怎么變成一串字符的新手另一類是做過多款 App、但一直用封裝庫、想自己掌控解碼參數(shù)和性能調(diào)優(yōu)的老手。它不依賴任何云端識(shí)別服務(wù)純本地解碼離線可用這一點(diǎn)在弱網(wǎng)倉庫、地下車庫這類場(chǎng)景里就是剛需。2. 技術(shù)選型與工程結(jié)構(gòu)ZXing 在 Android 上到底怎么接2.1 為什么是 ZXing而不是別的方案掃碼這件事市面上常見三條路調(diào)用系統(tǒng)相機(jī) 云端識(shí)別、集成商業(yè) SDK、以及本地開源庫。云端方案延遲高、有流量成本、隱私數(shù)據(jù)要出設(shè)備商業(yè) SDK 功能全但授權(quán)和包體積是負(fù)擔(dān)。ZXingZebra Crossing是 Java 實(shí)現(xiàn)的開源一維/二維條碼處理庫核心解碼邏輯不依賴網(wǎng)絡(luò)Apache 2.0 協(xié)議改起來沒有法律包袱。這份源碼走的就是 ZXing 本地解碼路線相機(jī)預(yù)覽用 Android 原生 Camera API部分分支用 CameraX解碼線程獨(dú)立主線程只負(fù)責(zé) UI 和結(jié)果回調(diào)。選型上有個(gè)細(xì)節(jié)值得說ZXing 官方提供的android-core和android-integration兩個(gè)模塊前者是給開發(fā)者自己寫掃描界面用的后者是 Intent 集成調(diào)起一個(gè)現(xiàn)成的掃碼 Activity。這份源碼用的是前者也就是說掃描界面、取景框、閃光燈按鈕、權(quán)限申請(qǐng)這些都要自己寫。對(duì)新手來說多了一點(diǎn)工作量但換來的是完全可控——取景框大小、解碼格式白名單、連續(xù)掃描間隔全都能改。2.2 目錄結(jié)構(gòu)一覽拿到源碼后先別急著跑花五分鐘把目錄過一遍后面改起來心里有數(shù)。典型結(jié)構(gòu)如下不同分支命名略有差異app/ ├── src/main/ │ ├── java/com/example/scanner/ │ │ ├── CameraManager.java // 相機(jī)打開、預(yù)覽、對(duì)焦 │ │ ├── DecodeThread.java // 解碼工作線程 │ │ ├── DecodeHandler.java // 處理預(yù)覽幀消息 │ │ ├── CaptureActivity.java // 掃描主界面 │ │ ├── ViewfinderView.java // 取景框自定義 View │ │ └── ResultActivity.java // 結(jié)果展示頁 │ ├── res/layout/ │ │ ├── activity_capture.xml │ │ └── activity_result.xml │ └── AndroidManifest.xml └── build.gradleCameraManager管硬件DecodeThread管計(jì)算CaptureActivity管生命周期和 UIViewfinderView負(fù)責(zé)畫那個(gè)會(huì)動(dòng)的掃描線。職責(zé)切得比較干凈二次開發(fā)時(shí)想換 UI 只動(dòng) View想調(diào)解碼策略只動(dòng) DecodeHandler。2.3 依賴與權(quán)限配置build.gradle里核心依賴就一個(gè)dependencies { implementation com.google.zxing:core:3.5.2 // 解碼核心純 Java implementation com.google.zxing:android-core:3.5.2 // 相機(jī)配置工具類 }AndroidManifest.xml里必須聲明相機(jī)權(quán)限和硬件特性u(píng)ses-permission android:nameandroid.permission.CAMERA / uses-permission android:nameandroid.permission.FLASHLIGHT / uses-feature android:nameandroid.hardware.camera android:requiredtrue / uses-feature android:nameandroid.hardware.camera.autofocus android:requiredfalse /注意autofocus設(shè)成false是常見做法因?yàn)椴糠值投嗽O(shè)備沒有自動(dòng)對(duì)焦設(shè)成true會(huì)導(dǎo)致應(yīng)用在應(yīng)用市場(chǎng)被過濾掉。運(yùn)行時(shí)權(quán)限Android 6.0在CaptureActivity的onResume里檢查沒授權(quán)就彈系統(tǒng)對(duì)話框拒絕后給一個(gè)引導(dǎo)去設(shè)置的提示。3. 核心鏈路拆解一幀預(yù)覽畫面如何變成掃碼結(jié)果3.1 相機(jī)預(yù)覽與幀數(shù)據(jù)獲取掃碼的起點(diǎn)是拿到相機(jī)的預(yù)覽幀。這份源碼用的是CameraAPI 的setPreviewCallbackWithBuffer比setPreviewCallback少一次內(nèi)存分配在連續(xù)掃碼場(chǎng)景下更穩(wěn)。// CameraManager.java 片段 public void startPreview() { Camera.Parameters params camera.getParameters(); // 選擇預(yù)覽尺寸優(yōu)先選接近 1280x720 的 Point previewSize findBestPreviewSize(params); params.setPreviewSize(previewSize.x, previewSize.y); // 連續(xù)對(duì)焦模式掃碼時(shí)畫面容易糊 params.setFocusMode(Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE); camera.setParameters(params); camera.setPreviewCallbackWithBuffer(this); // this 實(shí)現(xiàn) PreviewCallback camera.startPreview(); } Override public void onPreviewFrame(byte[] data, Camera camera) { // data 是 NV21 格式的原始幀直接丟給解碼線程 if (decodeHandler ! null) { decodeHandler.obtainMessage(DecodeHandler.DECODE, data).sendToTarget(); } camera.addCallbackBuffer(data); // 復(fù)用緩沖區(qū) }這里有幾個(gè)參數(shù)值得盯FOCUS_MODE_CONTINUOUS_PICTURE比FOCUS_MODE_AUTO更適合掃碼因?yàn)樗鼤?huì)持續(xù)微調(diào)焦距不會(huì)出現(xiàn)對(duì)焦一次后畫面又糊掉的情況。預(yù)覽尺寸不是越大越好1280x720 在多數(shù)中端機(jī)上解碼速度和清晰度平衡得最好選 1920x1080 反而會(huì)因?yàn)閱螏瑪?shù)據(jù)量大導(dǎo)致解碼線程排隊(duì)。3.2 解碼線程與 ZXing 核心調(diào)用解碼放在獨(dú)立線程避免阻塞主線程導(dǎo)致預(yù)覽卡頓。DecodeHandler收到幀數(shù)據(jù)后把 NV21 轉(zhuǎn)成 ZXing 能吃的PlanarYUVLuminanceSource再交給MultiFormatReader。// DecodeHandler.java 片段 private void decode(byte[] data, int width, int height) { long start System.currentTimeMillis(); // 裁剪取景框區(qū)域減少無效計(jì)算 Rect rect viewfinderView.getScanRect(); byte[] rotated rotateYUV420Degree90(data, width, height); // 豎屏旋轉(zhuǎn) PlanarYUVLuminanceSource source new PlanarYUVLuminanceSource( rotated, height, width, rect.top, rect.left, rect.width(), rect.height(), false); BinaryBitmap bitmap new BinaryBitmap(new HybridBinarizer(source)); try { Result result multiFormatReader.decodeWithState(bitmap); // 解碼成功回傳結(jié)果 Message.obtain(captureHandler, CaptureActivity.DECODE_SUCCESS, result).sendToTarget(); } catch (NotFoundException e) { // 沒掃到是常態(tài)不打印堆棧直接繼續(xù) } finally { multiFormatReader.reset(); } }HybridBinarizer是 ZXing 里對(duì)光照不均場(chǎng)景比較友好的二值化器比GlobalHistogramBinarizer更適合手機(jī)攝像頭。decodeWithState配合reset是官方推薦的復(fù)用寫法避免每次解碼都重建 Reader。裁剪取景框區(qū)域這一步很關(guān)鍵——整幀 1280x720 有 92 萬像素而取景框可能只占中間 60%裁剪后計(jì)算量直接砍掉近一半掃碼響應(yīng)肉眼可見變快。3.3 結(jié)果回調(diào)與連續(xù)掃描控制解碼成功后CaptureActivity收到消息通常會(huì)做三件事震動(dòng)/響鈴提示、把結(jié)果傳給調(diào)用方、決定是否繼續(xù)掃。// CaptureActivity.java 片段 private Handler captureHandler new Handler(Looper.getMainLooper()) { Override public void handleMessage(Message msg) { if (msg.what DECODE_SUCCESS) { Result result (Result) msg.obj; // 1. 反饋 playBeepSoundAndVibrate(); // 2. 回傳 Intent intent new Intent(); intent.putExtra(SCAN_RESULT, result.getText()); intent.putExtra(SCAN_FORMAT, result.getBarcodeFormat().name()); setResult(RESULT_OK, intent); // 3. 連續(xù)掃描模式延遲 1.5 秒再恢復(fù)預(yù)覽 if (continuousMode) { isDecoding false; handler.postDelayed(() - isDecoding true, 1500); } else { finish(); } } } };isDecoding這個(gè)標(biāo)志位是防重復(fù)的關(guān)鍵。掃碼槍式的連續(xù)掃描如果不加節(jié)流同一個(gè)碼會(huì)被連續(xù)識(shí)別十幾次業(yè)務(wù)層收到一堆重復(fù)回調(diào)。1.5 秒是個(gè)經(jīng)驗(yàn)值太短容易重復(fù)太長影響連續(xù)作業(yè)效率。result.getBarcodeFormat()返回的是QR_CODE、CODE_128、EAN_13這類枚舉業(yè)務(wù)層可以據(jù)此判斷掃的是二維碼還是一維碼。3.4 支持的碼制與格式白名單ZXing 默認(rèn)支持一大堆格式但實(shí)際項(xiàng)目里往往只需要幾種。在DecodeThread初始化時(shí)設(shè)置白名單能減少誤識(shí)別、提升速度// DecodeThread.java 片段 CollectionBarcodeFormat formats new ArrayList(); formats.add(BarcodeFormat.QR_CODE); formats.add(BarcodeFormat.CODE_128); formats.add(BarcodeFormat.EAN_13); hints.put(DecodeHintType.POSSIBLE_FORMATS, formats); hints.put(DecodeHintType.TRY_HARDER, Boolean.TRUE); // 提高識(shí)別率代價(jià)是慢一點(diǎn)TRY_HARDER這個(gè) hint 在光線差、碼有磨損時(shí)很有用但它會(huì)讓解碼嘗試更多旋轉(zhuǎn)和采樣單幀耗時(shí)可能翻倍。我的建議是正常場(chǎng)景關(guān)掉遇到難掃的碼再動(dòng)態(tài)打開或者干脆給用戶一個(gè)「增強(qiáng)模式」開關(guān)。4. 跑起來與真機(jī)調(diào)試從編譯到第一次掃出結(jié)果4.1 環(huán)境準(zhǔn)備與編譯這份源碼是標(biāo)準(zhǔn) Android Gradle 工程編譯前確認(rèn)三件事JDK 版本建議 17、Android SDK 版本compileSdk一般 33 或 34、以及 Gradle 版本匹配。用 Android Studio 打開后先Sync Project with Gradle Files如果卡在下載依賴檢查settings.gradle里的倉庫地址是否包含google()和mavenCentral()。# 命令行編譯 debug 包 ./gradlew assembleDebug # 安裝到已連接設(shè)備 ./gradlew installDebug編譯報(bào)錯(cuò)最常見的是minSdkVersion太低導(dǎo)致某些 API 找不到。ZXing 的android-core要求minSdk至少 19但相機(jī) API 的某些方法在 21 以下行為不一致建議直接設(shè)成 21。4.2 真機(jī)調(diào)試要點(diǎn)模擬器掃不了碼必須真機(jī)。第一次跑起來如果預(yù)覽是黑屏按這個(gè)順序排查相機(jī)權(quán)限是否授予、SurfaceView是否在surfaceCreated之后才startPreview、以及相機(jī)是否被其他應(yīng)用占用。日志過濾Camera和DecodeHandler兩個(gè) tag能看到幀回調(diào)和解碼嘗試的節(jié)奏。調(diào)試時(shí)我習(xí)慣在DecodeHandler里加一行耗時(shí)日志if (System.currentTimeMillis() - start 200) { Log.w(DecodeHandler, slow frame: (System.currentTimeMillis() - start) ms); }超過 200ms 的幀說明解碼壓力大可能是預(yù)覽尺寸選大了或者TRY_HARDER開著或者取景框裁剪沒生效。4.3 常見問題排查現(xiàn)象預(yù)覽畫面拉伸變形。原因相機(jī)預(yù)覽尺寸和 SurfaceView 寬高比不一致。解決在CameraManager里根據(jù) SurfaceView 的實(shí)際寬高比去選最接近的預(yù)覽尺寸而不是固定 1280x720?,F(xiàn)象掃二維碼秒出掃一維碼死活掃不出。原因一維碼需要更寬的取景區(qū)域和更高的水平分辨率二維碼是二維的對(duì)高度也敏感。解決給一維碼單獨(dú)調(diào)大取景框?qū)挾然蛘咛崾居脩舭汛a放遠(yuǎn)一點(diǎn)讓整條碼進(jìn)入畫面?,F(xiàn)象連續(xù)掃多個(gè)碼時(shí)第二個(gè)碼要等好幾秒才識(shí)別。原因解碼線程還在處理上一幀或者isDecoding標(biāo)志位恢復(fù)太慢。解決檢查DecodeThread的消息隊(duì)列是否堆積必要時(shí)用removeMessages清掉過期幀?,F(xiàn)象暗光下完全掃不出。原因沒有補(bǔ)光二值化失敗。解決加閃光燈開關(guān)或者在Camera.Parameters里開FLASH_MODE_TORCH。注意有些設(shè)備開閃光燈后預(yù)覽幀會(huì)變亮但解碼反而更差需要實(shí)測(cè)?,F(xiàn)象部分手機(jī)掃碼時(shí)畫面卡頓。原因onPreviewFrame里做了耗時(shí)操作或者緩沖區(qū)復(fù)用沒做好導(dǎo)致 GC 頻繁。解決確保onPreviewFrame里只做消息投遞所有計(jì)算都在解碼線程addCallbackBuffer一定要在投遞后立刻調(diào)用。5. 二次開發(fā)改造從能掃到好用我踩過的幾個(gè)坑5.1 把掃描結(jié)果接入業(yè)務(wù)流的正確姿勢(shì)很多新手拿到源碼后直接在CaptureActivity里寫業(yè)務(wù)邏輯比如掃到碼就發(fā)網(wǎng)絡(luò)請(qǐng)求。這是大忌——掃描界面應(yīng)該只負(fù)責(zé)「掃出字符串」業(yè)務(wù)處理交給調(diào)用方。正確做法是通過setResult回傳調(diào)用方用startActivityForResult或ActivityResultLauncher接收。// 調(diào)用方 ActivityResultLauncherIntent scanLauncher registerForActivityResult( new ActivityResultContracts.StartActivityForResult(), result - { if (result.getResultCode() RESULT_OK result.getData() ! null) { String code result.getData().getStringExtra(SCAN_RESULT); String format result.getData().getStringExtra(SCAN_FORMAT); handleScannedCode(code, format); // 業(yè)務(wù)處理 } }); scanLauncher.launch(new Intent(this, CaptureActivity.class));這樣掃描模塊可以獨(dú)立成 library 模塊多個(gè)業(yè)務(wù)頁面復(fù)用也方便單獨(dú)測(cè)試。5.2 性能調(diào)優(yōu)的三個(gè)抓手第一預(yù)覽尺寸別貪大。我實(shí)測(cè)過一臺(tái)驍龍 6 系的老機(jī)器1280x720 平均解碼 80ms1920x1080 直接飆到 220ms掃碼體驗(yàn)天差地別。第二取景框裁剪一定要做而且裁剪區(qū)域要跟著ViewfinderView的實(shí)際繪制區(qū)域走別寫死坐標(biāo)。第三解碼格式白名單按業(yè)務(wù)需要開一個(gè)只掃二維碼的 App 沒必要把CODE_39、PDF_417全打開。5.3 一個(gè)容易忽略的細(xì)節(jié)屏幕旋轉(zhuǎn)與幀方向豎屏 App 里相機(jī)傳感器輸出的幀默認(rèn)是橫的必須旋轉(zhuǎn) 90 度才能正確解碼。這份源碼在DecodeHandler里做了rotateYUV420Degree90但如果你改了Activity的screenOrientation旋轉(zhuǎn)邏輯也要跟著調(diào)。我見過一個(gè)項(xiàng)目把掃描頁鎖成橫屏結(jié)果旋轉(zhuǎn)代碼沒改二維碼怎么都掃不出查了一下午才發(fā)現(xiàn)是方向問題。從那以后我每次改掃描頁的屏幕方向配置都會(huì)強(qiáng)制走一遍豎屏和橫屏兩種真機(jī)測(cè)試確認(rèn)幀方向和解碼區(qū)域都對(duì)得上。5.4 擴(kuò)展方向從掃碼到圖像識(shí)別入口ZXing 解碼拿到的Result對(duì)象里除了文本還有g(shù)etRawBytes()和getResultPoints()。前者是原始字節(jié)掃二進(jìn)制碼比如某些工業(yè)條碼時(shí)有用后者是碼在畫面中的四個(gè)角點(diǎn)坐標(biāo)可以做 AR 疊加、掃碼動(dòng)畫跟隨。如果你后續(xù)想接 OCR 或物體識(shí)別onPreviewFrame拿到的幀數(shù)據(jù)可以直接喂給其他模型這份源碼的相機(jī)鏈路就是一個(gè)現(xiàn)成的圖像采集入口。改造時(shí)注意線程模型——?jiǎng)e在解碼線程里跑模型推理另開一個(gè)線程池用幀隊(duì)列做背壓否則預(yù)覽會(huì)卡成幻燈片。希望這份拆解能幫你少走點(diǎn)彎路把掃碼這個(gè)看似簡單、實(shí)則細(xì)節(jié)一堆的模塊真正吃透。本文還有配套的精品資源點(diǎn)擊獲取