[TOC]
前言
上一篇,扫码的基本功能已经实现,不过还存在一些问题
- 扫码界面不是我们常见的二维码扫描界面
- 方法调用过于繁琐,有大量的重复代码
本篇博文将介绍如何对第三方库进行二次开发,自定义我们想要的扫码界面以及对方法进行封装方便调用
自定义扫码界面
获取第三方库源码
- 下载源码
因为本篇是基于 zxing-android-embedded进行开发,因此首先获取该库的源码,打开该库的github地址,最新版本是3.5.0,,将项目clone或者download下来,下载后的项目结构入下图所示:
其中红框所示的zxing-android-embedded库就是我们正在使用的库,其目录结构如下所示
- 导入源码到项目中:
这个库的结构和常见的android studio创建的model不太一样,为了方便开发,我们选择重新创建一个android-library的model并将该库的代码导入。
第一步:
重新创建一个android-library的model,这里取名为zxingcore
第二步:
zxing-android-embedded目录下的src里面的所有java代码拷贝到zxingcore中java目录下;将res和res-orig两个目录下的文件全部拷贝到zxingcore的res目录下
将AndroidManifest.xml里面的内容拷贝到zxingcore中的AndroidManifest.xml。这里主要是权限的申请和扫码界面activity的注册,对于android 6.0及以上版本,需要再实际项目中进行动态权限的申请。
因为这个库需要引用到谷歌zxing库中的core代码,所以需要再build.gradle中加入对该库的依赖
compile 'com.google.zxing:core:3.2.1'
到此这个第三方库的导入已经完成,我们上一篇sample中通过gradle依赖zxing-android-embedded可以更改为依赖我们自己导入的zxingcore
// compile 'com.journeyapps:zxing-android-embedded:3.4.0'
// compile 'com.google.zxing:core:3.2.1'
compile project(':zxingcore')
自定义扫码界面
在上一篇中我们通过以下代码开启了扫码界面
new IntentIntegrator(this)
.setOrientationLocked(false)
.setDesiredBarcodeFormats(IntentIntegrator.ALL_CODE_TYPES)
.setPrompt("将二维码/条码放入框内,即可自动扫描")
.initiateScan(); // 初始化扫描
这个开启的界面就是CaptureActivity这个activity,这当然不是我们想要的界面,需要调整的地方有三个
- 方向调整,默认是横向的
- 扫码框大小调整
- 扫码框样式调整
- 调整方向
调整方向很简单,把manifest中activity声明的下面代码去掉就好
android:screenOrientation="sensorLandscape"//去掉这段代码
- 扫码框大小调整
打开activity布局文件的zxing_capture.xml,代码很简单,只有一个DecoratedBarcodeView
<merge xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<!--
This Activity is typically full-screen. Therefore we can safely use centerCrop scaling with
a SurfaceView, without fear of weird artifacts. -->
<com.journeyapps.barcodescanner.DecoratedBarcodeView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/zxing_barcode_scanner"
app:zxing_preview_scaling_strategy="centerCrop"
app:zxing_use_texture_view="false"/>
</merge>
在DecoratedBarcodeView中加入两行代码 大小可以自行设置
app:zxing_framing_rect_height="150dp"//扫码框高
app:zxing_framing_rect_width="200dp"//扫码框宽
重新build项目并运行我们的sample可以看到界面已经好看很多了,扫码框也变成了正常的大小
优化扫码框
扫码界面虽然变了,不过这还不是我们想要的,先看一下微信的扫码框
可以看到,正常扫码框都有一个滑动的条条和四个边框,接下来就来添加这部分东西。
从上面的步骤可以看出整个扫码界面其实就是一个DecoratedBarcodeView,打开DecoratedBarcodeView的代码可以看到它引用的布局就是layout文件中的zxing_barcode_scanner.xml,打开该布局文件
<merge xmlns:android="http://schemas.android.com/apk/res/android">
//封装了摄像头的一个类,用来获取拍摄画面
<com.journeyapps.barcodescanner.BarcodeView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/zxing_barcode_surface"/>
// 类似遮罩的作用覆盖在BarcodeView上
<com.journeyapps.barcodescanner.ViewfinderView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/zxing_viewfinder_view"/>
//提示语
<TextView android:id="@+id/zxing_status_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|center_horizontal"
android:background="@color/zxing_transparent"
android:text="@string/zxing_msg_default_status"
android:textColor="@color/zxing_status_text"/>
</merge>
三个view我都做了注释,想要改变看到的扫码界面和扫码框,需要重写的就是ViewfinderView这个类
首先明确一下我们需要加入的东西
- 取消原有红色条,加入滑动的扫码条
- 四个边框
- 提示文字移动到扫码框下方
首先定义需要的属性和方法,在修改源码的时候我习惯将自己写的代码都放在最后面,并且用一条分割线分割,
- 定义属性
/*-----------------------自定义方法和属性--------------------------*/
//画边框相关属性
private Paint mLinePaint;//边框画笔
private final int mLineColor = Color.BLUE;//边框的颜色
//滑动条相关属性
private Bitmap mLineBm;//滑动条图片
private RectF mLineReact;//滑动条区域
private final int mStepSize = 12;//滑动条每次滑动的速度
private final int mLineHeight = 30;//滑动条的高度
private boolean isBottom = false;//滑动条是否滑动到扫码框底部
//文字相关属性
private Paint mTextPaint;//画提示语的画笔
private String mPromptText;//扫码的提示语
private int mTextMargin;//提示语距离扫描框的大小
- 定义方法
有了属性自然需要有初始化的方法和操作的逻辑代码 创建2个新方法,初始化一般放在构造方法中,操作的代码是在onDraw中调用的
/**
* 改方法在构造方法中调用用来初始化属性
*
* @param context
*/
private void customInit(Context context) {
//初始化滑动线的画笔
mLinePaint = new Paint();
mLinePaint.setStyle(Paint.Style.FILL);
mLinePaint.setStrokeWidth(20);
mLinePaint.setColor(mLineColor);
//初始化滑动条
mLineBm = BitmapFactory.decodeResource(getResources(), R.drawable.lan);
//初始化提示语的画笔
mTextPaint = new Paint();
mTextPaint.setColor(Color.WHITE);
mTextPaint.setTextSize(sp2px(14));
mTextPaint.setTextAlign(Paint.Align.CENTER);
mTextMargin = sp2px(20);
}
/**
* 该方法在onDraw中调用,放在
* Rect frame = framingRect;
* Rect previewFrame = previewFramingRect;
* 两段代码之后
*
* @param frame
* @param canvas
*/
private void customDraw(Rect frame, Canvas canvas) {
drawSlipLine(frame, canvas);//画滑动的线
drawEdge(frame, canvas);//画边框
drawPromptText(frame, canvas);//画提示语
}
- 画滑动的线 drawSlipLine(frame, canvas)
首先要去掉原先的红色线,注释掉onDraw()方法中下图所示代码 ,还可以注释掉下面那一部分画跳动小点的代码,取消跳动小点,看个人需求。滑动的线一般是一张图片,这类我准备了一张图放在drawable目录下 lan.png 图片可以自行替换
实现drawSlipLine()方法
/**
* 画移动的短线
*
* @param frame
* @param canvas
*/
private void drawSlipLine(Rect frame, Canvas canvas) {
if (mLineReact == null) {
mLineReact = new RectF(frame.left + 5, frame.top, frame.right - 5, frame.top + mLineHeight);
}
if (isBottom) {
mLineReact.set(frame.left + 5, frame.top, frame.right - 5, frame.top + mLineHeight);
}
mLineReact.offset(0, mStepSize);
canvas.drawBitmap(mLineBm, null, mLineReact, null);
isBottom = mLineReact.bottom + mStepSize > frame.bottom;
}
效果
- 画边框 drawEdge(frame, canvas)
边框的颜色,在边界内还是边界外可以自行调整位置,这里面传入的fram是扫描框所在的长方形Rect
/**
* 画框边的四个角
*
* @param frame
* @param canvas
*/
private void drawEdge(Rect frame, Canvas canvas) {
canvas.drawRect(frame.left - 10, frame.top, frame.left, frame.top + 50, mLinePaint);
canvas.drawRect(frame.left - 10, frame.top - 10, frame.left + 50, frame.top, mLinePaint);
canvas.drawRect(frame.right - 50, frame.top - 10, frame.right + 10, frame.top, mLinePaint);
canvas.drawRect(frame.right, frame.top, frame.right + 10, frame.top + 50, mLinePaint);
canvas.drawRect(frame.left - 10, frame.bottom - 50, frame.left, frame.bottom, mLinePaint);
canvas.drawRect(frame.left - 10, frame.bottom, frame.left + 50, frame.bottom + 10, mLinePaint);
canvas.drawRect(frame.right - 50, frame.bottom, frame.right, frame.bottom + 10, mLinePaint);
canvas.drawRect(frame.right, frame.bottom - 50, frame.right + 10, frame.bottom + 10, mLinePaint);
}
效果
- 画提示语
从上面DecoratedBarcodeView的布局可以看出,提示语是一个textview,位置是bottom,这不符合我们的需要,通过调整textview距离底边框的margin也可以调整位置,不过我这边采用的是在ViewfinderView遮罩层画这个提示语,取消textview
第一步,注释掉zxing_barcode_scanner.xml里面的textview
<merge xmlns:android="http://schemas.android.com/apk/res/android">
<com.journeyapps.barcodescanner.BarcodeView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/zxing_barcode_surface"/>
<com.journeyapps.barcodescanner.ViewfinderView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/zxing_viewfinder_view"/>
<!--<TextView android:id="@+id/zxing_status_view"-->
<!--android:layout_width="wrap_content"-->
<!--android:layout_height="wrap_content"-->
<!--android:layout_gravity="bottom|center_horizontal"-->
<!--android:background="@color/zxing_transparent"-->
<!--android:text="@string/zxing_msg_default_status"-->
<!--android:textColor="@color/zxing_status_text"/>-->
</merge>
第二步,需要将提示语传递到ViewfinderView内部去,还记得提示语是在哪里设置的吗?重新看一下我们打开扫码页面的代码,
new IntentIntegrator(this)
.setOrientationLocked(false)
.setDesiredBarcodeFormats(IntentIntegrator.ALL_CODE_TYPES)
.setPrompt("将二维码/条码放入框内,即可自动扫描")
.initiateScan(); // 初始化扫描
通过setPrompt()方法传递,跟踪这个方法可以发现所有设置的信息最后都会放到一个intent并传递给CaptureActivity,重新打开CaptureActivity,在onCreate()中将设置的参数传递给一个CaptureManager
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
barcodeScannerView = initializeContent();
//初始化配置扫码界面
capture = new CaptureManager(this, barcodeScannerView);
//intent中携带了通过IntentIntegrator设置的参数
capture.initializeFromIntent(getIntent(), savedInstanceState);
capture.decode();
}
CaptureManager内部再次把intent传递给DecoratedBarcodeView的initializeFromIntent(Intent intent)方法,看这个方法中有这么一句
String customPromptMessage = intent.getStringExtra(Intents.Scan.PROMPT_MESSAGE);
if (customPromptMessage != null) {
setStatusText(customPromptMessage);
}
/**
/**
* 设置提示语
* @param text
*/
public void setStatusText(String text) {
// statusView is optional when using a custom layout
if(statusView != null) {
statusView.setText(text);
}
}
可以看出在这个方法里面将提示语设置给了statusView(即我们上面注释掉的textview)
理清了这个再进行修改就容易了
- 首先去掉DecoratedBarcodeView中statusView变量和与它相关的代码,注释掉就好了。
- 在ViewfinderView中添加一个方法,mPromptText属性的set()方法,这个属性我们一开始已经定义过了。在DecoratedBarcodeView的setStatusText中调用
- 完善之前在ViewfinderView中定义的drawPromptText(frame, canvas)方法将文字画到扫描界面
/**
* 传入提示语
*
* @param text
*/
public void setPromptText(String text) {
this.mPromptText = text;
}
修改DecoratedBarcodeView的setStatusText()方法
public void setStatusText(String text) {
// statusView is optional when using a custom layout
// if(statusView != null) {
// statusView.setText(text);
// }
//viewFinder就是DecoratedBarcodeView持有的ViewfinderView
viewFinder.setPromptText(text);
}
这样我们已经把提示语传递到了ViewfinderView中,接下来就是将它画到扫描框的下方,具体想要画的位置可以自行调整。
实现drawPromptText(frame, canvas)的代码, mTextPaint的初始化代码customInit()中,文字样式可以自行调整
/**
* 画提示语
*
* @param frame
* @param canvas
*/
private void drawPromptText(Rect frame, Canvas canvas) {
int startX = frame.left + frame.width() / 2;
int startY = frame.bottom + mTextMargin;
if (!TextUtils.isEmpty(mPromptText)) {
canvas.drawText(mPromptText, startX, startY, mTextPaint);
}
}
效果
扫码框位置调整
默认扫码框是在整个屏幕的中间,如果想要调整扫码框的位置,比如上移或者下一的话,找到CameraPreview中的calculateFramingRect方法,添加如下代码 intersection.offset(0,-150); 具体调整根据需求而定。
protected Rect calculateFramingRect(Rect container, Rect surface) {
// intersection is the part of the container that is used for the preview
Rect intersection = new Rect(container);
boolean intersects = intersection.intersect(surface);
if(framingRectSize != null) {
// Specific size is specified. Make sure it's not larger than the container or surface.
int horizontalMargin = Math.max(0, (intersection.width() - framingRectSize.width) / 2);
int verticalMargin = Math.max(0, (intersection.height() - framingRectSize.height) / 2);
intersection.inset(horizontalMargin, verticalMargin);
/**将默认的扫码框位置上调150px*/
intersection.offset(0,-150);
return intersection;
}
// margin as 10% (default) of the smaller of width, height
int margin = (int)Math.min(intersection.width() * marginFraction, intersection.height() * marginFraction);
intersection.inset(margin, margin);
if (intersection.height() > intersection.width()) {
// We don't want a frame that is taller than wide.
intersection.inset(0, (intersection.height() - intersection.width()) / 2);
}
return intersection;
}
总结
通过对源码的二次开发,优化了扫码界面,初步实现了常见的扫码框效果,下一篇将介绍对API方法的封装。如有不对的地方还请指正,感谢。