[TOC]
前言
上一篇,掃碼的基本功能已經(jīng)實現(xiàn),不過還存在一些問題
- 掃碼界面不是我們常見的二維碼掃描界面
- 方法調(diào)用過于繁瑣,有大量的重復(fù)代碼
本篇博文將介紹如何對第三方庫進(jìn)行二次開發(fā),自定義我們想要的掃碼界面以及對方法進(jìn)行封裝方便調(diào)用
自定義掃碼界面
獲取第三方庫源碼
- 下載源碼
因為本篇是基于 zxing-android-embedded進(jìn)行開發(fā),因此首先獲取該庫的源碼,打開該庫的github地址,最新版本是3.5.0,,將項目clone或者download下來,下載后的項目結(jié)構(gòu)入下圖所示:

其中紅框所示的zxing-android-embedded庫就是我們正在使用的庫,其目錄結(jié)構(gòu)如下所示

- 導(dǎo)入源碼到項目中:
這個庫的結(jié)構(gòu)和常見的android studio創(chuàng)建的model不太一樣,為了方便開發(fā),我們選擇重新創(chuàng)建一個android-library的model并將該庫的代碼導(dǎo)入。
第一步:
重新創(chuàng)建一個android-library的model,這里取名為zxingcore

第二步:
zxing-android-embedded目錄下的src里面的所有java代碼拷貝到zxingcore中java目錄下;將res和res-orig兩個目錄下的文件全部拷貝到zxingcore的res目錄下

將AndroidManifest.xml里面的內(nèi)容拷貝到zxingcore中的AndroidManifest.xml。這里主要是權(quán)限的申請和掃碼界面activity的注冊,對于android 6.0及以上版本,需要再實際項目中進(jìn)行動態(tài)權(quán)限的申請。

因為這個庫需要引用到谷歌zxing庫中的core代碼,所以需要再build.gradle中加入對該庫的依賴
compile 'com.google.zxing:core:3.2.1'
到此這個第三方庫的導(dǎo)入已經(jīng)完成,我們上一篇sample中通過gradle依賴zxing-android-embedded可以更改為依賴我們自己導(dǎo)入的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("將二維碼/條碼放入框內(nèi),即可自動掃描")
.initiateScan(); // 初始化掃描
這個開啟的界面就是CaptureActivity這個activity,這當(dāng)然不是我們想要的界面,需要調(diào)整的地方有三個
- 方向調(diào)整,默認(rèn)是橫向的
- 掃碼框大小調(diào)整
- 掃碼框樣式調(diào)整
- 調(diào)整方向
調(diào)整方向很簡單,把manifest中activity聲明的下面代碼去掉就好
android:screenOrientation="sensorLandscape"http://去掉這段代碼
- 掃碼框大小調(diào)整
打開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中加入兩行代碼 大小可以自行設(shè)置
app:zxing_framing_rect_height="150dp"http://掃碼框高
app:zxing_framing_rect_width="200dp"http://掃碼框?qū)?
重新build項目并運行我們的sample可以看到界面已經(jīng)好看很多了,掃碼框也變成了正常的大小

優(yōu)化掃碼框
掃碼界面雖然變了,不過這還不是我們想要的,先看一下微信的掃碼框

可以看到,正常掃碼框都有一個滑動的條條和四個邊框,接下來就來添加這部分東西。
從上面的步驟可以看出整個掃碼界面其實就是一個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這個類
首先明確一下我們需要加入的東西
- 取消原有紅色條,加入滑動的掃碼條
- 四個邊框
- 提示文字移動到掃碼框下方
首先定義需要的屬性和方法,在修改源碼的時候我習(xí)慣將自己寫的代碼都放在最后面,并且用一條分割線分割,
- 定義屬性
/*-----------------------自定義方法和屬性--------------------------*/
//畫邊框相關(guān)屬性
private Paint mLinePaint;//邊框畫筆
private final int mLineColor = Color.BLUE;//邊框的顏色
//滑動條相關(guān)屬性
private Bitmap mLineBm;//滑動條圖片
private RectF mLineReact;//滑動條區(qū)域
private final int mStepSize = 12;//滑動條每次滑動的速度
private final int mLineHeight = 30;//滑動條的高度
private boolean isBottom = false;//滑動條是否滑動到掃碼框底部
//文字相關(guān)屬性
private Paint mTextPaint;//畫提示語的畫筆
private String mPromptText;//掃碼的提示語
private int mTextMargin;//提示語距離掃描框的大小
- 定義方法
有了屬性自然需要有初始化的方法和操作的邏輯代碼 創(chuàng)建2個新方法,初始化一般放在構(gòu)造方法中,操作的代碼是在onDraw中調(diào)用的
/**
* 改方法在構(gòu)造方法中調(diào)用用來初始化屬性
*
* @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中調(diào)用,放在
* 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()方法中下圖所示代碼 ,還可以注釋掉下面那一部分畫跳動小點的代碼,取消跳動小點,看個人需求?;瑒拥木€一般是一張圖片,這類我準(zhǔn)備了一張圖放在drawable目錄下 lan.png 圖片可以自行替換

實現(xiàn)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)
邊框的顏色,在邊界內(nèi)還是邊界外可以自行調(diào)整位置,這里面?zhèn)魅氲膄ram是掃描框所在的長方形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,這不符合我們的需要,通過調(diào)整textview距離底邊框的margin也可以調(diào)整位置,不過我這邊采用的是在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內(nèi)部去,還記得提示語是在哪里設(shè)置的嗎?重新看一下我們打開掃碼頁面的代碼,
new IntentIntegrator(this)
.setOrientationLocked(false)
.setDesiredBarcodeFormats(IntentIntegrator.ALL_CODE_TYPES)
.setPrompt("將二維碼/條碼放入框內(nèi),即可自動掃描")
.initiateScan(); // 初始化掃描
通過setPrompt()方法傳遞,跟蹤這個方法可以發(fā)現(xiàn)所有設(shè)置的信息最后都會放到一個intent并傳遞給CaptureActivity,重新打開CaptureActivity,在onCreate()中將設(shè)置的參數(shù)傳遞給一個CaptureManager
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
barcodeScannerView = initializeContent();
//初始化配置掃碼界面
capture = new CaptureManager(this, barcodeScannerView);
//intent中攜帶了通過IntentIntegrator設(shè)置的參數(shù)
capture.initializeFromIntent(getIntent(), savedInstanceState);
capture.decode();
}
CaptureManager內(nèi)部再次把intent傳遞給DecoratedBarcodeView的initializeFromIntent(Intent intent)方法,看這個方法中有這么一句
String customPromptMessage = intent.getStringExtra(Intents.Scan.PROMPT_MESSAGE);
if (customPromptMessage != null) {
setStatusText(customPromptMessage);
}
/**
/**
* 設(shè)置提示語
* @param text
*/
public void setStatusText(String text) {
// statusView is optional when using a custom layout
if(statusView != null) {
statusView.setText(text);
}
}
可以看出在這個方法里面將提示語設(shè)置給了statusView(即我們上面注釋掉的textview)
理清了這個再進(jìn)行修改就容易了
- 首先去掉DecoratedBarcodeView中statusView變量和與它相關(guān)的代碼,注釋掉就好了。
- 在ViewfinderView中添加一個方法,mPromptText屬性的set()方法,這個屬性我們一開始已經(jīng)定義過了。在DecoratedBarcodeView的setStatusText中調(diào)用
- 完善之前在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);
}
這樣我們已經(jīng)把提示語傳遞到了ViewfinderView中,接下來就是將它畫到掃描框的下方,具體想要畫的位置可以自行調(diào)整。
實現(xiàn)drawPromptText(frame, canvas)的代碼, mTextPaint的初始化代碼customInit()中,文字樣式可以自行調(diào)整
/**
* 畫提示語
*
* @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);
}
}
效果

掃碼框位置調(diào)整
默認(rèn)掃碼框是在整個屏幕的中間,如果想要調(diào)整掃碼框的位置,比如上移或者下一的話,找到CameraPreview中的calculateFramingRect方法,添加如下代碼 intersection.offset(0,-150); 具體調(diào)整根據(jù)需求而定。
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);
/**將默認(rèn)的掃碼框位置上調(diào)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;
}
總結(jié)
通過對源碼的二次開發(fā),優(yōu)化了掃碼界面,初步實現(xiàn)了常見的掃碼框效果,下一篇將介紹對API方法的封裝。如有不對的地方還請指正,感謝。