Android平台下利用zxing實現二維碼開發
現在走在大街小巷都能看到二維碼,而且最近由於項目需要,所以研究了下二維碼開發的東西,開源的二維碼掃描庫主要有zxing和zbar,zbar在iPos平台上應用比較成熟,而在Android平台上主流還是用zxing庫,因此這里主要講述如何利用zxing進行二維碼開發。
1.如何將zxing的Android源碼導入工程。
在導入zxing的android源碼之前,先去官方下載zxing的源碼:http://code.google.com/p/zxing/downloads/list。
我這里下載的是1.6版本的,我試驗了幾個版本,發現2.0以后的版本實際用起來沒有1.6和1.7版本的好用,最后選擇了1.6版本。
zxing 1.6源碼結構如下:
其中android文件夾就是android平台下的官方例子。
在導入之前先要對core文件下的源碼進行編譯,得到核心包core.jar。
編譯方法請參照:http://blog.163.com/yimigao@126/blog/static/671560502011611111116747/
然后就可以導入android平台下的例子了,導入方法如下:
1)打開Eclipse,新建android項目:(注意不要直接把android文件夾拷到workspace下導入,那樣會無法導入)
2)導入核心包core.jar。
3)修改strings.xml文件。在導入core.jar之后工程還是會有錯誤:
出現這種錯誤可能是由於字符錯誤導致的,只需要把所有的%s 和%f改成 %1$s和%1$f 即可。
修改完之后重新清理項目,此時已經沒有錯誤了:
4)運行效果:
2.代碼簡化
上面代碼中,很多功能我們在自己的項目中都用不到,因此需要對其進行簡化,至於如何簡化這里就不贅述了,網上有很多教程。下面只給出簡化后的結果:
如果只進行二維碼識別和二維碼生成的話,只需要上面包中的文件。其中CaptureActivity.java是拍照取景的類,camera包下面的類主要與照相機相關的類,decoding和encoding是解碼和編碼相關的類,view是取景框相關的類。
3.將簡化的zxing代碼嵌入自己的工程。
在自己的工程中嵌入簡化的zxing代碼即可實現二維碼生成和識別功能。
嵌入方法:
1)將上述簡化的代碼拖到自己工程目錄下;
2)將values文件夾和raw文件夾復制自己工程目錄下;
3)建立CaptureActivity.java的布局文件capture.xml。
<?xml version="1.0" encoding="utf-8"?> <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="fill_parent" android:layout_height="fill_parent" > <SurfaceView android:id="@+id/preview_view" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center" /> <com.zxing.view.ViewfinderView android:id="@+id/viewfinder_view" android:layout_width="wrap_content" android:layout_height="wrap_content" /> <RelativeLayout android:layout_width="fill_parent" android:layout_height="fill_parent" android:layout_gravity="center" android:orientation="vertical" > <TextView android:layout_width="fill_parent" android:layout_height="wrap_content" android:layout_alignParentTop="true" android:layout_centerInParent="true" android:gravity="center" android:paddingBottom="10dp" android:paddingTop="10dp" android:text="Scan Barcode" android:textColor="@android:color/white" android:textSize="18sp" android:textStyle="bold" /> <Button android:id="@+id/btn_cancel_scan" android:layout_width="230dp" android:layout_height="40dp" android:layout_alignParentBottom="true" android:layout_centerInParent="true" android:layout_marginBottom="75dp" android:text="Cancel" android:textSize="15sp" android:textStyle="bold" /> </RelativeLayout> </FrameLayout>
3)導入core.jar包
4)修改AndrodMainfest.xml
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.qrcode" android:versionCode="1" android:versionName="1.0"> <uses-sdk android:minSdkVersion="7" /> <uses-permission android:name="android.permission.VIBRATE" /> <!-- 震動權限 --> <uses-permission android:name="android.permission.CAMERA" /> <uses-feature android:name="android.hardware.camera" /> <!-- 使用照相機權限 --> <uses-feature android:name="android.hardware.camera.autofocus" /> <!-- 自動聚焦權限 --> <application android:icon="@drawable/icon" android:label="@string/app_name"> <activity android:name=".MainActivity" android:label="@string/app_name"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> <!-- 隱藏鍵盤 --><!-- 全屏 --> <activity android:configChanges="orientation|keyboardHidden" android:name="com.zxing.activity.CaptureActivity" android:screenOrientation="portrait" android:theme="@android:style/Theme.NoTitleBar.Fullscreen" android:windowSoftInputMode="stateAlwaysHidden" > </activity> </application> </manifest>
5)運行效果:
在這里識別二維碼之后是把結果返回到上一個activity顯示,所以在MainActivity中調用方法是:
Intent openCameraIntent = new Intent(MainActivity.this,CaptureActivity.class); startActivityForResult(openCameraIntent, 0);
然后獲取結果在onActivityResult中進行:
@Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { super.onActivityResult(requestCode, resultCode, data); if (resultCode == RESULT_OK) { Bundle bundle = data.getExtras(); String scanResult = bundle.getString("result"); resultTextView.setText(scanResult); } }
如果要直接在CaptureActivity中彈出識別結果,可以更改CaptureActivity.java的handleDecode方法:
public void handleDecode(Result result, Bitmap barcode) { inactivityTimer.onActivity(); playBeepSoundAndVibrate(); String resultString = result.getText(); //FIXME if (resultString.equals("")) { Toast.makeText(CaptureActivity.this, "Scan failed!", Toast.LENGTH_SHORT).show(); }else { // System.out.println("Result:"+resultString); Intent resultIntent = new Intent(); Bundle bundle = new Bundle(); bundle.putString("result", resultString); resultIntent.putExtras(bundle); this.setResult(RESULT_OK, resultIntent); } CaptureActivity.this.finish(); }
上述代碼是得到結果之后傳給之前調用CaptureActivity的activity,所以如果要改的話只需要把else語句塊下面的語句和最后一句finish注釋掉,補上自己的代碼就可以了。比如可以改成:
public void handleDecode(Result result, Bitmap barcode) { inactivityTimer.onActivity(); playBeepSoundAndVibrate(); final String resultString = result.getText(); //FIXME if (resultString.equals("")) { Toast.makeText(CaptureActivity.this, "Scan failed!", Toast.LENGTH_SHORT).show(); }else { // System.out.println("Result:"+resultString); /*Intent resultIntent = new Intent(); Bundle bundle = new Bundle(); bundle.putString("result", resultString); resultIntent.putExtras(bundle); this.setResult(RESULT_OK, resultIntent);*/ AlertDialog resutlDialog = new AlertDialog.Builder(CaptureActivity.this).create(); resutlDialog.setTitle("掃描結果"); resutlDialog.setMessage(resultString); resutlDialog.setButton(AlertDialog.BUTTON_NEGATIVE, "取消", new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { dialog.dismiss(); } }); resutlDialog.show(); } //CaptureActivity.this.finish(); }
我的工程源碼下載:http://files.cnblogs.com/dolphin0520/QRCode.rar
3.關於zxing源碼的若干問題。
在實際使用過程中發現zxing源碼存在一些問題,下面逐一來說一下:
1)豎屏問題。
zxing給出的官方例子是橫屏的,但是對於手機實際上豎屏操作更加方便一點,如果要改成豎屏,需要修改以下幾個文件:
首先將AndrodMainfest.xml下CaptureActivity的配置改為豎屏:
android:screenOrientation="portrait"
然后需要修改CameraConfigurationManager.java下的setDesiredCameraParameters方法,該方法直接使用
camera.setDisplayOrientation(90);
來讓屏幕旋轉90度,這個辦法在android 2.2版本以后是可行的,但是2.2之前的版本並沒有提供這個接口,因此需要對sdk版本進行判斷:
if (Integer.parseInt(Build.VERSION.SDK) >=8) setDisplayOrientation(camera, 90); else { if (context.getResources().getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT) { parameters.set("orientation", "portrait"); parameters.set("rotation", 90); } if (context.getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE) { parameters.set("orientation", "landscape"); parameters.set("rotation", 90); } }
在修改為豎屏之后發現取景框會發生拉伸,需要進行以下幾點修改:
在CameraManager.java中將:
rect.left = rect.left * cameraResolution.x / screenResolution.x; rect.right = rect.right * cameraResolution.x / screenResolution.x; rect.top = rect.top * cameraResolution.y / screenResolution.y; rect.bottom = rect.bottom * cameraResolution.y / screenResolution.y;
修改為:
rect.left = rect.left * cameraResolution.y / screenResolution.x; rect.right = rect.right * cameraResolution.y / screenResolution.x; rect.top = rect.top * cameraResolution.x / screenResolution.y; rect.bottom = rect.bottom * cameraResolution.x / screenResolution.y;
然后將DecodeHandler類中的方法decode改為:
private void decode(byte[] data, int width, int height) { long start = System.currentTimeMillis(); Result rawResult = null; //modify here byte[] rotatedData = new byte[data.length]; for (int y = 0; y < height; y++) { for (int x = 0; x < width; x++) rotatedData[x * height + height - y - 1] = data[x + y * width]; } int tmp = width; // Here we are swapping, that's the difference to #11 width = height; height = tmp; PlanarYUVLuminanceSource source = CameraManager.get().buildLuminanceSource(rotatedData, width, height); BinaryBitmap bitmap = new BinaryBitmap(new HybridBinarizer(source)); try { rawResult = multiFormatReader.decodeWithState(bitmap); } catch (ReaderException re) { // continue } finally { multiFormatReader.reset(); } if (rawResult != null) { long end = System.currentTimeMillis(); Log.d(TAG, "Found barcode (" + (end - start) + " ms):\n" + rawResult.toString()); Message message = Message.obtain(activity.getHandler(), R.id.decode_succeeded, rawResult); Bundle bundle = new Bundle(); bundle.putParcelable(DecodeThread.BARCODE_BITMAP, source.renderCroppedGreyscaleBitmap()); message.setData(bundle); //Log.d(TAG, "Sending decode succeeded message..."); message.sendToTarget(); } else { Message message = Message.obtain(activity.getHandler(), R.id.decode_failed); message.sendToTarget(); } }
再將CameraConfigurationManager類中的方法initFromCameraParameters修改為:
void initFromCameraParameters(Camera camera) { Camera.Parameters parameters = camera.getParameters(); previewFormat = parameters.getPreviewFormat(); previewFormatString = parameters.get("preview-format"); Log.d(TAG, "Default preview format: " + previewFormat + '/' + previewFormatString); WindowManager manager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); Display display = manager.getDefaultDisplay(); screenResolution = new Point(display.getWidth(), display.getHeight()); Log.d(TAG, "Screen resolution: " + screenResolution); Point screenResolutionForCamera = new Point(); screenResolutionForCamera.x = screenResolution.x; screenResolutionForCamera.y = screenResolution.y; // preview size is always something like 480*320, other 320*480 if (screenResolution.x < screenResolution.y) { screenResolutionForCamera.x = screenResolution.y; screenResolutionForCamera.y = screenResolution.x; } cameraResolution = getCameraResolution(parameters, screenResolutionForCamera); //cameraResolution = getCameraResolution(parameters, screenResolution); Log.d(TAG, "Camera resolution: " + screenResolution); }
最后將PlanarYUVLuminanceSource.java中的renderCroppedGreyscaleBitmap方法改為:
public Bitmap renderCroppedGreyscaleBitmap() { int width = getWidth(); int height = getHeight(); int[] pixels = new int[width * height]; byte[] yuv = yuvData; int inputOffset = top * dataHeight + left; for (int y = 0; y < height; y++) { int outputOffset = y * width; for (int x = 0; x < width; x++) { int grey = yuv[inputOffset + x] & 0xff; pixels[outputOffset + x] = 0xFF000000 | (grey * 0x00010101); } inputOffset += dataHeight; } Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); bitmap.setPixels(pixels, 0, width, 0, 0, width, height); return bitmap; }
3)取景框的繪制
取景框的繪制可以參照這篇博文:http://www.cnblogs.com/forrestsun/archive/2012/11/06/2757005.html
4)閃光燈的開啟和關閉
如果需要開啟和關閉閃光燈,在CameraManager.java中添加2個方法:
public void openLight() //打開閃光燈 { if(camera!=null) { Parameters parameter=camera.getParameters(); parameter.setFlashMode(Parameters.FLASH_MODE_TORCH); camera.setParameters(parameter); } } public void closeLight() //關閉閃光燈 { if(camera!=null) { Parameters parameter=camera.getParameters(); parameter.setFlashMode(Parameters.FLASH_MODE_OFF); camera.setParameters(parameter); } }
然后在CaptureActivity中的initCamera方法中打開閃光燈:
CameraManager.get().openDriver(surfaceHolder); CameraManager.get().openLight(); //開閃光燈
在需要的地方關閉閃光燈即可。
5)連續掃描問題
如果在識別二維碼成功之后,需要連續多次掃描二維碼,只需在掃描完成之后添加代碼(在CaptureActivity的dandleDecode方法中添加):
if(handler!=null) //實現連續掃描 handler.restartPreviewAndDecode();
比如:
public void handleDecode(Result result, Bitmap barcode) { inactivityTimer.onActivity(); playBeepSoundAndVibrate(); final String resultString = result.getText(); //FIXME if (resultString.equals("")) { Toast.makeText(CaptureActivity.this, "Scan failed!", Toast.LENGTH_SHORT).show(); }else { // System.out.println("Result:"+resultString); /*Intent resultIntent = new Intent(); Bundle bundle = new Bundle(); bundle.putString("result", resultString); resultIntent.putExtras(bundle); this.setResult(RESULT_OK, resultIntent);*/ AlertDialog resutlDialog = new AlertDialog.Builder(CaptureActivity.this).create(); resutlDialog.setTitle("掃描結果"); resutlDialog.setMessage(resultString); resutlDialog.setButton(AlertDialog.BUTTON_POSITIVE, "打開鏈接", new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { dialog.dismiss(); if(!isLegalUrl(resultString)) //如果url不合法 { Toast.makeText(getApplicationContext(), "該鏈接不是合法的URL", Toast.LENGTH_SHORT).show(); if(handler!=null) //實現連續掃描 handler.restartPreviewAndDecode(); return; } Intent intent = new Intent(); //打開鏈接 intent.setAction("android.intent.action.VIEW"); Uri content_url = Uri.parse(resultString); intent.setData(content_url); startActivity(intent); } }); resutlDialog.setButton(AlertDialog.BUTTON_NEGATIVE, "取消", new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { dialog.dismiss(); if(handler!=null) //實現連續掃描 handler.restartPreviewAndDecode(); } }); resutlDialog.show(); } //CaptureActivity.this.finish(); }
6)識別完成之后的震動。
如果需要取消二維碼識別之后的震動,只需要在CaptureActivity類的onResume方法中將vibrate 設置為false即可。
7)修改取景框距屏幕頂部位置
如果需要修改取景框距屏幕頂部位置,只需要修改CameraManager.java的getFramingRect方法,在getFramingRect方法中,
int topOffset = (screenResolution.y - height) / 2
這句是控制取景框到屏幕頂部的距離,若需要減小距屏幕頂部的距離,只需要將分母變大即可。
8)取景框下方提示文字的繪制。
在ViewfinderView.java的 Collection<ResultPoint> currentPossible = possibleResultPoints前面加入以下代碼:
TextPaint textPaint = new TextPaint();
textPaint.setARGB(0xFF, 0xFF, 0xFF,0xFF); //字體顏色 textPaint.setTextSize(TEXT_SIZE * density); textPaint.setAntiAlias(true); //設置抗鋸齒,否則字跡會很模糊
StaticLayout layout = new StaticLayout(getResources().getString(R.string.scan_text),textPaint, frame.right-frame.left,Alignment.ALIGN_NORMAL,1.0F,0.0F,true); canvas.translate(frame.left, (float) (frame.bottom + (float)TEXT_PADDING_TOP *density)); //繪制起始位置 layout.draw(canvas);
這里解釋一下,textPaint.setAntiAlias(true); 是設置為抗鋸齒,否則字體會很模糊。StaticLayout的第一個參數就是要繪制的字符串,第二個是畫筆,第三個參數是設置每一行的寬度,即超過該寬度就換行,第四個是對齊方式。
canvas.translate(frame.left, (float) (frame.bottom + (float)TEXT_PADDING_TOP *density))的參數是繪制字符串的起始位置。
9)亂碼問題
在掃描一部分二維碼時會出現亂碼的情況,是由於編碼的問題,可以參照這篇文章:
http://www.2cto.com/kf/201209/152900.html
如果不想編譯core包,我的工程源碼中的core包是已經改好了的,可以識別GBK二維碼,需要的朋友可以直接拿去用。
關於zxing 二維碼識別的問題暫時就討論這么多,有興趣的朋友可以讀源碼深入研究一下。