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

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

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

推荐阅读更多精彩内容