美文网首页Android 技术问题及技术分享
Zxing二维码扫描的集成与优化

Zxing二维码扫描的集成与优化

作者: 雾中的影子 | 来源:发表于2017-08-24 17:31 被阅读28959次

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

    相关文章

      网友评论

        本文标题:Zxing二维码扫描的集成与优化

        本文链接:https://www.haomeiwen.com/subject/uzvedxtx.html