前言
現在的應用中二維碼掃描已經成為一個應用必不可少的功能,現在大部分Android二維碼掃描都是基于zxing和Zbar,這文章就來介紹一下基于zxing的二維碼掃描。先看下效果圖
雖然現在的關于二維碼的文章有很多,但是很多都是交我們怎么使用,在使用的時候我也遇到了很多問題,比如。UI界面太丑,沒有用的文件太多,掃描太慢,版本太老。本Demo使用的是3.x的是比較新的版本。我用的小米2A(api=19)親測在正常情況下掃描時間和QQ微信差不多,我的界面是類防QQ的也是比較美觀的。那么我們看看如何使用和文件的作用。
zxing
zxing官網 這是zxing官網。想了解更多的可以去官網,里面有文檔不過在我看來。第三方庫的使用我們沒有必要完全了解,整體上我們了解所需要功能即可,這樣減少了學習時間。
為什么選擇zxing
- google的開源項目,高可定制性
- 可以識別多種碼,不僅僅是二維碼
- 不依賴第三方庫,使用起來簡單
zxing的使用
zixng JAR
我們可以去官網,如果是Android Studio的話也可以在線搜索zxing jar,導入完成后別忘了ADD library。-
res文件
res文件drawable
文件中主要放的是一些我們顯示界面的圖片和一些點擊按鈕的backgroundlayout
activity_qrcode_capture_layout.xml是zxing掃描的主界面,另外兩個布局就是我們在開始圖片中看到的,一個頭部,一個腳部的布局-
colors,dis,strings,styles,raw,xml
這些是zxing中一些類的資源文件和我們自定義布局的一些資源文件,不導入會報錯,raw是我們掃描完成后的音效,我們也可以根據需求改成自己的音效(但是要注意的是文件格式和名字盡量要相同,避免出錯和資源找不到),xml就是zxing用到的資源文件,我們直接復制過來就行。PS:如果你是從官網拷貝,那么你自需要拷貝和我一樣的就行。我這里只是多了一些drawwable中的布局圖片和頭部腳部2個布局
-
關鍵類
關鍵類- app
CaptureActivity 主要是我們的掃碼界面,在這里我們引入我們自己的頭部腳部布局,并給控件點擊事件,在這個類中我們重點看這幾個方法:
- app
/**
* 閃光燈點擊事件
*/
private OnClickListener click = new OnClickListener() {
@Override
public void onClick(View v) {
int id = v.getId();
if (id == R.id.button_back) {//返回按鈕
finish();
} else if (id == R.id.flash_btn) {//打開關閉閃光燈
if (!isFlash) {
CameraManager.get().turnLightOn();
} else {
CameraManager.get().turnLightOff();
}
isFlash = !isFlash;
} else if (id == R.id.photo_btn) {//掃描二維碼圖片
// 打開手機中的相冊
Intent innerIntent = new Intent(Intent.ACTION_GET_CONTENT); // "android.intent.action.GET_CONTENT"
innerIntent.setType("image/*");
//封裝intent
Intent wrapperIntent = Intent.createChooser(innerIntent, "選擇二維碼圖片");
startActivityForResult(wrapperIntent, REQUEST_CODE);
} else if (id == R.id.qrcode_btn) {
// 跳轉到生成二維碼頁面
Bitmap b = createQRCode();
Intent intent = getIntent();
intent.putExtra("QR_CODE", b);
setResult(200, intent);
finish();
}
}
};
所有掃碼界面的點擊事件都在這個Activity中,可以看到zxing給我做了比較好的封裝,只需要2行代碼我們就可以控制閃光燈的開關。掃描圖片二維碼的點擊事件也比較簡單我們自需要打開相冊。并用startActivityForResult啟動相冊。
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (resultCode == RESULT_OK) {//圖片選擇返回
uri = data.getData();//獲取圖片Uri
//啟動線程完成圖片掃碼
new Thread(new Runnable() {
@Override
public void run() {
Result result = scanningImage(uri);
if (result == null) {
Looper.prepare();
Toast.makeText(getApplicationContext(), "圖片格式有誤", Toast.LENGTH_SHORT).show();
Looper.loop();
} else {
// 數據返回,在這里去處理掃碼結果
String recode = (result.toString());
Intent data = new Intent();
data.putExtra("result", recode);
//返回啟動掃碼界面的Activity
setResult(300, data);
finish();
}
}
}).start();
}
}
這個方法主要是處理上個方法打開相冊選取圖片后結果返回的處理。可以看到,我們拿到結果將掃碼掃碼界面finish()掉,并通過setResult()方法將數據交給跳轉我們的掃碼界面的活動去處理。 下面我們再來看下生成二維碼
- 生成二維碼:可以看到生成二維碼主要是調用了createQRCode();這個方法,那我來看下這個方法:
private Bitmap createQRCode() {
int QR_WIDTH = 100;//生成二維碼的寬
int QR_HEIGHT = 100;//生成二維碼的高
try {
// 需要引入core包
QRCodeWriter writer = new QRCodeWriter();
String text = Util.getIMEI(this);
if (text == null || "".equals(text) || text.length() < 1) {
return null;
}
// 把輸入的文本轉為二維碼
BitMatrix martix = writer.encode(text, BarcodeFormat.QR_CODE, QR_WIDTH, QR_HEIGHT);
System.out.println("w:" + martix.getWidth() + "h:" + martix.getHeight());
Hashtable<EncodeHintType, String> hints = new Hashtable<EncodeHintType, String>();
hints.put(EncodeHintType.CHARACTER_SET, "utf-8");
BitMatrix bitMatrix = new QRCodeWriter().encode(text, BarcodeFormat.QR_CODE, QR_WIDTH, QR_HEIGHT, hints);
int[] pixels = new int[QR_WIDTH * QR_HEIGHT];
for (int y = 0; y < QR_HEIGHT; y++) {
for (int x = 0; x < QR_WIDTH; x++) {
if (bitMatrix.get(x, y)) {
pixels[y * QR_WIDTH + x] = 0xff000000;
} else {
pixels[y * QR_WIDTH + x] = 0xffffffff;
}
}
}
// 生成的二維碼
Bitmap bitmap = Bitmap.createBitmap(QR_WIDTH, QR_HEIGHT, Bitmap.Config.ARGB_8888);
bitmap.setPixels(pixels, 0, QR_WIDTH, 0, 0, QR_WIDTH, QR_HEIGHT);
return bitmap;
} catch (WriterException e) {
e.printStackTrace();
}
return null;
}
它主要就是將一個文本生成了一個寬高為100*100的bitmap。那我們就把這個方法簡單就修改下,寬高由我們傳入。文本也是由我們自己來決定。因為生成二維碼并不依賴掃描的Activity,所以不管在那里我們只要調用createQRcode,就能生成二維碼。
- camera 主要是一些相機的管理類,FlashlightManager閃光燈的管理類,CameraManager相機的管理類,比如剛才我們的開關閃光燈。
- decode 這里面主要是解碼,因為解碼也是比較耗時的炒作,所以我們是放在線程中去執行并通過handle來進行消息傳遞。這里重要的類是CaptureActivityHandler。處理的結果都是通過這個類傳給我們的Activity的。既然是handle,那么我們就來看下handleMessage()方法
@Override
public void handleMessage(Message message) {
if (message.what == R.id.auto_focus) {
// Log.d(TAG, "Got auto-focus message");
// When one auto focus pass finishes, start another. This is the
// closest thing to
// continuous AF. It does seem to hunt a bit, but I'm not sure what
// else to do.
if (state == State.PREVIEW) {
CameraManager.get().requestAutoFocus(this, R.id.auto_focus);
}
} else if (message.what == R.id.restart_preview) {
Log.d(TAG, "Got restart preview message");
restartPreviewAndDecode();
} 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);
Result result = (Result) message.obj;
Intent mIntent = new Intent();
mIntent.putExtra("SCAN_RESULT", result.getText());
activity.setResult(Activity.RESULT_OK, mIntent);
activity.finish();
} else if (message.what == R.id.decode_failed) {
// We're decoding as fast as possible, so when one decode fails,
// start another.
state = State.PREVIEW;
CameraManager.get().requestPreviewFrame(decodeThread.getHandler(), R.id.decode);
} else if (message.what == R.id.return_scan_result) {
Log.d(TAG, "Got return scan result message");
activity.setResult(Activity.RESULT_OK, (Intent) message.obj);
activity.finish();
} else if (message.what == R.id.launch_product_query) {
Log.d(TAG, "Got product query message");
String url = (String) message.obj;
Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET);
activity.startActivity(intent);
}
}
可以看到處理的結果都在這里。有成功失敗和其他一些情況的處理,這里我們主要看成功時會返回resultCode=RESULT_OK,掃描的結果放在inent中key=SCAN_RESULT,這樣我們就可以根據intent攜帶的key和value去做們相應的處理
BeepManager是控制我們的消息音。
BeepManager manager = new BeepManager(activity);//開啟提示音
manager.playBeepSoundAndVibrate();
- encode,util,就是編碼和工具類,這里我們就不看了
- view中我們重點來看下ViewfinderView。這個就是我們掃碼界面的主布局。他和我們自己添加的頭部和腳部不同,它不是通過引入布局,而是自定義view畫上去的。既然是畫上去的那么就去看看onDraw()方法做了什么
@Override
public void onDraw(Canvas canvas) {
/**
* 想修改掃描框的位置修改CameraManager中的參數
*/
Rect frame = CameraManager.get().getFramingRect();
if (frame == null) {
return;
}
if (!isFirst) {
isFirst = true;
slideTop = frame.top;
slideBottom = frame.bottom;
}
int width = canvas.getWidth();
int height = canvas.getHeight();
/**
* 畫白線矩形
*/
paint.setStyle(Paint.Style.STROKE);
paint.setColor(Color.WHITE);
path.moveTo(frame.left + CORNER_OFFEST, frame.top + CORNER_OFFEST);
path.lineTo(frame.right - CORNER_OFFEST, frame.top + CORNER_OFFEST);
path.lineTo(frame.right - CORNER_OFFEST, frame.bottom - CORNER_OFFEST);
path.lineTo(frame.left + CORNER_OFFEST, frame.bottom - CORNER_OFFEST);
path.close();
canvas.drawPath(path, paint);
// Draw the exterior (i.e. outside the framing rect) darkened
paint.setColor(resultBitmap != null ? resultColor : maskColor);
paint.setStyle(Paint.Style.FILL);
/**
* 畫上下左右4個位置的半透明黑色布局
*/
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(OPAQUE);
canvas.drawBitmap(resultBitmap, null, frame, paint);
} else {
/**
* 畫4個角的小矩形,每個角2個矩形 共8個
*/
paint.setColor(getResources().getColor(R.color.view_rect));
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);
/**
* 定義每秒的移動速度
*/
slideTop += SPEEN_DISTANCE;
if (slideTop >= frame.bottom) {
slideTop = frame.top;
}
lineRect.left = frame.left;
lineRect.right = frame.right;
lineRect.top = slideTop;
lineRect.bottom = slideTop + 18;
canvas.drawBitmap(((BitmapDrawable) (getResources()
.getDrawable(R.drawable.fle))).getBitmap(), null, lineRect,
paint);
paint.setColor(Color.WHITE);
paint.setTextSize(TEXT_SIZE * density);
paint.setAlpha(0x40);
paint.setTypeface(Typeface.create("System", Typeface.BOLD));
String text = getResources().getString(R.string.scan_text);
float textWidth = paint.measureText(text);
canvas.drawText(
text,
(width - textWidth) / 2,
(float) (frame.bottom + (float) TEXT_PADDING_TOP * density),
paint);
Collection<ResultPoint> currentPossible = possibleResultPoints;
Collection<ResultPoint> currentLast = lastPossibleResultPoints;
if (currentPossible.isEmpty()) {
lastPossibleResultPoints = null;
} else {
possibleResultPoints = new HashSet<ResultPoint>(5);
lastPossibleResultPoints = currentPossible;
paint.setAlpha(OPAQUE);
paint.setColor(resultPointColor);
for (ResultPoint point : currentPossible) {
canvas.drawCircle(frame.left + point.getX(), frame.top
+ point.getY(), 6.0f, paint);
}
}
if (currentLast != null) {
paint.setAlpha(OPAQUE / 2);
paint.setColor(resultPointColor);
for (ResultPoint point : currentLast) {
canvas.drawCircle(frame.left + point.getX(), frame.top
+ point.getY(), 3.0f, paint);
}
}
//不斷重繪 完成光標移動
postInvalidateDelayed(ANIMATION_DELAY, frame.left, frame.top,
frame.right, frame.bottom);
}
}
注釋比較清晰,掌握基本的自定義View只是就可以畫出來我們展示的效果。
最后我們在看下我們MainActivity的調用:
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == 200) {
if (resultCode == Activity.RESULT_OK) {
//還記得我們在講CaptureActivityHandler時 成功就會發送SCAN_RESULT這個key值的intent嘛?
String code = data.getStringExtra("SCAN_RESULT");
Log.d(TAG, "onActivityResult:----> " + code);
if (code.contains("http") || code.contains("https")) {
//二維碼
mTextView.setText(code);
} else if ((!code.contains("http") || !code.contains("https")) && code != null && TextUtils.isEmpty(code)) {
//條形碼數字
//這里注意:如果你掃描的是商品條形碼。返回的條形碼數字
//這里如果你使用webview是無法解析的,一般我們需要查詢
//的api接口或是數據庫查找才能展示我們想要的結果
mTextView.setText(code);
} else {
Toast.makeText(this, "error", Toast.LENGTH_SHORT).show();
}
}
if (resultCode == 300) {
//掃描圖片
String code = data.getStringExtra("result");
Log.d(TAG, "onActivityResult:---->result " + code);
mTextView.setText(code);
}
if (resultCode == 200) {
//生成二維碼回調
Bitmap bitmap = Util.createQRCode(dip2px(this, 200), dip2px(this, 200), "https://www.baidu.com/");
mImageView.setImageBitmap(bitmap);
}
}
}
好了,zxing的使用和相關類的功能我們有了一個大致的了解。回頭我們在看看zxing的優點:可以看到封裝性比較好我們不需要多做什么處理,我們需要什么功能就在基礎上加什么功能,源碼都給我們了,我們也知道每個類是什么作用還不是想怎么改怎么改嘛(高可定制性),還有在使用第三方庫的時候我們比較擔心的就是我們需要一個庫的功能,但是這個庫卻依賴很多其他的庫,當其他庫發生改動的時候我們需要的庫也要改這就很煩。
結束
在這里我只是做一個拋磚引玉的作用,不管你想要什么樣的UI還是什么樣的布局,我在方法中加了注釋不喜歡那里將代碼刪除寫上自己想要的效果就好。畢竟我還是個小白菜鳥。如果你也在學習的路上,想在你自己應用加上掃碼功能,希望這篇文章能給你一些幫助。如果像直接使用下面我給出了源碼,可以直接放入項目中也可導入library。
寫的不好大家多多諒解。如有錯誤真心希望大家提出來。最后希望大家一起進步。加油!!!