1. 程式人生 > >Zxing二維碼掃描的整合與優化

Zxing二維碼掃描的整合與優化

Zxing已經是一個很成熟的框架了,但它是用maven構建的專案,在以gradle為基礎的AS中整合起來總感覺不太方便。網上有很多種方式,我這裡主要採取了複製程式碼到自己專案中的方式,這樣有利於學習和擴充套件。 第一步:整合 官方專案地址:https://github.com/zxing/zxing 當前最新版是 3.3.0,目錄結構如下:

clipboard.png

跟android有關的 是 core,android-core,android-integration ,以及android。其中 android 包是一個完整的demo。裡面包含了一些分享,歷史管理,設定,幫助之類的主選單。 進入release頁面:

https://github.com/zxing/zxing/releases,下載最新的程式碼

clipboard.png

點選原始碼下載。然後按照原始碼的包名,依次在自己專案中新建對應的包,最好不要改名字(改了名字會帶來大量的錯誤提示,改起來很累)。然後把所有的資原始檔複製到對應自己專案的目錄下。這樣在所有提示錯誤的檔案中,基本上都只有R類了。改成自己的R類匯入就好了。整合基本完成,可以正常執行,在AndroidManifest.xml中配置對應的一些元件,如CaptureActivity。就可以在某個地方通過Intent的方式執行起來了,startActivityForResult().

第二步:廋身 Zxing框架是集成了,但是太過龐大,很多對於我們來說沒用的東西。或許我們的專案只需要識別二維碼,生成二維碼之類的。執行CaptureActivity之後,會看到右上角有個選單,裡面有4個選單,share,history,setting,help。根據選單找到對應的配置檔案capture.xml。從這裡開始把 share,history,help先刪除。對應程式碼目錄結構client.android 下面,把share,history資料夾都刪掉,別忘了HelpActivity是一個單獨的存在於client.android目錄下。這時候程式碼裡面會很多地方報錯,主要是用到了 HistoryManager,找到報錯的地方只要遇到呼叫history有關的地方就註釋掉或者刪掉。此致,輕鬆刪掉了兩個模組。剩下的大部分都跟那個設定選單有關,裡面的設定項非常多,這個需要謹慎刪除,慢慢來。

第三步:優化 在優化之前,首先要大概瞭解一下這個框架,可以先在網上搜一把,原來再看原始碼,可能就沒有那麼生僻的感覺。主要有幾個重要的類: CaptureActivity,掃描介面,也是官方demo的主介面。 CaptureActivityHandler,輔助掃描介面,進行一些邏輯的處理,訊息的轉發。 CameraManager,Camera,相機有關的部分,如 預覽,自動聚焦 DecodeThread,DecodeHandler, 跟解碼有關的類,執行緒,訊息處理 BarcodeFormat, DecodeHintType, 支援的一些型別,格式,配置。如,二維碼,各種條形碼,字符集。 還有Result 和 各種ResultHandler,掃描出的結果型別,如,url,text,email,geo,wifi,address...等。 大致掃碼流程如下:

clipboard.png

1.框架預設支援所有的碼型別,有17種,在列舉類BarcodeFormat中已經定義,AZTEC, CODABAR, CODE_39, CODE_93, CODE_128, DATA_MATRIX, EAN_8, EAN_13, ITF, MAXICODE, PDF_417, QR_CODE, RSS_14, RSS_EXPANDED, UPC_A, UPC_E, UPC_EAN_EXTENSION; 如果我們只需要支援掃二維碼,可以這樣啟動我們的掃描介面, Intent intent = new Intent(getActivity(), CaptureActivity.class); intent.setAction(Intents.Scan.ACTION); intent.putExtra(Intents.Scan.FORMATS, "QR_CODE"); startActivityForResult(intent, REQUEST_CODE); 用intent傳遞一個引數,QR_CODE,如果不傳,則預設會加入所有的型別支援,根據選單中的設定項。程式碼在DecodeThread中,

clipboard.png

2.縮短自動聚焦的時間間隔。 在AutoFocusManager 中,有一個變數,AUTO_FOCUS_INTERVAL_MS,在自動聚焦的時候會根據該變數設定的時間來睡眠。

clipboard.png

3.PlanarYUVLuminanceSource,掃描精度。 在掃碼的時候發現非要把碼對準到框中才能掃出結果,原因在於官方為了減少解碼的資料,提高解碼效率和速度,採用了裁剪無用區域的方式。這樣會帶來一定的問題,整個二維碼資料需要完全放到聚焦框裡才有可能被識別,並且在buildLuminanceSource(byte[],int,int)這個方法簽名中,傳入的byte陣列便是影象的資料,並沒有因為裁剪而使資料量減小,而是採用了取這個陣列中的部分資料來達到裁剪的目的。對於目前CPU效能過剩的大多數智慧手機來說,這種裁剪顯得沒有必要。如果把解碼資料換成採用全幅影象資料,這樣在識別的過程中便不再拘束於聚焦框,也使得二維碼資料可以鋪滿整個螢幕。這樣使用者在使用程式來掃描二維碼時,儘管不完全對準聚焦框,也可以識別出來。這屬於一種策略上的讓步,給使用者造成了錯覺,但提高了識別的精度。解決辦法很簡單,就是不僅僅使用聚焦框裡的影象資料,而是採用全幅影象的資料。 在CameraManger中,

clipboard.png

把返回的,rect區域改成全圖,return new PlanarYUVLuminanceSource(data, width, height, 0, 0, width,height, false); 這樣掃碼的時候就不一定要完全對準了,哪怕只有一部分碼出現在聚焦框中也可以掃出結果。

4.掃描結果的處理。 在官方demo中,如果啟動CaptureActivity的時候不傳任何intent引數,則最後預設會有一個內部處理,在CaptureActivity的handleDecode方法中,有一個switch,預設會走Case NONE;呼叫 handleDecodeInternally(rawResult, resultHandler, barcode); 如果啟動掃描介面傳了 BarcodeFormat,則會走handleDecodeExternally(rawResult, resultHandler, barcode)方法。不管走那種方法,最後會在掃描結果的時候在螢幕上繪製出掃描的bitmap,

clipboard.png

把這一段註釋掉,因為實際專案不需要顯示這樣一個圖。如果你在自己的onAcitivityResult中處理跳轉瀏覽器,你會發現在跳轉之前會有延遲。CaptureActivity中有這樣一個變數, DEFAULT_INTENT_RESULT_DURATION_MS = 1500L,預設是1.5秒。也就是會延遲1.5秒才執行onAcitivityResult。

clipboard.png

clipboard.png

所以,把這個常量改成0,就沒有延遲了。

5.預設的掃描介面太醜了,是長方形的,而且中間一根紅線也不動,就是附近有幾個點在閃爍。改聚焦框的大小,程式碼在CameraManager中。

clipboard.png

此方法中,我簡單的把高度設定成跟寬度一樣了,至少現在是個正方形了。 還有幾十整個View的繪製,都在ViewfinderView這個類中onDraw方法實現。這是第一個自定義View,如果想要掃碼介面變得沒關漂亮,基本只需要改動這個類就好了。

6.關於預覽圖片拉伸的問題 Zxing 框架預設是橫屏掃描的,在不做更改的情況掃描二維碼的時候,發現二維碼會被拉伸。追蹤原始碼。發現在 CameraConfigurationManager中的initFromCameraParameters裡面有這樣兩行程式碼:

clipboard.png

關鍵就是這個,cameraResolution ,相機解析度,進入到CameraConfigurationUtils中的findBestPreviewSizeValue方法; public static Point findBestPreviewSizeValue(Camera.Parameters parameters, Point screenResolution) { List<Camera.Size> rawSupportedSizes = parameters.getSupportedPreviewSizes(); if (rawSupportedSizes == null) { Log.w(TAG, "Device returned no supported preview sizes; using default"); Camera.Size defaultSize = parameters.getPreviewSize(); if (defaultSize == null) { throw new IllegalStateException("Parameters contained no preview size!"); } return new Point(defaultSize.width, defaultSize.height); } // Sort by size, descending List<Camera.Size> supportedPreviewSizes = new ArrayList<>(rawSupportedSizes); Collections.sort(supportedPreviewSizes, new Comparator<Camera.Size>() { @Override public int compare(Camera.Size a, Camera.Size b) { int aPixels = a.height * a.width; int bPixels = b.height * b.width; if (bPixels < aPixels) { return -1; } if (bPixels > aPixels) { return 1; } return 0; } }); if (Log.isLoggable(TAG, Log.INFO)) { StringBuilder previewSizesString = new StringBuilder(); for (Camera.Size supportedPreviewSize : supportedPreviewSizes) { previewSizesString.append(supportedPreviewSize.width).append('x') .append(supportedPreviewSize.height).append(' '); } Log.i(TAG, "Supported preview sizes: " + previewSizesString); } double screenAspectRatio = screenResolution.x / (double) screenResolution.y; // Remove sizes that are unsuitable Iterator<Camera.Size> it = supportedPreviewSizes.iterator(); while (it.hasNext()) { Camera.Size supportedPreviewSize = it.next(); int realWidth = supportedPreviewSize.width; int realHeight = supportedPreviewSize.height; if (realWidth * realHeight < MIN_PREVIEW_PIXELS) { it.remove(); continue; } boolean isCandidatePortrait = realWidth < realHeight; int maybeFlippedWidth = isCandidatePortrait ? realHeight : realWidth; int maybeFlippedHeight = isCandidatePortrait ? realWidth : realHeight ; double aspectRatio = maybeFlippedWidth / (double) maybeFlippedHeight; double distortion = Math.abs(aspectRatio - screenAspectRatio); if (distortion > MAX_ASPECT_DISTORTION) { it.remove(); continue; } if (maybeFlippedWidth == screenResolution.x && maybeFlippedHeight == screenResolution.y) { Point exactPoint = new Point(realWidth, realHeight); Log.i(TAG, "Found preview size exactly matching screen size: " + exactPoint); return exactPoint; } } // If no exact match, use largest preview size. This was not a great idea on older devices because // of the additional computation needed. We're likely to get here on newer Android 4+ devices, where // the CPU is much more powerful. if (!supportedPreviewSizes.isEmpty()) { Camera.Size largestPreview = supportedPreviewSizes.get(0); Point largestSize = new Point(largestPreview.width, largestPreview.height); Log.i(TAG, "Using largest suitable preview size: " + largestSize); return largestSize; }

// If there is nothing at all suitable, return current preview size Camera.Size defaultPreview = parameters.getPreviewSize(); if (defaultPreview == null) { throw new IllegalStateException("Parameters contained no preview size!"); } Point defaultSize = new Point(defaultPreview.width, defaultPreview.height); Log.i(TAG, "No suitable preview sizes, using default: " + defaultSize); return defaultSize; }

這個方法目的就是根據當前螢幕的解析度選擇最合適的相機解析度, 首先,它對所有支援的解析度尺寸進行一個降序排列。 然後,根據寬高比值差異進行一輪淘汰,差異大於MAX_ASPECT_DISTORTION這個值就會從列表中刪除此解析度,這個值預設是0.15。 那麼問題就出在這裡了。我用一個7201280的手機進行除錯,發現根據現有的程式碼執行結果是 所有的都會被淘汰,差異值都會大於0.15, 我通過程式碼拿到的螢幕真實解析度為 7201184,我掃碼介面已經固定為豎屏。按照這個公式計算 double screenAspectRatio = screenResolution.x / (double) screenResolution.y; 那麼screenAspectRatio 這個值永遠是小於1的。而 double aspectRatio = maybeFlippedWidth / (double) maybeFlippedHeight;算出的結果永遠是大於1的,這兩個相減取絕對值,基本上結果都是大於 0.15的,所以都被淘汰了。 看看這三行程式碼, boolean isCandidatePortrait = realWidth < realHeight; int maybeFlippedWidth = isCandidatePortrait ? realHeight : realWidth; int maybeFlippedHeight = isCandidatePortrait ? realWidth : realHeight ;

maybeFlippedWidth 永遠大於 maybeFlippedHeight ,明顯是橫屏的效果。所以我做出如下改動:

int maybeFlippedWidth = isCandidatePortrait ? realWidth: realHeight ; int maybeFlippedHeight = isCandidatePortrait ? realHeight : realWidth; 就是把 寬和高 換位。 這樣aspectRatio的值才是小於1的數 ,才跟screenAspectRatio 有可比性,不然一直都是天差地別。 這樣改動之後,至少不至於每次整個列表都被淘汰光,但留下的也有點多。 根據列印的log,支援的列表為 : Supported preview sizes: 1680x1248 1920x1088 1920x1080 1280x720 960x540 800x600 864x480 860x480 800x480 720x480 640x480 480x368 480x320 352x288 320x240 176x144 根據斷點進行除錯,發現最後那個差值,基本在0.15以內,然後我把那個常量 MAX_ASPECT_DISTORTION 改成了0.05,這樣就又可以從這個列表中淘汰一部分了。 接下來,按照原來的流程走,會執行這個方法, if (!supportedPreviewSizes.isEmpty()) { Camera.Size largestPreview = supportedPreviewSizes.get(0); Point largestSize = new Point(largestPreview.width, largestPreview.height); Log.i(TAG, "Using largest suitable preview size: " + largestSize); return largestSize; } 選擇當前序列中最大的那個,但最大的那個並不是最接近螢幕解析度的,所以我決定對當前列表再次排序,按照與螢幕寬度差距由小到大的順序排列,那麼第一個就是最接近當前螢幕寬度的解析度了,修改程式碼如下: if (!supportedPreviewSizes.isEmpty()) { Collections.sort(supportedPreviewSizes, new Comparator<Camera.Size>() { @Override public int compare(Camera.Size o1, Camera.Size o2) { int delta1 = Math.abs(o1.height-screenResolution.x); int delta2 = Math.abs(o2.height-screenResolution.x); return delta1 - delta2; } }); Camera.Size bestPreview = supportedPreviewSizes.get(0); Point bestSize = new Point(bestPreview.width, bestPreview.height); return bestSize; } 這樣都改好之後,然後執行程式,列印log,會看到最後選出來的 cameraResolution 就是 1280*720的。 掃碼的時候 二維碼也不會拉伸了。大功告成!

作者:霧中的影子 連結:https://www.jianshu.com/p/9bd4e5d8a405 來源:簡書 簡書著作權歸作者所有,任何形式的轉載都請聯絡作者獲得授權並註明出處。