Android二维码识别与生成
最近几年二维码是越来越火了,特别是随着移动端的便利性,走到哪里都是扫一扫。二维码支付、二维码扫描登录、二维码扫描关注加好友.....越来越多的应用也都加上了二维码扫描的功能,作为移动开发者,对这些新奇的东西当然要尝试一下了。在查阅了一下网上的资料后,自己算是对二维码的扫描和生生成有了个初步的了解,写个笔记,以后想集成进项目时,也会方便很多。
首先,什么是二维码?
“二维条码/二维码(2-dimensional bar code)是用某种特定的几何图形按一定规律在平面(二维方向上)分布的黑白相间的图形记录数据符号信息的;在代码编制上巧妙地利用构成计算机内部逻辑基础的“0”、“1”比特流的概念,使用若干个与二进制相对应的几何形体来表示文字数值信息,通过图象输入设备或光电扫描设备自动识读以实现信息自动处理:它具有条码技术的一些共性:每种码制有其特定的字符集;每个字符占有一定的宽度;具有一定的校验功能等。同时还具有对不同行的信息自动识别功能、及处理图形旋转变化点。 ”
百科的解释如此。既然有二维码,那么肯定有一维码。相信你们对一维码认识的更早,因为一维码。最为常见的就是各种包装商品或者书籍后面的条码。当然本项目也是支持一维码的识别的。
二维码在不同的语言和平台下有不同的支持库,Android当然是选择java支持库了,这个项目是google的大神开发的ZXing库。github地址是:https://github.com/zxing/zxing。
相信大家都和我一样,下载后,都在考虑那怎么样集成进去项目里试一试了?因为这个库文件还是很大的。我们选择自己想要的即可。
首先你得找到zxing.jar包,并添加到项目库中,其次你需要这几个包和里面的文件,包名我改了点。然后将对应的资源也引进去,注意其是放在values文件夹下的。
其中carmera包,是对相机的初始化和一些配置处理;decoding是对相机扫描回的结果的分析处理;view是扫描框和接口处理回调。
项目里还有其他2个包,里面结构如下:
其中MainActivity是程序主入口,CustomScanViewActivity是自定义扫描窗口的FragmentActivity,CaptureFragment是自定义扫描窗口的view,在这进行相机的初始化,和处理扫描结果。扫描成功后将扫描的结果和二维码当作参数传递到handleDecode方法里进行处理,这里采用的是接口回调的方式,进行处理:
- /**
- * 处理扫描结果
- *
- * @param result
- * @param barcode
- */
- public void handleDecode(Result result, Bitmap barcode) {
- inactivityTimer.onActivity();
- playBeepSoundAndVibrate();
- if (result == null || TextUtils.isEmpty(result.getText())) {
- if (analyzeCallback != null) {
- analyzeCallback.onAnalyzeFailed();
- }
- } else {
- if (analyzeCallback != null) {
- analyzeCallback.onAnalyzeSuccess(barcode, result.getText());
- }
- }
- }
接口函数
- /**
- * 解析二维码结果接口函数
- */
- public interface AnalyticCallback{
- public void onAnalyzeSuccess(Bitmap mBitmap, String result);
- public void onAnalyzeFailed();
- }
- /**
- * 解析二维码图片方法
- * @param mBitmap
- * @param analyzeCallback
- */
- public static void analyticBitmap(Bitmap mBitmap, AnalyticCallback analyzeCallback) {
- MultiFormatReader multiFormatReader = new MultiFormatReader();
- // 解码的参数
- Hashtable<DecodeHintType, Object> hints = new Hashtable<DecodeHintType, Object>(2);
- // 可以解析的编码类型
- Vector<BarcodeFormat> decodeFormats = new Vector<BarcodeFormat>();
- if (decodeFormats == null || 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);
- // 设置解析配置参数
- multiFormatReader.setHints(hints);
- // 开始对图像资源解码
- Result rawResult = null;
- try {
- int width = mBitmap.getWidth();
- int height = mBitmap.getHeight();
- int[] pixels = new int[width * height];
- mBitmap.getPixels(pixels, 0, width, 0, 0, width, height);
- RGBLuminanceSource source = new RGBLuminanceSource(width, height, pixels);
- rawResult = multiFormatReader.decode(new BinaryBitmap(new HybridBinarizer(source)));
- } catch (Exception e) {
- e.printStackTrace();
- }
- if (rawResult != null) {
- if (analyzeCallback != null) {
- analyzeCallback.onAnalyzeSuccess(mBitmap, rawResult.getText());
- }
- } else {
- if (analyzeCallback != null) {
- analyzeCallback.onAnalyzeFailed();
- }
- }
- }
由上面可以看出,我把一维码也添加进去了,也就支持扫条形码了。当然还可以扫描本地的二维码图片,首先我们要打开本地的图库,这里有2个方法,但是有个方法有点小bug,他打不开我自己的截图,因为他打开的是手机里的图库(相册)应用,而截图不在图库里,所以不能添加(我华为荣耀手机是这样的,不知道其他型号手机是否是这样),为此我采用了另外一个方法,列出系统图片文件夹表,让用户自己选择。
方法一:
- Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
- intent.addCategory(Intent.CATEGORY_OPENABLE);
- intent.setType("image/*");//打开的是相册
- startActivityForResult(intent, REQUEST_IMAGE);
- Uri uri = data.getData();
- ContentResolver mContentResolver= getContentResolver();
- Bitmap mBitmap = MediaStore.Images.Media.getBitmap(mContentResolver, uri);//根据给定的图片uri,将其转换为bitmap
- QRCodeUtils.analyticBitmap(mBitmap, new QRCodeUtils.AnalyticCallback() {
- @Override
- public void onAnalyzeSuccess(Bitmap mBitmap, String result) {
- Toast.makeText(MainActivity.this, "解析结果: " + result,1).show();
- }
- @Override
- public void onAnalyzeFailed() {
- Toast.makeText(MainActivity.this, "解析图片失败", 1).show();
- }
- });
- Intent intent = new Intent(Intent.ACTION_PICK,
- android.provider.MediaStore.Images.Media.EXTERNAL_CONTENT_URI);//打开的是所有图片文件夹列表
- startActivityForResult(intent, REQUEST_IMAGE);
- Cursor cursor = getContentResolver().query(data.getData(), null, null, null, null);
- if (cursor.moveToFirst()) {
- photo_path = cursor.getString(cursor.getColumnIndex(MediaStore.Images.Media.DATA));
- Log.e("图片路径-----》", photo_path);
- }
- cursor.close();
- Bitmap mBitmap= getDecodeAbleBitmap(photo_path);
- private static Bitmap getDecodeAbleBitmap(String picturePath) {
- try {
- BitmapFactory.Options options = new BitmapFactory.Options();
- options.inJustDecodeBounds = true;
- BitmapFactory.decodeFile(picturePath, options);
- int sampleSize = options.outHeight / 400;
- if (sampleSize <= 0)
- sampleSize = 1;
- options.inSampleSize = sampleSize;
- options.inJustDecodeBounds = false;
- return BitmapFactory.decodeFile(picturePath, options);
- } catch (Exception e) {
- return null;
- }
- }
得到bitmap对象后,接下来的步骤就一样了,调用工具类进行解析。
如果对扫描框想更改大小、刷新速度或者外观的,可以在viewfinderView中进行修改
- /*
- * Copyright (C) 2008 ZXing authors
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
- package com.example.zxing.view;
- import android.content.Context;
- import android.content.res.Resources;
- import android.content.res.TypedArray;
- import android.graphics.Bitmap;
- import android.graphics.BitmapFactory;
- import android.graphics.Canvas;
- import android.graphics.Color;
- import android.graphics.Paint;
- import android.graphics.PixelFormat;
- import android.graphics.Point;
- import android.graphics.Rect;
- import android.graphics.drawable.Drawable;
- import android.util.AttributeSet;
- import android.util.Log;
- import android.view.View;
- import com.example.qrcodedemo.R;
- import com.google.zxing.ResultPoint;
- import com.example.zxing.camera.CameraManager;
- import java.util.Collection;
- import java.util.HashSet;
- /**
- * 自定义组件实现,扫描功能
- */
- public final class ViewfinderView extends View {
- /**
- * 刷新界面的时间
- */
- private static final long ANIMATION_DELAY = 10L;
- private static final int OPAQUE = 0xFF;
- private final Paint paint;
- private Bitmap resultBitmap;
- private final int maskColor;
- private final int resultColor;
- private final int resultPointColor;
- private Collection<ResultPoint> possibleResultPoints;
- private Collection<ResultPoint> lastPossibleResultPoints;
- // 扫描框边角颜色
- private int innercornercolor;
- // 扫描框边角长度
- private int innercornerlength;
- // 扫描框边角宽度
- private int innercornerwidth;
- // 扫描线移动的y
- private int scanLineTop;
- // 扫描线移动速度
- private int SCAN_VELOCITY;
- // 扫描线
- Bitmap scanLight;
- public ViewfinderView(Context context, AttributeSet attrs) {
- super(context, attrs);
- paint = new Paint();
- Resources resources = getResources();
- maskColor = resources.getColor(R.color.viewfinder_mask);
- resultColor = resources.getColor(R.color.result_view);
- resultPointColor = resources.getColor(R.color.possible_result_points);
- possibleResultPoints = new HashSet<ResultPoint>(5);
- scanLight = BitmapFactory.decodeResource(resources,
- R.drawable.scan_light);//扫描线
- initInnerRect(context, attrs);
- }
- /**
- * 初始化内部框的大小
- *
- * @param context
- * @param attrs
- */
- private void initInnerRect(Context context, AttributeSet attrs) {
- TypedArray ta = context.obtainStyledAttributes(attrs,
- R.styleable.innerrect);
- // 扫描框距离顶部
- int innerMarginTop = ta.getInt(R.styleable.innerrect_inner_margintop,
- -1);
- if (innerMarginTop != -1) {
- CameraManager.FRAME_MARGINTOP = dip2px(context, innerMarginTop);
- }
- // 扫描框的宽度
- int innerrectWidth = ta.getInt(R.styleable.innerrect_inner_width, 210);
- CameraManager.FRAME_WIDTH = dip2px(context, innerrectWidth);
- // 扫描框的高度
- int innerrectHeight = ta
- .getInt(R.styleable.innerrect_inner_height, 210);
- CameraManager.FRAME_HEIGHT = dip2px(context, innerrectHeight);
- // 扫描框边角颜色
- innercornercolor = ta.getColor(
- R.styleable.innerrect_inner_corner_color,
- Color.parseColor("#45DDDD"));
- // 扫描框边角长度
- innercornerlength = ta.getInt(
- R.styleable.innerrect_inner_corner_length, 65);
- // 扫描框边角宽度
- innercornerwidth = ta.getInt(R.styleable.innerrect_inner_corner_width,
- 15);
- // 扫描bitmap
- Drawable drawable = ta
- .getDrawable(R.styleable.innerrect_inner_scan_bitmap);
- if (drawable != null) {
- }
- // 扫描控件
- scanLight = BitmapFactory.decodeResource(getResources(), ta
- .getResourceId(R.styleable.innerrect_inner_scan_bitmap,
- R.drawable.scan_light));
- // 扫描速度
- SCAN_VELOCITY = ta.getInt(R.styleable.innerrect_inner_scan_speed, 10);
- ta.recycle();
- }
- @Override
- public void onDraw(Canvas canvas) {
- Rect frame = CameraManager.get().getFramingRect();
- if (frame == null) {
- return;
- }
- int width = canvas.getWidth();
- int height = canvas.getHeight();
- // Draw the exterior (i.e. outside the framing rect) darkened
- paint.setColor(resultBitmap != null ? resultColor : maskColor);
- canvas.drawRect(0, 0, width, frame.top, paint);
- canvas.drawRect(0, frame.top, frame.left, frame.bottom + 1, paint);
- canvas.drawRect(frame.right + 1, frame.top, width, frame.bottom + 1,
- paint);
- canvas.drawRect(0, frame.bottom + 1, width, height, paint);
- if (resultBitmap != null) {
- // Draw the opaque result bitmap over the scanning rectangle
- paint.setAlpha(OPAQUE);
- canvas.drawBitmap(resultBitmap, frame.left, frame.top, paint);
- } else {
- drawFrameBounds(canvas, frame);
- drawScanLight(canvas, frame);
- Collection<ResultPoint> currentPossible = possibleResultPoints;
- Collection<ResultPoint> currentLast = lastPossibleResultPoints;
- if (currentPossible.isEmpty()) {
- lastPossibleResultPoints = null;
- } else {
- possibleResultPoints = new HashSet<ResultPoint>(5);
- lastPossibleResultPoints = currentPossible;
- paint.setAlpha(OPAQUE);
- paint.setColor(resultPointColor);
- for (ResultPoint point : currentPossible) {
- canvas.drawCircle(frame.left + point.getX(), frame.top
- + point.getY(), 6.0f, paint);
- }
- }
- if (currentLast != null) {
- paint.setAlpha(OPAQUE / 2);
- paint.setColor(resultPointColor);
- for (ResultPoint point : currentLast) {
- canvas.drawCircle(frame.left + point.getX(), frame.top
- + point.getY(), 3.0f, paint);
- }
- }
- postInvalidateDelayed(ANIMATION_DELAY, frame.left, frame.top,
- frame.right, frame.bottom);
- }
- }
- /**
- * 绘制移动扫描线
- *
- * @param canvas
- * @param frame
- */
- private void drawScanLight(Canvas canvas, Rect frame) {
- if (scanLineTop == 0) {
- scanLineTop = frame.top;
- }
- if (scanLineTop >= frame.bottom - 30) {
- scanLineTop = frame.top;
- } else {
- scanLineTop += SCAN_VELOCITY;
- }
- Rect scanRect = new Rect(frame.left, scanLineTop, frame.right,
- scanLineTop + 30);
- canvas.drawBitmap(scanLight, null, scanRect, paint);
- }
- /**
- * 绘制取景框边框
- *
- * @param canvas
- * @param frame
- */
- private void drawFrameBounds(Canvas canvas, Rect frame) {
- paint.setColor(innercornercolor);
- paint.setStyle(Paint.Style.FILL);
- int corWidth = innercornerwidth;
- int corLength = innercornerlength;
- // 左上角
- canvas.drawRect(frame.left, frame.top, frame.left + corWidth, frame.top
- + corLength, paint);
- canvas.drawRect(frame.left, frame.top, frame.left + corLength,
- frame.top + corWidth, paint);
- // 右上角
- canvas.drawRect(frame.right - corWidth, frame.top, frame.right,
- frame.top + corLength, paint);
- canvas.drawRect(frame.right - corLength, frame.top, frame.right,
- frame.top + corWidth, paint);
- // 左下角
- canvas.drawRect(frame.left, frame.bottom - corLength, frame.left
- + corWidth, frame.bottom, paint);
- canvas.drawRect(frame.left, frame.bottom - corWidth, frame.left
- + corLength, frame.bottom, paint);
- // 右下角
- canvas.drawRect(frame.right - corWidth, frame.bottom - corLength,
- frame.right, frame.bottom, paint);
- canvas.drawRect(frame.right - corLength, frame.bottom - corWidth,
- frame.right, frame.bottom, paint);
- }
- public void drawViewfinder() {
- resultBitmap = null;
- invalidate();
- }
- public void addPossibleResultPoint(ResultPoint point) {
- possibleResultPoints.add(point);
- }
- /**
- * 根据手机的分辨率从 dp 的单位 转成为 px(像素)
- */
- public static int dip2px(Context context, float dpValue) {
- final float scale = context.getResources().getDisplayMetrics().density;
- return (int) (dpValue * scale + 0.5f);
- }
- }
至此,扫描二维码和解析本地图片二维码已介绍完了,接下来是如何生成二维码的介绍。
生成二维码采用的是这个方法
- BitMatrix bitMatrix = new QRCodeWriter().encode(text, BarcodeFormat.QR_CODE, width, height, hints);
- int[] pixels = new int[width * height];
- //下面这里按照二维码的算法,逐个生成二维码的图片,
- //两个for循环是图片横列扫描的结果
- for (int y = 0; y < height; y++)
- {
- for (int x = 0; x < width; x++)
- {
- if (bitMatrix.get(x, y))
- {
- pixels[y * width + x] = 0xff000000;//当然我们也可以自己更改颜色
- }
- else
- {
- pixels[y * width + x] = 0xffffffff;
- }
- }
- }
- //生成二维码图片的格式,使用ARGB_8888
- Bitmap bitmap = Bitmap.createBitmap(QR_WIDTH, QR_HEIGHT, Bitmap.Config.ARGB_8888);
- bitmap.setPixels(pixels, 0, QR_WIDTH, 0, 0, QR_WIDTH, QR_HEIGHT);
如果想对扫描的界面想增加点自己的按钮或者功能的,因为采用的是fragment,也可以替换,实现定制扫描的UI界面
1,在新的activity中定义我们的布局
- <?xml version="1.0" encoding="utf-8"?>
- <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
- android:id="@+id/activity_second"
- android:layout_width="match_parent"
- android:layout_height="match_parent">
- <Button
- android:id="@+id/second_button1"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:text="取消"
- android:layout_marginTop="20dp"
- android:layout_marginLeft="20dp"
- android:layout_marginRight="20dp"
- android:layout_marginBottom="10dp"
- android:layout_gravity="bottom|center_horizontal"
- />
- <FrameLayout
- android:id="@+id/fl_my_container"
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- ></FrameLayout>
- </FrameLayout>
在Activity中执行Fragment的初始化操作
- protected void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- setContentView(R.layout.activity_second);
- /**
- * 执行扫面Fragment的初始化操作
- */
- CaptureFragment captureFragment = new CaptureFragment();
- // 为二维码扫描界面设置定制化界面
- QRCodeUtils.setFragmentArgs(captureFragment, R.layout.my_camera);
- captureFragment.setAnalyzeCallback(analyzeCallback);
- /**
- * 替换我们的扫描控件
- */ getSupportFragmentManager().beginTransaction().replace(R.id.fl_my_container, captureFragment).commit();
- }
而此activity此时要改成要继承于FragmentActivity。
在上面的onCreate方法中,我们调用了QRCodeUtils.setFragmentArgs(captureFragment, R.layout.my_camera);方法。该方法主要用于修改扫描界面扫描框与透明框相对位置的,与若不调用的话,其会显示默认的组件效果,而如果调用该方法的话,可以修改扫描框与透明框的相对位置等UI效果,我们可以看一下my_camera布局文件的实现。
- <?xml version="1.0" encoding="utf-8"?>
- <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:app="http://schemas.android.com/apk/res-auto"
- android:layout_width="fill_parent"
- android:layout_height="fill_parent" >
- <SurfaceView
- android:id="@+id/preview_view"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- />
- <com.example.zxing.view.ViewfinderView
- android:id="@+id/viewfinder_view"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- app:inner_width="180"
- app:inner_height="180"
- app:inner_margintop="120"
- app:inner_corner_color="@color/scan_corner_color"
- app:inner_corner_length="60"
- app:inner_corner_width="10"
- app:inner_scan_bitmap="@drawable/scan_image"
- app:inner_scan_speed="10"
- />
- </FrameLayout>
- <?xml version="1.0" encoding="utf-8"?>
- <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
- android:layout_width="fill_parent"
- android:layout_height="fill_parent" >
- <SurfaceView
- android:id="@+id/preview_view"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_gravity="center" />
- <com.example.zxing.view.ViewfinderView
- android:id="@+id/viewfinder_view"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content" />
- </FrameLayout>
好了,项目介绍到这里了,总体来说,效果功能都实现了,待实际需求时,可以直接集成更改即可。源码地址: 点我下载
参考别人的github项目,发现别人的扫描速度稍微快一点,以后有待研究。https://github.com/bingoogolapple/BGAQRCode-Android
注:解决关于扫描时出现图片拉伸的问题。
如果是直接引用的zxing包里面的camera文件时,可能会出现扫描的二维码在扫描框内出现拉伸问题,因为Zxing包里的二维码扫描默认是横屏扫描的,改为竖屏后出现比例问题,所以要修正过来。
可以在camera包里面的CameraConfigurationManager.java文件里的
void initFromCameraParameters(Camera camera)方法
在 Log.d(TAG, "Screen resolution: " + screenResolution);这句之后增加
- Point screenResolutionForCamera = new Point();
- screenResolutionForCamera.x = screenResolution.x;
- screenResolutionForCamera.y = screenResolution.y;
- // preview size is always something like 480*320, other 320*480
- if (screenResolution.x < screenResolution.y) {
- screenResolutionForCamera.x = screenResolution.y;
- screenResolutionForCamera.y = screenResolution.x;
- }
- cameraResolution = getCameraResolution(parameters, screenResolution);
- cameraResolution = getCameraResolution(parameters, screenResolutionForCamera);
8/15日更新
增加二维码生成识别容错率
在我们生产二维码的时候,有时二维码中间放logo的话,识别起来比不放困难点,但是logo又不能太小,否则就起不到宣传认识效果,太大了后识别就稍微困难点了。一方面我们的图片大小要适中,另一方面又要增加二维码的识别容错率,这样可以加快识别速度。
默认的二维码容错率是ErrorCorrectionLevel.L
在QRCodeWriter源码中(位置是com.google.zxing.qrcode)可以看到有默认值
- ErrorCorrectionLevel errorCorrectionLevel = ErrorCorrectionLevel.L;
- int quietZone = QUIET_ZONE_SIZE;
- if (hints != null) {
- ErrorCorrectionLevel requestedECLevel = (ErrorCorrectionLevel) hints.get(EncodeHintType.ERROR_CORRECTION);
- if (requestedECLevel != null) {
- errorCorrectionLevel = requestedECLevel;
- }
- Integer quietZoneInt = (Integer) hints.get(EncodeHintType.MARGIN);
- if (quietZoneInt != null) {
- quietZone = quietZoneInt;
- }
- }
- Hashtable hints = new Hashtable();
- hints.put(EncodeHintType.CHARACTER_SET, "utf-8");//设置编码格式,否则中文会识别不了
- hints.put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.H);//容错率默认是7%,现改为30%