Android 二維碼掃描,強(qiáng)大的相機(jī)遮罩CameraMask

開篇

??在我們的開發(fā)過程中經(jīng)常需要用到二維碼掃描,而二維碼掃描界面的相機(jī)遮罩又是常用控件,在近期的項(xiàng)目開發(fā)中有使用到,所以就整理出來,優(yōu)化成一個(gè)強(qiáng)大的相機(jī)遮罩控件分享給童鞋們。

  • 支持修改遮罩顏色以及透明度
  • 支持相機(jī)鏡頭:圖片鏡頭、方形掃描框
  • 支持相機(jī)鏡頭(或掃描框)的大小
  • 支持設(shè)置提示文字以及位置、字體、顏色
  • 可獲取相機(jī)鏡頭位置Rect

效果截屏

camera_lens_view:pic
camera_lens_view:circle
camera_lens_view:square
scanner_bar_view

立即體驗(yàn)

掃描以下二維碼下載體驗(yàn)App(體驗(yàn)App內(nèi)嵌版本更新檢測(cè)功能):


CameraMask庫傳送門:https://github.com/JustinRoom/CameraMaskDemo

簡(jiǎn)析源碼

  • CameraLensView屬性:
名稱 類型 描述
clvCameraLensSizeRatio float 相機(jī)鏡頭(或掃描框)大小占View寬度的百分比
clvCameraLensTopMargin dimension 相機(jī)鏡頭(或掃描框)與頂部的間距
clvCameraLensShape enum(squarecircular) 相機(jī)鏡頭(或掃描框)形狀
clvCameraLens reference 相機(jī)鏡頭圖片資源
clvMaskColor color 相機(jī)鏡頭遮罩顏色
clvBoxBorderColor color 掃描框邊的顏色
clvBoxBorderWidth dimension 掃描框邊的粗細(xì)
clvBoxAngleColor color 掃描框四個(gè)角的顏色
clvBoxAngleBorderWidth dimension 掃描框四個(gè)角邊的粗細(xì)
clvBoxAngleLength dimension 掃描框四個(gè)角邊的長(zhǎng)度
clvText string 提示文字
clvTextColor color 提示文字顏色
clvTextSize dimension 提示文字字體大小
clvTextMathParent boolean 提示文字是否填充View的寬度。true與View等寬,false與相機(jī)鏡頭(或掃描框)等寬。
clvTextLocation enum(belowCameraLensaboveCameraLens) 提示文字位于相機(jī)鏡頭(或掃描框)上方(或下方)
clvTextVerticalMargin dimension 提示文字與相機(jī)鏡頭(或掃描框)的間距
clvTextLeftMargin dimension 提示文字與View(或相機(jī)鏡頭或掃描框)的左間距
clvTextRightMargin dimension 提示文字與View(或相機(jī)鏡頭或掃描框)的右間距
  • 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計(jì)算相機(jī)鏡頭的尺寸:

    @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);
        }
    }

相機(jī)鏡頭尺寸: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的實(shí)現(xiàn)我們有兩種方式:

一、填充相機(jī)鏡頭四周,分割成四個(gè)部分填充。適用于實(shí)現(xiàn)方形的Mask,以下是實(shí)現(xiàn)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);
    }

二、橡皮擦方式:先全屏蒙版,在用橡皮擦擦除相機(jī)鏡頭(或相框)區(qū)域。此方式支持各種shape的Mask,本控件中暫時(shí)只支持正方形square和圓形circular。以下是實(shí)現(xiàn)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);清除模式。

畫相機(jī)鏡頭(或掃描框)。
畫提示文字。

方法列表

方法名稱以及返回 描述
Bitmap getCameraLensBitmap() 獲取鏡頭Bitmap
void setCameraLensBitmap(Bitmap cameraLensBitmap) 設(shè)置鏡頭Bitmap
int getMaskColor() 獲取整個(gè)遮罩的顏色
void setMaskColor(@ColorInt int maskColor) 設(shè)置整個(gè)遮罩的顏色
int getBoxBorderColor() 獲取鏡頭邊框顏色
void setBoxBorderColor(@ColorInt int boxBorderColor) 設(shè)置鏡頭邊框顏色
int getBoxBorderWidth() 獲取鏡頭邊框粗細(xì)
void setBoxBorderWidth(int boxBorderWidth) 設(shè)置鏡頭邊框粗細(xì)
int getBoxAngleColor() 獲取鏡頭四角顏色
void setBoxAngleColor(@ColorInt int boxAngleColor) 設(shè)置鏡頭四角顏色
int getBoxAngleBorderWidth() 獲取鏡頭四角粗細(xì)
void setBoxAngleBorderWidth(int boxAngleBorderWidth) 設(shè)置鏡頭四角粗細(xì)
int getBoxAngleLength() 獲取鏡頭四角長(zhǎng)度
void setBoxAngleLength(int boxAngleLength) 設(shè)置鏡頭四角長(zhǎng)度
int getCameraLensTopMargin() 獲取鏡頭與頂部的距離
void setCameraLensTopMargin(int cameraLensTopMargin) 設(shè)置鏡頭與頂部的距離
float getCameraLensSizeRatio() 獲取鏡頭占寬度的百分比
void setCameraLensSizeRatio(@FloatRange(from = 0.0, to = 1.0) float cameraLensSizeRatio) 設(shè)置鏡頭占寬度的百分比
String getText() 獲取提示文字
void setText(String text) 設(shè)置提示文字
boolean isTextMathParent() 提示文字是否對(duì)齊邊緣
void setTextMathParent(boolean textMathParent) 設(shè)置提示文字是否對(duì)齊邊緣
int getTextLocation() 獲取提示文字是在鏡頭上方(下方)
void setTextLocation(@TextLocation int textLocation) 設(shè)置提示文字是在鏡頭上方(下方)
int getTextVerticalMargin() 獲取提示文字與鏡頭間的距離
void setTextVerticalMargin(int textVerticalMargin) 設(shè)置提示文字與鏡頭間的距離
int getTextLeftMargin() 獲取提示文字相對(duì)于左邊的偏移量
void setTextLeftMargin(int textLeftMargin) 設(shè)置提示文字相對(duì)于左邊的偏移量
int getTextRightMargin() 獲取提示文字相對(duì)于右邊的偏移量
void setTextRightMargin(int textRightMargin) 設(shè)置提示文字相對(duì)于右邊的偏移量
int getCameraLensShape() 獲取鏡頭的形狀(圓形、方形)
void setCameraLensShape(@CameraLensShape int cameraLensShape) 設(shè)置鏡頭的形狀(圓形、方形)
getCameraLensRect() 獲取相機(jī)鏡頭在View中的位置。我們可以利用這個(gè)Rect位置信息做很多事情,例如在相機(jī)預(yù)覽中生成這塊區(qū)域的圖片(或者識(shí)別此區(qū)域的數(shù)據(jù)信息)

使用示例

組件類型 使用示例
CameraLensView CameraLensViewFragment
ScannerBarView ScannerBarViewFragment

擴(kuò)展控件:組合CameraLensView、ScannerBarView實(shí)現(xiàn)掃描動(dòng)畫——CameraScannerMaskView

camera_scanner_mask_view

屬性

子View 類型 屬性
cameraLensView CameraLensView CameraLensView所有屬性
scannerBarView ScannerBarView ScannerBarView所有屬性

初始化視圖:添加cameraLensView、scannerBarView

    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));
    }

根據(jù)相機(jī)鏡頭(或掃描框)的位置,放置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);
    }

提供動(dòng)畫控制相關(guān)方法

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

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

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

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

使用示例

組件類型 使用示例
CameraScannerMaskView CameraScannerMaskViewFragment

一個(gè)強(qiáng)大的二維碼掃描相機(jī)遮罩控件從此問世。

篇尾!

??給個(gè)??支持下唄,謝謝!QQ:1006368252、WeChat:eoy9527

最甜美的是愛情,最苦澀的也是愛情。 —— 菲·貝利

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • ¥開啟¥ 【iAPP實(shí)現(xiàn)進(jìn)入界面執(zhí)行逐一顯】 〖2017-08-25 15:22:14〗 《//首先開一個(gè)線程,因...
    小菜c閱讀 7,328評(píng)論 0 17
  • 1: 獲取控件寬高 控件View有g(shù)etHeight()和getwidth()方法可以獲取寬高,但是如果直接在on...
    自由人是工程師閱讀 2,003評(píng)論 0 0
  • 微信里怨氣最重的群?看到這個(gè)標(biāo)題,你腦海里跳出的是什么群?工作群?家族群?閨蜜群?同學(xué)群?反正肯定不是007的群....
    封封林閱讀 1,287評(píng)論 8 11
  • 很多人害怕買茶被忽悠,這篇文章,可以讓你初步掌握綠茶的購買技巧。 1. 淺井是一個(gè)茶葉小白,有一天她去買綠茶,老板...
    立七閱讀 1,029評(píng)論 2 12
  • 最近瞎忙活太多,熬夜成癮,沒有上課沒有及時(shí)作業(yè)。剛聽了第二課,看到作業(yè)布置,然后公眾號(hào)里正好有個(gè)文章寫:“女人過的...
    Niniane閱讀 169評(píng)論 0 0

友情鏈接更多精彩內(nèi)容