赞
踩
鸿蒙中目前选用开源三方库Zxing进行二次封装开发来完成二维码扫描和生成,Zxing目前已经相当的成熟和稳定,是纯Java库,所以可以直接在鸿蒙工程中引用.
首先简单的概括一下二维码扫描需要的准备工作:
(1) 引入Zxing三方库或Zxing.jar包,推荐gradle引入方式。
api ‘com.google.zxing:core:3.4.0’
(2) 自定义二维码扫描视觉框,绘制黑色边界,聚焦框可以方便用户对准二维码,动态扫描移动线可提升用户体验。
(3) Camera封装,摄像头捕捉到二维码图片。
(4) 根据Zxing提供的api,对摄像头捕捉到的图片进行编码,解码。
下面详解准备工作和拍照后的流程:
简述视觉框几个开发流程,绘制黑色边界,绘制边框四角,绘制移动线,然后不断重绘让移动线移动。
绘制黑色边界,绘制边框四角都使用canvas.drawRect进行绘制。
绘制移动线canvas.drawPixelMapHolderRect进行绘制移动线设置与顶部的距离,每重绘一次,与顶部距离缩短一点,然后使用Eventhandler设置定时不断重绘后移动线视觉上便是动画。
该开发需要在ability或slice中进行,因为需要SurfaceProvider来支持,该类相当于之前使用的SurfaceView为打开摄像头提供了一个预览视窗。
简述基本开发流程,首先实例一个SurfaceProvider,再做一些基本设置
private SurfaceProvider surfaceProvider; surfaceProvider = new SurfaceProvider(this); surfaceProvider.pinToZTop(false); surfaceProvider.getSurfaceOps().get().addCallback(new SurfaceCallBack()); class SurfaceCallBack implements SurfaceOps.Callback { @Override public void surfaceCreated(SurfaceOps callbackSurfaceOps) { if (callbackSurfaceOps != null) { callbackSurfaceOps.setFixedSize(SCREEN_WIDTH,SCREEN_HEIGHT ); } openCamera(); } @Override public void surfaceChanged(SurfaceOps callbackSurfaceOps, int format, int width, int height) {} @Override public void surfaceDestroyed(SurfaceOps callbackSurfaceOps) {} }
这里要重点谈一下SurfaceCallBack,因为这里会存在扫描二维码时图像拉伸的问题。
在拍照预览页面,预览照片的拉伸问题主要与下面两个因素有关:
SurfaceProvider的大小和 Camera中的Preview的大小
手机camera的尺寸大小为25601920(横屏,比例为:1.333)预览尺寸大小为640480(横屏,比例为1.333)
手机SurfaceProvider大小为1280720(横屏,比例为:1.777)预览尺寸大小为960720(横屏,比例为1.777)
SurfaceProvider的宽高比例跟camera preview的宽高比例不一样才会导致打开camera后,照相机会出现拉伸的情况。
解决的方法就是计算SurfaceProvider尺寸比例跟camera预览尺寸比例相同。
方案一:简单设置
在SurfaceCallBack中设callbackSurfaceOps.setFixedSize(height,width);
这里宽高与手机预览尺寸的宽高是反向比例设置的,这样就能大幅改善拉伸问题。
方案二:精确计算
可以大幅度减少误差,更精确的处理比例不匹配的问题。
callbackSurfaceOps.setFixedSize (size.width,size.height) private Size getOptimalSize(CameraKit cameraKit, String camId, int screenWidth, int screenHeight) { List<Size> sizes = cameraKit.getCameraAbility(camId).getSupportedSizes(ImageFormat.YUV420_888); final double ASPECT_TOLERANCE = 0.1; //竖屏screenHeight/screenWidth,横屏是screenWidth/screenHeight double targetRatio = (double) screenHeight / screenWidth; Size optimalSize = null; double minDiff = Double.MAX_VALUE; int targetHeight = screenWidth; for (Size size : sizes) { double ratio = (double) size.width / size.height; if (Math.abs(ratio - targetHeight) < minDiff) { optimalSize = size; minDiff = Math.abs(size.height - targetHeight); } } if (optimalSize == null) { minDiff = Double.MAX_VALUE; for (Size size : sizes) { if (Math.abs(size.height - targetHeight) < minDiff) { optimalSize = size; minDiff = Math.abs(size.height - targetHeight); } } } return optimalSize; }
在有横盘扫描需要的时候,又需要重新调换回来,有横屏扫描需求这里要做一下横竖屏判断。
其次,需要在SurfaceCallBack的surfaceCreated方法中初始化ImageReceiver ,CameraKit。
ImageReceiver主要用于最后回调方法中接收摄像头捕捉到的帧图。
imageReceiver = ImageReceiver.create(SCREEN_WIDTH, SCREEN_HEIGHT, ImageFormat.JPEG, IMAGE_RCV_CAPACITY);
imageReceiver.setImageArrivalListener(this);
这里也需要设置屏幕的宽高值,有横屏二维码扫描需求的也是需要判断横竖屏进行设置宽高。否则,横屏扫描二维码时会扫描不出来。
imageReceiver监听回调,就可以拿到摄像头捕捉到的图片。
@Override public void onImageArrival(ImageReceiver imageReceiver) { try { Image image = imageReceiver.readLatestImage(); if (image == null) { return; } if (isAnalyze) { image.release(); return; } isAnalyze = true; Image.Component component = image.getComponent(ImageFormat.ComponentType.JPEG); byte[] bytes = new byte[component.remaining()]; ByteBuffer buffer = component.getBuffer(); buffer.get(bytes); ImageSource imageSource = ImageSource.create(bytes, new ImageSource.SourceOptions()); ImageSource.DecodingOptions options = new ImageSource.DecodingOptions(); options.rotateDegrees = 90f; options.desiredRegion = new Rect( viewfinderView.getFramingRect().top, viewfinderView.getFramingRect().left, viewfinderView.getFramingRect().getHeight(), viewfinderView.getFramingRect().getWidth()); PixelMap map = imageSource.createPixelmap(options); saveImage(bytes); String result = CodeUtils.parseInfoFromBitmap(map); Log.debug("result=" + result); if (result != null) { showMap(map, result); return; } image.release(); isAnalyze = false; } catch (Exception e) { Log.debug(e.getMessage()); } }
再通过我封装的方法CodeUtils.parseInfoFromBitmap(map);对图片进行解码,即可拿到二维码图片信息。
CameraKit主要开启摄像头,同时还可以根据不同需要在FrameConfig.Builder中配置。我们需要用循环帧方式捕获图片,因为单帧方式类似拍照会卡顿。
CameraKit cameraKit = CameraKit.getInstance(getApplicationContext());
String[] cameraList = cameraKit.getCameraIds();
String cameraId = null;
for (String id : cameraList) {
if (cameraKit.getCameraInfo(id).getFacingType() == CameraInfo.FacingType.CAMERA_FACING_BACK) {
cameraId = id;
}
}
if (cameraId == null) {
return;
}
CameraStateCallbackImpl cameraStateCallback = new CameraStateCallbackImpl();
cameraKit.createCamera(cameraId, cameraStateCallback, creamEventHandler);
其中一些摄像头的配置需要在FrameConfig.Builder中进行配置,启动循环帧捕获。
class CameraStateCallbackImpl extends CameraStateCallback { CameraStateCallbackImpl() {} @Override public void onCreated(Camera camera) { Log.debug("create camera onCreated"); Log.debug("surfaceProvider==null =" + (surfaceProvider)); if (surfaceProvider == null) { return; } previewSurface = surfaceProvider.getSurfaceOps().get().getSurface(); if (previewSurface == null) { Log.debug("create camera filed, preview surface is null"); return; } // Wait until the preview surface is created. try { Thread.sleep(200); } catch (InterruptedException e) { Log.debug("Waiting to be interrupted"); } CameraConfig.Builder cameraConfigBuilder = camera.getCameraConfigBuilder(); cameraConfigBuilder.addSurface(previewSurface); cameraConfigBuilder.addSurface(imageReceiver.getRecevingSurface()); camera.configure(cameraConfigBuilder.build()); cameraDevice = camera; framePreviewConfigBuilder = camera.getFrameConfigBuilder(FRAME_CONFIG_PREVIEW); framePreviewConfigBuilder.addSurface(imageReceiver.getRecevingSurface()); framePreviewConfigBuilder.addSurface(previewSurface); camera.triggerLoopingCapture(framePreviewConfigBuilder.build()); } @Override public void onConfigured(Camera camera) { Log.debug("onConfigured...."); framePreviewConfigBuilder.setFlashMode(FLASH_OPEN); framePreviewConfigBuilder.addSurface(previewSurface); camera.triggerLoopingCapture(framePreviewConfigBuilder.build()); } } @Override protected void onStop() { super.onStop(); if (cameraDevice != null) { framePreviewConfigBuilder = null; try { cameraDevice.release(); cameraDevice = null; surfaceProvider.clearFocus(); surfaceProvider.removeFromWindow(); surfaceProvider = null; } catch (Exception e) { Log.debug( e.getMessage()); } } }
初始化工作的配置好之后,先将SurfaceProvider添加到界面组件中,然后再把自定义视图框,添加到组件顶层即可。打开界面开始扫描二维码,从imageReceiver接口中拿到的图片信息,使用Zxing提供的方法进行解码后即可拿到需要的字符串信息。
下面简述一下如何解码。
解码是将扫描二维码图片解码为字符串信息。
对摄像头捕捉到的图片帧转成PixelMap,然后用PixelMap使用Zxing提供的接口LuminanceSource进行像素数组采集。
public class BitmapLuminanceSource extends LuminanceSource { private byte[] bitmapPixels; public BitmapLuminanceSource(PixelMap pixelMap) { super(pixelMap.getImageInfo().size.width, pixelMap.getImageInfo().size.height); // 首先,要取得该图片的像素数组内容 int[] data = new int[getWidth() * getHeight()]; this.bitmapPixels = new byte[getWidth() * getHeight()]; pixelMap.readPixels(data, 0, getWidth(), new Rect(0, 0, getWidth(), getHeight())); // 将int数组转换为byte数组,也就是取像素值中蓝色值部分作为辨析内容 for (int i = 0; i < data.length; i++) { this.bitmapPixels[i] = (byte) data[i]; } } @Override public byte[] getMatrix() { // 返回我们生成好的像素数据 return bitmapPixels; } @Override public byte[] getRow(int y, byte[] row) { // 这里要得到指定行的像素数据 System.arraycopy(bitmapPixels, y * getWidth(), row, 0, getWidth()); return row; } }
然后将像素数据交给zxing提供的分析算法,HybridBinarizer和GlobalHistogramBinarizer算法。
new HybridBinarizer(new BitmapLuminanceSource(pixelMap))
或
new GlobalHistogramBinarizer(new BitmapLuminanceSource(pixelMap))
zxing官方默认的HybridBinarizer,两者区别HybridBinarizer算法执行效率上要慢一些,但是更有效,专门针对黑白相间的图像设计,也更适用于有阴影和渐变的二维码图像。GlobalHistogramBinarizer算法适用于低端机,对手机要求不高,识别速度快,精度高,但是无法处理阴影和渐变两个情况。
默认使用HybridBinarizer算法解析数据源。
通过算法得到的数据,使用BinaryBitmap构造二值图像比特流。
BinaryBitmap binaryBitmap = new BinaryBitmap(new HybridBinarizer(new BitmapLuminanceSource(pixelMap)));
然后使用MultiFormatReader来解析图像,MultiFormatReader可以解析很多种数据格式,因此还要对它进行参数设置。
MultiFormatReader multiFormatReader = new MultiFormatReader(); // 解码的参数 Hashtable<DecodeHintType, Object> hints = new Hashtable<DecodeHintType, Object>(3); // 可以解析的编码类型 Vector<BarcodeFormat> decodeFormats = new Vector<BarcodeFormat>(); if (decodeFormats.isEmpty()) { decodeFormats = new Vector<BarcodeFormat>(); // 这里设置可扫描的类型,我这里选择了都支持 decodeFormats.addAll(DecodeFormatManager.ONE_D_FORMATS); decodeFormats.addAll(DecodeFormatManager.QR_CODE_FORMATS); decodeFormats.addAll(DecodeFormatManager.DATA_MATRIX_FORMATS); } hints.put(DecodeHintType.POSSIBLE_FORMATS, decodeFormats); // 设置继续的字符编码格式为UTF8 hints.put(DecodeHintType.CHARACTER_SET, "UTF8"); // 设置解析配置参数 multiFormatReader.setHints(hints);
最后参数设置好之后使用MultiFormatReader进行解析获取Result。
try {
BinaryBitmap binaryBitmap = new BinaryBitmap(new HybridBinarizer(new BitmapLuminanceSource(map)));
Result rawResult = multiFormatReader.decodeWithState(binaryBitmap);
if (rawResult != null) {
result = rawResult.getText();
}
} catch (Exception ignored) {
Log.error(ignored.getMessage());
}
最后的result就是最后二维码扫描解析后得到字符串信息。
这里贴上整个解析过程代码:
public static String parseInfoFromBitmap(PixelMap map) { String result = null; MultiFormatReader multiFormatReader = new MultiFormatReader(); // 解码的参数 Hashtable<DecodeHintType, Object> hints = new Hashtable<DecodeHintType, Object>(3); // 可以解析的编码类型 Vector<BarcodeFormat> decodeFormats = new Vector<BarcodeFormat>(); if (decodeFormats.isEmpty()) { decodeFormats = new Vector<BarcodeFormat>(); // 这里设置可扫描的类型,我这里选择了都支持 decodeFormats.addAll(DecodeFormatManager.ONE_D_FORMATS); decodeFormats.addAll(DecodeFormatManager.QR_CODE_FORMATS); decodeFormats.addAll(DecodeFormatManager.DATA_MATRIX_FORMATS); } hints.put(DecodeHintType.POSSIBLE_FORMATS, decodeFormats); // 设置继续的字符编码格式为UTF8 hints.put(DecodeHintType.CHARACTER_SET, "UTF8"); // 设置解析配置参数 multiFormatReader.setHints(hints); try { BinaryBitmap binaryBitmap = new BinaryBitmap(new HybridBinarizer(new BitmapLuminanceSource(map))); Result rawResult = multiFormatReader.decodeWithState(binaryBitmap); if (rawResult != null) { result = rawResult.getText(); } } catch (Exception ignored) { Log.error(ignored.getMessage()); } return result; } DecodeFormatManager解码类型的代码: public class DecodeFormatManager { private static final Pattern COMMA_PATTERN = Pattern.compile(","); private static final Vector<BarcodeFormat> PRODUCT_FORMATS; /** * one 格式 */ public static final Vector<BarcodeFormat> ONE_D_FORMATS; /** * qr 格式 */ public static final Vector<BarcodeFormat> QR_CODE_FORMATS; /** * data 格式 */ public static final Vector<BarcodeFormat> DATA_MATRIX_FORMATS; static { PRODUCT_FORMATS = new Vector<BarcodeFormat>(5); PRODUCT_FORMATS.add(BarcodeFormat.UPC_A); PRODUCT_FORMATS.add(BarcodeFormat.UPC_E); PRODUCT_FORMATS.add(BarcodeFormat.EAN_13); PRODUCT_FORMATS.add(BarcodeFormat.EAN_8); PRODUCT_FORMATS.add(BarcodeFormat.RSS_14); ONE_D_FORMATS = new Vector<BarcodeFormat>(PRODUCT_FORMATS.size() + 4); ONE_D_FORMATS.addAll(PRODUCT_FORMATS); ONE_D_FORMATS.add(BarcodeFormat.CODE_39); ONE_D_FORMATS.add(BarcodeFormat.CODE_93); ONE_D_FORMATS.add(BarcodeFormat.CODE_128); ONE_D_FORMATS.add(BarcodeFormat.ITF); QR_CODE_FORMATS = new Vector<BarcodeFormat>(1); QR_CODE_FORMATS.add(BarcodeFormat.QR_CODE); DATA_MATRIX_FORMATS = new Vector<BarcodeFormat>(1); DATA_MATRIX_FORMATS.add(BarcodeFormat.DATA_MATRIX); } private DecodeFormatManager() {} }
编码:编码是将字符串生成二维码,相当于解码一个逆向的过程。
核心点就是使用Zxing提供的MultiFormatWriter来创建比特矩阵。
BitMatrix matrix = new MultiFormatWriter().encode(str, BarcodeFormat.QR_CODE, widthAndHeight, widthAndHeight);
然后矩阵数据构造一个颜色数组。
int width = matrix.getWidth();
int height = matrix.getHeight();
int[] pixels = new int[width * height];
for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) {
if (matrix.get(x, y)) {
pixels[y * width + x] = BLACK;
}
}
}
PixelMap拿到颜色数组创建一个PixelMap图片。
贴上编码整个源码如下:
public static PixelMap createQRCode(String str, int widthAndHeight) throws WriterException { Hashtable<EncodeHintType, String> hints = new Hashtable<EncodeHintType, String>(); hints.put(EncodeHintType.CHARACTER_SET, "utf-8"); BitMatrix matrix = new MultiFormatWriter().encode(str, BarcodeFormat.QR_CODE, widthAndHeight, widthAndHeight); int width = matrix.getWidth(); int height = matrix.getHeight(); int[] pixels = new int[width * height]; for (int y = 0; y < height; y++) { for (int x = 0; x < width; x++) { if (matrix.get(x, y)) { pixels[y * width + x] = BLACK; } } } PixelMap.InitializationOptions options = new PixelMap.InitializationOptions(); options.size = new Size(width, height); return PixelMap.create(pixels, options); }
最后拿到PixelMap去显示就是一张二维码图片了。效果图如下:
整个流程简述就是通过手机摄像头一帧一帧的捕捉到对准的二维码图片,然后将图片使用封装好的解码方法进行解码,如果能够解码出字符串,即完成扫描,输出字符串信息。因为整个流程稍微有些复杂需要解释清楚,所以贴上的代码量还是挺大的,需要很耐心的看。整个封装集成流程,需要几个要注意的地方。
(1) 二维码扫描横竖屏的问题,需要判断横竖屏需要对imageReceiver初始化,SurfaceCallBack初始化的时候动态设置宽高比的问题。
(2) 在二维码扫描的时候图像会出现拉伸的情况,在初始化相机尺寸的时候分别对预览尺寸值和图片尺寸值都设定为比例最接近屏幕尺寸的尺寸值,需要在SurfaceCallBack中设置宽高比。这里仅仅是将屏幕宽和高的数值进行倒置强行设置,便大幅改善,如果需要更精准需要动态计算。
如需更多查看源码,请移步gitee开源项目:ohos-zblibrary项目下的二维码封装库qrcodelibrary。
https://gitee.com/openharmony-tpc/ohos-ZBLibrary
扫描成功拿到解析结果效果图如下:
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。