Android 二维码扫描,强大的相机遮罩CameraMask

开篇

  在我们的开发过程中经常需要用到二维码扫描,而二维码扫描界面的相机遮罩又是常用控件,在近期的项目开发中有使用到,所以就整理出来,优化成一个强大的相机遮罩控件分享给童鞋们。

  • 支持修改遮罩颜色以及透明度
  • 支持相机镜头:图片镜头、方形扫描框
  • 支持相机镜头(或扫描框)的大小
  • 支持设置提示文字以及位置、字体、颜色
  • 可获取相机镜头位置Rect

效果截屏

camera_lens_view:pic
camera_lens_view:circle
camera_lens_view:square
scanner_bar_view

立即体验

扫描以下二维码下载体验App(体验App内嵌版本更新检测功能):


CameraMask库传送门:https://github.com/JustinRoom/CameraMaskDemo

简析源码

  • CameraLensView属性:
名称 类型 描述
clvCameraLensSizeRatio float 相机镜头(或扫描框)大小占View宽度的百分比
clvCameraLensTopMargin dimension 相机镜头(或扫描框)与顶部的间距
clvCameraLensShape enum(squarecircular) 相机镜头(或扫描框)形状
clvCameraLens reference 相机镜头图片资源
clvMaskColor color 相机镜头遮罩颜色
clvBoxBorderColor color 扫描框边的颜色
clvBoxBorderWidth dimension 扫描框边的粗细
clvBoxAngleColor color 扫描框四个角的颜色
clvBoxAngleBorderWidth dimension 扫描框四个角边的粗细
clvBoxAngleLength dimension 扫描框四个角边的长度
clvText string 提示文字
clvTextColor color 提示文字颜色
clvTextSize dimension 提示文字字体大小
clvTextMathParent boolean 提示文字是否填充View的宽度。true与View等宽,false与相机镜头(或扫描框)等宽。
clvTextLocation enum(belowCameraLensaboveCameraLens) 提示文字位于相机镜头(或扫描框)上方(或下方)
clvTextVerticalMargin dimension 提示文字与相机镜头(或扫描框)的间距
clvTextLeftMargin dimension 提示文字与View(或相机镜头或扫描框)的左间距
clvTextRightMargin dimension 提示文字与View(或相机镜头或扫描框)的右间距
  • ScannerBarView属性:
名称 类型 描述
sbvSrc reference 扫描条图片

CameraLensView初始化视图以及解析attribute:

    public void init(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.CameraLensView, defStyleAttr, 0);
        cameraLensTopMargin = a.getDimensionPixelSize(R.styleable.CameraLensView_clvCameraLensTopMargin, 0);

        if (a.hasValue(R.styleable.CameraLensView_clvCameraLens)) {
            int resId = a.getResourceId(R.styleable.CameraLensView_clvCameraLens, -1);
            if (resId != -1)
                cameraLensBitmap = BitmapFactory.decodeResource(getResources(), resId);
        }

        cameraLensShape = a.getInt(R.styleable.CameraLensView_clvCameraLensShape, CAMERA_LENS_SHAPE_SQUARE);
        boxBorderColor = a.getColor(R.styleable.CameraLensView_clvBoxBorderColor, 0x99FFFFFF);
        boxBorderWidth = a.getDimensionPixelSize(R.styleable.CameraLensView_clvBoxBorderWidth, 2);
        boxAngleColor = a.getColor(R.styleable.CameraLensView_clvBoxAngleColor, Color.YELLOW);
        int defaultScannerBoxAngleBorderWidth = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 2, getResources().getDisplayMetrics());
        boxAngleBorderWidth = a.getDimensionPixelSize(R.styleable.CameraLensView_clvBoxAngleBorderWidth, defaultScannerBoxAngleBorderWidth);
        int defaultScannerBoxAngleLength = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 16, getResources().getDisplayMetrics());
        boxAngleLength = a.getDimensionPixelSize(R.styleable.CameraLensView_clvBoxAngleLength, defaultScannerBoxAngleLength);
        maskColor = a.getColor(R.styleable.CameraLensView_clvMaskColor, 0x99000000);
        cameraLensSizeRatio = a.getFloat(R.styleable.CameraLensView_clvCameraLensSizeRatio, .6f);
        if (cameraLensSizeRatio < .3f)
            cameraLensSizeRatio = .3f;
        if (cameraLensSizeRatio > 1.0f)
            cameraLensSizeRatio = 1.0f;

        text = a.getString(R.styleable.CameraLensView_clvText);
        int textColor = a.getColor(R.styleable.CameraLensView_clvTextColor, Color.WHITE);
        int defaultTextSize = (int) (TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, 12, getResources().getDisplayMetrics()) + .5f);
        float textSize = a.getDimension(R.styleable.CameraLensView_clvTextSize, defaultTextSize);
        textMathParent = a.getBoolean(R.styleable.CameraLensView_clvTextMathParent, false);
        textLocation = a.getInt(R.styleable.CameraLensView_clvTextLocation, BELOW_CAMERA_LENS);
        textVerticalMargin = a.getDimensionPixelSize(R.styleable.CameraLensView_clvTextVerticalMargin, 0);
        textLeftMargin = a.getDimensionPixelSize(R.styleable.CameraLensView_clvTextLeftMargin, 0);
        textRightMargin = a.getDimensionPixelSize(R.styleable.CameraLensView_clvTextRightMargin, 0);
        a.recycle();

        textPaint.setColor(textColor);
        textPaint.setTextSize(textSize);
    }

CameraLensView计算相机镜头的尺寸:

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        initCameraLensSize(getMeasuredWidth());
    }

    private void initCameraLensSize(int width) {
        int cameraLensSize = (int) (width * cameraLensSizeRatio);
        int left = (width - cameraLensSize) / 2;
        cameraLensRect.set(left, cameraLensTopMargin, left + cameraLensSize, cameraLensTopMargin + cameraLensSize);
        updateStaticLayout();
    }

    //初始化提示文字layout
    private void updateStaticLayout() {
        if (text == null || text.trim().length() == 0) {
            textStaticLayout = null;
            return;
        }
        int textWidth = textMathParent ? getWidth() : cameraLensRect.width();
        textWidth = textWidth - textLeftMargin - textRightMargin;
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
            textStaticLayout = StaticLayout.Builder.obtain(text, 0, text.length(), textPaint, textWidth)
                    .setAlignment(StaticLayout.Alignment.ALIGN_CENTER)
                    .setLineSpacing(0, 1.0f)
                    .build();
        } else {
            textStaticLayout = new StaticLayout(text, textPaint, textWidth, StaticLayout.Alignment.ALIGN_CENTER, 1.0f, 0, true);
        }
    }

相机镜头尺寸:size = View宽度 * cameraLensSizeRatio
CameraLensView绘制:

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        drawMask(canvas, cameraLensShape);

        float translateX = 0;
        float translateY = 0;
        if (cameraLensBitmap != null) {
            translateX = cameraLensRect.left;
            translateY = cameraLensTopMargin;
            float scale = cameraLensRect.width() * 1.0f / cameraLensBitmap.getWidth();
            cameraLensMatrix.setScale(scale, scale);
            canvas.save();
            canvas.translate(translateX, translateY);
            canvas.drawBitmap(cameraLensBitmap, cameraLensMatrix, null);
            canvas.translate(-translateX, -translateY);
            canvas.restore();
        } else {
            paint.setStyle(Paint.Style.STROKE);
            switch (cameraLensShape) {
                case CAMERA_LENS_SHAPE_SQUARE:
                    if (boxAnglePath == null) {
                        boxAnglePath = new Path();
                    }
                    paint.setStrokeWidth(boxBorderWidth);
                    paint.setColor(boxBorderColor);
                    canvas.drawRect(cameraLensRect, paint);

                    paint.setStrokeWidth(boxAngleBorderWidth);
                    paint.setColor(boxAngleColor);
                    //左上角
                    boxAnglePath.reset();
                    boxAnglePath.moveTo(cameraLensRect.left, cameraLensRect.top + boxAngleLength);
                    boxAnglePath.lineTo(cameraLensRect.left, cameraLensRect.top);
                    boxAnglePath.lineTo(cameraLensRect.left + boxAngleLength, cameraLensRect.top);
                    canvas.drawPath(boxAnglePath, paint);
                    //右上角
                    boxAnglePath.reset();
                    boxAnglePath.moveTo(cameraLensRect.right - boxAngleLength, cameraLensRect.top);
                    boxAnglePath.lineTo(cameraLensRect.right, cameraLensRect.top);
                    boxAnglePath.lineTo(cameraLensRect.right, cameraLensRect.top + boxAngleLength);
                    canvas.drawPath(boxAnglePath, paint);
                    //右下角
                    boxAnglePath.reset();
                    boxAnglePath.moveTo(cameraLensRect.right, cameraLensRect.bottom - boxAngleLength);
                    boxAnglePath.lineTo(cameraLensRect.right, cameraLensRect.bottom);
                    boxAnglePath.lineTo(cameraLensRect.right - boxAngleLength, cameraLensRect.bottom);
                    canvas.drawPath(boxAnglePath, paint);
                    //左下角
                    boxAnglePath.reset();
                    boxAnglePath.moveTo(cameraLensRect.left + boxAngleLength, cameraLensRect.bottom);
                    boxAnglePath.lineTo(cameraLensRect.left, cameraLensRect.bottom);
                    boxAnglePath.lineTo(cameraLensRect.left, cameraLensRect.bottom - boxAngleLength);
                    canvas.drawPath(boxAnglePath, paint);
                    break;
                case CAMERA_LENS_SHAPE_CIRCULAR:
                    paint.setStrokeWidth(boxBorderWidth);
                    paint.setColor(boxBorderColor);
                    float cx = cameraLensRect.left + cameraLensRect.width() / 2.0f;
                    float cy = cameraLensRect.top + cameraLensRect.height() / 2.0f;
                    float radius = cameraLensRect.width() / 2.0f - boxBorderWidth / 2.0f;
                    canvas.drawCircle(cx, cy, radius, paint);

                    paint.setStrokeWidth(boxAngleBorderWidth);
                    paint.setColor(boxAngleColor);
                    float halfBoxAngleBorderWidth = boxAngleBorderWidth / 16.0f;
                    rectF.set(
                            cx - radius - halfBoxAngleBorderWidth,
                            cy - radius - halfBoxAngleBorderWidth,
                            cx + radius + halfBoxAngleBorderWidth,
                            cy + radius + halfBoxAngleBorderWidth
                    );
                    float angle = (float) (boxAngleLength * 180 / (Math.PI * radius));
                    float startAngle;
                    //左上角
                    startAngle = 225 - angle / 2;
                    canvas.drawArc(rectF, startAngle, angle, false, paint);
                    //右上角
                    startAngle = 315 - angle / 2;
                    canvas.drawArc(rectF, startAngle, angle, false, paint);
                    //右下角
                    startAngle = 45 - angle / 2;
                    canvas.drawArc(rectF, startAngle, angle, false, paint);
                    //左下角
                    startAngle = 135 - angle / 2;
                    canvas.drawArc(rectF, startAngle, angle, false, paint);
                    break;
            }

        }

        //提示文字
        if (textStaticLayout != null) {
            canvas.save();
            translateX = textMathParent ? 0 : cameraLensRect.left;
            translateX = translateX + textLeftMargin;
            translateY = textLocation == BELOW_CAMERA_LENS ? cameraLensRect.bottom + textVerticalMargin : cameraLensRect.top - textVerticalMargin - textStaticLayout.getHeight();
            canvas.translate(translateX, translateY);
            textStaticLayout.draw(canvas);
            canvas.translate(-translateX, -translateY);
            canvas.restore();
        }
    }

Mask的实现我们有两种方式:

一、填充相机镜头四周,分割成四个部分填充。适用于实现方形的Mask,以下是实现code

    /**
     * The first way to draw square mask.
     * Only the square mask supported in this way.
     *
     * @param canvas canvas
     */
    private void drawMask(Canvas canvas) {
        paint.setColor(maskColor);
        paint.setStyle(Paint.Style.FILL);
        canvas.drawRect(0, 0, getWidth(), topMargin, paint);
        canvas.drawRect(0, cameraLensRect.bottom, getWidth(), getHeight(), paint);
        canvas.drawRect(0, topMargin, cameraLensRect.left, cameraLensRect.bottom, paint);
        canvas.drawRect(cameraLensRect.right, topMargin, getWidth(), cameraLensRect.bottom, paint);
    }

二、橡皮擦方式:先全屏蒙版,在用橡皮擦擦除相机镜头(或相框)区域。此方式支持各种shape的Mask,本控件中暂时只支持正方形square和圆形circular。以下是实现code

    /**
     * The second way to draw mask. In this way, there are two different shapes.
     * Square: {@link #MASK_SHAPE_SQUARE}、Circular: {@link #MASK_SHAPE_CIRCULAR}.
     *
     * @param canvas  canvas
     * @param maskShape mask shape. One of {@link #MASK_SHAPE_SQUARE}、{@link #MASK_SHAPE_CIRCULAR}.
     */
    private void drawMask(Canvas canvas, int maskShape) {
        //满屏幕bitmap
        Bitmap bitmap = Bitmap.createBitmap(getWidth(), getHeight(), Bitmap.Config.ARGB_8888);
        Canvas mCanvas = new Canvas(bitmap);
        paint.setColor(maskColor);
        paint.setStyle(Paint.Style.FILL);
        mCanvas.drawRect(0, 0, getWidth(), getHeight(), paint);

        paint.setXfermode(xfermode);
        switch (maskShape) {
            case MASK_SHAPE_SQUARE:
                mCanvas.drawRect(cameraLensRect, paint);
                break;
            case MASK_SHAPE_CIRCULAR:
                float radius = cameraLensRect.height() / 2.0f;
                mCanvas.drawCircle(getWidth() / 2.0f, cameraLensRect.top + radius, radius, paint);
                break;
        }
        paint.setXfermode(null);
        canvas.drawBitmap(bitmap, 0, 0, null);
    }

Xfermode xfermode = new PorterDuffXfermode(PorterDuff.Mode.CLEAR);清除模式。

画相机镜头(或扫描框)。
画提示文字。

方法列表

方法名称以及返回 描述
Bitmap getCameraLensBitmap() 获取镜头Bitmap
void setCameraLensBitmap(Bitmap cameraLensBitmap) 设置镜头Bitmap
int getMaskColor() 获取整个遮罩的颜色
void setMaskColor(@ColorInt int maskColor) 设置整个遮罩的颜色
int getBoxBorderColor() 获取镜头边框颜色
void setBoxBorderColor(@ColorInt int boxBorderColor) 设置镜头边框颜色
int getBoxBorderWidth() 获取镜头边框粗细
void setBoxBorderWidth(int boxBorderWidth) 设置镜头边框粗细
int getBoxAngleColor() 获取镜头四角颜色
void setBoxAngleColor(@ColorInt int boxAngleColor) 设置镜头四角颜色
int getBoxAngleBorderWidth() 获取镜头四角粗细
void setBoxAngleBorderWidth(int boxAngleBorderWidth) 设置镜头四角粗细
int getBoxAngleLength() 获取镜头四角长度
void setBoxAngleLength(int boxAngleLength) 设置镜头四角长度
int getCameraLensTopMargin() 获取镜头与顶部的距离
void setCameraLensTopMargin(int cameraLensTopMargin) 设置镜头与顶部的距离
float getCameraLensSizeRatio() 获取镜头占宽度的百分比
void setCameraLensSizeRatio(@FloatRange(from = 0.0, to = 1.0) float cameraLensSizeRatio) 设置镜头占宽度的百分比
String getText() 获取提示文字
void setText(String text) 设置提示文字
boolean isTextMathParent() 提示文字是否对齐边缘
void setTextMathParent(boolean textMathParent) 设置提示文字是否对齐边缘
int getTextLocation() 获取提示文字是在镜头上方(下方)
void setTextLocation(@TextLocation int textLocation) 设置提示文字是在镜头上方(下方)
int getTextVerticalMargin() 获取提示文字与镜头间的距离
void setTextVerticalMargin(int textVerticalMargin) 设置提示文字与镜头间的距离
int getTextLeftMargin() 获取提示文字相对于左边的偏移量
void setTextLeftMargin(int textLeftMargin) 设置提示文字相对于左边的偏移量
int getTextRightMargin() 获取提示文字相对于右边的偏移量
void setTextRightMargin(int textRightMargin) 设置提示文字相对于右边的偏移量
int getCameraLensShape() 获取镜头的形状(圆形、方形)
void setCameraLensShape(@CameraLensShape int cameraLensShape) 设置镜头的形状(圆形、方形)
getCameraLensRect() 获取相机镜头在View中的位置。我们可以利用这个Rect位置信息做很多事情,例如在相机预览中生成这块区域的图片(或者识别此区域的数据信息)

使用示例

组件类型 使用示例
CameraLensView CameraLensViewFragment
ScannerBarView ScannerBarViewFragment

扩展控件:组合CameraLensViewScannerBarView实现扫描动画——CameraScannerMaskView

camera_scanner_mask_view

属性

子View 类型 属性
cameraLensView CameraLensView CameraLensView所有属性
scannerBarView ScannerBarView ScannerBarView所有属性

初始化视图:添加cameraLensViewscannerBarView

    public CameraScannerMaskView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        cameraLensView = new CameraLensView(context, attrs, defStyleAttr);
        scannerBarView = new ScannerBarView(context, attrs, defStyleAttr);
        addView(cameraLensView, new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT));
        addView(scannerBarView, new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT));
    }

根据相机镜头(或扫描框)的位置,放置scannerBarView

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        ViewGroup.MarginLayoutParams params = (ViewGroup.MarginLayoutParams) scannerBarView.getLayoutParams();
        Rect rect = cameraLensView.getCameraLensRect();
        params.width = rect.width();
        params.height = rect.height();
        params.leftMargin = rect.left;
        params.topMargin = rect.top;
        scannerBarView.setLayoutParams(params);
    }

提供动画控制相关方法

    public void start() {
        scannerBarView.start();
    }

    public void pause() {
        scannerBarView.pause();
    }

    public void resume() {
        scannerBarView.resume();
    }

    public void stop() {
        scannerBarView.stop();
    }

使用示例

组件类型 使用示例
CameraScannerMaskView CameraScannerMaskViewFragment

一个强大的二维码扫描相机遮罩控件从此问世。

篇尾!

  给个❤️支持下呗,谢谢!QQ:1006368252WeChat:eoy9527

最甜美的是爱情,最苦涩的也是爱情。 —— 菲·贝利

你可能感兴趣的:(Android 二维码扫描,强大的相机遮罩CameraMask)