我们知道zxing是一个强大的处理二维码和条形码等的开源库,本篇文章记录一下自己在项目中集成zxing开源库的过程。
implementation 'com.google.zxing:core:3.3.3'
在AndroidManifest中申请相应权限:
导入的代码文件如下(源码在末尾):
相关的资源文件:
1、在res/values下新建ids.xml文件,引入下面id:
2、在res/values下新建attrs.xml文件,加入扫码框的属性,主要是ViewfinderView在使用:
3、在res下新建raw目录,导入beep.mp3,实现扫码成功的滴滴音效,BeepManager在使用
上面是一些比较重要的资源。
然后介绍一下几个主要的类:
1、ViewfinderView:自定义扫描框,代码如下,因为有注释,就不多说明了。
public final class ViewfinderView extends View {
private static final long ANIMATION_DELAY = 10L;
private static final int OPAQUE = 1;
private static final int CORNER_INSIDE = 1; //四个边角在扫描区内
private static final int CORNER_OUTSIDE = 2; //四个边角在扫描区外
private Paint paint;
//扫描区四个边角的颜色
private int cornerColor;
//扫描区边角的大小
private float cornerSize;
//扫描区边角的宽度
private float cornerStrokeWidth;
//边角的方向,在扫描区域内还是扫描区域外
private int cornerPosition;
//扫描线颜色
private int lineColor;
//扫描线高度
private float lineHeight;
//扫描线移动距离
private float lineMoveDistance;
//扫描区域宽度度
private float frameWidth;
//扫描区域高度
private float frameHeight;
//扫描区域中心位置的X坐标,默认正中间,在onLayout中设置
private float frameCenterX;
//扫描区域中心位置的Y坐标,默认正中间,在onLayout中设置
private float frameCenterY;
//扫描区域边框颜色
private int frameColor;
//扫描区域边框宽度
private float frameStrokeWidth;
//模糊区域颜色
private int maskColor;
//扫描点的颜色
private int resultPointColor;
//扫描区域提示文本
private String labelText;
//扫描区域提示文本颜色
private int labelTextColor;
//扫描区域提示文本字体大小
private float labelTextSize;
//扫描区域提示文本的边距
private float labelTextMargin;
public static int scannerStart = 0;
public static int scannerEnd = 0;
private Collection possibleResultPoints;
private Collection lastPossibleResultPoints;
// This constructor is used when the class is built from an XML resource.
public ViewfinderView(Context context, AttributeSet attrs) {
super(context, attrs);
//初始化自定义属性信息
TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.ViewfinderView);
cornerColor = ta.getColor(R.styleable.ViewfinderView_corner_color, getResources().getColor(R.color.colorPrimary));
cornerSize = ta.getDimension(R.styleable.ViewfinderView_corner_size, dp2px(context, 28));
cornerStrokeWidth = ta.getDimension(R.styleable.ViewfinderView_corner_stroke_width, dp2px(context, 4));
cornerPosition = ta.getInt(R.styleable.ViewfinderView_corner_position, CORNER_INSIDE);
lineColor = ta.getColor(R.styleable.ViewfinderView_line_color, getResources().getColor(R.color.colorPrimary));
lineHeight = ta.getDimension(R.styleable.ViewfinderView_line_height, dp2px(context, 3));
lineMoveDistance = ta.getDimension(R.styleable.ViewfinderView_line_move_distance, dp2px(context, 2));
frameWidth = ta.getDimension(R.styleable.ViewfinderView_frame_width, dp2px(context, 220));
frameHeight = ta.getDimension(R.styleable.ViewfinderView_frame_height, dp2px(context, 220));
frameCenterX = ta.getDimension(R.styleable.ViewfinderView_frame_centerX, -1);
frameCenterY = ta.getDimension(R.styleable.ViewfinderView_frame_centerY, -1);
frameColor = ta.getColor(R.styleable.ViewfinderView_frame_color, Color.parseColor("#90FFFFFF"));
frameStrokeWidth = ta.getDimension(R.styleable.ViewfinderView_frame_stroke_width, dp2px(context, 0.2f));
maskColor = ta.getColor(R.styleable.ViewfinderView_mask_color, Color.parseColor("#60000000"));
resultPointColor = ta.getColor(R.styleable.ViewfinderView_result_point_color, Color.TRANSPARENT);
labelText = ta.getString(R.styleable.ViewfinderView_label_text);
labelTextColor = ta.getColor(R.styleable.ViewfinderView_label_text_color, Color.WHITE);
labelTextSize = ta.getDimension(R.styleable.ViewfinderView_label_text_size, sp2px(context, 15));
labelTextMargin = ta.getDimension(R.styleable.ViewfinderView_label_text_margin, dp2px(context, 18));
ta.recycle();
paint = new Paint();
paint.setAntiAlias(true);
possibleResultPoints = new HashSet(5);
}
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
//如果没有设置frameCenterX和frameCenterY默认布局正中间的X、Y坐标
frameCenterX = (frameCenterX == -1) ? getWidth() / 2f : frameCenterX;
frameCenterY = (frameCenterY == -1) ? getHeight() / 2f : frameCenterY;
//设置扫描区域位置
int leftOffset = (int) (frameCenterX - frameWidth / 2f);
int topOffset = (int) (frameCenterY - frameHeight / 2f);
//设置扫描区不超过屏幕
leftOffset = leftOffset > 0 ? leftOffset : 0;
topOffset = topOffset > 0 ? topOffset : 0;
Rect rect = new Rect();
rect.left = leftOffset;
rect.top = topOffset;
rect.right = (int) (leftOffset + frameWidth);
rect.bottom = (int) (topOffset + frameHeight);
CameraManager.get().setFramingRect(rect);
}
@Override
public void onDraw(Canvas canvas) {
Rect frame = CameraManager.get().getFramingRect();
if (frame == null) {
return;
}
if (scannerStart == 0 || scannerEnd == 0) {
scannerStart = frame.top;
scannerEnd = frame.bottom;
}
int width = canvas.getWidth();
int height = canvas.getHeight();
//绘制模糊区域
drawExterior(canvas, frame, width, height);
//绘制扫描区边框
drawFrame(canvas, frame);
//绘制边角
drawCorner(canvas, frame);
//绘制提示信息
drawTextInfo(canvas, frame);
//绘制扫描线
drawScanLine(canvas, frame);
//绘制闪烁点
drawResultPoint(canvas, frame);
// Request another update at the animation interval, but only repaint the laser line,
// not the entire viewfinder mask.
//指定重绘区域,该方法会在子线程中执行
postInvalidateDelayed(ANIMATION_DELAY, frame.left, frame.top, frame.right, frame.bottom);
}
// 绘制模糊区域 Draw the exterior (i.e. outside the framing rect) darkened
private void drawExterior(Canvas canvas, Rect frame, int width, int height) {
paint.setColor(maskColor);
canvas.drawRect(0, 0, width, frame.top, paint);
canvas.drawRect(0, frame.top, frame.left, frame.bottom, paint);
canvas.drawRect(frame.right, frame.top, width, frame.bottom, paint);
canvas.drawRect(0, frame.bottom, width, height, paint);
}
// 绘制扫描区边框 Draw a two pixel solid black border inside the framing rect
private void drawFrame(Canvas canvas, Rect frame) {
if (frameStrokeWidth > 0) {
paint.setColor(frameColor);
if (cornerPosition == CORNER_INSIDE) { //边角在扫描区内
//左边
canvas.drawRect(frame.left, frame.top, frame.left + frameStrokeWidth, frame.bottom, paint);
//上边
canvas.drawRect(frame.left, frame.top, frame.right, frame.top + frameStrokeWidth, paint);
//右边
canvas.drawRect(frame.right - frameStrokeWidth, frame.top, frame.right, frame.bottom, paint);
//下边
canvas.drawRect(frame.left, frame.bottom - frameStrokeWidth, frame.right, frame.bottom, paint);
} else { //边角在扫描区外
//左边
canvas.drawRect(frame.left - frameStrokeWidth, frame.top - frameStrokeWidth,
frame.left, frame.bottom + frameStrokeWidth, paint);
//上边
canvas.drawRect(frame.left - frameStrokeWidth, frame.top - frameStrokeWidth,
frame.right + frameStrokeWidth, frame.top, paint);
//右边
canvas.drawRect(frame.right, frame.top - frameStrokeWidth, frame.right + frameStrokeWidth,
frame.bottom + frameStrokeWidth, paint);
//下边
canvas.drawRect(frame.left - frameStrokeWidth, frame.bottom, frame.right + frameStrokeWidth,
frame.bottom + frameStrokeWidth, paint);
}
}
}
//绘制边角
private void drawCorner(Canvas canvas, Rect frame) {
if (cornerSize > 0 && cornerStrokeWidth > 0) {
paint.setColor(cornerColor);
if (cornerPosition == CORNER_INSIDE) { //绘制在扫描区域内区
//左上
canvas.drawRect(frame.left, frame.top, frame.left + cornerSize, frame.top + cornerStrokeWidth, paint);
canvas.drawRect(frame.left, frame.top, frame.left + cornerStrokeWidth, frame.top + cornerSize, paint);
//右上
canvas.drawRect(frame.right - cornerSize, frame.top, frame.right, frame.top + cornerStrokeWidth, paint);
canvas.drawRect(frame.right - cornerStrokeWidth, frame.top, frame.right, frame.top + cornerSize, paint);
//左下
canvas.drawRect(frame.left, frame.bottom - cornerSize, frame.left + cornerStrokeWidth, frame.bottom, paint);
canvas.drawRect(frame.left, frame.bottom - cornerStrokeWidth, frame.left + cornerSize, frame.bottom, paint);
//右下
canvas.drawRect(frame.right - cornerSize, frame.bottom - cornerStrokeWidth, frame.right, frame.bottom, paint);
canvas.drawRect(frame.right - cornerStrokeWidth, frame.bottom - cornerSize, frame.right, frame.bottom, paint);
} else { //绘制在扫描区域外区
//左上
canvas.drawRect(frame.left - cornerStrokeWidth, frame.top - cornerStrokeWidth,
frame.left - cornerStrokeWidth + cornerSize, frame.top, paint);
canvas.drawRect(frame.left - cornerStrokeWidth, frame.top - cornerStrokeWidth,
frame.left, frame.top - cornerStrokeWidth + cornerSize, paint);
//右上
canvas.drawRect(frame.right + cornerStrokeWidth - cornerSize, frame.top - cornerStrokeWidth,
frame.right + cornerStrokeWidth, frame.top, paint);
canvas.drawRect(frame.right, frame.top - cornerStrokeWidth,
frame.right + cornerStrokeWidth, frame.top - cornerStrokeWidth + cornerSize, paint);
//左下
canvas.drawRect(frame.left - cornerStrokeWidth, frame.bottom, frame.left - cornerStrokeWidth + cornerSize,
frame.bottom + cornerStrokeWidth, paint);
canvas.drawRect(frame.left - cornerStrokeWidth, frame.bottom + cornerStrokeWidth - cornerSize,
frame.left, frame.bottom + cornerStrokeWidth, paint);
//右下
canvas.drawRect(frame.right + cornerStrokeWidth - cornerSize, frame.bottom,
frame.right + cornerStrokeWidth, frame.bottom + cornerStrokeWidth, paint);
canvas.drawRect(frame.right, frame.bottom + cornerStrokeWidth - cornerSize,
frame.right + cornerStrokeWidth, frame.bottom + cornerStrokeWidth, paint);
}
}
}
//绘制文本
private void drawTextInfo(Canvas canvas, Rect frame) {
if (!TextUtils.isEmpty(labelText)) {
paint.setColor(labelTextColor);
paint.setTextSize(labelTextSize);
paint.setTextAlign(Paint.Align.CENTER);
Paint.FontMetrics fm = paint.getFontMetrics();
float baseY = frame.bottom + labelTextMargin - fm.ascent;
canvas.drawText(labelText, frame.left + frame.width() / 2, baseY, paint);
}
}
//绘制扫描线
private void drawScanLine(Canvas canvas, Rect frame) {
if (lineHeight > 0) {
paint.setColor(lineColor);
RadialGradient radialGradient = new RadialGradient(
(float) (frame.left + frame.width() / 2),
(float) (scannerStart + lineHeight / 2),
360f,
lineColor,
shadeColor(lineColor),
Shader.TileMode.MIRROR);
paint.setShader(radialGradient);
if (scannerStart <= scannerEnd) {
//椭圆
RectF rectF = new RectF(frame.left + 2 * lineHeight, scannerStart, frame.right - 2 * lineHeight,
scannerStart + lineHeight);
canvas.drawOval(rectF, paint);
scannerStart += lineMoveDistance;
} else {
scannerStart = frame.top;
}
paint.setShader(null);
}
}
private void drawResultPoint(Canvas canvas, Rect frame) {
if (resultPointColor != Color.TRANSPARENT) {
Collection currentPossible = possibleResultPoints;
Collection currentLast = lastPossibleResultPoints;
if (currentPossible.isEmpty()) {
lastPossibleResultPoints = null;
} else {
possibleResultPoints = new HashSet(5);
lastPossibleResultPoints = currentPossible;
paint.setAlpha(OPAQUE);
paint.setColor(resultPointColor);
for (ResultPoint point : currentPossible) {
canvas.drawCircle(frame.left + point.getX(), frame.top + point.getY(), 6.0f, paint);
}
}
if (currentLast != null) {
paint.setAlpha(OPAQUE / 2);
paint.setColor(resultPointColor);
for (ResultPoint point : currentLast) {
canvas.drawCircle(frame.left + point.getX(), frame.top + point.getY(), 3.0f, paint);
}
}
}
}
//处理颜色模糊
public int shadeColor(int color) {
String hax = Integer.toHexString(color);
String result = "20" + hax.substring(2);
return Integer.valueOf(result, 16);
}
public void drawViewfinder() {
invalidate();
}
public void addPossibleResultPoint(ResultPoint point) {
possibleResultPoints.add(point);
}
private int dp2px(Context context, float dpValue) {
float density = context.getApplicationContext().getResources().getDisplayMetrics().density;
return (int) (dpValue * density + 0.5f);
}
private int sp2px(Context context, float spValue) {
float scaleDensity = context.getApplicationContext().getResources().getDisplayMetrics().scaledDensity;
return (int) (spValue * scaleDensity + 0.5f);
}
}
2、CaptureActivity:扫码的Activity基类,代码如下;
/**
* Created by xuzhb on 2019/11/16
* Desc:扫码的Activity类
* 整个Activity最重要的两个控件是一个SurfaceView(摄像头)和一个ViewfinderView(扫描区)
* 对于继承CaptureActivity的Activity子类来说,
* 可以选择在自己的布局中定义和CaptureActivity的布局文件id相同的控件,
* 这样即使它们在两个布局中表现不同也能执行相同的逻辑,包括其他控件
* 或者选择重写getSurfaceView()和getViewfinderView()返回对应的两个控件,
* 扫码最终是在handleDecode(Result result, Bitmap bitmap)处理扫描后的结果
*/
public class CaptureActivity extends AppCompatActivity implements SurfaceHolder.Callback {
private static final String TAG = "CaptureActivity";
private static final int IMAGE_PICKER = 1999;
private BeepManager mBeepManager;
private CaptureActivityHandler mHandler;
private Vector mDecodeFormats;
private String mCharacterSet;
private InactivityTimer mInactivityTimer;
private boolean hasSurface = false;
private boolean isLightOn = false; //是否打开闪光灯
private boolean isPlayBeep = true; //是否开启扫描后的滴滴声
private boolean isVibrate = true; //是否震动
private String mPhotoPath; //选中的图片路径
private TitleBar mTitleBar;
private SurfaceView mSurfaceView;
private ViewfinderView mViewfinderView;
private LinearLayout mLightLl;
private ImageView mLightIv;
private TextView mLightTv;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(getLayoutId());
CameraManager.init(getApplicationContext());
mBeepManager = new BeepManager(this);
hasSurface = false;
mInactivityTimer = new InactivityTimer(this);
handleView(savedInstanceState);
initView();
initListener();
}
protected int getLayoutId() {
return R.layout.activity_capture;
}
protected void handleView(@Nullable Bundle savedInstanceState) {
}
private void initView() {
mTitleBar = findViewById(R.id.title_bar);
mSurfaceView = findViewById(R.id.surfaceView);
mViewfinderView = findViewById(R.id.viewfinderView);
mLightLl = findViewById(R.id.light_ll);
mLightIv = findViewById(R.id.light_iv);
mLightTv = findViewById(R.id.light_tv);
}
protected void initListener() {
//因为继承CaptureActivity的Activity子类的布局不一定包含id为title_bar和light_ll的控件,
//没有的话如果子类通过super.initListener()覆写时会因为找不到而报异常,所以这里加了一个判空;
//如果子类的布局中包含id相同的控件,则不需要在子类中再重写相同的逻辑
if (mTitleBar != null) {
StatusBarUtil.INSTANCE.darkModeAndPadding(this, mTitleBar, Color.BLACK, 0, false);
mTitleBar.setOnLeftClickListener(v -> {
finish();
return null;
});
mTitleBar.setOnRightClickListener(v -> {
openAlbum(); //打开相册选取图片扫描
return null;
});
}
if (mLightLl != null) {
mLightLl.setOnClickListener(v -> switchLight()); //打开或关闭闪光灯
}
}
//打开相册
protected void openAlbum() {
Intent intent = new Intent(Intent.ACTION_PICK, null);
intent.setDataAndType(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, "image/*");
startActivityForResult(intent, IMAGE_PICKER);
}
//开启/关闭闪光灯
private void switchLight() {
if (CameraManager.get() != null) {
if (isLightOn) {
mLightTv.setText("轻触点亮");
CameraManager.get().turnLightOffFlashLight();
} else {
mLightTv.setText("轻触关闭");
CameraManager.get().turnOnFlashLight();
}
isLightOn = !isLightOn;
mLightIv.setSelected(isLightOn);
}
}
@Override
protected void onResume() {
super.onResume();
SurfaceHolder holder = getSurfaceView().getHolder();
if (hasSurface) {
initCamera(holder);
} else {
holder.addCallback(this);
holder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);
}
mDecodeFormats = null;
mCharacterSet = null;
}
@Override
protected void onPause() {
super.onPause();
if (mHandler != null) {
mHandler.quitSynchronously();
mHandler = null;
}
CameraManager.get().closeDriver();
}
@Override
protected void onDestroy() {
mInactivityTimer.shutdown();
mBeepManager.releaseRing();
super.onDestroy();
}
@Override
public void surfaceCreated(SurfaceHolder holder) {
}
@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
if (!hasSurface) {
hasSurface = true;
initCamera(holder);
}
}
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
hasSurface = false;
}
private void initCamera(SurfaceHolder holder) {
try {
CameraManager.get().openDriver(holder);
} catch (Exception e) {
e.printStackTrace();
}
if (mHandler == null) {
mHandler = new CaptureActivityHandler(this, mDecodeFormats, mCharacterSet);
}
}
//继承CaptureActivity的Activity类,如果SurfaceView的id和CaptureActivity布局中SurfaceView的id不同
//需要重写这个方法,返回自己布局中的SurfaceView
public SurfaceView getSurfaceView() {
return mSurfaceView;
}
//继承CaptureActivity的Activity类,如果ViewfinderView的id和CaptureActivity布局中ViewfinderView的id不同
//需要重写这个方法,返回自己布局中的ViewfinderView
public ViewfinderView getViewfinderView() {
return mViewfinderView;
}
public Handler getHandler() {
return mHandler;
}
public void drawViewfinder() {
getViewfinderView().drawViewfinder();
}
//处理扫描后的结果
public void handleDecode(Result result, Bitmap bitmap) {
mInactivityTimer.onActivity();
if (result != null) {
String text = result.getText();
Log.i(TAG, "识别的结果:" + text);
if (!TextUtils.isEmpty(text)) { //识别成功
playBeepSoundAndVibrate();
returnQRCodeResult(text);
} else {
showToast("很抱歉,识别二维码失败!");
}
} else {
showToast("未发现二维码!");
}
}
private void playBeepSoundAndVibrate() {
if (isPlayBeep) {
mBeepManager.startRing(); //播放扫码的滴滴声
}
if (isVibrate) {
Vibrator vibrator = (Vibrator) getSystemService(Context.VIBRATOR_SERVICE);
if (vibrator != null) {
vibrator.vibrate(200); //震动200毫秒
}
}
}
//返回扫描结果
private void returnQRCodeResult(String result) {
Intent intent = new Intent();
intent.putExtra(QRConstant.SCAN_QRCODE_RESULT, result);
setResult(Activity.RESULT_OK, intent);
finish();
}
private void showToast(CharSequence text) {
runOnUiThread(() -> {
ToastUtil.INSTANCE.showToast(text, true, false, getApplicationContext());
});
}
@Override
protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == IMAGE_PICKER && resultCode == Activity.RESULT_OK) {
if (data != null) {
Uri uri = data.getData();
if (uri != null) {
Cursor cursor = getContentResolver().query(uri, null, null, null, null);
if (cursor != null) {
if (cursor.moveToFirst()) {
mPhotoPath = cursor.getString(cursor.getColumnIndex(MediaStore.Images.Media.DATA));
}
cursor.close();
if (!TextUtils.isEmpty(mPhotoPath)) {
//可以加个提示正在扫描的加载框,如showLoadingDialog("正在扫描...")
new Thread(() -> {
handleDecode(QRCodeUtil.decodeImage(mPhotoPath), null);
//取消加载框,dismissLoadingDialog()
}).start();
} else {
Log.e(TAG, "未找到图片");
}
}
}
}
}
}
}
最后,附上整个项目的github地址,注:项目使用了视图绑定ViewBinding,所以需要使用AndroidStudio 3.6.x版本。