一款好用的二维码扫描组件
2016-10-28 本文已影响3055人
宇是我
简介
之前项目中使用到扫描功能,那时逻辑业务和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();
}
});
最后附上源码地址:https://github.com/yushiwo/QrScan