简介
之前项目中使用到扫描功能,那时逻辑业务和UI是完全耦合在一起,不好维护,也难移植。趁着这次新项目中要使用扫一扫的功能,就将二维码扫描单独提出作为一个组件库,与业务完全分离。最终扫描结果通过回调的方式提供给调用者,用户可以在自己的app中处理扫描结果。库仿造Universal-Image-Loader进行封装,提供一个配置文件,可简单配置扫一扫界面的样式,实现用户UI定制。提供一个控制类,用户通过其提供的接口与组件进行交互,内部实现相对于用户都是透明的。
具体实现
组件是调用zxing进行二维码的编解码计算,这部分不是本文研究的重点。本文主要关注以下几点:
扫描界面的绘制
扫描结果如何回调给用户
组件是如何封装的
如何使用该组件
界面绘制
ViewfinderView是我们的扫描界面,实在onDraw方法中绘制。这里我直接上代码,关键部分会有注释
@Override public void onDraw(Canvas canvas) { //中间的扫描框,你要修改扫描框的大小,去CameraManager里面修改 CameraManager.init(this.getContext().getApplicationContext()); Rect frame = null; try { frame = CameraManager.get().getFramingRect(); } catch (Exception e) { e.printStackTrace(); return; } if (frame == null) { return; } //获取屏幕的宽和高 int width = canvas.getWidth(); int height = canvas.getHeight(); paint.setColor(maskColor); //画出扫描框外面的阴影部分,共四个部分,扫描框的上面到屏幕上面,扫描框的下面到屏幕下面 //扫描框的左边面到屏幕左边,扫描框的右边到屏幕右边 canvas.drawRect(0, 0, width, frame.top, paint); //上 canvas.drawRect(0, frame.top, frame.left, frame.bottom - 1, paint); //左 canvas.drawRect(frame.right, frame.top, width, frame.bottom - 1, paint); //右 canvas.drawRect(0, frame.bottom - 1, width, height, paint); paint.setColor(0xffffffff); canvas.drawLine(frame.left + 1, frame.top + 1, frame.right - 1, frame.top + 1, paint); canvas.drawLine(frame.left + 1,frame.top + 1,frame.left + 1,frame.bottom - 1, paint); canvas.drawLine(frame.left + 1,frame.bottom - 1,frame.right -1,frame.bottom - 1,paint); canvas.drawLine(frame.right -1,frame.top + 1,frame.right - 1,frame.bottom - 1,paint); if (resultBitmap != null) { // Draw the opaque result bitmap over the scanning rectangle paint.setAlpha(OPAQUE); canvas.drawBitmap(resultBitmap, frame.left, frame.top, paint); } else { //画扫描框边上的角,总共8个部分 paint.setColor(angleColor); canvas.drawRect(frame.left, frame.top, frame.left + ScreenRate, frame.top + CORNER_WIDTH, paint); canvas.drawRect(frame.left, frame.top, frame.left + CORNER_WIDTH, frame.top + ScreenRate, paint); canvas.drawRect(frame.right - ScreenRate, frame.top, frame.right, frame.top + CORNER_WIDTH, paint); canvas.drawRect(frame.right - CORNER_WIDTH, frame.top, frame.right, frame.top + ScreenRate, paint); canvas.drawRect(frame.left, frame.bottom - CORNER_WIDTH, frame.left + ScreenRate, frame.bottom, paint); canvas.drawRect(frame.left, frame.bottom - ScreenRate, frame.left + CORNER_WIDTH, frame.bottom, paint); canvas.drawRect(frame.right - ScreenRate, frame.bottom - CORNER_WIDTH, frame.right, frame.bottom, paint); canvas.drawRect(frame.right - CORNER_WIDTH, frame.bottom - ScreenRate, frame.right, frame.bottom, paint); // 如果设置了slideIcon,则显示 if(mSlideIcon != null){// mBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.capture_add_scanning); BitmapDrawable bd = (BitmapDrawable) mSlideIcon; mBitmap = bd.getBitmap(); //绘制中间的线,每次刷新界面,中间的线往下移动SPEEN_DISTANCE if (mBitmap != null){ mBitmap = Bitmap.createScaledBitmap(mBitmap, frame.right - frame.left, mBitmap.getHeight(), true); } //初始化中间线滑动的最上边和最下边 if(!isFirst){ isFirst = true; slideTop = frame.top + mBitmap.getHeight(); slideBottom = frame.bottom; } slideTop += SPEEN_DISTANCE; if(slideTop >= frame.bottom){ slideTop = frame.top + mBitmap.getHeight(); } canvas.drawBitmap(mBitmap, frame.left, slideTop - mBitmap.getHeight(), paint); }else{ //初始化中间线滑动的最上边和最下边 if(!isFirst){ isFirst = true; slideTop = frame.top + MIDDLE_LINE_WIDTH; slideBottom = frame.bottom; } slideTop += SPEEN_DISTANCE; if(slideTop >= frame.bottom){ slideTop = frame.top + MIDDLE_LINE_WIDTH; } canvas.drawRect(frame.left + MIDDLE_LINE_PADDING, slideTop - MIDDLE_LINE_WIDTH,frame.right - MIDDLE_LINE_PADDING, slideTop, paint); } // 画扫描框下面的字 paint.setColor(mTipColor); paint.setTextSize(TEXT_SIZE * density); paint.setTextAlign(Paint.Align.CENTER); paint.setTypeface(Typeface.create("System", Typeface.NORMAL)); canvas.drawText(scanTip, width/2, (float) (frame.bottom + (float)mTipmMargin * density), paint); //只刷新扫描框的内容,其他地方不刷新 postInvalidateDelayed(ANIMATION_DELAY, frame.left, frame.top, frame.right, frame.bottom); } }
其中中间的扫描框,是在CameraManager中实现。
public Rect getFramingRect() { Point screenResolution = configManager.getScreenResolution(); if (framingRect == null) { if (camera == null) { return null; } if (screenResolution == null){ return null; } int width; int height; int topOffset; int leftOffset; int rightOffset; mFrameRate = QrScanProxy.getInstance().getScanFrameRectRate(); width = (int) (screenResolution.x * mFrameRate); if (width < MIN_FRAME_WIDTH) { width = MIN_FRAME_WIDTH; } height = width; leftOffset = (screenResolution.x - width) / 2; topOffset = DeviceUtil.dip2px(DEFAULT_FRAME_MARGIN_TOP); if((height + topOffset) > screenResolution.y){ topOffset = screenResolution.y - height; } framingRect = new Rect(leftOffset, topOffset, leftOffset + width, topOffset + height); Log.d(TAG, "Calculated framing rect: " + framingRect); } return framingRect; }
扫描结果回调
在CaptureActivityHandler中,有一个handleMessage方法。扫描的二维码信息会在这边进行分发。
@Override public void handleMessage(Message message) { ... } else if (message.what == R.id.decode_succeeded) { Log.d(TAG, "Got decode succeeded message"); state = State.SUCCESS; Bundle bundle = message.getData(); Bitmap barcode = bundle == null ? null : (Bitmap) bundle.getParcelable(DecodeThread.BARCODE_BITMAP); activity.handleDecode((Result) message.obj, barcode); ...
组件封装
对外部调用者,提供QrScan.java和QrScanConfiguration.java,前者是组件提供给外部用于和组件交互的方法,后者用于配置组件的相关属性。在组件内部,有一个代理类QrScanProxy.java,所有外部设置的属性作用于内部,都是通过这个类进行分发。具体实现大家可以参考源码~
使用
build.gradle配置
compile 'com.netease.scan:lib-qr-scan:1.0.0'
AndroidManifest配置
// 设置权限 <uses-permission android:name="android.permission.VIBRATE"/> <uses-permission android:name="android.permission.CAMERA" /> <uses-feature android:name="android.hardware.camera" /> <uses-feature android:name="android.hardware.camera.autofocus" /> // 注册activity <activity android:name="com.netease.scan.ui.CaptureActivity" android:screenOrientation="portrait" android:theme="@style/Theme.AppCompat.NoActionBar"/>
初始化
在需要使用此组件的Activity的onCreate方法中,或者在自定义Application的onCreate方法中初始化。
/** * @author hzzhengrui * @Date 16/10/27 * @Description */public class MyApplication extends Application { @Override public void onCreate() { super.onCreate();// // 默认配置// QrScanConfiguration configuration = QrScanConfiguration.createDefault(this); // 自定义配置 QrScanConfiguration configuration = new QrScanConfiguration.Builder(this) .setTitleHeight(53) .setTitleText("来扫一扫") .setTitleTextSize(18) .setTitleTextColor(R.color.white) .setTipText("将二维码放入框内扫描~") .setTipTextSize(14) .setTipMarginTop(40) .setTipTextColor(R.color.white) .setSlideIcon(R.mipmap.capture_add_scanning) .setAngleColor(R.color.white) .setMaskColor(R.color.black_80) .setScanFrameRectRate((float) 0.8) .build(); QrScan.getInstance().init(configuration); } }
启动扫描并处理扫描结果
QrScan.getInstance().launchScan(MainActivity.this, new IScanModuleCallBack() { @Override public void OnReceiveDecodeResult(final Context context, String result) { mCaptureContext = (CaptureActivity)context; AlertDialog dialog = new AlertDialog.Builder(mCaptureContext) .setMessage(result) .setCancelable(false) .setNegativeButton("取消", new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { dialog.dismiss(); QrScan.getInstance().restartScan(mCaptureContext); } }) .setPositiveButton("关闭", new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { dialog.dismiss(); QrScan.getInstance().finishScan(mCaptureContext); } }) .create(); dialog.show(); } });