Zxing二维码开源库学习和优化

本文对Google官方开源的二维码扫描库ZXing做一个总体概述,然后进行Android开发环境下的实战(二次)开发和优化,并用于生产项目。
ZXing开源项目官方地址:https://github.com/zxing/zxing


前期学习和准备

各种百度和google,先了解二维码的技术概念和原理,他人经验和文章等等。以下摘自百度百科:

国外对二维码技术的研究始于20世纪80年代末,在二维码符号表示技术研究方面已研制出多种码制,常见的有PDF417、QR Code、Code 49、Code 16K、Code One等。这些二维码的信息密度都比传统的一维码有了较大提高,如PDF417的信息密度是一维码CodeC39的20多倍。在二维码标准化研究方面,国际自动识别制造商协会(AIM)、美国标准化协会(ANSI)已完成了PDF417、QR Code、Code 49、Code 16K、Code One等码制的符号标准。国际标准技术委员会和国际电工委员会还成立了条码自动识别技术委员会(ISO/IEC/JTC1/SC31),已制定了QR Code的国际标准(ISO/IEC 18004:2000《自动识别与数据采集技术—条码符号技术规范—QR码》),起草了PDF417、Code 16K、Data Matrix、Maxi Code等二维码的ISO/IEC标准草案。在二维码设备开发研制、生产方面,美国、日本等国的设备制造商生产的识读设备、符号生成设备,已广泛应用于各类二维码应用系统。二维码作为一种全新的信息存储、传递和识别技术,自诞生之日起就得到了世界上许多国家的关注。美国、德国、日本等国家,不仅已将二维码技术应用于公安、外交、军事等部门对各类证件的管理,而且也将二维码应用于海关、税务等部门对各类报表和票据的管理,商业、交通运输等部门对商品及货物运输的管理、邮政部门对邮政包裹的管理、工业生产领域对工业生产线的自动化管理。

我国对二维码技术的研究开始于1993年。中国物品编码中心对几种常用的二维码PDF417、QRCCode、Data Matrix、Maxi Code、Code 49、Code 16K、Code One的技术规范进行了翻译和跟踪研究。随着我国市场经济的不断完善和信息技术的迅速发展,国内对二维码这一新技术的需求与日俱增。中国物品编码中心在原国家质量技术监督局和国家有关部门的大力支持下,对二维码技术的研究不断深入。在消化国外相关技术资料的基础上,制定了两个二维码的国家标准:二维码网格矩阵码(SJ/T 11349-2006)和二维码紧密矩阵码(SJ/T 11350-2006),从而大大促进了我国具有自主知识产权技术的二维码的研发。

实战

1. 下载ZXing并选择包

截止本文攥写之时,ZXing的最新版本为3.2.1,在Github网站上下载ZXing的主分支(zxing-master.zip),解压完成之后,目录树如下:
ZXing项目解压的目录层次

以下对解压的目录结构(包含模块)的主要部分做一个概述(此处以3.2.1版本基准,不同的版本包含的模块可能稍有差异,请留意):

  • android

    Android client Barcode Scanner,中文意为“条形扫码器”,下文简称BS。可作为独立的扫码APP使用。

  • android-core

    Android-related code shared among android, androidtest, glass,即android、androidtest和glass三个模块共享的android相关库,当前其实就是一个相机配置工具类(CameraConfigurationUtils.java).

  • android-integration

    Supports integration with Barcode Scanner via Intent,即通过Intent的方式提供一种便捷的途径将BS整合到用户的APP中。

  • androidtest

    Android test app, ZXing Test,即模拟调用方app(相当于用户APP的角色),通过android-integration整合Barcode Scanner

  • core

    The core image decoding library, and test code.即核心的图片编解码库,整个条形码的核心处理库,另外还包括测试代码。

  • glass

    Simple Google Glass application

  • zxingorg

    The source behind zxing.org

  • zxing.appspot.com

    The source behind web-based barcode generator at zxing.appspot.com

  • javase

    JavaSE-specific client code

其中,与开发Android二维码扫码相关的模块有android、android-core、android-integration、androidtest和core模块。

androidtest、android-integration、android三个模块的关系图如下:

2.试用并厘清ZXing项目的源码

  • 试用:

    用Eclipse导入(按照导入现有Android工程的方式)上面所述的androidtest模块,导入之后,源代码部分会报错。主要有以下两种错误:

    • 由于ZXing项目编写者采用的java编译版本不低于1.7.0,因此如果你的Eclipse的java编译版本设置为1.6.0或者更低版本时,将会报错,比如new ArrayList<>(),在1.6.0版本则不能省略具体的泛型类型,应该为new ArrayList();

    • 引用了大量core模块、android-integration和android-core的类,因此需要将core模块的jar包(当然也可以直接copy源码)添加到工程的构建路径(core包的下载地址:http://repo1.maven.org/maven2/com/google/zxing/core/),另外需要将android-integration和android-core的源码复制到工程中(新建对应的package,copy类);

      按照上面步骤处理完之后,则可以运行的测试APP诞生了。工程目录图如下:
      ZXing试用工程目录

      运行并安装APK至手机或模拟器,测试APP界面如下图:

      ZXing试用工程目录

      可以体验扫码和生成二维码的功能。

  • 源码分析

    其实这里要关注的源码主要有四部分:core、android、android-integration和android-core,其中android-integration和android-core较为简单,这里不再赘述。而core涉及图片处理和二维码的理论技术,暂不做深入研究。

    android模块即BS,可以作为单独APP使用,是一个功能强大的条码扫描器,不仅支持多种类型的条码,还支持多国语言,分享二维码,查看扫描历史,反向扫描等功能。

    因此,这里主要就android模块进行分析阐述。导入Eclipse之后,android模块的包结构图如下:

    BS包结构

    如上图所示,BS主要包括下列组件:

    • android:与CaptureActivity直接相关的核心组件。包含了发生震动管理器,闪光灯等等。

    • book:如果查询的结果是图书信息,用户可以选择查询该书的更进一步的详细信息,该包即包含了搜索与展示书籍的相关类。

    • camera/camera.open:摄像头相关组件,核心类是CameraManager

    • clipboard:剪贴板

    • encode:编码功能的各个组件集合。核心类为QRCodeEncoder,最终实施编码的是MultiFormatWriter类

    • history:扫描历史管理,核心类是HistoryManager

    • result:条码扫描的结果被分为不同的类型,所有的类型都定义在com.google.zxing.client.result.ParsedResultType中,对于不同的类型都有对应的处理方法:xxxResultHandler,所有的ResultHandler都包含在此包中。不同的xxxResultHandler还提供了扫描结果页面要展示几个button,每个button的文本以及需要绑定的事件等等。

    • result.supplement:对已经扫描并解码的结果做额外处理的工具集。比如扫描出来的是isbn号,如果在设置中选择了“检索更多信息”则会在扫描出isbn号之后自动去网上查询该书的信息,最后将书的信息展示出来,而如果没选中,则只会将isbn号码展示。

    • share:分享二维码功能,亦是编码功能的入口所在。

    • wifi:是WifiResultHandler的辅助类集合。如果扫描到的二维码是对wifi信息的编码,那么最终扫描结果页会展示一个“连接到网络”的按钮,点击此按钮就会自动尝试连接。该包中所包含的类则是链接网络所需的工具类。

      打开BS,即进入扫描界面时,BS大致做了如下的事情:配置Camera并启动Camera、构建preview与扫描窗口、捕捉画面并解码、将解码结果交给不同ResultHandler去处理。下面逐一进行分析。

    1. 配置Camera并启动Camera

      启动Camera是在CaptureActivity.initCamera中进行的,最重要的几句代码是:

      cameraManager.openDriver(surfaceHolder);
      // Creating the handler starts the preview, which can also throw a
      // RuntimeException.
      if (handler == null) {
          handler = new CaptureActivityHandler(this, decodeFormats,
                  decodeHints, characterSet, cameraManager);
      }
      

      CameraManager是相机管理类,是BS中唯一与Camera打交道的类,CameraManager.openDriver主要做了三件事:

      /**
       * Opens the camera driver and initializes the hardware parameters.
       * 
       * @param holder
       *            The surface object which the camera will draw preview frames
       *            into.
       * @throws IOException
       *             Indicates the camera driver failed to open.
       */
      public synchronized void openDriver(SurfaceHolder holder)
              throws IOException {
          Camera theCamera = camera;
          if (theCamera == null) {
              // 1. 获取手机背面的摄像头
              theCamera = OpenCameraInterface.open(requestedCameraId);
              if (theCamera == null) {
                  throw new IOException();
              }
              camera = theCamera;
          }
          // 2. 设置摄像头预览view
          theCamera.setPreviewDisplay(holder);
      
          if (!initialized) {
              initialized = true;
              configManager.initFromCameraParameters(theCamera);
              if (requestedFramingRectWidth > 0 && requestedFramingRectHeight > 0) {
                  setManualFramingRect(requestedFramingRectWidth,
                          requestedFramingRectHeight);
                  requestedFramingRectWidth = 0;
                  requestedFramingRectHeight = 0;
              }
          }
      
          Camera.Parameters parameters = theCamera.getParameters();
          String parametersFlattened = parameters == null ? null : parameters
                  .flatten(); // Save these, temporarily
          try {
              // 3. 读取配置并设置相机参数
              configManager.setDesiredCameraParameters(theCamera, false);
          } catch (RuntimeException re) {
              // Driver failed
              Log.w(TAG,
                      "Camera rejected parameters. Setting only minimal safe-mode parameters");
              Log.i(TAG, "Resetting to saved camera params: "
                      + parametersFlattened);
              // Reset:
              if (parametersFlattened != null) {
                  parameters = theCamera.getParameters();
                  parameters.unflatten(parametersFlattened);
                  try {
                      theCamera.setParameters(parameters);
                      configManager.setDesiredCameraParameters(theCamera, true);
                  } catch (RuntimeException re2) {
                      // Well, darn. Give up
                      Log.w(TAG,
                              "Camera rejected even safe-mode parameters! No configuration");
                  }
              }
          }
      
      }
      

      CameraConfigurationManager是相机辅助类,主要用于设置相机的各类参数。核心方法有两个:

      • initFromCameraParameters:计算了屏幕分辨率和当前最适合的相机像素

      • setDesiredCameraParameters:读取配置设置相机的对焦模式、闪光灯模式等等

        CaptureActivityHandler类是一个针对扫描任务的Handler,可接收的message有启动扫描(restart_preview)、扫描成功(decode_succeeded)、扫描失败(decode_failed)等等。

        在创建一个CaptureActivityHandler对象的时候也做了三件事:

        CaptureActivityHandler(CaptureActivity activity,

                   Collection<BarcodeFormat> decodeFormats,
                   Map<DecodeHintType,?> baseHints,
                   String characterSet,
                   CameraManager cameraManager) {
          this.activity = activity;
          // 1. 启动扫描线程
          decodeThread = new DecodeThread(activity, decodeFormats, baseHints, characterSet,
              new ViewfinderResultPointCallback(activity.getViewfinderView()));
          decodeThread.start();
          state = State.SUCCESS;
        
          // Start ourselves capturing previews and decoding.
          this.cameraManager = cameraManager;
          // 2. 开启相机预览界面
          cameraManager.startPreview();
          // 3. 将preview回调函数与decodeHandler绑定、调用viewfinderView
          restartPreviewAndDecode();
        }
        

        restartPreviewAndDecode方法又调用了CameraManager.requestPreviewFrame:

        /**

        • A single preview frame will be returned to the handler supplied. The data
        • will arrive as byte[] in the message.obj field, with width and height
        • encoded as message.arg1 and message.arg2, respectively.

        • 1:将handler与preview回调函数绑定;

        • 2:注册preview回调函数
        • 综上,该函数的作用是当相机的预览界面准备就绪后就会调用hander向其发送传入的message
        • @param handler
        • The handler to send the message to.
        • @param message
        • The what field of the message to be sent.
          */
          public synchronized void requestPreviewFrame(Handler handler, int message) {
          Camera theCamera = camera;
          if (theCamera != null && previewing) {
          previewCallback.setHandler(handler, message);
          / 绑定相机回调函数,当预览界面准备就绪后会回调Camera.PreviewCallback.onPreviewFrame
          theCamera.setOneShotPreviewCallback(previewCallback);
          }
          }
2. 构建preview与扫描窗口

    首先相机有自己的preview界面,然后我们需要构造一个扫描窗口,引导用户将条码置于窗口中完成扫描。

    构造扫描窗口是在CaptureActivityHandler.restartPreviewAndDecode中,通过调用activity.drawViewfinder()来实现的。这里有个画扫描窗口的类叫ViewfinderView,该类也是想要改变扫描窗口风格所必须重构的一个类。

    重构ViewfinderView涉及Android的高级话题:自定义View及其属性,这里暂不赘述。

    相机的preview界面显示出来后即可开始扫描,所以需要监听preview是否已经显示这个事件,这就是Camera.PreviewCallback的作用。PreviewCallback.onPreviewFrame做的事便是当preview界面展示出来的时候向DecodeHandler发送一个decode消息,DecodeHandler收到该消息后会执行decode方法来解码。

    注意,检测并触发捕获画面动作的,是Camera.setOneShotPreviewCallback()这个方法。该函数被调用后,如果预览界面已经打开,就会将包含当前preview frame的byte数组传给回调函数,此时再向DecodeHandler发送decode消息。

3. 捕捉画面并解码

    具体参考DecodeHandler.decode方法。(本文只从宏观上对zxing进行分析,对于解码的原理暂不做介绍)

4. 将解码结果交给不同ResultHandler去处理

    当DecodeHandler.decode完成解码后会向CaptureActivityHandler发消息。如果编码成功则调用CaptureActivity.handleDecode方法对扫描到的结果进行分类处理。

    该方法中首先获取ResultHandler:

        ResultHandler resultHandler = ResultHandlerFactory.makeResultHandler(
                this, rawResult);
    然后调用handleDecodeInternally和handleDecodeExternally对ResultHandler进行处理。谈到这两个方法,就不得不再分析一下    IntentSource。

        enum IntentSource {

          /**
           * 本地app向BS(Barcode Scanner)发起的启动指令
           * 比如在androidtest项目中,利用整合的android-integration对BS发起调用指令:com.google.zxing.client.android.SCAN
           * BS中该启动命令对应的Source类型便是NATIVE_APP_INTENT
           */
            NATIVE_APP_INTENT,

          /**
           * 打开BS的时候传入查询商品的url,与最终扫描到的product id结合进行查询
           * 两种url的形式不同
           */
          PRODUCT_SEARCH_LINK,
          ZXING_LINK,

          /**
           * 直接打开BS
           */
          NONE

        }

    结合CaptureActivity.onResume中的部分代码来理解:

        else if (dataString != null
                    && dataString.contains("http://www.google")
                    && dataString.contains("/m/products/scan")) {

                // Scan only products and send the result to mobile Product
                // Search.
                source = IntentSource.PRODUCT_SEARCH_LINK;
                sourceUrl = dataString;
                decodeFormats = DecodeFormatManager.PRODUCT_FORMATS;

        } else if (isZXingURL(dataString)) {

            // Scan formats requested in query string (all formats if none
            // specified).
            // If a return URL is specified, send the results there.
            // Otherwise, handle it ourselves.
            source = IntentSource.ZXING_LINK;
            sourceUrl = dataString;
            Uri inputUri = Uri.parse(dataString);
            scanFromWebPageManager = new ScanFromWebPageManager(inputUri);
            decodeFormats = DecodeFormatManager
                    .parseDecodeFormats(inputUri);
            // Allow a sub-set of the hints to be specified by the caller.
            decodeHints = DecodeHintManager.parseDecodeHints(inputUri);

        }

    NATIVE_APP_INTENT和NONE很好理解,而PRODUCT_SEARCH_LINK和ZXING_LINK是指定查询商品的url(而不是交给zxing分析后再决定去哪里查询),将扫描出来的内容拼凑到url中,然后在浏览器中展示结果。

    理解了IntentSource,就容易看懂handleDecodeInternally其实就是将结果展示到界面上。handleDecodeExternally稍复杂些,当source == IntentSource.NATIVE_APP_INTENT时,BS会将扫描分析的结果存到Intent中返回给调用方app,因此调用方app在启动BS的时候一定要使用startActivityForResult。这一点可以在androidtest的IntentIntegrator.initiateScan方法的最后看到。

3.BS优化

BS可直接作为独立APP运行,但是项目中的很多功能我们并不需要,而且扫描的界面为横向,因此进行相关优化。接下来,我们在BS项目工程基础上,直接修改其中代码或者xml文件,达到修改扫码界面为竖屏且美化扫码界面的目的。

  1. 修改BS默认的横屏为竖屏

    针对ZXing3.2.1版本,这里一共需要6步,需要提醒的是:不同的ZXing版本需要的步骤可能有差异,因为里面的源代码逻辑有修改。因此,有的时候尽信书不如无书,本文的步骤可能仅适合ZXing3.2.1,如果在ZXing其他版本上完成以下六步无法实现竖屏,或者修改之后会出错,请仔细排查(可以断点调试跟踪)。其实,要实现竖屏,从原理上应该是共通的,主要包括Activity本身的方向(属性)设置,同时关联到相机的参数(如预览窗口)调整等方面。

    • 第一步:调整CaptureActivity的方向为竖屏显示

      修改工程的AndroidManifest.xml,CaptureActivity的android:screenOrientation属性,设置为portrait

    • 第二步:调整相机预览界面方向

      在 CameraConfigurationManager.setDesiredCameraParameters 的最后(或第一行也可)增加如下代码:
      //调整扫描activity为竖屏,step2.调整相机预览界面方向
      camera.setDisplayOrientation(90);
      注意:调整相机preview的时钟方向与手机竖屏的自然方向一致。该方法必须在相机的startPreview之前被调用,

      在预览界面展示出来后设置是无效的。
      
    • 第三步:调整扫描窗口尺寸

      修改 CameraManager.getFramingRectInPreview()方法中的部分代码:
      原代码段为:

      rect.left = rect.left * cameraResolution.x / screenResolution.x;
      rect.right = rect.right * cameraResolution.x / screenResolution.x;
      rect.top = rect.top * cameraResolution.y / screenResolution.y;
      rect.bottom = rect.bottom * cameraResolution.y / screenResolution.y;
      framingRectInPreview = rect;
      

      修改后代码段为:

      /*
       * 调整扫描activity为竖屏,step3.调整扫描窗口尺寸
       * 由于修改了屏幕的初始方向,手机分辨率由原来的 width\*height 变为 height\*width
       * 形式,但是相机的分辨率则是固定的,因此这里需做些调整以计算出正确的缩放比率。
       */
      rect.left = rect.left * cameraResolution.y / screenResolution.x;
      rect.right = rect.right * cameraResolution.y / screenResolution.x;
      rect.top = rect.top * cameraResolution.x / screenResolution.y;
      rect.bottom = rect.bottom * cameraResolution.x / screenResolution.y;
      framingRectInPreview = rect;
      

      原因:由于修改了屏幕的初始方向,手机分辨率由原来的 width*height 变为 height*width 形式,

      但是相机的分辨率则是固定的,因此这里需做些调整以计算出正确的缩放比率。
      
    • 第四步:将扫描框设置为正方形

      修改 CameraManager.getFramingRect()方法中的部分代码:
      原代码段为:

      int width = findDesiredDimensionInRange(screenResolution.x,
              MIN_FRAME_WIDTH, MAX_FRAME_WIDTH);
      
      int height = findDesiredDimensionInRange(screenResolution.y,
              MIN_FRAME_HEIGHT, MAX_FRAME_HEIGHT);
      Log.d(TAG,"screenResolution.x:"+screenResolution.x+",screenResolution.y:"+screenResolution.y);
      int leftOffset = (screenResolution.x - width) / 2;
      int topOffset = (screenResolution.y - height) / 2;
      framingRect = new Rect(leftOffset, topOffset, leftOffset + width,
              topOffset + height);
      Log.d(TAG, "Calculated framing rect: " + framingRect);
      

      修改后代码段为:

      /*
        * 调整扫描activity为竖屏,step4.将扫描框设置为正方形
       */
      //后面设置height = width的前提下,如x>y时可能会导致topOffset为负值,因此需要对此进行修正,加入下面这行代码
      int resolutionMin = Math.min(screenResolution.x, screenResolution.y);
      int width = findDesiredDimensionInRange(resolutionMin,
      MIN_FRAME_WIDTH, MAX_FRAME_WIDTH);
      
      int height = width;//宽度等于高度,即为正方形
      Log.d(TAG,"screenResolution.x:"+screenResolution.x+",screenResolution.y:"+screenResolution.y);
      int leftOffset = (screenResolution.x - width) / 2;
      int topOffset = (screenResolution.y - height) / 2;
      framingRect = new Rect(leftOffset, topOffset, leftOffset + width,
              topOffset + height);
      Log.d(TAG, "Calculated framing rect: " + framingRect);
      
    • 第五步:反转扫描到的图形

      修改 DecodeHandler.decode 方法,增加以下代码:
      

      private void decode(byte[] data, int width, int height) {

      long start = System.currentTimeMillis();
      Result rawResult = null;
      
      /*
       * 调整扫描activity为竖屏,step5.反转扫描到的图形
       */
      // 新增反转数据代码开始
      byte[] rotatedData = new byte[data.length];
      for (int y = 0; y < height; y++) {
          for (int x = 0; x < width; x++)
              rotatedData[x * height + height - y - 1] = data[x + y * width];
      }
      int tmp = width;
      width = height;
      height = tmp;
      // 新增代码结束
      
      PlanarYUVLuminanceSource source = activity.getCameraManager()
              .buildLuminanceSource(rotatedData, width, height);
      //...后续代码
      

      }

    • 第六步:(关键)修改CaptureActivity的onresume方法

      完成以上五步后,在ZXing的某些版本上应该是可以实现竖屏了,但是在3.2.1版本上,由于在CaptureActivity
      的onresume方法里面涉及到了修改activity的方向,因此需要针对性修改这部分代码。
      原代码段为:
         if (prefs.getBoolean(PreferencesActivity.KEY_DISABLE_AUTO_ORIENTATION,
         true)) {
             setRequestedOrientation(getCurrentOrientation());
         } else {
             setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE);
         }
      

      修改后代码段为:

      if (prefs.getBoolean(PreferencesActivity.KEY_DISABLE_AUTO_ORIENTATION,
      true)) {
          setRequestedOrientation(getCurrentOrientation());
      } else {
          // setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE);
          setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT);
      }
      

      同时,需要修改getCurrentOrientation方法:
      原方法为:

      private int getCurrentOrientation() {
          int rotation = getWindowManager().getDefaultDisplay().getRotation();
          switch (rotation) {
          case Surface.ROTATION_0:
          case Surface.ROTATION_90:
              return ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE;
          default:
              return ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE;
          }
      }
      

      修改后为:

      private int getCurrentOrientation() {
          int rotation = getWindowManager().getDefaultDisplay().getRotation();
          switch (rotation) {
          case Surface.ROTATION_0:
          case Surface.ROTATION_90:
              // return ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE;
              return ActivityInfo.SCREEN_ORIENTATION_PORTRAIT;
          default:
              // return ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE;
              return ActivityInfo.SCREEN_ORIENTATION_REVERSE_PORTRAIT;
          }
      }
      
  2. 美化扫描界面

    原生的扫描界面比较朴素,如果需要做出更漂亮些的扫描界面,就必须重写ViewfinderView类(res/layout/capture.xml布局中使用该类作为扫描窗口界面)。这里,我们为了实现类微信或支付宝的扫描界面效果,在将扫码界面修改为竖屏的基础上,可以做如下修改:

    • 在ViewfinderView类添加变量:

      /**
       * 四个边角对应的宽度
       */
       private static final int CORNER_WIDTH = 8;
      
      /**
       * 四个边角对应的长度
       */
      private int ScreenRate;
      
      /**
       * 手机的屏幕密度
       */
      private static float density;
      
      /**
       * 四个边角的颜色
       */
      private final int cornerColor;
      
      /**
       * 扫描框中的中间线的宽度
       */
      private static final int MIDDLE_LINE_WIDTH = 2;
      
      /**
       * 扫描框中的中间线的与扫描框左右的间隙
       */
      private static final int MIDDLE_LINE_PADDING = 10;
      
      /**
       * 中间那条线每次刷新移动的距离
       */
      private static final int SPEEN_DISTANCE = 5;
      
      /**
       * 中间滑动线的最顶端位置
       */
      private int slideTop;
      
      /**
       * 中间滑动线的最底端位置
       */
      private int slideBottom;
      
      boolean isFirst;
      
    • 修改ViewfinderView类的构造方法和onDraw方法。注:对于Android自定义View,绘制的核心就是onDraw方法。

      a.在构造方法中添加初始化或设置变量的值:

      // This constructor is used when the class is built from an XML resource.
      public ViewfinderView(Context context, AttributeSet attrs) {

      super(context, attrs);
      
      // Initialize these once for performance rather than calling them every
      // time in onDraw().
      paint = new Paint(Paint.ANTI_ALIAS_FLAG);
      Resources resources = getResources();
      maskColor = resources.getColor(R.color.viewfinder_mask);
      resultColor = resources.getColor(R.color.result_view);
      laserColor = resources.getColor(R.color.viewfinder_laser);
      
      //设置添加的变量值--start
      cornerColor = Color.GREEN;
      density = context.getResources().getDisplayMetrics().density;  
      //将像素转换成dp  
      ScreenRate = (int)(20 * density);  
      //设置添加的变量值--end
      
      resultPointColor = resources.getColor(R.color.possible_result_points);
      scannerAlpha = 0;
      possibleResultPoints = new ArrayList<>(5);
      lastPossibleResultPoints = null;
      

      }

      b.修改onDraw方法中部分代码:

      修改前代码段:

      if (resultBitmap != null) {

      // Draw the opaque result bitmap over the scanning rectangle
      paint.setAlpha(CURRENT_POINT_OPACITY);
      canvas.drawBitmap(resultBitmap, null, frame, paint);
      

      } else {

      // Draw a red "laser scanner" line through the middle to show
      // decoding is active
      paint.setColor(laserColor);
      paint.setAlpha(SCANNER_ALPHA[scannerAlpha]);
      scannerAlpha = (scannerAlpha + 1) % SCANNER_ALPHA.length;
      int middle = frame.height() / 2 + frame.top;
      canvas.drawRect(frame.left + 2, middle - 1, frame.right - 1,
              middle + 2, paint);
      
      float scaleX = frame.width() / (float) previewFrame.width();
      float scaleY = frame.height() / (float) previewFrame.height();
      
      List<ResultPoint> currentPossible = possibleResultPoints;
      List<ResultPoint> currentLast = lastPossibleResultPoints;
      int frameLeft = frame.left;
      int frameTop = frame.top;
      if (currentPossible.isEmpty()) {
          lastPossibleResultPoints = null;
      } else {
          possibleResultPoints = new ArrayList<>(5);
          lastPossibleResultPoints = currentPossible;
          paint.setAlpha(CURRENT_POINT_OPACITY);
          paint.setColor(resultPointColor);
          synchronized (currentPossible) {
              for (ResultPoint point : currentPossible) {
                  canvas.drawCircle(frameLeft
                          + (int) (point.getX() * scaleX), frameTop
                          + (int) (point.getY() * scaleY), POINT_SIZE,
                          paint);
              }
          }
      }
      if (currentLast != null) {
          paint.setAlpha(CURRENT_POINT_OPACITY / 2);
          paint.setColor(resultPointColor);
          synchronized (currentLast) {
              float radius = POINT_SIZE / 2.0f;
              for (ResultPoint point : currentLast) {
                  canvas.drawCircle(frameLeft
                          + (int) (point.getX() * scaleX), frameTop
                          + (int) (point.getY() * scaleY), radius, paint);
              }
          }
      }
      
      // Request another update at the animation interval, but only
      // repaint the laser line,
      // not the entire viewfinder mask.
      postInvalidateDelayed(ANIMATION_DELAY, frame.left - POINT_SIZE,
              frame.top - POINT_SIZE, frame.right + POINT_SIZE,
              frame.bottom + POINT_SIZE);
      

      }

      修改后代码段:

      if (resultBitmap != null) {
      // Draw the opaque result bitmap over the scanning rectangle
      paint.setAlpha(CURRENT_POINT_OPACITY);
      canvas.drawBitmap(resultBitmap, null, frame, paint);
      } else {

      // Draw a red "laser scanner" line through the middle to show
      // decoding is active
      /*paint.setColor(laserColor);
      paint.setAlpha(SCANNER_ALPHA[scannerAlpha]);
      scannerAlpha = (scannerAlpha + 1) % SCANNER_ALPHA.length;
      int middle = frame.height() / 2 + frame.top;
      canvas.drawRect(frame.left + 2, middle - 1, frame.right - 1,
              middle + 2, paint);*/
      
      float scaleX = frame.width() / (float) previewFrame.width();
      float scaleY = frame.height() / (float) previewFrame.height();
      
      List<ResultPoint> currentPossible = possibleResultPoints;
      List<ResultPoint> currentLast = lastPossibleResultPoints;
      int frameLeft = frame.left;
      int frameTop = frame.top;
      if (currentPossible.isEmpty()) {
          lastPossibleResultPoints = null;
      } else {
          possibleResultPoints = new ArrayList<>(5);
          lastPossibleResultPoints = currentPossible;
          paint.setAlpha(CURRENT_POINT_OPACITY);
          paint.setColor(resultPointColor);
          synchronized (currentPossible) {
              for (ResultPoint point : currentPossible) {
                  canvas.drawCircle(frameLeft
                          + (int) (point.getX() * scaleX), frameTop
                          + (int) (point.getY() * scaleY), POINT_SIZE,
                          paint);
              }
          }
      }
      if (currentLast != null) {
          paint.setAlpha(CURRENT_POINT_OPACITY / 2);
          paint.setColor(resultPointColor);
          synchronized (currentLast) {
              float radius = POINT_SIZE / 2.0f;
              for (ResultPoint point : currentLast) {
                  canvas.drawCircle(frameLeft
                          + (int) (point.getX() * scaleX), frameTop
                          + (int) (point.getY() * scaleY), radius, paint);
              }
          }
      }
      
      /*
       * 如下为了优化扫描框,绘制边角
       */
      paint.setColor(cornerColor);
      canvas.drawRect(frame.left, frame.top, frame.left + ScreenRate,
              frame.top + CORNER_WIDTH, paint);
      canvas.drawRect(frame.left, frame.top, frame.left + CORNER_WIDTH,
              frame.top + ScreenRate, paint);
      canvas.drawRect(frame.right - ScreenRate, frame.top, frame.right,
              frame.top + CORNER_WIDTH, paint);
      canvas.drawRect(frame.right - CORNER_WIDTH, frame.top, frame.right,
              frame.top + ScreenRate, paint);
      canvas.drawRect(frame.left, frame.bottom - CORNER_WIDTH, frame.left
              + ScreenRate, frame.bottom, paint);
      canvas.drawRect(frame.left, frame.bottom - ScreenRate, frame.left
              + CORNER_WIDTH, frame.bottom, paint);
      canvas.drawRect(frame.right - ScreenRate, frame.bottom
              - CORNER_WIDTH, frame.right, frame.bottom, paint);
      canvas.drawRect(frame.right - CORNER_WIDTH, frame.bottom
              - ScreenRate, frame.right, frame.bottom, paint);
      
      /*
       * 如下绘制中间上下滚动的横线
       */
      //初始化中间线滑动的最上边和最下边
      if(!isFirst){
          isFirst = true;
          slideTop = frame.top;
          slideBottom = frame.bottom;
      }
      
      //绘制中间的线,每次刷新界面,中间的线往下移动SPEEN_DISTANCE
      slideTop += SPEEN_DISTANCE;
      if(slideTop >= frame.bottom){
          slideTop = frame.top;
      }
      canvas.drawRect(frame.left + MIDDLE_LINE_PADDING, slideTop - MIDDLE_LINE_WIDTH/2,
                  frame.right - MIDDLE_LINE_PADDING,slideTop + MIDDLE_LINE_WIDTH/2, paint);
      
            // Request another update at the animation interval, but only
            // repaint the laser line,
            // not the entire viewfinder mask.
            postInvalidateDelayed(ANIMATION_DELAY, frame.left - POINT_SIZE,
                    frame.top - POINT_SIZE, frame.right + POINT_SIZE,
                    frame.bottom + POINT_SIZE);
        }

**注意**:上面的代码中,中间那根线微信是用的图片(图片可以自己制作,也可以反编译微信apk从中获取),这里是画的线条,如果你想更加仿真点就将下面的代码:

    canvas.drawRect(frame.left + MIDDLE_LINE_PADDING, slideTop - MIDDLE_LINE_WIDTH/2,
                frame.right - MIDDLE_LINE_PADDING,slideTop + MIDDLE_LINE_WIDTH/2, paint);



修改为:

    Rect lineRect = new Rect();  
    lineRect.left = frame.left;  
    lineRect.right = frame.right;  
    lineRect.top = slideTop;  
    lineRect.bottom = slideTop + 18;  
    canvas.drawBitmap(((BitmapDrawable)(getResources().getDrawable(R.drawable.qrcode_scan_line))).getBitmap(), null, lineRect, paint);  

4.实战并整合

待续(实际使用中,一般都是应用APP包括了扫描功能,而不是将扫码功能单独作为一个APP,因此需要对BS工程进行简化,使其可以作为其他应用工程的依赖lib,并去除那些不需要的功能,然后就直接整合进APP。)