一 ,前言
現在的掃條碼是非常普遍了,連華爲手機在系統照相時,還可以自動toast顯示掃到的二維碼等,特別廣泛。
這陣子需要搞個掃條碼的功能,主要是條碼,二維碼等。之前用的一直都是 Zxing 的jar包,按照網上的弄。最近客戶反映有些長條碼特別難掃。試用了ZBar 後,發現掃描速度有提升,並且長條碼識別很高。
參考了網絡上的一些ZBar或者zxing應用,總是不如意,有些橫屏,有些是中文亂碼,有些是 ISBN13不能識別。基於此,我在解決了上述問題後,做了一些集成。
以下爲參考鏈接,感謝!
參考鏈接: http://www.itnose.net/detail/6155831.html (改掃描框)
http://blog.csdn.net/skillcollege (zbar zxing) 基本上我的項目就是在這個基礎上弄的,後面參考了github上的一個類似的項目,應該也是這個博主的,只不過這個鏈接的項目,掃ISBN13 條碼有問題,以及沒有橫屏的。爲了方便被別人用,我界面不是用 xml寫的,照着xml的屬性用純java代碼寫。後來發現多此一舉,不過不管了。粗糙地用還是可以,掃描框不滿意,自己替換圖片吧。
二, 說明
我將最終形成的代碼打成jar 包(包含圖片資源),如果對界面要求不高,可以直接用(方法見後面說明),或者替換圖片。如果要定製就只能修改 initUI 了(有兩種方法)。
此項目的特點: 1. 界面基本固定,可以替換圖片,更改掃描框樣式和掃描線
2. 支持橫屏和豎屏(全屏) ,識別區域與相機預覽區域相匹配
3. zxing 管理相機,zbar 解碼。二合一,超強解碼。
4. 界面上方有 取消按鈕 和 閃光燈(後期有時間考慮增加選擇圖庫中的圖片來解碼,也可能沒時間)
接下來的第三第四步是爲了後面的集成工程服務的。
三、Zbar工程NDK編譯
這一步確實麻煩,可以找網上的方法。 我這裏直接給zbar的工程,有NDK的可以試試(導入後添加本地支持就可以編譯了)。不過覺得沒必要。底層一般都沒有變。可以直接應用它的so庫和jar包,這個已經修復中文亂碼的了。等會會說明一下幾個zbar 的方法
zbar jni 工程下載地址: NDK工程 (也是下載 csdn 的博友的,忘記是誰的了。)
生成的 so 文件 和 jar包 : zbar so 文件
四、ZXing 核心庫簡化
已經簡化的ZXing 的代碼(java 1.6版本),地址:zxing java 核心庫
順便說一句,之前直接用了網上的簡化zxing.jar,但它是 java 1.7編譯的,有些1.7 的API 在1.6沒有。所以直接給別人用(有可能他用的是jdk 1.6),會發生某個類沒找到的異常
jar 包下載地址:zxing.jar
好了,前面弄完了,開始我們的主要內容了。
五、集成工程(主要工作是說明一些疑惑的地方)
1. 創建一個工程,我幫你們都弄好了。直接下吧,地址: XZbar
2. 一些代碼的說明:
(1)CameraConfigurationManager.java
這裏作了以下的修改:
public void initFromCameraParameters(Camera camera) {
Camera.Parameters parameters = camera.getParameters();
WindowManager manager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
Display display = manager.getDefaultDisplay();
Point theScreenResolution = new Point();
theScreenResolution = getDisplaySize(display);
screenResolution = theScreenResolution;
Log.i(TAG, "Screen resolution: " + screenResolution);
/** 因爲換成了豎屏顯示,所以不替換屏幕寬高得出的預覽圖是變形的 */
//如果爲豎屏就替換
if (screenResolution.x < screenResolution.y) {
Point screenResolutionForCamera = new Point();
screenResolutionForCamera.x = screenResolution.y;
screenResolutionForCamera.y = screenResolution.x;
//findBestPreviewSizeValue 是以屏幕橫屏(W x H)數據求最佳預覽界面數據(W x H)//等到最佳預覽後,要把長寬互換,這樣對外輸出的長寬定義是一致的。
Point cameraHW = findBestPreviewSizeValue(parameters, screenResolutionForCamera);
cameraResolution = new Point();
cameraResolution.x = cameraHW.y;
cameraResolution.y = cameraHW.x;
}
else
{
cameraResolution = findBestPreviewSizeValue(parameters, screenResolution);
}
Log.i(TAG, "Camera resolution x: " + cameraResolution.x);
Log.i(TAG, "Camera resolution y: " + cameraResolution.y);
}
但cameraResolution還是有可能被改變,在這個方法 setDesiredCameraParameters ,所以在這個方法順便處理一下。
//相機並不是對所有參數都響應,有可能設置失敗。
public void setDesiredCameraParameters(Camera camera, boolean safeMode) {
Camera.Parameters parameters = camera.getParameters();
if (parameters == null) {
Log.w(TAG, "Device error: no camera parameters are available. Proceeding without configuration.");
return;
}
Log.i(TAG, "Initial camera parameters: " + parameters.flatten());
if (safeMode) {
Log.w(TAG, "In camera config safe mode -- most settings will not be honored");
}
//如果爲豎屏,設置參數
if(screenResolution.x < screenResolution.y)
{
//有些設備的相機有可能不響應這個參數,所以還是在外面翻轉預覽的data[]數據
//parameters.setRotation(90);
/** 設置相機預覽爲豎屏 */
camera.setDisplayOrientation(90);
//preview size 總是設置爲橫屏時纔可能生效
//試驗發現,豎屏支持的preview 有 480x800,可是傳入480,800時,參數設置不成功,導致設置爲默認640x480。 如果傳800,480.設置參數成功。
parameters.setPreviewSize(cameraResolution.y, cameraResolution.x);</span>
}
else
{
parameters.setPreviewSize(cameraResolution.x, cameraResolution.y);
}
camera.setParameters(parameters);
Camera.Parameters afterParameters = camera.getParameters();
Camera.Size afterSize = afterParameters.getPreviewSize();
//主要是因爲前面作了動作,這裏得到豎屏的preview size還是長寬顛倒的。
if(screenResolution.x < screenResolution.y)
{
if (afterSize != null && (cameraResolution.y != afterSize.width || cameraResolution.x != afterSize.height)) {
Log.w(TAG, "Camera said it supported preview size " + cameraResolution.x + 'x' + cameraResolution.y + ", but after setting it, preview size is " + afterSize.width + 'x' + afterSize.height);
cameraResolution.y = afterSize.width;
cameraResolution.x = afterSize.height;
}
}else
{
if (afterSize != null && (cameraResolution.x != afterSize.width || cameraResolution.y != afterSize.height)) {
Log.w(TAG, "Camera said it supported preview size " + cameraResolution.x + 'x' + cameraResolution.y + ", but after setting it, preview size is " + afterSize.width + 'x' + afterSize.height);
cameraResolution.x = afterSize.width;
cameraResolution.y = afterSize.height;
}
}
}
(2)DecodeHandler.java
這個文件,主要是對previewcallback 的數據做處理。
主要是幾個方法的說明:
Image barcode = new Image(size.width, size.height,"Y800"); // 創建一個image 大小和preview size一致
barcode.setData(rotatedData); //填充數據
barcode.setCrop( rect.left ,rect.top, rect.width(),rect.height());//圈出識別區域
int ret =scanner.scanImage(barcode); //掃描
難點就是怎麼確定識別區域? 這個在前面的參考連接中有詳細的說明。不過因爲我的全屏,所以不用得到狀態欄的高度。
/**
* Decode the data within the viewfinder rectangle, and time how long it
* took. For efficiency, reuse the same reader objects from one decode to
* the next.
*
* @param data
* The YUV preview frame.
* @param width
* The width of the preview frame.
* @param height
* The height of the preview frame.
*/
private void decode(byte[] data, int width, int height) {
Size size = activity.getCameraManager().getPreviewSize();
Log.w("PreView size:","size : w "+ size.width + " h:" + size.height);
Rect rect = activity.getCropRect();
Log.i("CropRect","左上點:("+rect.left+","+ rect.top+") 右下點:("+rect.right+","+ rect.bottom+")寬高:width:"+rect.width()+" height:"+rect.height());
byte[] rotatedData = new byte[data.length];
if(activity.getResources().getConfiguration().orientation==Configuration.ORIENTATION_PORTRAIT)
{
// 這裏需要將獲取的data翻轉一下,因爲相機默認拿的的橫屏的數據
rotatedData = new byte[data.length];
for (int y = 0; y < size.height; y++) {
for (int x = 0; x < size.width; x++)
rotatedData[x * size.height + size.height - y - 1] = data[x + y * size.width];
}
// 寬高也要調整
int tmp = size.width;
size.width = size.height;
size.height = tmp;
}
else
{
System.arraycopy(data, 0, rotatedData, 0, data.length);
}
//zbar解碼
Image barcode = new Image(size.width, size.height, "Y800");
barcode.setData(rotatedData);
barcode.setCrop( rect.left ,rect.top, rect.width(),rect.height());
int ret = scanner.scanImage(barcode);
if (ret != 0) {
if (null != activity.getHandler()) {
SymbolSet syms = scanner.getResults();
String resultString = "";
for (Symbol sym : syms) {
resultString="" + sym.getData();
}
if (null == resultString || resultString.equals(""))
{
//繼續掃描
}else{
PlanarYUVLuminanceSource source = buildLuminanceSource(rotatedData, size.width, size.height);
Message msg = new Message();
Bundle bundle = new Bundle();
bundleThumbnail(source, bundle);
msg.setData(bundle);
msg.obj = resultString;
msg.what = decode_succeeded_id;
activity.getHandler().sendMessage(msg);
}
}
} else {
if (null != activity.getHandler()) {
activity.getHandler().sendEmptyMessage(decode_failed_id);
}
}
}
六,如果希望直接引入jar 來使用,使用說明在這裏。打包好的jar以及資源文件下載地址: XZabr.jar
1. AndroidManifest.xml 添加權限:
<supports-screens
android:anyDensity="true"
android:largeScreens="true"
android:normalScreens="true"
android:smallScreens="true"
android:xlargeScreens="true" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.FLASHLIGHT" />
<uses-feature android:name="android.hardware.camera" />
<uses-feature android:name="android.hardware.camera.autofocus" />
<uses-permission android:name="android.permission.VIBRATE" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
申明acticity
<activity android:name="com.ZbarZxing.XZbar.ZbarActivity" >
</activity>
2. 將資源文件放置好。
3. 放置 動態庫文件和jar包
4. 在 mainActivity 創建對象
HxBarcode hxBarcode = new HxBarcode();
//參數爲 context,requestCode(在下面的 onResultActivity 中有用),boolean值(是否橫屏)
hxBarcode.scan(MainActivity.this, 501,true);
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
// TODO Auto-generated method stub
super.onActivityResult(requestCode, resultCode, data);
System.out.printf("onActivityResult : %s,%s\n",requestCode,resultCode);
switch(requestCode)
{
case 501:
if(data!=null)
{
Bundle extras = data.getBundleExtra("data");
if (null != extras) {
int width = extras.getInt("width");
int height = extras.getInt("height");
String result = extras.getString("result");
mResultText.setText(result);
//以下只是爲了顯示圖片。
LayoutParams lps = new LayoutParams(width,height);
lps.topMargin = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 30, getResources().getDisplayMetrics());
lps.leftMargin = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 20, getResources().getDisplayMetrics());
lps.rightMargin = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 20, getResources().getDisplayMetrics());
mResultImage.setLayoutParams(lps);
Bitmap barcode = null;
byte[] compressedBitmap = extras.getByteArray(DecodeThread.BARCODE_BITMAP);
if (compressedBitmap != null) {
barcode = BitmapFactory.decodeByteArray(compressedBitmap, 0, compressedBitmap.length, null);
// Mutable copy:
barcode = barcode.copy(Bitmap.Config.RGB_565, true);
}
mResultImage.setImageBitmap(barcode);
}
}
break;
}
}
2015.2.9 號 增加相冊選擇圖片解碼
有網友需要從圖片解碼,我自己也沒時間實現。
我的思路是:
1. 取得圖片的原始數據(不包含頭信息什麼的),這樣就跟預覽時一樣了。問題是我用 bitmap.getPixels取得數據是 int數組,應該是ARGB格式。barcode.setData(int[] in)試驗不能解碼。(結果不成功,思路應該對吧。)
2. 取得圖片的純RGB數據,再轉爲預覽時回調的那種數據格式,可能是YUV42sp之類的,結果還是不懂怎麼轉爲YUV42sp,而且還要保證,出來的數據是byte數組。(卡了)
然後今天,無意看到 Image barcode = new Image(W,H,"GREY")可以指定爲灰度數據解析。而我看了一下ARGB數據,A一直爲 0xff,也就是透明度不用管了。我將RGB用灰度公式轉成灰度值,並放在byte數組,這樣就剛好了。於是選擇圖片解碼,終於可以了。哇咔咔。
(1)ZbarActivity.java中,在addLisenter() 增加一個按鍵監聽
gallery.setOnClickListener(new OnClickListener() {
public void onClick(View v) {
Intent intent = new Intent(Intent.ACTION_GET_CONTENT, null);
intent.setType("image/*");
intent.putExtra("crop", "true");
intent.putExtra("aspectX", 3);
intent.putExtra("aspectY", 2);
intent.putExtra("outputX", 600);
intent.putExtra("outputY", 400);
intent.putExtra("scale", true);
intent.putExtra("return-data", false);
intent.putExtra(MediaStore.EXTRA_OUTPUT, imageUri);
intent.putExtra("outputFormat",Bitmap.CompressFormat.JPEG.toString());
intent.putExtra("noFaceDetection", true); // no face detection
startActivityForResult(intent,PHOTO_REQUEST_CUT);
}
});
增加 onActivityResult
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
// TODO Auto-generated method stub
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == PHOTO_REQUEST_CUT
&& resultCode == Activity.RESULT_OK)
{
if (imageUri != null) {
Bitmap bitmap = decodeUriAsBitmap(imageUri);// decode bitmap
int W = bitmap.getWidth();
int H = bitmap.getHeight();
int[] photodata = new int[W * H];
bitmap.getPixels(photodata, 0, W, 0, 0, W, H); //獲取圖片原始ARGB數據
//將RGB轉爲灰度數據。
byte[] greyData = new byte[W * H];
for (int i = 0; i < greyData.length; i++) {
greyData[i] = (byte) ((((photodata[i] & 0x00ff0000) >> 16)
* 19595 + ((photodata[i] & 0x0000ff00) >> 8)
* 38469 + ((photodata[i] & 0x000000ff)) * 7472) >> 16);
}
Image barcode = new Image(W, H, "GREY");
barcode.setData(greyData);
ImageScanner scanner = new ImageScanner();
int ret = scanner.scanImage(barcode);
Log.e("zbh", "" + ret);
if (ret != 0) {
SymbolSet syms = scanner.getResults();
String resultString = "";
for (Symbol sym : syms) {
resultString = "" + sym.getData();
Log.e("zbh", "sym.getData()" + resultString);
}
if (null == resultString || resultString.equals("")) {
} else {
Bundle resultbundle = new Bundle();
resultbundle.putByteArray(DecodeThread.BARCODE_BITMAP,
Bitmap2Bytes(bitmap));
inactivityTimer.onActivity();
beepManager.playBeepSoundAndVibrate();
Intent dataStr = new Intent();
resultbundle.putInt("width", W);
resultbundle.putInt("height", H);
resultbundle.putString("result", resultString);
dataStr.putExtra("data", resultbundle);
setResult(Activity.RESULT_OK, dataStr);
finish();
}
} else {
// 解碼失敗
Intent dataStr = null;
setResult(Activity.RESULT_CANCELED, dataStr);
finish();
}
}
}
}
public byte[] Bitmap2Bytes(Bitmap bm) {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
bm.compress(Bitmap.CompressFormat.PNG, 100, baos);
return baos.toByteArray();
}
private Bitmap decodeUriAsBitmap(Uri uri){
Bitmap bitmap = null;
try {
bitmap = BitmapFactory.decodeStream(getContentResolver().openInputStream(uri));
} catch (FileNotFoundException e) {
e.printStackTrace();
return null;
}
return bitmap;
}
增加兩個變量:
private String IMAGE_FILE_LOCATION="file://"+Environment.getExternalStorageDirectory().getAbsolutePath()+"/temp.jpg";
Uri imageUri=Uri.parse(IMAGE_FILE_LOCATION);//The Uri to store the big bitmap
修改
// Intent intent = new Intent(Intent.ACTION_GET_CONTENT, null);
爲 Intent intent = new Intent(Intent.ACTION_PICK, null);因爲發現在 4.4 我的華爲手機,老是提示 文件沒找到 異常。 修改後正常。