開篇
??在我們的開發(fā)過程中經(jīng)常需要用到二維碼掃描,而二維碼掃描界面的相機(jī)遮罩又是常用控件,在近期的項(xiàng)目開發(fā)中有使用到,所以就整理出來,優(yōu)化成一個(gè)強(qiáng)大的相機(jī)遮罩控件分享給童鞋們。
- 支持修改遮罩顏色以及透明度
- 支持相機(jī)鏡頭:圖片鏡頭、方形掃描框
- 支持相機(jī)鏡頭(或掃描框)的大小
- 支持設(shè)置提示文字以及位置、字體、顏色
- 可獲取相機(jī)鏡頭位置Rect
效果截屏




立即體驗(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(square、circular) |
相機(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(belowCameraLens、 aboveCameraLens) |
提示文字位于相機(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

屬性
| 子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
最甜美的是愛情,最苦澀的也是愛情。 —— 菲·貝利