本篇文章已授权微信公众号 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);这个方法主要是的内容是寻找最好的预览尺寸。寻找最佳预览尺寸的逻辑我就不说了,这块,可以看下这位兄弟写的
http://iluhcm.com/2016/01/08/scan-qr-code-and-recognize-it-from-picture-fastly-using-zxing/ 里面说明了寻找最佳预览尺寸的逻辑,及优化。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 decodeFormats,                         Map 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 hints这个变量,这个变量是用来存储支持扫码类型的。然后我们看到decodeThread线程里面的run方法实现如下:

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

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

DecodeHandler(CaptureActivity activity, Map 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 decodeFormats,                         Map 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库的实现效果:



(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'(建议更新至2.1)  --->compile 'com.yangy:YZxing-lib:2.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);//        //设置是否启用从相册获取二维码(默认为FALSE,不启用)。//        intent.putExtra(Constant.EXTRA_IS_ENABLE_SCAN_FROM_PIC,true);//        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项目地址

补充

Android 基于Zxing的扫码功能实现(三)之实现从相册获取二维码


扫码加入我的个人微信公众号:Android开发圈 ,一起学习Android知识!!


扫码体验小程序:微捷径

更多相关文章

  1. “罗永浩抖音首秀”销售数据的可视化大屏是怎么做出来的呢?
  2. Nginx系列教程(三)| 一文带你读懂Nginx的负载均衡
  3. 不吹不黑!GitHub 上帮助人们学习编码的 12 个资源,错过血亏...
  4. Android(安卓)读取Manifest文件下Application 等节点下的meta-da
  5. Android(安卓)BroastCast的使用详解
  6. 【Android】使用Intent实现数据传递
  7. Android核心分析之一:分析方法论探讨之设计意图
  8. android 访问远程数据库与发送email问题
  9. Android面试题整理(selfmade)——坚持每天回答一个

随机推荐

  1. 《Android》Lesson17-用Fragment实现简易
  2. 每周总结20130821——android控件的尺寸
  3. android tabhost --android UI 学习
  4. Android(安卓)面试之开篇
  5. android中使用OpenCV之调用设备摄像头
  6. Android硬件之传感器
  7. 【Gradle】Android(安卓)Gradle 插件
  8. android中使用OpenCV之调用设备摄像头
  9. Android中RelativeLayout各个属性介绍
  10. android:elevation属性,控制View底部渐变