Android 基于Zxing的扫码功能实现(二)之探索扫码流程,自定义扫码界面

2,458 阅读10分钟

本篇文章已授权微信公众号 guolin_blog (郭霖)独家发布

引言

本篇博文是基于 Android 二维码的扫码功能实现(一) 文章写的,建议阅读这篇文章之前,先看看上篇文章。还有建议阅读本文的同学,结合zxing的源码理解。
上篇博客说明zxing的使用方式,并大致说了IntentIntegrator这个辅助类的作用,及内部的部分源码讲解。通过上篇博文的讲解,虽然我们成功使用了zxing 的扫码功能,但是我们发现它的界面是这样的:

这里写图片描述

这显然不是我们想要的效果。所以我们必须要对zxing库进行修改,变成我们项目所要的扫码库。
那现在我们打算实现一个样式类似于微信扫一扫样子的二维码。大多数项目的界面应该跟这个差不多。该怎么下手呢?我们看一下微信扫一扫的效果:
这里写图片描述

Zxing扫码流程分析

我们首先分析一波zxing扫码的整个流程。我们知道想实现上面的界面效果,主要的布局的变化,扫码的核心算法与思路应该是跟Zxing原来一样的。而且zxing的库是比较庞大的,我们只是实现扫码功能的话,zxing里面的很多东西,我们是用不到的,所以需要对其简化,去掉不用的东西。 首先我们看CaptureActivity这个类,上篇文章也有提到过这个类,这个Activity就是官方的扫码界面。我们看他的setContentView(R.layout.capture);这行语句,进入capture布局,可以看到,一下眼熟的控件。CaptureActivity里面有一个很重要的方法。如下:

private void initCamera(SurfaceHolder surfaceHolder) {
    if (surfaceHolder == null) {
      throw new IllegalStateException("No SurfaceHolder provided");
    }
    if (cameraManager.isOpen()) {
      Log.w(TAG, "initCamera() while already open -- late SurfaceView callback?");
      return;
    }
    try {
      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);
      }
      decodeOrStoreSavedBitmap(null, null);
    } catch (IOException ioe) {
      Log.w(TAG, ioe);
      displayFrameworkBugMessageAndExit();
    } catch (RuntimeException e) {
      // Barcode Scanner has seen crashes in the wild of this variety:
      // java.?lang.?RuntimeException: Fail to connect to camera service
      Log.w(TAG, "Unexpected error initializing camera", e);
      displayFrameworkBugMessageAndExit();
    }
  }

这个initCamera方法涉及到相机的初始化配置,以及扫码配置与启动。CameraManager是相机管理类,里面有着很多很重要的方法,比如开始预览的方法,停止预览以及获取每一帧画面的数据信息等方法。我们先看cameraManager.openDriver(surfaceHolder);这行语句是,点击进去:

/**
   * 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 {
    OpenCamera theCamera = camera;
    if (theCamera == null) {
      theCamera = OpenCameraInterface.open(requestedCameraId);
      if (theCamera == null) {
        throw new IOException("Camera.open() failed to return object from driver");
      }
      camera = theCamera;
    }

    if (!initialized) {
      initialized = true;
      configManager.initFromCameraParameters(theCamera);
      if (requestedFramingRectWidth > 0 && requestedFramingRectHeight > 0) {
        setManualFramingRect(requestedFramingRectWidth, requestedFramingRectHeight);
        requestedFramingRectWidth = 0;
        requestedFramingRectHeight = 0;
      }
    }

    Camera cameraObject = theCamera.getCamera();
    Camera.Parameters parameters = cameraObject.getParameters();
    String parametersFlattened = parameters == null ? null : parameters.flatten(); // Save these, temporarily
    try {
      configManager.setDesiredCameraParameters(theCamera, false);
    } catch (RuntimeException re) {

点看后我们看到描述的很清楚,这个方法的作用是打开相机设备,并且配置一些相机参数的。OpenCamera是Camera的包装类。CameraConfigurationManager是设置相机硬件参数的一个类。configManager.initFromCameraParameters(theCamera);这个方法主要是的内容是寻找最好的预览尺寸。寻找最佳预览尺寸的逻辑我就不说了,这块,可以看下这位兄弟写的
iluhcm.com/2016/01/08/… 里面说明了寻找最佳预览尺寸的逻辑,及优化。configManager.setDesiredCameraParameters(theCamera, false);这个方法主要就是设置我们想要的相机参数了。这里会把上面方法中找到的最佳预览大小bestPreviewSize设置给parameters.setPreviewSize(bestPreviewSize.x, bestPreviewSize.y);我们也可以在这个方法里面调用camera.setDisplayOrientation(90);来实现竖屏的效果。
以上是initCamera()方法里面的cameraManager.openDriver这一块分析,接着我们来看 handler = new CaptureActivityHandler(this, decodeFormats, decodeHints, characterSet, cameraManager);语句。进入进去代码如下:

CaptureActivityHandler(CaptureActivity activity,
                         Collection<BarcodeFormat> decodeFormats,
                         Map<DecodeHintType,?> baseHints,
                         String characterSet,
                         CameraManager cameraManager) {
    this.activity = activity;
    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;
    cameraManager.startPreview();
    restartPreviewAndDecode();
  }

这个方法中我们看到decodeThread线程,我们进去看一下发现里面的代码主要是设置了Map

@Override
  public void run() {
    Looper.prepare();
    handler = new DecodeHandler(activity, hints);
    handlerInitLatch.countDown();
    Looper.loop();
  }

run方法里面主要是创建了一个decodeHandler对象,并把hints这个存储支持扫码类型的变量给传进去了。我们接着看decodeHandler是什么鬼?

DecodeHandler(CaptureActivity activity, Map<DecodeHintType,Object> hints) {
    multiFormatReader = new MultiFormatReader();
    multiFormatReader.setHints(hints);
    this.activity = activity;
  }

  @Override
  public void handleMessage(Message message) {
    if (message == null || !running) {
      return;
    }
    if (message.what == R.id.decode) {
      decode((byte[]) message.obj, message.arg1, message.arg2);

    } else if (message.what == R.id.quit) {
      running = false;
      Looper.myLooper().quit();

    }
  }

代码很好理解,首先创建了一个MultiFormatReader,并把支持扫码格式传给他,MultiFormatReader是专门解密的一个核心类。很重要。然后我们看到当该Handler收到R.id.decode改消息的时候,会调用decode((byte[]) message.obj, message.arg1, message.arg2);这个方法,我们看下:

private void decode(byte[] data, int width, int height) {
    long start = System.currentTimeMillis();
    Result rawResult = null;
    PlanarYUVLuminanceSource source = activity.getCameraManager().buildLuminanceSource(data, width, height);
    if (source != null) {
      BinaryBitmap bitmap = new BinaryBitmap(new HybridBinarizer(source));
      try {
        rawResult = multiFormatReader.decodeWithState(bitmap);
      } catch (ReaderException re) {
        // continue
      } finally {
        multiFormatReader.reset();
      }
    }

    Handler handler = activity.getHandler();
    if (rawResult != null) {
      // Don't log the barcode contents for security.
      long end = System.currentTimeMillis();
      Log.d(TAG, "Found barcode in " + (end - start) + " ms");
      if (handler != null) {
        Message message = Message.obtain(handler, R.id.decode_succeeded, rawResult);
        Bundle bundle = new Bundle();
        bundleThumbnail(source, bundle);        
        message.setData(bundle);
        message.sendToTarget();
      }
    } else {
      if (handler != null) {
        Message message = Message.obtain(handler, R.id.decode_failed);
        message.sendToTarget();
      }
    }
  }

O(∩_∩)O哈!找了半天终于找到了,这方法重要了,这就是我们扫码逻辑中最重要的解密的逻辑了。代码虽然多但是并不难。首先它构建了一个PlanarYUVLuminanceSource对象,接着根据source创建了二进制的BinaryBitmap。然后rawResult =
multiFormatReader.decodeWithState(bitmap);通过该语句,实现了解密,把解码的结果封装赋值给了Result类。
最后把结果传给了CaptureActivityHandler,在其handlemessage方法中实现对结果的处理。在这里要注意一个问题,就是需要把传进来的data数据中的数据旋转一下,这里的数据是横屏的画面数据。需要转化为竖屏画面数据。该方法传进来的width,height这两个参数的值也需要调换一下。具体的转化代码,可以看YZxing-lib库DecodeHandler类里的实现。
我们现在想一个问题,就是decode这个方法是在什么时候实现的呢?也就是说decodeHandler是在什么时候发送了R.id.decode这个消息?我们看这个方法:

CaptureActivityHandler(CaptureActivity activity,
                         Collection<BarcodeFormat> decodeFormats,
                         Map<DecodeHintType,?> baseHints,
                         String characterSet,
                         CameraManager cameraManager) {
    this.activity = activity;
    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;
    cameraManager.startPreview();
    restartPreviewAndDecode();
  }

这个方法里面的
cameraManager.startPreview();
restartPreviewAndDecode();

这两行语句我们还没看呢。首先看第一行语句,很好理解,这是开始预览画面的执行语句。第二句是 restartPreviewAndDecode();,我们进去看一下:

if (state == State.SUCCESS) {
      state = State.PREVIEW;
      cameraManager.requestPreviewFrame(decodeThread.getHandler(), R.id.decode);
      activity.drawViewfinder();
    }

这里我们看到了R.id.decode这个消息的what值。我们看cameraManager的requestPreviewFrame方法:

public synchronized void requestPreviewFrame(Handler handler, int message) {
    OpenCamera theCamera = camera;
    if (theCamera != null && previewing) {
      previewCallback.setHandler(handler, message);
      theCamera.getCamera().setOneShotPreviewCallback(previewCallback);
    }
  }

这里是获取预览界面的一帧。我们看previewCallback里面的代码:

void setHandler(Handler previewHandler, int previewMessage) {
    this.previewHandler = previewHandler;
    this.previewMessage = previewMessage;
  }

  @Override
  public void onPreviewFrame(byte[] data, Camera camera) {
    Point cameraResolution = configManager.getCameraResolution();
    Handler thePreviewHandler = previewHandler;
    if (cameraResolution != null && thePreviewHandler != null) {
      Message message = thePreviewHandler.obtainMessage(previewMessage, cameraResolution.x,
          cameraResolution.y, data);
      message.sendToTarget();
      previewHandler = null;
    } else {
      Log.d(TAG, "Got preview callback, but no handler or resolution available");
    }
  }

挖了这么久终于找到了,onPreviewFrame方法里,在这decodeHandler发送了解码的消息,并把一帧的图像数据发送了过去。如果decodeHandler里面的decode 方法扫码失败的话,就发送一个R.id.decode_failed消息给CaptureActivityHandler,CaptureActivityHandler里会调用:

} else if (message.what == R.id.decode_failed) {// We're decoding as fast as possible, so when one decode fails, start another.
      state = State.PREVIEW;
      cameraManager.requestPreviewFrame(decodeThread.getHandler(), R.id.decode);

该方法,继续请求下一帧的画面数据,去解析。

分析到此,zxing的扫码流程,大致的脉络就是这个样子。这里总结一下吧,就是点击扫码,跳转到CaptureActivity,CaptureActivity里面调用了initCamera方法,该方法中一方面通过cameraManager.openDriver(surfaceHolder);对相机进行初始化,及硬件配置;一方面通过对CaptureActivityHandler的创建,实现解码类MultiFormatReader的配置,画面的预览实现,每一帧画面的数据请求,传递,解码逻辑实现。最后根据这一帧画面数据扫码结果 是成功还是失败发送,来决定是继续请求下一帧的画面信息还是处理扫码成功的结果。


在观察CaptureActivity的时候,我们发现了一个自定义控件,叫做ViewfinderVIew.通过阅读其代码,发现这就是绘制扫码框样式的地方。那我们在修改zxing库的时候就可以重写这个类,来实现对扫码框样式的修改。

YZxing-lib

YZxing-lib这个库,是我基于zxing库修改的扫码库,去除了原来ZXing库中多余的部分,并对扫码效率进行了优化。我们先来看一下YZxing库的实现效果:

效果图1


效果图2(扫码成功)
(ps:演示效果图,弹窗逻辑已删除)


这里写图片描述
(扫码成功后,结果的回调)
微信的扫一扫,它聚焦框内有一条不断从上到下移动的绿线,我这边没做成他那样(比较懒),我这边实现的效果是跟zxing sample效果类似,是一条绿色的,一闪一闪的激光线。想实现微信它那种一条绿线从上到下不停移动的效果的话,让UI设计一张“绿线图片”(好拗口)设为ImageView的背景,通过Animation补间动画就可以实现了。

看过效果图之后这里就介绍一下YZxing-lib的结构,方便大家看源码。

callback包里面是请求每帧画面数据信息的回调。camera包是相机相关的类,具体类的介绍这里不再赘述,大家也可以进YZxing-lib源码看,有详细说明。decode包下主要是解码这块功能的类,以及扫码结果的处理。scannerView相当于zxing里面的viewfinderview,在这个类里实现了扫码界面的样式绘制。

使用方式

首先通过在build.gradle文件中添加如下编译语句将YZxing-lib库添加到项目中。

compile 'com.yangy:YZxing-lib:1.1'

或者在直接把GitHub上面的YZxing库下载下来,添加到项目中。 然后在点击跳转到扫码界面的点击事件中,调用如下方法:

 Intent intent = new Intent(this, ScannerActivity.class);
        //这里可以用intent传递一些参数,比如扫码聚焦框尺寸大小,支持的扫码类型。
//        //设置扫码框的宽
//        intent.putExtra(Constant.EXTRA_SCANNER_FRAME_WIDTH, 400);
//        //设置扫码框的高
//        intent.putExtra(Constant.EXTRA_SCANNER_FRAME_HEIGHT, 400);
//        //设置扫码框距顶部的位置
//        intent.putExtra(Constant.EXTRA_SCANNER_FRAME_TOP_PADDING, 100);
//        Bundle bundle = new Bundle();
//        //设置支持的扫码类型
//        bundle.putSerializable(Constant.EXTRA_SCAN_CODE_TYPE, mHashMap);
//        intent.putExtras(bundle);
        startActivityForResult(intent, RESULT_REQUEST_CODE);

这里可以使用intent传递一些配置参数。支持有设置扫码框的大小,及位置;设置支持的扫码类型。目前支持的自定义配置不多,后续有机会再扩充。 跳转的时候要有startActivityForResult来跳转,这样在扫码成功之后,返回的结果可以在onActivityResult方法中处理代码如下:

@Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        if (resultCode == RESULT_OK) {
            switch (requestCode) {
                case RESULT_REQUEST_CODE:
                    if (data == null) return;
                    String type = data.getStringExtra(Constant.EXTRA_RESULT_CODE_TYPE);
                    String content = data.getStringExtra(Constant.EXTRA_RESULT_CONTENT);
                    Toast.makeText(MainActivity.this,"codeType:" + type
                            + "-----content:" + content,Toast.LENGTH_SHORT).show();
                    break;
                default:
                    break;

            }
        }
        super.onActivityResult(requestCode, resultCode, data);
    }

优化问题

基于zxing的二维码扫码可能会出现扫码速率比较低的问题。这里我所用的几点解决方法。 1.zxing源码是截取的扫码聚焦框里面的图像数据信息来解码,这里可以改成获取全屏的图像信息。实现代码如下:

public PlanarYUVLuminanceSource buildLuminanceSource(byte[] data, int width, int height) {
        return new PlanarYUVLuminanceSource(data, width, height, 0, 0,
                width, height, false);
    }

2.尽量减少支持的扫码类型。zxing源码默认是支持所有的扫码类型。我们项目中使用的话,一般不需要支持这么多。仅支持BarcodeFormat.QR_CODE(二维码)、BarcodeFormat.CODE_128(一维码)就可以应对很多场景了。
3.添加 hints.put(DecodeHintType.TRY_HARDER, true);语句,能够提高扫码精确度,准确率。
这三点是我在使用的,并且取得很大的效果的方法。还有一些提高的扫码速率的方法我就不细说了,这里推荐一篇文章写的蛮好的。
扫码优化策略

总结

在看源码的过程中,别想着一下能看明白,得慢慢看慢慢琢磨,实在想不明白的地方,就别去纠结了,过段时间再去看你当时迷惑的地方,可能就会想明白了。最后附上项目的地址,觉得还不错就start下吧(^__^) 。
YZxing项目地址