Android 详解使用 Zxing实现前置摄像头扫描二维码、生成二维码

本文同步到CSDN

现在二维码使用越来越广泛了,几乎处处可见,并且 公司相关的项目中几乎全部都和二维码扫描有关,所以总结一下自己的使用心路历程,总觉得要做点什么来记录自己的成长,让自己的成长有迹可循,如果恰好能够帮助到你,我当然会很开心啦,如果没帮到,请忽略。。

废话结束,正文开始

小白之旅,如有问题 望指正,万分感谢 🙏🙏

首先推荐几篇

Android 二维码的扫码功能实现

二维码ZXING源码分析(一)

zxing扫描二维码和识别图片二维码及其优化策略

Android中常用的就是 zxing ,开源项目地址:https://github.com/zxing/zxing。首先我们下载项目到本地,然后加载到自己的工程中,可参考 我的上一片博文 AndroidStudio 导入 Zxing Android 项目,这个是作为库文件导入的,当然我们也可以单独在工程中分出一个包,来实现扫描二维码的功能。

特别提醒: 如果作为 module 完整导入项目则配置好后就可以运行,如果单独作为一个包,独立出自己需要的内容,需要复制 layout 文件,和资源文件 res-->values 里面的 ids.xml 和 res-->raw 里面的 beep.ogg 文件

一、了解 ZXing

image.png

ZXing 导入后,所有的内容如上图所示,运行示例代码,发现是横屏用来扫描条形码的,包括识别相册中的二维码,扫描记录,剪切板,生成二维码等功能,我们可以根据需要,分离出自己需要的那一部分,首先了解 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()方法的是 CameraManageropenDriver(),也就是说需要在 openDriver()里面传递参数,再往外找,发现CaptureActivityinitCamera()是打开相机,设置参数的地方,所以在这里把设置前后摄像头的参数传递过去,

 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项目中自己需要的功能提取出来,主要还是要有耐心,看不懂就多看几遍对我来说还是有效果的啊,哈哈哈,当然,自己也去找了很多的资料帮助自己理解,感谢你们。

惯例,最后送给自己一句话:凡事往简单处想,往认真处行

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342

推荐阅读更多精彩内容