一款二维码扫描组件

1,705 阅读3分钟
原文链接: www.jianshu.com

简介

之前项目中使用到扫描功能,那时逻辑业务和UI是完全耦合在一起,不好维护,也难移植。趁着这次新项目中要使用扫一扫的功能,就将二维码扫描单独提出作为一个组件库,与业务完全分离。最终扫描结果通过回调的方式提供给调用者,用户可以在自己的app中处理扫描结果。库仿造Universal-Image-Loader进行封装,提供一个配置文件,可简单配置扫一扫界面的样式,实现用户UI定制。提供一个控制类,用户通过其提供的接口与组件进行交互,内部实现相对于用户都是透明的。实现效果如下图所示:


具体实现

组件是调用zxing进行二维码的编解码计算,这部分不是本文研究的重点。本文主要关注以下几点:

  • 扫描界面的绘制
  • 扫描结果如何回调给用户
  • 组件是如何封装的
  • 如何使用该组件

界面绘制

ViewfinderView是我们的扫描界面,实在onDraw方法中绘制。这里我直接上代码,关键部分会有注释

@Override
    public void onDraw(Canvas canvas) {
        //中间的扫描框,你要修改扫描框的大小,去CameraManager里面修改
        CameraManager.init(this.getContext().getApplicationContext());

        Rect frame = null;
        try {
            frame = CameraManager.get().getFramingRect();
        } catch (Exception e) {
            e.printStackTrace();
            return;
        }


        if (frame == null) {
            return;
        }

        //获取屏幕的宽和高
        int width = canvas.getWidth();
        int height = canvas.getHeight();

        paint.setColor(maskColor);

        //画出扫描框外面的阴影部分,共四个部分,扫描框的上面到屏幕上面,扫描框的下面到屏幕下面
        //扫描框的左边面到屏幕左边,扫描框的右边到屏幕右边
        canvas.drawRect(0, 0, width, frame.top, paint); //上
        canvas.drawRect(0, frame.top, frame.left, frame.bottom - 1, paint);  //左
        canvas.drawRect(frame.right, frame.top, width, frame.bottom - 1, paint); //右
        canvas.drawRect(0, frame.bottom - 1, width, height, paint);

        paint.setColor(0xffffffff);
        canvas.drawLine(frame.left + 1, frame.top + 1, frame.right - 1, frame.top + 1, paint);
        canvas.drawLine(frame.left + 1,frame.top + 1,frame.left + 1,frame.bottom - 1, paint);
        canvas.drawLine(frame.left + 1,frame.bottom - 1,frame.right -1,frame.bottom - 1,paint);
        canvas.drawLine(frame.right -1,frame.top + 1,frame.right - 1,frame.bottom - 1,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 {

            //画扫描框边上的角,总共8个部分
            paint.setColor(angleColor);
            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);

            // 如果设置了slideIcon,则显示
            if(mSlideIcon != null){
//                mBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.capture_add_scanning);

                BitmapDrawable bd = (BitmapDrawable) mSlideIcon;
                mBitmap = bd.getBitmap();

                //绘制中间的线,每次刷新界面,中间的线往下移动SPEEN_DISTANCE
                if (mBitmap != null){
                    mBitmap = Bitmap.createScaledBitmap(mBitmap, frame.right - frame.left, mBitmap.getHeight(), true);
                }


                //初始化中间线滑动的最上边和最下边
                if(!isFirst){
                    isFirst = true;
                    slideTop = frame.top + mBitmap.getHeight();
                    slideBottom = frame.bottom;
                }

                slideTop += SPEEN_DISTANCE;
                if(slideTop >= frame.bottom){
                    slideTop = frame.top + mBitmap.getHeight();
                }

                canvas.drawBitmap(mBitmap, frame.left, slideTop - mBitmap.getHeight(), paint);
            }else{
                //初始化中间线滑动的最上边和最下边
                if(!isFirst){
                    isFirst = true;
                    slideTop = frame.top + MIDDLE_LINE_WIDTH;
                    slideBottom = frame.bottom;
                }

                slideTop += SPEEN_DISTANCE;
                if(slideTop >= frame.bottom){
                    slideTop = frame.top + MIDDLE_LINE_WIDTH;
                }

                canvas.drawRect(frame.left + MIDDLE_LINE_PADDING, slideTop - MIDDLE_LINE_WIDTH,frame.right - MIDDLE_LINE_PADDING, slideTop, paint);

            }

            // 画扫描框下面的字
            paint.setColor(mTipColor);
            paint.setTextSize(TEXT_SIZE * density);
            paint.setTextAlign(Paint.Align.CENTER);
            paint.setTypeface(Typeface.create("System", Typeface.NORMAL));
            canvas.drawText(scanTip, width/2, (float) (frame.bottom + (float)mTipmMargin * density), paint);

            //只刷新扫描框的内容,其他地方不刷新
            postInvalidateDelayed(ANIMATION_DELAY, frame.left, frame.top, frame.right, frame.bottom);

        }
    }

其中中间的扫描框,是在CameraManager中实现。

public Rect getFramingRect() {
    Point screenResolution = configManager.getScreenResolution();
    if (framingRect == null) {
      if (camera == null) {
        return null;
      }

      if (screenResolution == null){
        return null;
      }

      int width;
      int height;
      int topOffset;
      int leftOffset;
      int rightOffset;

      mFrameRate = QrScanProxy.getInstance().getScanFrameRectRate();

      width = (int) (screenResolution.x * mFrameRate);
      if (width < MIN_FRAME_WIDTH) {
        width = MIN_FRAME_WIDTH;
      }
      height = width;
      leftOffset = (screenResolution.x - width) / 2;
      topOffset = DeviceUtil.dip2px(DEFAULT_FRAME_MARGIN_TOP);
      if((height + topOffset) > screenResolution.y){
        topOffset = screenResolution.y - height;
      }

      framingRect = new Rect(leftOffset, topOffset, leftOffset + width, topOffset + height);
      Log.d(TAG, "Calculated framing rect: " + framingRect);
    }
    return framingRect;
  }

扫描结果回调

在CaptureActivityHandler中,有一个handleMessage方法。扫描的二维码信息会在这边进行分发。

 @Override
  public void handleMessage(Message message) {
    ...

    } else if (message.what == R.id.decode_succeeded) {
      Log.d(TAG, "Got decode succeeded message");
      state = State.SUCCESS;
      Bundle bundle = message.getData();
      Bitmap barcode = bundle == null ? null :
              (Bitmap) bundle.getParcelable(DecodeThread.BARCODE_BITMAP);

      activity.handleDecode((Result) message.obj, barcode);
      ...

组件封装

对外部调用者,提供QrScan.java和QrScanConfiguration.java,前者是组件提供给外部用于和组件交互的方法,后者用于配置组件的相关属性。在组件内部,有一个代理类QrScanProxy.java,所有外部设置的属性作用于内部,都是通过这个类进行分发。具体实现大家可以参考源码

使用

compile 'com.netease.scan:lib-qr-scan:1.0.0'
    // 设置权限
     
    
    
    

    // 注册activity
    
  • 初始化
    在需要使用此组件的Activity的onCreate方法中,或者在自定义Application的onCreate方法中初始化。
/**
 * @author hzzhengrui
 * @Date 16/10/27
 * @Description
 */
public class MyApplication extends Application {
    @Override
    public void onCreate() {
        super.onCreate();

//        // 默认配置
//        QrScanConfiguration configuration = QrScanConfiguration.createDefault(this);

        // 自定义配置
        QrScanConfiguration configuration = new QrScanConfiguration.Builder(this)
                .setTitleHeight(53)
                .setTitleText("来扫一扫")
                .setTitleTextSize(18)
                .setTitleTextColor(R.color.white)
                .setTipText("将二维码放入框内扫描~")
                .setTipTextSize(14)
                .setTipMarginTop(40)
                .setTipTextColor(R.color.white)
                .setSlideIcon(R.mipmap.capture_add_scanning)
                .setAngleColor(R.color.white)
                .setMaskColor(R.color.black_80)
                .setScanFrameRectRate((float) 0.8)
                .build();
        QrScan.getInstance().init(configuration);
    }
}
QrScan.getInstance().launchScan(MainActivity.this, new IScanModuleCallBack() {
                    @Override
                    public void OnReceiveDecodeResult(final Context context, String result) {
                        mCaptureContext = (CaptureActivity)context;

                        AlertDialog dialog = new AlertDialog.Builder(mCaptureContext)
                                .setMessage(result)
                                .setCancelable(false)
                                .setNegativeButton("取消", new DialogInterface.OnClickListener() {
                                    @Override
                                    public void onClick(DialogInterface dialog, int which) {
                                        dialog.dismiss();
                                        QrScan.getInstance().restartScan(mCaptureContext);
                                    }
                                })
                                .setPositiveButton("关闭", new DialogInterface.OnClickListener() {
                                    @Override
                                    public void onClick(DialogInterface dialog, int which) {
                                        dialog.dismiss();
                                        QrScan.getInstance().finishScan(mCaptureContext);
                                    }
                                })
                                .create();
                        dialog.show();
                    }
                });

最后附上源码地址:github.com/yushiwo/QrS…