我们知道zxing是一个强大的处理二维码和条形码等的开源库,本篇文章记录一下自己在项目中集成zxing开源库的过程。
导入依赖
implementation 'com.google.zxing:core:3.3.3'
申请权限
在AndroidManifest中申请相应权限:
<!--相机--> <uses-permission android:name="android.permission.CAMERA" /> <!--震动--> <uses-permission android:name="android.permission.VIBRATE" /> <!--存储--> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
导入相关代码和资源文件
导入的代码文件如下(源码在末尾):
相关的资源文件:
1、在res/values下新建ids.xml文件,引入下面id:
<!--二维码/条形码扫描相关--> <item name="auto_focus" type="id" /> <item name="decode" type="id" /> <item name="decode_failed" type="id" /> <item name="decode_succeeded" type="id" /> <item name="encode_failed" type="id" /> <item name="encode_succeeded" type="id" /> <item name="launch_product_query" type="id" /> <item name="quit" type="id" /> <item name="restart_preview" type="id" /> <item name="return_scan_result" type="id" /> <item name="search_book_contents_failed" type="id" /> <item name="search_book_contents_succeeded" type="id" />
2、在res/values下新建attrs.xml文件,加入扫码框的属性,主要是ViewfinderView在使用:
<!--扫码框属性--> <declare-styleable name="ViewfinderView"> <attr name="corner_color" format="color" /> <attr name="corner_size" format="dimension" /> <attr name="corner_stroke_width" format="dimension" /> <attr name="corner_position" format="enum"> <enum name="inside" value="1" /> <enum name="outside" value="2" /> </attr> <attr name="line_color" format="color" /> <attr name="line_height" format="dimension" /> <attr name="line_move_distance" format="dimension" /> <attr name="frame_width" format="dimension" /> <attr name="frame_height" format="dimension" /> <attr name="frame_centerX" format="dimension" /> <attr name="frame_centerY" format="dimension" /> <attr name="frame_color" format="color" /> <attr name="frame_stroke_width" format="dimension" /> <attr name="mask_color" format="color" /> <attr name="result_point_color" format="color" /> <attr name="label_text" format="string" /> <attr name="label_text_color" format="color" /> <attr name="label_text_size" format="dimension" /> <attr name="label_text_margin" format="dimension" /> </declare-styleable>
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<ResultPoint> possibleResultPoints; private Collection<ResultPoint> 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<ResultPoint>(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<ResultPoint> currentPossible = possibleResultPoints; Collection<ResultPoint> currentLast = lastPossibleResultPoints; if (currentPossible.isEmpty()) { lastPossibleResultPoints = null; } else { possibleResultPoints = new HashSet<ResultPoint>(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<BarcodeFormat> 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版本。