Android 详解使用 Zxing实现前置摄像头扫描二维码、生
本文同步到CSDN
现在二维码使用越来越广泛了,几乎处处可见,并且 公司相关的项目中几乎全部都和二维码扫描有关,所以总结一下自己的使用心路历程,总觉得要做点什么来记录自己的成长,让自己的成长有迹可循,如果恰好能够帮助到你,我当然会很开心啦,如果没帮到,请忽略。。
废话结束,正文开始
小白之旅,如有问题 望指正,万分感谢 🙏🙏
首先推荐几篇
Android中常用的就是 zxing ,开源项目地址:https://github.com/zxing/zxing。首先我们下载项目到本地,然后加载到自己的工程中,可参考 我的上一片博文 AndroidStudio 导入 Zxing Android 项目,这个是作为库文件导入的,当然我们也可以单独在工程中分出一个包,来实现扫描二维码的功能。
特别提醒: 如果作为 module 完整导入项目则配置好后就可以运行,如果单独作为一个包,独立出自己需要的内容,需要复制 layout 文件,和资源文件 res-->values 里面的 ids.xml 和 res-->raw 里面的 beep.ogg 文件
一、了解 ZXing
image.pngZXing 导入后,所有的内容如上图所示,运行示例代码,发现是横屏用来扫描条形码的,包括识别相册中的二维码,扫描记录,剪切板,生成二维码等功能,我们可以根据需要,分离出自己需要的那一部分,首先了解 ZXing 这个项目中各个部分的作用,然后开始 DELETE 👹操作。
把 CaptureActivity 作为入口开始分析.....
(PS:不一定正确,是我自己的理解,不过 大概是这样子的,如有失误,后续会修正)
CaptureActivity: 打开相机并在后台线程进行实际扫描,绘制取景框,并进行图像扫描反馈。
CameraManager:相机管理类,调用相机 预览,绘制扫描框的具体内容都在这里。相关的相机的配置也在这里设置,例如前后摄像头切换,是否自动聚焦等。在 CaptureActivity 中,CameraManager 在 onResume() 中获取对象,openDriver() 用来打开相机
CaptureActivityHandler: 处理所有的捕获的状态消息,在 initCamera()中,打开相机后,创建该对象,根据描述,应该是传递消息的, 把需要解码的内容 传递给 然后 把结果返回到 CaptureActivity
DecodeThread: 处理最困难图像解析工作,包括解析和生成二维码的内容,和 DecodeHandler 搭档
DecodeHandler: 把扫码结果返回给 CaptureActivityHandler。
ViewfinderView:自定义的扫描界面,如果想绘制自己想要的扫描效果,可以在这里动手👋
大致是这样子的,从 CaptureActivity 入手,一点点看,就会明白是怎么回事,写不明白的感觉。大家 也可以看 我这里实现的 DEMO , 里面相应的都给注释了下。Github:
二、修改UI, 修改扫描页面
大致的流程清楚后,首先来最简单的,把扫描的界面绘制成我们想要的样子.
首先,把屏幕方向改为竖屏,相应地 相机扫描方向也要旋转,
在 CameraManager 中 getFramingRectInPreview() 修改:
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;
然后,在 DecodeHandler 里面 decode() 中添加:
// 旋转摄像头扫描方向后 处理 可以扫描二维码 也可以扫描条形码
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;
data = rotatedData;
PlanarYUVLuminanceSource source = activity.getCameraManager().buildLuminanceSource(data, width, height);
我们想要的其实是一个正方形的扫描框,然后,调整 ViewfinderView 绘制扫描框预览界面,具体的可以在 ViewfinderView 的 onDraw() 方法中实现,
Rect frame = cameraManager.getFramingRect();
想要绘制正方形,getFramingRect() 获取的预览界面宽高,可以设置成一样的,均使用屏幕的宽来设置
int width = findDesiredDimensionInRange(screenResolution.x, MIN_FRAME_WIDTH, MAX_FRAME_WIDTH);
int height = findDesiredDimensionInRange(screenResolution.x, MIN_FRAME_WIDTH, MAX_FRAME_WIDTH);
// int height = findDesiredDimensionInRange(screenResolution.y, MIN_FRAME_HEIGHT, MAX_FRAME_HEIGHT);
绘制四个角
//画扫描框边上的角,总共8个部分
paint.setColor(getResources().getColor(R.color.result_view));
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);
//绘制中间的线,每次刷新界面,中间的线往下移动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);
//画扫描框下面的字
paint.setColor(getResources().getColor(R.color.white));
paint.setTextSize(TEXT_SIZE * density);
paint.setAlpha(225);
paint.setTypeface(Typeface.DEFAULT);
String text = getResources().getString(R.string.msg_default_status);
float textWidth = paint.measureText(text);
canvas.drawText(text, (width - textWidth) / 2, (float) (frame.bottom + (float) TEXT_PADDING_TOP * density), paint);
实现效果图:
image.png
三、实现扫一扫
UI 绘制好后,启动 CaptureActivity 调用扫一扫,CaptureActivity 中的 handleDecode() 处理扫描后的结果,把处理好的结果返回,
public void handleDecode(Result rawResult, Bitmap barcode, float scaleFactor) {
inactivityTimer.onActivity();
String result = rawResult.getText();
if (result.equals("")) {
Toast.makeText(CaptureActivity.this, "Scan Failed!", Toast.LENGTH_SHORT).show();
} else {
Log.e(TAG, "扫描的结果" + result);
// 把扫描结果返回到扫描的页面
Intent intent = new Intent();
Bundle bundle = new Bundle();
bundle.putString("result", result);
intent.putExtras(bundle);
setResult(RESULT_OK, intent);
}
CaptureActivity.this.finish();
}
四、添加切换前后摄像头
扫描二维码调用的相机是系统相机,如果手机本身支持前后摄像头的话(废话,现在还有不支持前置摄像头的手机吗,我要不能自拍的手机干嘛。。。),应该都没有问题,主要是切换下前后摄像头就可以了。所以呢,主要看调用相机部分,camera -> open 下 OpenCameraInterface 主要是用来处理相机相关的,所以看下,发现
while (cameraId < numCameras) {
Camera.CameraInfo cameraInfo = new Camera.CameraInfo();
Camera.getCameraInfo(cameraId, cameraInfo);
if (CameraFacing.values()[cameraInfo.facing] == CameraFacing.BACK) {
break;
}
cameraId++;
}
CameraFacing.BACK,当为后置摄像头时返回了当前相机,也就是,默认扫一扫仅仅支持后置扫一扫,这里我们改为支持前后摄像头,根据摄像头传递过来的参数进行修改,open() 打开相机的方法中添加一个参数,用来判断是前置摄像头还是后置摄像头
public static OpenCamera open(int cameraId, CameraFacing cf) {
int numCameras = Camera.getNumberOfCameras();
if (numCameras == 0) {
Log.w(TAG, "No cameras!");
return null;
}
if (cameraId >= numCameras) {
Log.w(TAG, "Requested camera does not exist: " + cameraId);
return null;
}
if (cameraId <= NO_REQUESTED_CAMERA) {
cameraId = 0;
while (cameraId < numCameras) {
Camera.CameraInfo cameraInfo = new Camera.CameraInfo();
Camera.getCameraInfo(cameraId, cameraInfo);
if(cf == CameraFacing.BACK){
if (CameraFacing.values()[cameraInfo.facing] == cf.BACK) {
break;
}
}
if(cf == CameraFacing.FRONT){
if (CameraFacing.values()[cameraInfo.facing] == cf.FRONT) {
break;
}
}
cameraId++;
}
if (cameraId == numCameras) {
Log.i(TAG, "No camera facing " + CameraFacing.BACK + "; returning camera #0");
cameraId = 0;
}
}
Log.i(TAG, "Opening camera #" + cameraId);
Camera.CameraInfo cameraInfo = new Camera.CameraInfo();
Camera.getCameraInfo(cameraId, cameraInfo);
Camera camera = Camera.open(cameraId);
if (camera == null) {
return null;
}
return new OpenCamera(cameraId,
camera,
CameraFacing.values()[cameraInfo.facing],
cameraInfo.orientation);
}
然后发现 其实调用 OpenCameraInterface
里面 open()
方法的是 CameraManager
的 openDriver()
,也就是说需要在 openDriver()
里面传递参数,再往外找,发现CaptureActivity
的initCamera()
是打开相机,设置参数的地方,所以在这里把设置前后摄像头的参数传递过去,
private void initCamera(SurfaceHolder surfaceHolder) {
if (surfaceHolder == null) {
throw new IllegalStateException("No SurfaceHolder provide");
}
if (cameraManager.isOpen()) {
Log.w(TAG, "initCamera() while already open -- late SurfaceView callback?");
// 如果相机已经打开 则关闭当前相机 重建一个 切换摄像头,,如果不需要切换前置摄像头 则这里直接return
handler = null;
cameraManager.closeDriver();
// return;
}
try {
cameraManager.openDriver(surfaceHolder, cfbf);
// 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();
}
}
这样就会调用相应的前置或后置摄像头。
BUT 修改后,发现 报错了,前后摄像头不能切换 直接卡死 !!
Error:
Unexpected exception while focusing
Camera is being used after Camera.release() was called
看报错信息大致是说,相机对象已经被释放了,但是还在使用,嗯,想想自己干了什么会这样,对,cameraManager.closeDriver();
,在切换前后摄像头时,当相机是打开的时候就先释放,重新创建一个对象,所以在释放的时候,不能继续使用相机,在closeDriver()
添加,相机释放之前,先停止预览。
camera.getCamera().setPreviewCallback(null);
camera.getCamera().lock();
stopPreview();
在capture.xml中添加一个组件,用来切换相机前后摄像头。
实现效果:
image.png
五、生成二维码
生成二维码部分的功能主要在QRCodeEncoder.java
里面,这里仅仅生成二维码,对于二维码内容的格式使用默认的,重写构造函数只传入我们需要的参数
public QRCodeEncoder(Context activity, int dimension, String contnt) {
this.activity = activity;
this.dimension = dimension; // 生成二维码图片的尺寸
this.contents = contnt; // 生成的二维码的内容
}
public Bitmap encodeAsBitmap() throws WriterException {
Log.e("二维码图片参数",String.valueOf(dimension));
String contentsToEncode = contents;
if (contentsToEncode == null) {
return null;
}
Map<EncodeHintType,Object> hints = new HashMap<>();
// String encoding = guessAppropriateEncoding(contentsToEncode);
// if (encoding != null) {
// hints = new EnumMap<>(EncodeHintType.class);
// hints.put(EncodeHintType.CHARACTER_SET, encoding);
// }
hints.put(EncodeHintType.CHARACTER_SET, "UTF-8");
BitMatrix result;
try {
format = BarcodeFormat.QR_CODE;
Log.e(TAG,"contentsToEncode == " + contentsToEncode);
result = new MultiFormatWriter().encode(contentsToEncode, format, dimension, dimension, hints);
int width = result.getWidth();
int height = result.getHeight();
int[] pixels = new int[width * height];
for (int y = 0; y < height; y++) {
int offset = y * width;
for (int x = 0; x < width; x++) {
pixels[offset + x] = result.get(x, y) ? BLACK : WHITE;
}
}
Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
bitmap.setPixels(pixels, 0, width, 0, 0, width, height);
return bitmap;
} catch (IllegalArgumentException iae) {
Log.e(TAG,"Error == " + iae.toString());
// Unsupported format
return null;
}
}
调用encodeAsBitmap(),就可以生成Bitmap对象。
实现效果:输入内容 点击生成二维码即可生成二维码
image.png
这里仅仅把zxing项目中自己需要的功能提取出来,主要还是要有耐心,看不懂就多看几遍对我来说还是有效果的啊,哈哈哈,当然,自己也去找了很多的资料帮助自己理解,感谢你们。
惯例,最后送给自己一句话:凡事往简单处想,往认真处行