开篇
在我们的开发过程中经常需要用到二维码扫描,而二维码扫描界面的相机遮罩又是常用控件,在近期的项目开发中有使用到,所以就整理出来,优化成一个强大的相机遮罩控件分享给童鞋们。
- 支持修改遮罩颜色以及透明度
- 支持相机镜头:图片镜头、方形扫描框
- 支持相机镜头(或扫描框)的大小
- 支持设置提示文字以及位置、字体、颜色
- 可获取相机镜头位置Rect
效果截屏
立即体验
扫描以下二维码下载体验App(体验App内嵌版本更新检测功能):
CameraMask库传送门:https://github.com/JustinRoom/CameraMaskDemo
简析源码
-
CameraLensView
属性:
名称 | 类型 | 描述 |
---|---|---|
clvCameraLensSizeRatio | float | 相机镜头(或扫描框)大小占View宽度的百分比 |
clvCameraLensTopMargin | dimension | 相机镜头(或扫描框)与顶部的间距 |
clvCameraLensShape | enum(square 、circular ) |
相机镜头(或扫描框)形状 |
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(belowCameraLens 、 aboveCameraLens ) |
提示文字位于相机镜头(或扫描框)上方(或下方) |
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 |
扩展控件:组合CameraLensView
、ScannerBarView
实现扫描动画——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));
}
根据相机镜头(或扫描框)的位置,放置
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:1006368252
、WeChat:eoy9527
最甜美的是爱情,最苦涩的也是爱情。 —— 菲·贝利