Zxing二維碼掃描的集成與優(yōu)化

Zxing已經(jīng)是一個(gè)很成熟的框架了,但它是用maven構(gòu)建的項(xiàng)目,在以gradle為基礎(chǔ)的AS中集成起來(lái)總感覺(jué)不太方便。網(wǎng)上有很多種方式,我這里主要采取了復(fù)制代碼到自己項(xiàng)目中的方式,這樣有利于學(xué)習(xí)和擴(kuò)展。
第一步:集成
官方項(xiàng)目地址:https://github.com/zxing/zxing
當(dāng)前最新版是 3.3.0,目錄結(jié)構(gòu)如下:

clipboard.png

跟android有關(guān)的 是 core,android-core,android-integration ,以及android。其中 android 包是一個(gè)完整的demo。里面包含了一些分享,歷史管理,設(shè)置,幫助之類的主菜單。
進(jìn)入release頁(yè)面:https://github.com/zxing/zxing/releases,下載最新的代碼

clipboard.png

點(diǎn)擊源碼下載。然后按照源碼的包名,依次在自己項(xiàng)目中新建對(duì)應(yīng)的包,最好不要改名字(改了名字會(huì)帶來(lái)大量的錯(cuò)誤提示,改起來(lái)很累)。然后把所有的資源文件復(fù)制到對(duì)應(yīng)自己項(xiàng)目的目錄下。這樣在所有提示錯(cuò)誤的文件中,基本上都只有R類了。改成自己的R類導(dǎo)入就好了。集成基本完成,可以正常運(yùn)行,在AndroidManifest.xml中配置對(duì)應(yīng)的一些組件,如CaptureActivity。就可以在某個(gè)地方通過(guò)Intent的方式運(yùn)行起來(lái)了,startActivityForResult().

第二步:廋身
Zxing框架是集成了,但是太過(guò)龐大,很多對(duì)于我們來(lái)說(shuō)沒(méi)用的東西。或許我們的項(xiàng)目只需要識(shí)別二維碼,生成二維碼之類的。運(yùn)行CaptureActivity之后,會(huì)看到右上角有個(gè)菜單,里面有4個(gè)菜單,share,history,setting,help。根據(jù)菜單找到對(duì)應(yīng)的配置文件capture.xml。從這里開(kāi)始把 share,history,help先刪除。對(duì)應(yīng)代碼目錄結(jié)構(gòu)client.android 下面,把share,history文件夾都刪掉,別忘了HelpActivity是一個(gè)單獨(dú)的存在于client.android目錄下。這時(shí)候代碼里面會(huì)很多地方報(bào)錯(cuò),主要是用到了 HistoryManager,找到報(bào)錯(cuò)的地方只要遇到調(diào)用history有關(guān)的地方就注釋掉或者刪掉。此致,輕松刪掉了兩個(gè)模塊。剩下的大部分都跟那個(gè)設(shè)置菜單有關(guān),里面的設(shè)置項(xiàng)非常多,這個(gè)需要謹(jǐn)慎刪除,慢慢來(lái)。

第三步:優(yōu)化
在優(yōu)化之前,首先要大概了解一下這個(gè)框架,可以先在網(wǎng)上搜一把,原來(lái)再看源碼,可能就沒(méi)有那么生僻的感覺(jué)。主要有幾個(gè)重要的類:
CaptureActivity,掃描界面,也是官方demo的主界面。
CaptureActivityHandler,輔助掃描界面,進(jìn)行一些邏輯的處理,消息的轉(zhuǎn)發(fā)。
CameraManager,Camera,相機(jī)有關(guān)的部分,如 預(yù)覽,自動(dòng)聚焦
DecodeThread,DecodeHandler, 跟解碼有關(guān)的類,線程,消息處理
BarcodeFormat, DecodeHintType, 支持的一些類型,格式,配置。如,二維碼,各種條形碼,字符集。
還有Result 和 各種ResultHandler,掃描出的結(jié)果類型,如,url,text,email,geo,wifi,address...等。
大致掃碼流程如下:

clipboard.png

1.框架默認(rèn)支持所有的碼類型,有17種,在枚舉類BarcodeFormat中已經(jīng)定義,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;
如果我們只需要支持掃二維碼,可以這樣啟動(dòng)我們的掃描界面,
Intent intent = new Intent(getActivity(), CaptureActivity.class);
intent.setAction(Intents.Scan.ACTION);
intent.putExtra(Intents.Scan.FORMATS, "QR_CODE");
startActivityForResult(intent, REQUEST_CODE);
用intent傳遞一個(gè)參數(shù),QR_CODE,如果不傳,則默認(rèn)會(huì)加入所有的類型支持,根據(jù)菜單中的設(shè)置項(xiàng)。代碼在DecodeThread中,

clipboard.png

2.縮短自動(dòng)聚焦的時(shí)間間隔。
在AutoFocusManager 中,有一個(gè)變量,AUTO_FOCUS_INTERVAL_MS,在自動(dòng)聚焦的時(shí)候會(huì)根據(jù)該變量設(shè)定的時(shí)間來(lái)睡眠。

clipboard.png

3.PlanarYUVLuminanceSource,掃描精度。
在掃碼的時(shí)候發(fā)現(xiàn)非要把碼對(duì)準(zhǔn)到框中才能掃出結(jié)果,原因在于官方為了減少解碼的數(shù)據(jù),提高解碼效率和速度,采用了裁剪無(wú)用區(qū)域的方式。這樣會(huì)帶來(lái)一定的問(wèn)題,整個(gè)二維碼數(shù)據(jù)需要完全放到聚焦框里才有可能被識(shí)別,并且在buildLuminanceSource(byte[],int,int)這個(gè)方法簽名中,傳入的byte數(shù)組便是圖像的數(shù)據(jù),并沒(méi)有因?yàn)椴眉舳箶?shù)據(jù)量減小,而是采用了取這個(gè)數(shù)組中的部分?jǐn)?shù)據(jù)來(lái)達(dá)到裁剪的目的。對(duì)于目前CPU性能過(guò)剩的大多數(shù)智能手機(jī)來(lái)說(shuō),這種裁剪顯得沒(méi)有必要。如果把解碼數(shù)據(jù)換成采用全幅圖像數(shù)據(jù),這樣在識(shí)別的過(guò)程中便不再拘束于聚焦框,也使得二維碼數(shù)據(jù)可以鋪滿整個(gè)屏幕。這樣用戶在使用程序來(lái)掃描二維碼時(shí),盡管不完全對(duì)準(zhǔn)聚焦框,也可以識(shí)別出來(lái)。這屬于一種策略上的讓步,給用戶造成了錯(cuò)覺(jué),但提高了識(shí)別的精度。解決辦法很簡(jiǎn)單,就是不僅僅使用聚焦框里的圖像數(shù)據(jù),而是采用全幅圖像的數(shù)據(jù)。
在CameraManger中,

clipboard.png

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

4.掃描結(jié)果的處理。
在官方demo中,如果啟動(dòng)CaptureActivity的時(shí)候不傳任何intent參數(shù),則最后默認(rèn)會(huì)有一個(gè)內(nèi)部處理,在CaptureActivity的handleDecode方法中,有一個(gè)switch,默認(rèn)會(huì)走Case NONE;調(diào)用
handleDecodeInternally(rawResult, resultHandler, barcode);
如果啟動(dòng)掃描界面?zhèn)髁?BarcodeFormat,則會(huì)走h(yuǎn)andleDecodeExternally(rawResult, resultHandler, barcode)方法。不管走那種方法,最后會(huì)在掃描結(jié)果的時(shí)候在屏幕上繪制出掃描的bitmap,

clipboard.png

把這一段注釋掉,因?yàn)閷?shí)際項(xiàng)目不需要顯示這樣一個(gè)圖。如果你在自己的onAcitivityResult中處理跳轉(zhuǎn)瀏覽器,你會(huì)發(fā)現(xiàn)在跳轉(zhuǎn)之前會(huì)有延遲。CaptureActivity中有這樣一個(gè)變量,
DEFAULT_INTENT_RESULT_DURATION_MS = 1500L,默認(rèn)是1.5秒。也就是會(huì)延遲1.5秒才執(zhí)行onAcitivityResult。

clipboard.png
clipboard.png

所以,把這個(gè)常量改成0,就沒(méi)有延遲了。

5.默認(rèn)的掃描界面太丑了,是長(zhǎng)方形的,而且中間一根紅線也不動(dòng),就是附近有幾個(gè)點(diǎn)在閃爍。改聚焦框的大小,代碼在CameraManager中。

clipboard.png

此方法中,我簡(jiǎn)單的把高度設(shè)置成跟寬度一樣了,至少現(xiàn)在是個(gè)正方形了。
還有幾十整個(gè)View的繪制,都在ViewfinderView這個(gè)類中onDraw方法實(shí)現(xiàn)。這是第一個(gè)自定義View,如果想要掃碼界面變得沒(méi)關(guān)漂亮,基本只需要改動(dòng)這個(gè)類就好了。

6.關(guān)于預(yù)覽圖片拉伸的問(wèn)題
Zxing 框架默認(rèn)是橫屏掃描的,在不做更改的情況掃描二維碼的時(shí)候,發(fā)現(xiàn)二維碼會(huì)被拉伸。追蹤源碼。發(fā)現(xiàn)在
CameraConfigurationManager中的initFromCameraParameters里面有這樣兩行代碼:


clipboard.png

關(guān)鍵就是這個(gè),cameraResolution ,相機(jī)分辨率,進(jìn)入到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;
}

這個(gè)方法目的就是根據(jù)當(dāng)前屏幕的分辨率選擇最合適的相機(jī)分辨率,
首先,它對(duì)所有支持的分辨率尺寸進(jìn)行一個(gè)降序排列。
然后,根據(jù)寬高比值差異進(jìn)行一輪淘汰,差異大于MAX_ASPECT_DISTORTION這個(gè)值就會(huì)從列表中刪除此分辨率,這個(gè)值默認(rèn)是0.15。
那么問(wèn)題就出在這里了。我用一個(gè)7201280的手機(jī)進(jìn)行調(diào)試,發(fā)現(xiàn)根據(jù)現(xiàn)有的代碼執(zhí)行結(jié)果是 所有的都會(huì)被淘汰,差異值都會(huì)大于0.15,
我通過(guò)代碼拿到的屏幕真實(shí)分辨率為 720
1184,我掃碼界面已經(jīng)固定為豎屏。按照這個(gè)公式計(jì)算 double screenAspectRatio = screenResolution.x / (double) screenResolution.y;
那么screenAspectRatio 這個(gè)值永遠(yuǎn)是小于1的。而 double aspectRatio = maybeFlippedWidth / (double) maybeFlippedHeight;算出的結(jié)果永遠(yuǎn)是大于1的,這兩個(gè)相減取絕對(duì)值,基本上結(jié)果都是大于
0.15的,所以都被淘汰了。
看看這三行代碼,
boolean isCandidatePortrait = realWidth < realHeight;
int maybeFlippedWidth = isCandidatePortrait ? realHeight : realWidth;
int maybeFlippedHeight = isCandidatePortrait ? realWidth : realHeight ;

maybeFlippedWidth 永遠(yuǎn)大于 maybeFlippedHeight ,明顯是橫屏的效果。所以我做出如下改動(dòng):

int maybeFlippedWidth = isCandidatePortrait ? realWidth: realHeight ;
int maybeFlippedHeight = isCandidatePortrait ? realHeight : realWidth;
就是把 寬和高 換位。
這樣aspectRatio的值才是小于1的數(shù) ,才跟screenAspectRatio 有可比性,不然一直都是天差地別。
這樣改動(dòng)之后,至少不至于每次整個(gè)列表都被淘汰光,但留下的也有點(diǎn)多。
根據(jù)打印的log,支持的列表為 :
Supported preview sizes: 1680x1248 1920x1088 1920x1080 1280x720 960x540 800x600 864x480 860x480 800x480 720x480 640x480 480x368 480x320 352x288 320x240 176x144
根據(jù)斷點(diǎn)進(jìn)行調(diào)試,發(fā)現(xiàn)最后那個(gè)差值,基本在0.15以內(nèi),然后我把那個(gè)常量 MAX_ASPECT_DISTORTION 改成了0.05,這樣就又可以從這個(gè)列表中淘汰一部分了。
接下來(lái),按照原來(lái)的流程走,會(huì)執(zhí)行這個(gè)方法,
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;
}
選擇當(dāng)前序列中最大的那個(gè),但最大的那個(gè)并不是最接近屏幕分辨率的,所以我決定對(duì)當(dāng)前列表再次排序,按照與屏幕寬度差距由小到大的順序排列,那么第一個(gè)就是最接近當(dāng)前屏幕寬度的分辨率了,修改代碼如下:
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;
}
這樣都改好之后,然后運(yùn)行程序,打印log,會(huì)看到最后選出來(lái)的 cameraResolution 就是 1280*720的。 掃碼的時(shí)候 二維碼也不會(huì)拉伸了。大功告成!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

推薦閱讀更多精彩內(nèi)容

  • 二維碼掃描最近兩年簡(jiǎn)直是風(fēng)靡移動(dòng)互聯(lián)網(wǎng)時(shí)代,尤其在國(guó)內(nèi)發(fā)展神速。圍繞條碼掃碼功能,首先說(shuō)說(shuō)通過(guò)本文你可以知道啥。一...
    55book閱讀 4,211評(píng)論 0 1
  • 了解二維碼這個(gè)東西還是從微信中,當(dāng)時(shí)微信推出二維碼掃描功能,自己感覺(jué)挺新穎的,從一張圖片中掃一下竟然能直接加好友,...
    AiPuff閱讀 878評(píng)論 0 1
  • ¥開(kāi)啟¥ 【iAPP實(shí)現(xiàn)進(jìn)入界面執(zhí)行逐一顯】 〖2017-08-25 15:22:14〗 《//首先開(kāi)一個(gè)線程,因...
    小菜c閱讀 6,550評(píng)論 0 17
  • 一,Google原生zXing包使用: 1.CaptureActivity就是掃描界面 2.掃描結(jié)束后回調(diào) 3.在...
    whstywh閱讀 3,451評(píng)論 1 7
  • 背景 一年多以前我在知乎上答了有關(guān)LeetCode的問(wèn)題, 分享了一些自己做題目的經(jīng)驗(yàn)。 張土汪:刷leetcod...
    土汪閱讀 12,776評(píng)論 0 33