參考
zxing掃描二維碼和識(shí)別圖片二維碼及其優(yōu)化策略
一飒炎、zxing 集成
我集成 zxing 的主要方法是復(fù)制關(guān)鍵代碼到自己的項(xiàng)目中。集成的代碼中刪除了share笆豁,history郎汪,help等無用的代碼赤赊。集成后的效果圖如下所示:
注:該UI是自定義的UI界面,并不是官方自帶的
zxing 的官方項(xiàng)目地址是: https://github.com/zxing/zxing煞赢。當(dāng)前最新版是3.3.0抛计。目錄結(jié)構(gòu)如下:
跟android有關(guān)的 是 core,android-core,android-integration ,以及android照筑。其中 android 包是一個(gè)完整的demo吹截。里面包含了一些分享,歷史管理凝危,設(shè)置波俄,幫助之類的主菜單。
進(jìn)入release頁面:https://github.com/zxing/zxing/releases媒抠,下載最新的代碼弟断。
在使用時(shí)可以把 目錄 core 導(dǎo)出為 jar 包,然后放入項(xiàng)目的lib目錄中趴生,也可以直接添加依賴(添加依賴時(shí)去官網(wǎng)看看當(dāng)前的最新版本):
implementation 'com.google.zxing:android-core:3.3.0'
implementation 'com.google.zxing:core:3.3.3'
把 zxing 包中的關(guān)鍵類拷貝到項(xiàng)目中
拷貝后如下圖所示
拷貝相關(guān)資源文件
1. 將 drawable 中的內(nèi)容拷貝過來
2. 將raw文件夾都拷過去
3. values下的文件拷貝要小心一點(diǎn)阀趴!比如:對(duì)于colors.xml文件,我們需要把其中Zxing相關(guān)的代碼考到自己的colors.xml中去苍匆, 而不是簡單的替換刘急,其他文件也類似!
4. 將 xml 文件拷貝過去
5 拷貝manifest.xml文件中的相關(guān)權(quán)限及CaptureActivity 類加入代碼
<uses-permission android:name="android.permission.CAMERA"/>
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.VIBRATE"/>
<uses-permission android:name="android.permission.FLASHLIGHT"/>
// CaptureActivity 類加入的部分代碼浸踩,掃描頁面全屏沒有狀態(tài)欄
@Override
public void onCreate(Bundle icicle) {
requestWindowFeature(Window.FEATURE_NO_TITLE);
getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN);
super.onCreate(icicle);
//window設(shè)置標(biāo)志位叔汁,保證屏幕常亮不會(huì)黑屏
Window window = getWindow();
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
setContentView(R.layout.activity_qrcode_capture_layout);
inactivityTimer = new InactivityTimer(this);
beepManager = new BeepManager(this);
ambientLightManager = new AmbientLightManager(this);
cameraManager = new CameraManager(getApplication());
}
window 設(shè)置標(biāo)志位,保證屏幕常亮不會(huì)黑屏检碗,
InactivityTimer 保證在電量較低的時(shí)候且一段時(shí)間沒有激活的時(shí)候据块,關(guān)閉CaptureActivity,
BeepManager 是用來掃碼時(shí)發(fā)出聲音和震動(dòng)的折剃,
AmbientLightManager 是用來控制感光的另假,以此來控制閃光燈的開閉
CaptureActivity,掃描界面,也是官方demo的主界面怕犁。
CaptureActivityHandler,輔助掃描界面边篮,進(jìn)行一些邏輯的處理,消息的轉(zhuǎn)發(fā)奏甫。
CameraManager,Camera,相機(jī)有關(guān)的部分戈轿,如 預(yù)覽,自動(dòng)聚焦
DecodeThread阵子,DecodeHandler, 跟解碼有關(guān)的類思杯,線程,消息處理
BarcodeFormat, DecodeHintType, 支持的一些類型挠进,格式色乾,配置腾么。如,二維碼杈湾,各種條形碼解虱,字符集。
自定義掃碼框 ViewfinderView
以下是關(guān)鍵代碼
@Override
public void onDraw(Canvas canvas) {
if (cameraManager == null) {
return; // not ready yet, early draw before done configuring
}
Rect frame = cameraManager.getFramingRect();
Rect previewFrame = cameraManager.getFramingRectInPreview();
if (frame == null || previewFrame == null) {
return;
}
int width = canvas.getWidth();
int height = canvas.getHeight();
// 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 {
// Draw a red "laser scanner" line through the middle to show decoding is active
paint.setColor(Color.BLUE);
paint.setAlpha(SCANNER_ALPHA[scannerAlpha]);
scannerAlpha = (scannerAlpha + 1) % SCANNER_ALPHA.length;
//繪制左上角的兩個(gè)小矩形
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);
//繪制右上角的兩個(gè)小矩形
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);
//繪制左下角的兩個(gè)小矩形
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);
//繪制右下角的兩個(gè)小矩形
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);
//記錄矩形框的上和下
if (!isFirst) {
isFirst = true;
slideTop = frame.top;
slideBottom = frame.bottom;
}
//定義好掃描線每秒移動(dòng)速度
slideTop += SPEED_DISTANCE;
if (slideTop >= slideBottom) {
slideTop = frame.top;
}
//繪制掃描線
Rect lineRect = new Rect();
lineRect.left = frame.left;
lineRect.right = frame.right;
lineRect.top = slideTop;
lineRect.bottom = slideTop + 18;
//繪制一個(gè)bitmap
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, frame.bottom + (float) TEXT_PADDING_TOP * density, paint);
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);
synchronized (currentPossible) {
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);
synchronized (currentLast) {
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(ANIMATION_DELAY,
frame.left - POINT_SIZE,
frame.top - POINT_SIZE,
frame.right + POINT_SIZE,
frame.bottom + POINT_SIZE);
}
}
二漆撞、zxing 識(shí)別圖片二維碼
主要代碼是在 CaptureActivity 中完成.
首先要打開手機(jī)相冊殴泰,找到二維碼圖片,代碼如下所示:
// 打開手機(jī)中的相冊
// "android.intent.action.GET_CONTENT"
Intent innerIntent = new Intent(Intent.ACTION_GET_CONTENT);
innerIntent.setType("image/*");
Intent wrapperIntent = Intent.createChooser(innerIntent, "選擇二維碼圖片");
startActivityForResult(wrapperIntent, REQUEST_CODE);
然后在以下方法中接收處理返回結(jié)果
private Uri uri;
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (resultCode == RESULT_OK) {
// 首先獲取到此圖片的Uri
uri = data.getData();
//獲取選取的圖片的絕對(duì)地址
String photoPath = getRealFilePath(this, data.getData());
if (photoPath == null) {
Toast.makeText(getApplicationContext(), "路徑獲取失敗", Toast.LENGTH_SHORT).show();
} else {
//解析圖片
parsePhoto(photoPath);
}
}
}
/**
* 獲取選取的圖片的絕對(duì)地址
* @param c
* @param uri
* @return
*/
private String getRealFilePath(Context c, Uri uri) {
String result;
Cursor cursor = c.getContentResolver().query(uri,
new String[]{MediaStore.Images.ImageColumns.DATA},
null, null, null);
if (cursor == null) {
result = uri.getPath();
} else {
cursor.moveToFirst();
int index = cursor.getColumnIndex(MediaStore.Images.ImageColumns.DATA);
result = cursor.getString(index);
cursor.close();
}
return result;
}
其中的核心內(nèi)容是zxing掃描路徑圖片的功能浮驳,是parsePhoto方法 由于這個(gè)方法是耗時(shí)的 所以需要用AsyncTask 或者Rxjava 這里用的是AsyncTask悍汛。具體代碼如下:
/**
* 啟動(dòng)線程解析二維碼圖片
*
* @param path
*/
private void parsePhoto(String path) {
//啟動(dòng)線程完成圖片掃碼
new QrCodeAsyncTask(this, path).execute(path);
}
/**
* AsyncTask 靜態(tài)內(nèi)部類,防止內(nèi)存泄漏
*/
static class QrCodeAsyncTask extends AsyncTask<String, Integer, String> {
private WeakReference<Activity> mWeakReference;
private String path;
public QrCodeAsyncTask(Activity activity, String path) {
mWeakReference = new WeakReference<>(activity);
this.path = path;
}
@Override
protected String doInBackground(String... strings) {
// 解析二維碼/條碼
return QRCodeDecoder.syncDecodeQRCode(path);
}
@Override
protected void onPostExecute(String s) {
super.onPostExecute(s);
//識(shí)別出圖片二維碼/條碼至会,內(nèi)容為s
CaptureActivity activity = (CaptureActivity) mWeakReference.get();
if (activity != null) {
activity.handleQrCode(s);
}
}
}
/**
* 處理圖片二維碼解析的數(shù)據(jù)
* @param s
*/
public void handleQrCode(String s) {
if (null == s) {
Toast.makeText(getApplicationContext(), "圖片格式有誤", Toast.LENGTH_SHORT).show();
} else {
// 識(shí)別出圖片二維碼/條碼离咐,內(nèi)容為s
handleResult(s);
//Toast.makeText(getApplicationContext(), s, Toast.LENGTH_SHORT).show();
}
}
private void handleResult(String result) {
Intent intent = new Intent();
intent.putExtra(SCAN_RESULT, result);
setResult(Activity.RESULT_OK, intent);
finish();
}
其中 QRCodeDecoder 類是自己寫的,是解析二維碼圖片的關(guān)鍵類奉件,代碼如下:
/**
* @desciption: 解析二維碼/條碼
*/
public class QRCodeDecoder {
public static final Map<DecodeHintType, Object> HINTS = new EnumMap<>(DecodeHintType.class);
static {
List<BarcodeFormat> allFormats = new ArrayList<>();
allFormats.add(BarcodeFormat.AZTEC);
allFormats.add(BarcodeFormat.CODABAR);
allFormats.add(BarcodeFormat.CODE_39);
allFormats.add(BarcodeFormat.CODE_93);
allFormats.add(BarcodeFormat.CODE_128);
allFormats.add(BarcodeFormat.DATA_MATRIX);
allFormats.add(BarcodeFormat.EAN_8);
allFormats.add(BarcodeFormat.EAN_13);
allFormats.add(BarcodeFormat.ITF);
allFormats.add(BarcodeFormat.MAXICODE);
allFormats.add(BarcodeFormat.PDF_417);
allFormats.add(BarcodeFormat.QR_CODE);
allFormats.add(BarcodeFormat.RSS_14);
allFormats.add(BarcodeFormat.RSS_EXPANDED);
allFormats.add(BarcodeFormat.UPC_A);
allFormats.add(BarcodeFormat.UPC_E);
allFormats.add(BarcodeFormat.UPC_EAN_EXTENSION);
HINTS.put(DecodeHintType.TRY_HARDER, BarcodeFormat.QR_CODE);
HINTS.put(DecodeHintType.POSSIBLE_FORMATS, allFormats);
HINTS.put(DecodeHintType.CHARACTER_SET, "utf-8");
}
private QRCodeDecoder() {
}
/**
* 同步解析本地圖片二維碼宵蛀。該方法是耗時(shí)操作,請(qǐng)?jiān)谧泳€程中調(diào)用县貌。
*
* @param picturePath 要解析的二維碼圖片本地路徑
* @return 返回二維碼圖片里的內(nèi)容 或 null
*/
public static String syncDecodeQRCode(String picturePath) {
return syncDecodeQRCode(getDecodeAbleBitmap(picturePath));
}
/**
* 同步解析bitmap二維碼术陶。該方法是耗時(shí)操作,請(qǐng)?jiān)谧泳€程中調(diào)用煤痕。
*
* @param bitmap 要解析的二維碼圖片
* @return 返回二維碼圖片里的內(nèi)容 或 null
*/
public static String syncDecodeQRCode(Bitmap bitmap) {
Result result = null;
RGBLuminanceSource source = null;
try {
int width = bitmap.getWidth();
int height = bitmap.getHeight();
int[] pixels = new int[width * height];
bitmap.getPixels(pixels, 0, width, 0, 0, width, height);
source = new RGBLuminanceSource(width, height, pixels);
result = new MultiFormatReader().decode(new BinaryBitmap(new HybridBinarizer(source)), HINTS);
return result.getText();
} catch (Exception e) {
e.printStackTrace();
if (source != null) {
try {
result = new MultiFormatReader().decode(new BinaryBitmap(new GlobalHistogramBinarizer(source)), HINTS);
return result.getText();
} catch (Throwable e2) {
e2.printStackTrace();
}
}
return null;
}
}
/**
* 將本地圖片文件轉(zhuǎn)換成可解碼二維碼的 Bitmap梧宫。為了避免圖片太大,這里對(duì)圖片進(jìn)行了壓縮摆碉。感謝 https://github.com/devilsen 提的 PR
*
* @param picturePath 本地圖片文件路徑
* @return
*/
private static Bitmap getDecodeAbleBitmap(String picturePath) {
try {
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeFile(picturePath, options);
int sampleSize = options.outHeight / 400;
if (sampleSize <= 0) {
sampleSize = 1;
}
options.inSampleSize = sampleSize;
options.inJustDecodeBounds = false;
return BitmapFactory.decodeFile(picturePath, options);
} catch (Exception e) {
return null;
}
}
三塘匣、生成二維碼圖片
主要類如下所示:
/**
* @desciption: 二維碼生成
*/
public class CodeCreator {
/**
* 生成二維碼
*/
public static Bitmap createQRCode(String content, int w, int h, Bitmap logo) {
if (TextUtils.isEmpty(content)) {
return null;
}
/*偏移量*/
int offsetX = w / 2;
int offsetY = h / 2;
/*生成logo*/
Bitmap logoBitmap = null;
if (logo != null) {
Matrix matrix = new Matrix();
float scaleFactor = Math.min(w * 1.0f / 5 / logo.getWidth(), h * 1.0f / 5 / logo.getHeight());
matrix.postScale(scaleFactor, scaleFactor);
logoBitmap = Bitmap.createBitmap(logo, 0, 0, logo.getWidth(), logo.getHeight(), matrix, true);
}
/*如果log不為null,重新計(jì)算偏移量*/
int logoW = 0;
int logoH = 0;
if (logoBitmap != null) {
logoW = logoBitmap.getWidth();
logoH = logoBitmap.getHeight();
offsetX = (w - logoW) / 2;
offsetY = (h - logoH) / 2;
}
/*指定為UTF-8*/
Hashtable<EncodeHintType, Object> hints = new Hashtable<EncodeHintType, Object>();
hints.put(EncodeHintType.CHARACTER_SET, "utf-8");
//容錯(cuò)級(jí)別
hints.put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.H);
//設(shè)置空白邊距的寬度
hints.put(EncodeHintType.MARGIN, 0);
// 生成二維矩陣,編碼時(shí)指定大小,不要生成了圖片以后再進(jìn)行縮放,這樣會(huì)模糊導(dǎo)致識(shí)別失敗
BitMatrix matrix = null;
try {
matrix = new MultiFormatWriter().encode(content,
BarcodeFormat.QR_CODE, w, h, hints);
// 二維矩陣轉(zhuǎn)為一維像素?cái)?shù)組,也就是一直橫著排了
int[] pixels = new int[w * h];
for (int y = 0; y < h; y++) {
for (int x = 0; x < w; x++) {
if (x >= offsetX && x < offsetX + logoW && y >= offsetY && y < offsetY + logoH) {
int pixel = logoBitmap.getPixel(x - offsetX, y - offsetY);
if (pixel == 0) {
if (matrix.get(x, y)) {
pixel = 0xff000000;
} else {
pixel = 0xffffffff;
}
}
pixels[y * w + x] = pixel;
} else {
if (matrix.get(x, y)) {
pixels[y * w + x] = 0xff000000;
} else {
pixels[y * w + x] = 0xffffffff;
}
}
}
}
Bitmap bitmap = Bitmap.createBitmap(w, h,
Bitmap.Config.ARGB_8888);
bitmap.setPixels(pixels, 0, w, 0, 0, w, h);
return bitmap;
} catch (WriterException e) {
System.out.print(e);
return null;
}
}
}