美文网首页
Android 详解使用 Zxing实现前置摄像头扫描二维码、生

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

作者: HappyGhh | 来源:发表于2020-01-17 15:08 被阅读0次

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

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

    相关文章

      网友评论

          本文标题:Android 详解使用 Zxing实现前置摄像头扫描二维码、生

          本文链接:https://www.haomeiwen.com/subject/whdfnqtx.html