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,
我通过代码拿到的屏幕真实分辨率为 720
1184,我扫码界面已经固定为竖屏。按照这个公式计算 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的。 扫码的时候 二维码也不会拉伸了。大功告成!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,547评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,399评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,428评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,599评论 1 274
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,612评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,577评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,941评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,603评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,852评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,605评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,693评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,375评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,955评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,936评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,172评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,970评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,414评论 2 342

推荐阅读更多精彩内容

  • 二维码扫描最近两年简直是风靡移动互联网时代,尤其在国内发展神速。围绕条码扫码功能,首先说说通过本文你可以知道啥。一...
    55book阅读 4,134评论 0 1
  • 了解二维码这个东西还是从微信中,当时微信推出二维码扫描功能,自己感觉挺新颖的,从一张图片中扫一下竟然能直接加好友,...
    AiPuff阅读 854评论 0 1
  • ¥开启¥ 【iAPP实现进入界面执行逐一显】 〖2017-08-25 15:22:14〗 《//首先开一个线程,因...
    小菜c阅读 6,350评论 0 17
  • 一,Google原生zXing包使用: 1.CaptureActivity就是扫描界面 2.扫描结束后回调 3.在...
    whstywh阅读 3,382评论 1 7
  • 背景 一年多以前我在知乎上答了有关LeetCode的问题, 分享了一些自己做题目的经验。 张土汪:刷leetcod...
    土汪阅读 12,719评论 0 33