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的。 扫码的时候 二维码也不会拉伸了。大功告成!

推荐阅读更多精彩内容

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