您的位置:首页 > 移动开发 > Android开发

Android二维码扫一扫功能实现,解析Zxing源码的执行过程

2017-11-27 00:00 756 查看


今日科技快讯

日前,第五届中国移动合作伙伴大会在广州召开,5G商用的步伐日益临近,随着政策支持与企业的境外布局,我国5G产业建设已走在世界前列,有望成为全球5G领跑者,国内部分5G研发企业已跻身全球第一梯队。在大会上,5G研发应用的最新成果亮相。首批国内5G高频基站功放、射频前端器件,以及业界首款5G终端射频器件模组原型产品等。

作者简介

各位小伙伴们大家好,新的一周又开始了,希望大家都能有个好心情迎接新的一周。
本篇来自 宇宝守护神[b][b][b][b][b][b][b][b][b][b] [/b][/b][/b][/b][/b][/b][/b][/b][/b][/b] 的投稿,分享了
Android 中基于 zxing 的扫码功能实现,希望大家喜欢!

[b][b]宇宝守护神[/b] [/b][b][b][b][b][b][b][b][b][b][b] [/b][/b][/b][/b][/b][/b][/b][/b][/b][/b]的博客地址:
http://my.csdn.net/qq_34902522
开始

建议阅读本文的同学,结合 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<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 的配置,画面的预览实现,每一帧画面的数据请求,传递,解码逻辑实现。最后根据这一帧画面数据扫码结果 是成功还是失败发送,来决定是继续请求下一帧的画面信息还是处理扫码成功的结果。

 YZxing-lib

在观察 CaptureActivity 的时候,我们发现了一个自定义控件,叫做 ViewfinderVIew ,通过阅读其代码,发现这就是绘制扫码框样式的地方。那我们在修改 zxing 库的时候就可以重写这个类,来实现对扫码框样式的修改。
YZxing-lib 这个库,是我基于 zxing 库修改的扫码库,去除了原来 Zxing 库中多余的部分,并对扫码效率进行了优化。我们先来看一下 YZxing 库的实现效果:



演示效果图,弹窗逻辑已删除:



扫码成功后,结果的回调:



微信的扫一扫,它聚焦框内有一条不断从上到下移动的绿线,我这边没做成他那样(比较懒),我这边实现的效果是跟 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 的二维码扫码可能会出现扫码速率比较低的问题。这里我所用的几点解决方法:

zxing 源码是截取的扫码聚焦框里面的图像数据信息来解码,这里可以改成获取全屏的图像信息。实现代码如下:

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


尽量减少支持的扫码类型。zxing 源码默认是支持所有的扫码类型。我们项目中使用的话,一般不需要支持这么多。仅支持 BarcodeFormat.QR_CODE(二维码)、BarcodeFormat.CODE_128(一维码)就可以应对很多场景了。 

添加 hints.put(DecodeHintType.TRY_HARDER, true);语句,能够提高扫码精确度,准确率。 

这三点是我在使用的,并且取得很大的效果的方法。还有一些提高的扫码速率的方法我就不细说了,这里推荐一篇文章写的蛮好的。 
扫码优化策略:
http://iluhcm.com/2016/01/08/scan-qr-code-and-recognize-it-from-picture-fastly-using-zxing/
总结

在看源码的过程中,别想着一下能看明白,得慢慢看慢慢琢磨,实在想不明白的地方,就别去纠结了,过段时间再去看你当时迷惑的地方,可能就会想明白了。最后附上项目的地址,觉得还不错就start下吧(^__^) 。 
YZxing项目地址:
https://github.com/MRYangY/YZxing
欢迎长按下图 -> 识别图中二维码

或者 扫一扫 关注我的公众号



内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: