摘要
最近,在公司項目上需要加入“二維碼掃描”的功能(Android端),筆者在網上查閱了一些資料,實現了這個功能。最后給自己做個筆記,給各位做下分享。
原理說明
“二維碼掃描”實際上就是通過手機相機掃描『二維碼圖片』,將『二維碼圖片』中的字符串數據通過解碼的方式解析出來。
實現方式
借助開源庫 ZXing Android Embedded 實現二維碼掃描。
Github地址: https://github.com/journeyapps/zxing-android-embedded
接下來,筆者分兩部分進行講解:
第1部分:ZXing Android Embedded簡介及使用方法。
第2部分:自定義掃描界面。
一、ZXing Android Embedded簡介及使用方法
1.簡介
ZXing Android Embedded 是用于Android的條形碼掃描庫,使用ZXing進行解碼。
注:二維碼是條形碼中的一種,該庫也可以掃描二維碼。
2.引入方法
添加gradle庫依賴:
dependencies {
......
compile 'com.journeyapps:zxing-android-embedded:3.5.0'
}
注意事項:
- 該庫在需要時會自動引入ZXing庫,無需額外手動引入。
- buildToolsVersion '23.0.2'(構建工具的版本要>=23.0.2)
- compile 'com.android.support:appcompat-v7:23.1.0' (support-v7包版本要在23+以上)
- 最低支持的Android版本(API level 9+)
想要了解更多詳情,可打開Github鏈接研究學習。
3.使用方法
接下來,筆者用一個實例來介紹一下該庫的使用方法。
1.新建一個Android工程。
2.添加gradle庫依賴,引入ZXing Android Embedded庫。
3.在MainActivity的布局文件中放置一個Button(用于打開二維碼掃描界面)。
4.在MainActivity中為Button設置點擊事件,點擊后跳轉至掃描界面。
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
findViewById(R.id.btn).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
// 創建IntentIntegrator對象
IntentIntegrator intentIntegrator = new IntentIntegrator(MainActivity.this);
// 開始掃描
intentIntegrator.initiateScan();
}
});
}
}
5.重寫onActivityResult方法接收掃描結果。
public class MainActivity extends AppCompatActivity {
......
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
// 獲取解析結果
IntentResult result = IntentIntegrator.parseActivityResult(requestCode, resultCode, data);
if (result != null) {
if (result.getContents() == null) {
Toast.makeText(this, "取消掃描", Toast.LENGTH_LONG).show();
} else {
Toast.makeText(this, "掃描內容:" + result.getContents(), Toast.LENGTH_LONG).show();
}
} else {
super.onActivityResult(requestCode, resultCode, data);
}
}
}
完成此步,基本的二維碼掃描功能就已經出來了。
接下來,我們可以準備二維碼圖片試驗一下。如果沒有二維碼圖片,可以用草料二維碼生成器在線生成一個二維碼使用(如下圖所示)。
6.跑一下Android程序,掃描一下二維碼。(如下圖所示)
我們看到掃描成功了,最后Toast出了“http://www.baidu.com”這個信息。
但這個掃描過程怎么感覺天旋地轉的,一點也不流暢?.../(ㄒoㄒ)/~~
這是由于ZXing Android Embedded庫提供的掃碼Activity默認是橫屏的。
不過,掃描界面的方向是可調的,Github文檔也有說明,舉個例子。
固定豎屏(僅需在manifest文件中添加如下配置)
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="com.example.wangnan7.qrcodescandemo">
<application
......
<!-- 調整二維碼掃描界面為豎屏 -->
<activity
android:name="com.journeyapps.barcodescanner.CaptureActivity"
android:screenOrientation="portrait"
tools:replace="screenOrientation" />
</application>
</manifest>
重新跑下程序,如下所示:
7.其他配置項
在上述實例中,我們用兩行代碼(如下所示)實現了啟動二維碼掃描界面。
IntentIntegrator intentIntegrator = new IntentIntegrator(MainActivity.this);
intentIntegrator.initiateScan();
基本上沒有添加什么配置。但是,該庫還提供了其他配置項(如下所示)。
接下來,筆者詳解一下這8個配置項。
1. setBarcodeImageEnabled(boolean enabled)
該方法用于設置“被掃描的二維碼圖片”可以保存在本地。
舉個例子說明一下:
接著之前的例子,我們在布局文件中添加一個ImageView(用于顯示二維碼圖片):
MainActivity修改后的代碼如下:
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
findViewById(R.id.btn).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
IntentIntegrator intentIntegrator = new IntentIntegrator(MainActivity.this);
// 設置可以保存條形碼(二維碼)圖片
intentIntegrator.setBarcodeImageEnabled(true);
intentIntegrator.initiateScan();
}
});
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
// 獲取解析結果
IntentResult result = IntentIntegrator.parseActivityResult(requestCode, resultCode, data);
if (result != null) {
if (result.getBarcodeImagePath() != null) {
// 顯示條形碼(二維碼)圖片的保存路徑
Toast.makeText(this, result.getBarcodeImagePath(), Toast.LENGTH_LONG).show();
// 顯示條形碼(二維碼)圖片
showBarcodeImage(result.getBarcodeImagePath());
}
} else {
super.onActivityResult(requestCode, resultCode, data);
}
}
/**
* 加載并顯示條形碼圖片
*/
private void showBarcodeImage(String barcodeImagePath) {
FileInputStream fis = null;
try {
fis = new FileInputStream(new File(barcodeImagePath));
((ImageView)findViewById(R.id.iv)).setImageBitmap(BitmapFactory.decodeStream(fis));
} catch (FileNotFoundException e) {
e.printStackTrace();
} finally {
try {
fis.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
跑下程序,如下圖所示:
可以看到,筆者Toast出了二維碼圖片被保存后的路徑信息,并根據文件保存路徑將二維碼圖片顯示了出來。
所以,如果添加這個配置:
intentIntegrator.setBarcodeImageEnabled(true);
掃描后的二維碼圖片會被保存;如果不添加這個配置或參數設置為false,二維碼圖片不會被保存,我們拿到的路徑result.getBarcodeImagePath()
就會變成null。
2. setCaptureActivity(Class<?> captureActivity)
該方法用于設置掃描Activity。如果你不想用該庫提供的掃描Activity,可以自定義一個掃描Activity,將該Acitivty的運行時類作為參數傳進去,這個方法后續用到時再詳細說明。
3. setBeepEnabled(boolean enabled)
該方法用于設置掃碼成功后的提示音,傳true為開啟,不設置或設置false為關閉。
4. setCameraId(int cameraId)
該方法用于設置相機ID。我們使用的手機一般都有前置和后置攝像頭,該方法傳0將會使用后置攝像頭,傳1將會使用前置攝像頭。不設置則默認使用后置攝像頭。
現在有些手機后置雙攝像頭,相機ID可能有所變化,有興趣的朋友請自行研究。
5. setDesiredBarcodeFormats(Collection<String> desiredBarcodeFormats)
該方法用于設置你期望的條形碼格式。(該庫提供了5種格式,如下所示)
注:不設置默認為全部類型
所以對于掃描二維碼,你可以選擇不設置,如果設置可以使用QR_CODE_TYPES和ALL_CODE_TYPES。但是,筆者建議設置QR_CODE_TYPES,即:
intentIntegrator.setDesiredBarcodeFormats(IntentIntegrator.QR_CODE_TYPES);
因為不設置或設置支持全部類型,會附帶掃描其他條形碼的功能,筆者認為實際功能應與描述功能相一致。
6. setOrientationLocked(boolean locked)
該方法用于設置方向鎖。(源碼解釋如下:)
這個功能是用來調整掃描界面方向的,可以配合傳感器使用,舉個例子。
修改一下之前的manifest文件,如下所示:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="com.example.wangnan7.qrcodescandemo">
<application
......
<!-- 調整二維碼掃描界面方向為"完全依賴傳感器" -->
<activity
android:name="com.journeyapps.barcodescanner.CaptureActivity"
android:screenOrientation="fullSensor"
tools:replace="screenOrientation" />
</application>
</manifest>
在MainActivity中添加方向鎖設置,如下所示:
運行一下程序,如下所示:
可以看到調整手機方向時,掃描布局也會重新布置,最后筆者按Back返回鍵取消了掃描。
7. setPrompt(String prompt)
該方法用于設置掃描界面的提示信息。
舉個例子,筆者設置一條提示信息(如下圖所示)
運行一下程序,可以看到掃描界面的“提示文字”(如下圖所示)
8. setTimeout(long timeout)
該方法用于設置掃描界面的超時時間。(避免用戶打開掃描頁面,忘記關閉)
舉個例子,筆者設置一個2秒的超時時間(如下圖所示)
運行一下程序,如下圖所示:
可以看到,2秒后,掃描自動取消了。
ZXing Android Embedded的基本使用方法介紹完了。想了解更多用法的朋友可以通過GitHub鏈接或查看源碼的方式學習。
二、自定義掃描界面
各位可能發現 ZXing Android Embedded庫 提供的默認的掃描界面有些簡陋(或丑陋),滿足不了產品和設計的需求,舉個例子:
產品想要下圖這種效果,該怎么辦呢?
這時就需要我們自定義掃描界面了...
自定義策略:比著葫蘆畫瓢
由于源碼中的類在AndroidStudio中默認是被加鎖的,我們無權直接修改。但我們可以仿寫其中的一些類,方便我們添加自己的邏輯。自定義起點可以從Activity開始。
1.自定義掃描Activity
在源碼中可以查到,我們之前一直在使用一個CaptureActivity進行二維碼掃描(如下所示):
接下來,我們可以仿照CaptureActivity寫一個自己的Activity(直接Copy也可以)。
筆者仿寫的代碼如下:
/**
* @Class: CustomCaptureActivity
* @Description: 自定義條形碼/二維碼掃描
* @Author: wangnan7
* @Date: 2017/5/19
*/
public class CustomCaptureActivity extends AppCompatActivity {
/**
* 條形碼掃描管理器
*/
private CaptureManager mCaptureManager;
/**
* 條形碼掃描視圖
*/
private DecoratedBarcodeView mBarcodeView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(com.google.zxing.client.android.R.layout.zxing_capture);
mBarcodeView = (DecoratedBarcodeView)findViewById(com.google.zxing.client.android.R.id.zxing_barcode_scanner);
mCaptureManager = new CaptureManager(this, mBarcodeView);
mCaptureManager.initializeFromIntent(getIntent(), savedInstanceState);
mCaptureManager.decode();
}
@Override
protected void onResume() {
super.onResume();
mCaptureManager.onResume();
}
@Override
protected void onPause() {
super.onPause();
mCaptureManager.onPause();
}
@Override
protected void onDestroy() {
super.onDestroy();
mCaptureManager.onDestroy();
}
@Override
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
mCaptureManager.onSaveInstanceState(outState);
}
/**
* 權限處理
*/
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String permissions[], @NonNull int[] grantResults) {
mCaptureManager.onRequestPermissionsResult(requestCode, permissions, grantResults);
}
/**
* 按鍵處理
*/
@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
return mBarcodeView.onKeyDown(keyCode, event) || super.onKeyDown(keyCode, event);
}
}
注:XML布局還是使用的源碼中CaptureActivity的布局。
緊接著,我們可以在manifest文件中聲明一下這個新創建的Activity。
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.wangnan7.qrcodescandemo">
<application
.......
<!-- 設置二維碼掃描界面方向為豎屏 -->
<activity
android:name=".CustomCaptureActivity"
android:label="自定義掃描界面"
android:screenOrientation="portrait"/>
</application>
</manifest>
最后,我們就可以在MainActivity中調用這個新的掃描Activity了。
運行程序,效果如下:
可以看到我們自定義的掃描Activity可以正常運行,掃碼也成功了。
但是,我們自定義Activty使用的布局還是源碼中的布局文件,對于這個布局文件我們沒有權限修改,接下來就需要自定義掃描布局了。
2.自定義掃描布局
源碼布局如下:
筆者仿寫的自定義掃描布局 (activity_zxing_layout.xml):
屬性簡介:
app:zxing_preview_scaling_strategy : 預覽視圖的縮放策略,使用centerCrop即可
app:zxing_use_texture_view : 是否使用紋理視圖(黑色背景)
接下來,我們就可以把自定義掃描Activity的布局文件給替換掉了。
/**
* @Class: CustomCaptureActivity
* @Description: 自定義條形碼/二維碼掃描
* @Author: wangnan7
* @Date: 2017/5/19
*/
public class CustomCaptureActivity extends AppCompatActivity {
......
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_zxing_layout);
mBarcodeView = (DecoratedBarcodeView)findViewById(R.id.zxing_barcode_scanner);
......
}
......
}
最后,我們跑程序驗證一下:
可以看到我們的自定義布局文件也沒有問題。
我們的自定義Activity和自定義布局文件都完成了,剩下的就是修改掃描視圖的樣式了。
3.修改掃描視圖的樣式
想要修改掃描視圖的樣式,需要略微研究下DecoratedBarcodeView的源碼。
1.DecoratedBarcodeView初始化分析
補充:可以看到 scannerLayout 最后被作為掃描布局inflate進了DecorateBarcodeView中。
2.默認布局R.layout.zxing_barcode_scanner分析
分析到這里,我們需要做的工作就顯現出來了。那就是:
自定義View(繼承ViewfinderView),重寫onDraw方法,然后替換掉這里的ViewfinderView。
因為R.layout.zxing_barcode_scanner是源碼中的布局文件,無法直接修改,所以還要重寫一份布局文件給DecoratedBarcodeView加載。那么,接下來需要做兩步準備工作:
(1)仿寫默認布局文件R.layout.zxing_barcode_scanner
(2)讓DecoratedBarcodeView加載剛剛仿寫布局,不再使用默認布局。
3.開始自定義掃描視圖(繼承ViewfinderView重寫onDraw方法)
小技巧:如果不知道如何開始,可以先將原ViewfinderView的onDraw方法copy進來一點一點研究修改。
筆者直接將自己的自定義掃描布局粘貼出來,需要的朋友可以借鑒或Copy:
/**
* @Class: CustomViewfinderView
* @Description: 自定義掃描框樣式
* @Author: wangnan7
* @Date: 2017/5/22
*/
public class CustomViewfinderView extends ViewfinderView {
/**
* 重繪時間間隔
*/
public static final long CUSTOME_ANIMATION_DELAY = 16;
/* ****************************************** 邊角線相關屬性 ************************************************/
/**
* "邊角線長度/掃描邊框長度"的占比 (比例越大,線越長)
*/
public float mLineRate = 0.1F;
/**
* 邊角線厚度 (建議使用dp)
*/
public float mLineDepth = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 4, getResources().getDisplayMetrics());
/**
* 邊角線顏色
*/
public int mLineColor = Color.WHITE;
/* ******************************************* 掃描線相關屬性 ************************************************/
/**
* 掃描線起始位置
*/
public int mScanLinePosition = 0;
/**
* 掃描線厚度
*/
public float mScanLineDepth = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 4, getResources().getDisplayMetrics());
/**
* 掃描線每次重繪的移動距離
*/
public float mScanLineDy = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 3, getResources().getDisplayMetrics());
/**
* 線性梯度
*/
public LinearGradient mLinearGradient;
/**
* 線性梯度位置
*/
public float[] mPositions = new float[]{0f, 0.5f, 1f};
/**
* 線性梯度各個位置對應的顏色值
*/
public int[] mScanLineColor = new int[]{0x00FFFFFF, Color.WHITE, 0x00FFFFFF};
public CustomViewfinderView(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
public void onDraw(Canvas canvas) {
refreshSizes();
if (framingRect == null || previewFramingRect == null) {
return;
}
Rect frame = framingRect;
Rect previewFrame = previewFramingRect;
int width = canvas.getWidth();
int height = canvas.getHeight();
//繪制4個角
paint.setColor(mLineColor); // 定義畫筆的顏色
canvas.drawRect(frame.left, frame.top, frame.left + frame.width() * mLineRate, frame.top + mLineDepth, paint);
canvas.drawRect(frame.left, frame.top, frame.left + mLineDepth, frame.top + frame.height() * mLineRate, paint);
canvas.drawRect(frame.right - frame.width() * mLineRate, frame.top, frame.right, frame.top + mLineDepth, paint);
canvas.drawRect(frame.right - mLineDepth, frame.top, frame.right, frame.top + frame.height() * mLineRate, paint);
canvas.drawRect(frame.left, frame.bottom - mLineDepth, frame.left + frame.width() * mLineRate, frame.bottom, paint);
canvas.drawRect(frame.left, frame.bottom - frame.height() * mLineRate, frame.left + mLineDepth, frame.bottom, paint);
canvas.drawRect(frame.right - frame.width() * mLineRate, frame.bottom - mLineDepth, frame.right, frame.bottom, paint);
canvas.drawRect(frame.right - mLineDepth, frame.bottom - frame.height() * mLineRate, frame.right, frame.bottom, paint);
// Draw the exterior (i.e. outside the framing rect) darkened
paint.setColor(resultBitmap != null ? resultColor : maskColor);
canvas.drawRect(0, 0, width, frame.top, paint);
canvas.drawRect(0, frame.top, frame.left, frame.bottom + 1, paint);
canvas.drawRect(frame.right + 1, frame.top, width, frame.bottom + 1, paint);
canvas.drawRect(0, frame.bottom + 1, width, height, paint);
if (resultBitmap != null) {
// Draw the opaque result bitmap over the scanning rectangle
paint.setAlpha(CURRENT_POINT_OPACITY);
canvas.drawBitmap(resultBitmap, null, frame, paint);
} else {
// 繪制掃描線
mScanLinePosition += mScanLineDy;
if(mScanLinePosition > frame.height()){
mScanLinePosition = 0;
}
mLinearGradient = new LinearGradient(frame.left, frame.top + mScanLinePosition, frame.right, frame.top + mScanLinePosition, mScanLineColor, mPositions, Shader.TileMode.CLAMP);
paint.setShader(mLinearGradient);
canvas.drawRect(frame.left, frame.top + mScanLinePosition, frame.right, frame.top + mScanLinePosition + mScanLineDepth, paint);
paint.setShader(null);
float scaleX = frame.width() / (float) previewFrame.width();
float scaleY = frame.height() / (float) previewFrame.height();
List<ResultPoint> currentPossible = possibleResultPoints;
List<ResultPoint> currentLast = lastPossibleResultPoints;
int frameLeft = frame.left;
int frameTop = frame.top;
if (currentPossible.isEmpty()) {
lastPossibleResultPoints = null;
} else {
possibleResultPoints = new ArrayList<>(5);
lastPossibleResultPoints = currentPossible;
paint.setAlpha(CURRENT_POINT_OPACITY);
paint.setColor(resultPointColor);
for (ResultPoint point : currentPossible) {
canvas.drawCircle(frameLeft + (int) (point.getX() * scaleX),
frameTop + (int) (point.getY() * scaleY),
POINT_SIZE, paint);
}
}
if (currentLast != null) {
paint.setAlpha(CURRENT_POINT_OPACITY / 2);
paint.setColor(resultPointColor);
float radius = POINT_SIZE / 2.0f;
for (ResultPoint point : currentLast) {
canvas.drawCircle(frameLeft + (int) (point.getX() * scaleX),
frameTop + (int) (point.getY() * scaleY),
radius, paint);
}
}
}
// Request another update at the animation interval, but only repaint the laser line,
// not the entire viewfinder mask.
postInvalidateDelayed(CUSTOME_ANIMATION_DELAY,
frame.left,
frame.top,
frame.right,
frame.bottom);
}
}
代碼簡介:
(1)onDraw方法中的大部分代碼Copy自ViewfinderView,筆者添加了兩部分邏輯:第一部分是邊角線的繪制;第二部分是用“掃描線”替換掉了原有的“激光線”。
(2)代碼的核心是在onDraw方法的第5行代碼:
Rect frame = framingRect;
這個矩陣記錄了掃描框四個頂點的坐標,有了這個變量,各位可以發揮想象力自定義自己需要的掃描樣式。
接下來,我們用CustomViewfinderView替換掉ViewfinderView(如下圖所示)
最后,跑下程序(如下圖所示)
4.樣式調整(UI優化)
我們的自定義掃描界面搞定了,但UI樣式還需要再優化一下:
(1) 框體大小調整 (DecoratedBarcodeView有屬性支持修改)
調整后的效果圖:
(2) 將掃描界面底部文字平移至掃描框底部
調整后的效果圖:
(3) 將掃描框向上平移
掃描框在默認情況下是相對于相機視圖居中的,想要調整掃描框的位置還要去修改源碼...
筆者想了一個投機取巧的辦法:透明掉標題欄和狀態欄讓相機預覽視圖向上延伸,使掃描框在視覺上略微上移。
這部分代碼和二維碼掃描沒有直接關系,筆者就不貼代碼了,各位可以嘗試自己實現,但最后筆者會附上本Demo的GitHub鏈接。
最終的效果:
Demo的Github鏈接: