Android開發學習之基於ZBar實現微信掃一掃
蟄伏半月有餘,一直在準備期末考試,期間抽空研究了一些Android的原始碼,現在我就把在這其中的一些收穫分享給大家。
今天想分享給大家的是二維碼掃描。說起二維碼,大家一定不會陌生,尤其是微信火了以後,在我們的生活中幾乎隨處都可以看到二維碼的影
子。相關科技媒體甚至把二維碼當成是未來移動網際網路的入口,因此研究二維碼的相關技術就顯得意義非凡。目前在移動開發領域,使用最為廣泛的二
維碼庫有兩個,分別是ZXing和ZBar,其中ZXing在Android開發中較為常見,而ZBar則在IOS開發中較為常見,更重要的一點是,這兩個庫都是開源
的,因此我們可以從原始碼中獲得很多有用的東西。關於ZXing,網上有很多
不是用在IOS中,怎麼今天要說ZBar呢?其實我是從這兩個庫使用的難易程度來選擇的,ZXing功能強大,但是使用起來比較繁瑣,網上有很多簡化的
教程,大家可以自行前去研究。相比較而言,ZBar則比較簡單,使用起來容易上手,因此我們今天選擇了ZBar作為我們的庫來使用。
一、準備工作
二、匯入專案
下載完成後,我們直接解壓,可以看到下面的目錄結構
開啟android資料夾,我們可以找到一個Example的資料夾,這是官方給出的示例程式碼,我們下面的所有工作都是基於這個示例程式而來。我們
自行建立一個Android專案,並將這兩個檔案拷貝到我們的專案中,同時引入ZBar相關的庫檔案。
三、建立佈局
首先建立主介面,即掃描二維碼的介面,介面佈局程式碼如下:
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" android:layout_width="match_parent" android:layout_height="match_parent"> <RelativeLayout android:layout_width="match_parent" android:layout_height="40dp" android:background="@drawable/title_bg" > <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_centerHorizontal="true" android:layout_centerVertical="true" android:text="@string/Scan" android:textColor="#ffffff" android:textSize="18sp" /> <Button android:id="@+id/BtnAbout" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignParentRight="true" android:layout_alignParentTop="true" android:text="@string/BtnAbout" /> </RelativeLayout> <FrameLayout android:id="@+id/cameraPreview" android:layout_width="match_parent" android:layout_height="0dip" android:layout_weight="1"/> </LinearLayout>
實現的佈局效果如下圖所示:
接下里,我們在來設計一個用於顯示結果的介面,介面佈局程式碼如下:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#ffffff"
android:orientation="vertical" >
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="40dp"
android:background="@drawable/title_bg" >
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerHorizontal="true"
android:layout_centerVertical="true"
android:textSize="18sp"
android:textColor="#ffffff"
android:text="@string/Result" />
<Button
android:id="@+id/BtnBack"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_alignParentTop="true"
android:text="@string/BtnBack" />
</RelativeLayout>
<TextView
android:id="@+id/TextResult"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textColor="#000000"
android:layout_margin="8dp"
android:textIsSelectable="true"/>
</LinearLayout>
實現的介面效果如圖所示:
四、編寫程式碼
首先我們來寫一個用於掃描的相機預覽檢視CameraPreview,此檔案由ZBar的SDK提供,這裡我做了下簡單的修改
package com.Android.ZBar4Android;
import java.io.IOException;
import android.util.Log;
import android.view.SurfaceView;
import android.view.SurfaceHolder;
import android.annotation.SuppressLint;
import android.content.Context;
import android.graphics.Canvas;
import android.hardware.Camera;
import android.hardware.Camera.Parameters;
import android.hardware.Camera.PreviewCallback;
import android.hardware.Camera.AutoFocusCallback;
//此類由ZBar專案的SDK提供,我做了下修改
@SuppressLint("ViewConstructor")
public class CameraPreview extends SurfaceView implements SurfaceHolder.Callback
{
private SurfaceHolder mHolder;
private Camera mCamera;
private PreviewCallback mPreviewCallBack;
private AutoFocusCallback mAutoFocusCallBack;
public CameraPreview(Context context, Camera camera,
PreviewCallback previewCb,
AutoFocusCallback autoFocusCb) {
super(context);
mCamera = camera;
mPreviewCallBack = previewCb;
mAutoFocusCallBack = autoFocusCb;
/*
* 自動聚焦
* 要求API版本>9
*/
Camera.Parameters parameters = camera.getParameters();
for (String f : parameters.getSupportedFocusModes()) {
if (f == Parameters.FOCUS_MODE_CONTINUOUS_PICTURE) {
parameters.setFocusMode(Parameters.FOCUS_MODE_CONTINUOUS_PICTURE);
mAutoFocusCallBack = null;
break;
}
}
// Install a SurfaceHolder.Callback so we get notified when the
// underlying surface is created and destroyed.
mHolder = getHolder();
mHolder.addCallback(this);
// deprecated setting, but required on Android versions prior to 3.0
mHolder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);
}
public void surfaceCreated(SurfaceHolder holder) {
// The Surface has been created, now tell the camera where to draw the preview.
try {
mCamera.setPreviewDisplay(holder);
} catch (IOException e) {
Log.d("DBG", "Error setting camera preview: " + e.getMessage());
}
}
public void surfaceDestroyed(SurfaceHolder holder) {
// Camera preview released in activity
}
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
/*
* If your preview can change or rotate, take care of those events here.
* Make sure to stop the preview before resizing or reformatting it.
*/
if (mHolder.getSurface() == null){
// preview surface does not exist
return;
}
// stop preview before making changes
try {
mCamera.stopPreview();
} catch (Exception e){
// ignore: tried to stop a non-existent preview
}
try {
// Hard code camera surface rotation 90 degs to match Activity view in portrait
mCamera.setDisplayOrientation(90);
mCamera.setPreviewDisplay(mHolder);
mCamera.setPreviewCallback(mPreviewCallBack);
mCamera.startPreview();
mCamera.autoFocus(mAutoFocusCallBack);
} catch (Exception e){
Log.d("DBG", "Error starting camera preview: " + e.getMessage());
}
}
/*
* 繪製校準框
* 修改:秦元培
* 時間:2013年11月22日
*
*/
@Override
protected void onDraw(Canvas mCanvas)
{
//這裡不會寫了?
}
}
接下來,我們來編寫主介面的邏輯程式碼,在這裡我們需要搞清楚的幾個問題有:
1、相機的獲取及相機的互動處理
2、二維碼圖片的獲取
3、二維碼圖片的解析
對於第一個問題,需要我們深入地瞭解相機的工作原理,即我們需要了解Camera類。
獲取相機的程式碼如下:
//獲取照相機的方法
public static Camera getCameraInstance()
{
Camera mCamera = null;
try
{
mCamera = Camera.open();
//沒有後置攝像頭,嘗試開啟前置攝像頭*******************
if (mCamera == null)
{
Camera.CameraInfo mCameraInfo = new Camera.CameraInfo();
int cameraCount = Camera.getNumberOfCameras();
for (int camIdx = 0; camIdx < cameraCount; camIdx++)
{
Camera.getCameraInfo(camIdx, mCameraInfo);
if (mCameraInfo.facing == Camera.CameraInfo.CAMERA_FACING_FRONT)
{
mCamera = Camera.open(camIdx);
}
}
}
}
catch (Exception e)
{
e.printStackTrace();
}
return mCamera;
}
釋放照相機的方法
//釋放照相機
private void releaseCamera()
{
if (mCamera != null)
{
IsPreview = false;
mCamera.setPreviewCallback(null);
mCamera.release();
mCamera = null;
}
}
相機反饋的方法,即掃描到二維碼後的處理
PreviewCallback previewCb = new PreviewCallback()
{
public void onPreviewFrame(byte[] data, Camera camera)
{
Camera.Parameters parameters = camera.getParameters();
//獲取掃描圖片的大小
Size mSize = parameters.getPreviewSize();
//構造儲存圖片的Image
Image mResult = new Image(mSize.width, mSize.height, "Y800");//第三個引數不知道是幹嘛的
//設定Image的資料資源
mResult.setData(data);
//獲取掃描結果的程式碼
int mResultCode = mScanner.scanImage(mResult);
//如果程式碼不為0,表示掃描成功
if (mResultCode != 0)
{
//停止掃描
IsPreview = false;
mCamera.setPreviewCallback(null);
mCamera.stopPreview();
//開始解析掃描圖片
SymbolSet Syms = mScanner.getResults();
for (Symbol mSym : Syms)
{
//mSym.getType()方法可以獲取掃描的型別,ZBar支援多種掃描型別,這裡實現了條形碼、二維碼、ISBN碼的識別
if (mSym.getType() == Symbol.CODE128 || mSym.getType() == Symbol.QRCODE ||
mSym.getType() == Symbol.CODABAR || mSym.getType() == Symbol.ISBN10 ||
mSym.getType() == Symbol.ISBN13|| mSym.getType()==Symbol.DATABAR ||
mSym.getType()==Symbol.DATABAR_EXP || mSym.getType()==Symbol.I25)
{
//新增震動效果,提示使用者掃描完成
Vibrator mVibrator=(Vibrator)getSystemService(VIBRATOR_SERVICE);
mVibrator.vibrate(400);
Intent intent=new Intent(MainActivity.this,ResultActivity.class);
intent.putExtra("ScanResult", "掃描型別:"+GetResultByCode(mSym.getType())+"\n"+ mSym.getData());
//這裡需要注意的是,getData方法才是最終返回識別結果的方法
//但是這個方法是返回一個標識型的字串,換言之,返回的值中包含每個字串的含義
//例如N代表姓名,URL代表一個Web地址等等,其它的暫時不清楚,如果可以對這個進行一個較好的分割
//效果會更好,如果需要返回掃描的圖片,可以對Image做一個合適的處理
startActivity(intent);
IsScanned = true;
}
else
{
//否則繼續掃描
IsScanned = false;
mCamera.setPreviewCallback(previewCb);
mCamera.startPreview();
IsPreview = true;
mCamera.autoFocus(autoFocusCB);
}
}
}
}
};
對於第二個問題,從上面的程式碼中我們可以看出,Image類用於獲取二維碼圖片,ImageScanner類用於對圖片的初步解析,而圖片的最終解析是在SymbolSet類和
Symbol中去實現的,由此,第三個問題得以解答。下面給出完整程式碼:
/*
* ZBar4Android
* 作者:秦元培
* 時間:2013年12月21日
* 需要解決的問題有:
* 1、返回內容的正則解析
* 2、如果鎖屏後開啟程式會報錯
* 3、沒有校正框,畫不出來啊,鬱悶
* 4、可能會與其它相機應用衝突,如微信
* 5、條形碼還是讀不出來
*/
package com.Android.ZBar4Android;
import com.Android.ZBar4Android.CameraPreview;
import com.Android.ZBar4Android.R;
import android.app.Activity;
import android.app.AlertDialog;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.pm.ActivityInfo;
import android.os.Bundle;
import android.os.Handler;
import android.os.Vibrator;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup.LayoutParams;
import android.view.Window;
import android.view.View.OnClickListener;
import android.widget.FrameLayout;
import android.widget.Button;
import android.widget.PopupWindow;
import android.hardware.Camera;
import android.hardware.Camera.PreviewCallback;
import android.hardware.Camera.AutoFocusCallback;
import android.hardware.Camera.Size;
import net.sourceforge.zbar.ImageScanner;
import net.sourceforge.zbar.Image;
import net.sourceforge.zbar.Symbol;
import net.sourceforge.zbar.SymbolSet;
import net.sourceforge.zbar.Config;
public class MainActivity extends Activity
{
//關於按鈕
private Button BtnAbout;
//相機
private Camera mCamera;
//預覽檢視
private CameraPreview mPreview;
//自動聚焦
private Handler mAutoFocusHandler;
//圖片掃描器
private ImageScanner mScanner;
//彈出視窗
private PopupWindow mPopupWindow;
//是否掃描完畢
private boolean IsScanned = false;
//是否處於預覽狀態
private boolean IsPreview = true;
//是否顯示彈出層
private boolean IsShowPopup=false;
//載入iconvlib
static
{
System.loadLibrary("iconv");
}
public void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
//去除標題欄
requestWindowFeature(Window.FEATURE_NO_TITLE);
setContentView(R.layout.layout_main);
//設定螢幕方向為豎屏
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
//自動聚焦執行緒
mAutoFocusHandler = new Handler();
//獲取相機例項
mCamera = getCameraInstance();
if(mCamera == null)
{
//在這裡寫下獲取相機失敗的程式碼
AlertDialog.Builder mBuilder=new AlertDialog.Builder(this);
mBuilder.setTitle("ZBar4Android");
mBuilder.setMessage("ZBar4Android獲取相機失敗,請重試!");
mBuilder.setPositiveButton("確定", new DialogInterface.OnClickListener()
{
@Override
public void onClick(DialogInterface mDialogInterface, int mIndex)
{
MainActivity.this.finish();
}
});
AlertDialog mDialog=mBuilder.create();
mDialog.show();
}
//例項化Scanner
mScanner = new ImageScanner();
mScanner.setConfig(0, Config.X_DENSITY, 3);
mScanner.setConfig(0, Config.Y_DENSITY, 3);
//設定相機預覽檢視
mPreview = new CameraPreview(this, mCamera, previewCb, autoFocusCB);
FrameLayout preview = (FrameLayout)findViewById(R.id.cameraPreview);
preview.addView(mPreview);
if (IsScanned)
{
IsScanned = false;
mCamera.setPreviewCallback(previewCb);
mCamera.startPreview();
IsPreview = true;
mCamera.autoFocus(autoFocusCB);
}
//獲取BtnAbout,顯示程式資訊
BtnAbout=(Button)findViewById(R.id.BtnAbout);
BtnAbout.setOnClickListener(new OnClickListener()
{
@Override
public void onClick(View v)
{
//如果彈出層已開啟,銷燬彈出層
if(IsShowPopup)
{
mPopupWindow.dismiss();
IsShowPopup=false;
}
else
{
//否則顯示彈出層
mPopupWindow=new PopupWindow();
LayoutInflater mInflater=LayoutInflater.from(getApplicationContext());
View view=mInflater.inflate(R.layout.layout_about, null);
mPopupWindow.setContentView(view);
mPopupWindow.setWidth(LayoutParams.WRAP_CONTENT);
mPopupWindow.setHeight(LayoutParams.WRAP_CONTENT);
mPopupWindow.showAtLocation(mPreview, 0, 100, 100);
IsShowPopup=true;
}
}
});
}
//實現Pause方法
public void onPause()
{
super.onPause();
releaseCamera();
}
//獲取照相機的方法
public static Camera getCameraInstance()
{
Camera mCamera = null;
try
{
mCamera = Camera.open();
//沒有後置攝像頭,嘗試開啟前置攝像頭*******************
if (mCamera == null)
{
Camera.CameraInfo mCameraInfo = new Camera.CameraInfo();
int cameraCount = Camera.getNumberOfCameras();
for (int camIdx = 0; camIdx < cameraCount; camIdx++)
{
Camera.getCameraInfo(camIdx, mCameraInfo);
if (mCameraInfo.facing == Camera.CameraInfo.CAMERA_FACING_FRONT)
{
mCamera = Camera.open(camIdx);
}
}
}
}
catch (Exception e)
{
e.printStackTrace();
}
return mCamera;
}
//釋放照相機
private void releaseCamera()
{
if (mCamera != null)
{
IsPreview = false;
mCamera.setPreviewCallback(null);
mCamera.release();
mCamera = null;
}
}
private Runnable doAutoFocus = new Runnable()
{
public void run()
{
if (IsPreview)
mCamera.autoFocus(autoFocusCB);
}
};
PreviewCallback previewCb = new PreviewCallback()
{
public void onPreviewFrame(byte[] data, Camera camera)
{
Camera.Parameters parameters = camera.getParameters();
//獲取掃描圖片的大小
Size mSize = parameters.getPreviewSize();
//構造儲存圖片的Image
Image mResult = new Image(mSize.width, mSize.height, "Y800");//第三個引數不知道是幹嘛的
//設定Image的資料資源
mResult.setData(data);
//獲取掃描結果的程式碼
int mResultCode = mScanner.scanImage(mResult);
//如果程式碼不為0,表示掃描成功
if (mResultCode != 0)
{
//停止掃描
IsPreview = false;
mCamera.setPreviewCallback(null);
mCamera.stopPreview();
//開始解析掃描圖片
SymbolSet Syms = mScanner.getResults();
for (Symbol mSym : Syms)
{
//mSym.getType()方法可以獲取掃描的型別,ZBar支援多種掃描型別,這裡實現了條形碼、二維碼、ISBN碼的識別
if (mSym.getType() == Symbol.CODE128 || mSym.getType() == Symbol.QRCODE ||
mSym.getType() == Symbol.CODABAR || mSym.getType() == Symbol.ISBN10 ||
mSym.getType() == Symbol.ISBN13|| mSym.getType()==Symbol.DATABAR ||
mSym.getType()==Symbol.DATABAR_EXP || mSym.getType()==Symbol.I25)
{
//新增震動效果,提示使用者掃描完成
Vibrator mVibrator=(Vibrator)getSystemService(VIBRATOR_SERVICE);
mVibrator.vibrate(400);
Intent intent=new Intent(MainActivity.this,ResultActivity.class);
intent.putExtra("ScanResult", "掃描型別:"+GetResultByCode(mSym.getType())+"\n"+ mSym.getData());
//這裡需要注意的是,getData方法才是最終返回識別結果的方法
//但是這個方法是返回一個標識型的字串,換言之,返回的值中包含每個字串的含義
//例如N代表姓名,URL代表一個Web地址等等,其它的暫時不清楚,如果可以對這個進行一個較好的分割
//效果會更好,如果需要返回掃描的圖片,可以對Image做一個合適的處理
startActivity(intent);
IsScanned = true;
}
else
{
//否則繼續掃描
IsScanned = false;
mCamera.setPreviewCallback(previewCb);
mCamera.startPreview();
IsPreview = true;
mCamera.autoFocus(autoFocusCB);
}
}
}
}
};
//用於重新整理自動聚焦的方法
AutoFocusCallback autoFocusCB = new AutoFocusCallback()
{
public void onAutoFocus(boolean success, Camera camera)
{
mAutoFocusHandler.postDelayed(doAutoFocus, 1000);
}
};
//根據返回的程式碼值來返回相應的格式化資料
public String GetResultByCode(int CodeType)
{
String mResult="";
switch(CodeType)
{
//條形碼
case Symbol.CODABAR:
mResult="條形碼";
break;
//128編碼格式二維碼)
case Symbol.CODE128:
mResult="二維碼";
break;
//QR碼二維碼
case Symbol.QRCODE:
mResult="二維碼";
break;
//ISBN10圖書查詢
case Symbol.ISBN10:
mResult="圖書ISBN號";
break;
//ISBN13圖書查詢
case Symbol.ISBN13:
mResult="圖書ISBN號";
break;
}
return mResult;
}
}
對於顯示掃描結果的介面,程式碼比較簡單
/*
* 返回掃描結果
* 作者:秦元培
* 時間:2013年12月21日
* 總結:這裡有一個問題,就是在這個介面上按下返回鍵的時候程式會立即報錯,試著重寫過相關的方法都解決不了問題
* 誰要是知道這個問題怎麼解決,記得給我說一聲啊
*/
package com.Android.ZBar4Android;
import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.Window;
import android.widget.Button;
import android.widget.TextView;
public class ResultActivity extends Activity
{
private TextView tv;
private Button BtnBack;
@Override
protected void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
requestWindowFeature(Window.FEATURE_NO_TITLE);
setContentView(R.layout.layout_result);
//獲取掃描結果
Intent intent=getIntent();
Bundle mData=intent.getExtras();
CharSequence mResult=mData.getCharSequence("ScanResult");
StringHelper mHelper=new StringHelper(mResult.toString());
mResult=mHelper.SplitFormDict();
tv=(TextView)findViewById(R.id.TextResult);
tv.setText(mResult);
//返回掃描介面
BtnBack=(Button)findViewById(R.id.BtnBack);
BtnBack.setOnClickListener(new OnClickListener()
{
@Override
public void onClick(View arg0)
{
Intent intent=new Intent(ResultActivity.this,MainActivity.class);
startActivity(intent);
}
});
}
@Override
protected void onPause()
{
super.onPause();
}
}
五、總結
經過測試,可以快速地對二維碼進行識別,並顯示掃描結果。目前尚存在的問題有:
1、官方的文件說它是支援條形碼、ISBN、二維碼等多種形式的編碼的,並且在程式程式碼中亦有所體現,但是實際測試中,發現二維碼可以掃,其餘的無法掃描
2、鎖屏後再次開啟程式會報錯
3、與微信等類似的需要相機功能的軟體衝突
4、校準框死活畫不出來
5、在掃描結果介面下按下返回鍵,程式報錯,無法攔截
歡迎大家積極尋找解決問題的方法,再次謝謝大家!