參考連結
前言 最近公司的Android项目需要用到摄像头做条码或二维码的扫描,Google一下,发现一个以Apache License 2.0 开源的ZXing项目。Zxing项目里的Android实现太过复杂多余东西太多,得对其进行简化。 前提条件 下载源代码: ZXing-1.6.zip http://code.google.com/p/zxing/downloads/detail?name=ZXing-1.6.zip&can=2&q= 最新的好像是1.7 编译核心库:Zxing的主页上有介绍具体步骤,大家也可以参照这篇博文:android 条码识别软件开发全解析
导入项目 打开Eclipse 导入 源码中的 Android 项目,然后右击项目 选择“Build path”——》"Add External Archives" 把核心库 core.jar文件加入到项目中。 此时编译一下项目,会发现报错,“ Multiple substitutions specified in non-positional format; did you mean to add the formatted="false" attribute?”之类的。打开raw 下的Values 发现错误是在一个<String>上。这里把 “preferences_custom_product_search_summary” 里的 %s %f 全部都改成 %1$s %1$f(因为我们用不到多国语言,建议只保留默认的Value ,其他全部删除)。 原因:由于新的SDK采用了新版本的aapt(Android项目编译器),这个版本的aapt编译起来会比老版本更加的严格,然后在Android最新的开发文档的描述String的部分,已经说明如何去设置 %s 等符号 “If you need to format your strings using String.format(String, Object...) , then you can do so by putting your format arguments in the string resource. For example, with the following resource: <string name="welcome_messages">Hello, %1$s! You have %2$d new messages.</string> In this example, the format string has two arguments: %1$s is a string and %2$d is a decimal number. You can format the string with arguements from your application...“ 经过以上步骤后项目应该就可以运行了。 但是ZXing的android项目东西太多了,有很多是我们不需要的,得新建另一个项目简化它。 简化 在开始前大致介绍一下简化ZXing需要用到各个包 、类的职责。
- CaptureActivity。这个是启动Activity 也就是扫描器(如果是第一安装,它还会跳转到帮助界面)。
- CaptureActivityHandler 解码处理类,负责调用另外的线程进行解码。
- DecodeThread 解码的线程。
- com.google.zxing.client.android.camera 包,摄像头控制包。
- ViewfinderView 自定义的View,就是我们看见的拍摄时中间的框框了。
新建另一个项目 新建另一个项目将启动的Activity命名为CaptureActivity,并导入核心库。项目新建完成后我们打开 CaptureActivity 的布局文件,我这里为main。把里面的XML修改为:
1 <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
2 android:layout_width="fill_parent" android:layout_height="fill_parent">
3 <SurfaceView android:id="@+id/preview_view"
4 android:layout_width="fill_parent" android:layout_height="fill_parent"
5 android:layout_centerInParent="true"/>
6
7 <com.Zxing.Demo.view.ViewfinderView
8 android:id="@+id/viewfinder_view" android:layout_width="fill_parent"
9 android:layout_height="fill_parent" android:background="@android:color/transparent"/>
10 <TextView android:layout_width="wrap_content"
11 android:id="@+id/txtResult"
12 android:layout_height="wrap_content" android:text="@string/hello"/>
13
14 </FrameLayout>
可以看到在XML里面用到了 ViewfinderView 自定义view 。所以新建一个View 的包,然后把:ViewfinderView 和 ViewfinderResultPointCallback 靠到里面(记得对应修改XML里面的包)。 打开 CaptureActivity 覆盖 onCreate 方法:
1 @Override
2 public void onCreate(Bundle savedInstanceState) {
3 super.onCreate(savedInstanceState);
4 setContentView(R.layout.main);
5 //初始化 CameraManager
6 CameraManager.init(getApplication());
7
8 viewfinderView = (ViewfinderView) findViewById(R.id.viewfinder_view);
9 txtResult = (TextView) findViewById(R.id.txtResult);
10 hasSurface =false;
11 inactivityTimer =new InactivityTimer(this);
12 }
这里调用到的 CameraManager 类是控制摄像头的包里的类。新建一个camera包把:com.google.zxing.client.android.camera 里面的类全部拷入,另外我把PlanarYUVLuminanceSource也拷入到这个包里面。根据错误的提示来修正代码,主要是修改正包结构。(整个简化的流程都是如此:“根据错误提示,修改代码”)。 
在修改的过程中,有很多是关于R 资源的问题,在此我们需要将Values 里面的两个xml资源文件拷入项目中:colos.xml 和ids.xml 。 ctrl+b 一下看看error 是不是少了很多。在CameraManager中有些地方需要用到项目的配置,这里需要把配置直接写入代码中: // SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
//是否使用前灯
// if (prefs.getBoolean(PreferencesActivity.KEY_FRONT_LIGHT, false)) {
// FlashlightManager.enableFlashlight();
// }
FlashlightManager.enableFlashlight();
使用摄像头需要加入相应的权限:
<uses-permission android:name="android.permission.CAMERA"></uses-permission>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"></uses-permission>
<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.FLASHLIGHT"/>
当View 和 camera 包里的错误修正完成后,我们继续来看CaptureActivity。 覆盖onResume方法初始化摄像头:
@Override
protected void onResume() {
super.onResume();
SurfaceView surfaceView = (SurfaceView) findViewById(R.id.preview_view);
SurfaceHolder surfaceHolder = surfaceView.getHolder();
if (hasSurface) {
initCamera(surfaceHolder);
} else {
surfaceHolder.addCallback(this);
surfaceHolder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);
}
decodeFormats =null;
characterSet =null;
playBeep =true;
AudioManager audioService = (AudioManager) getSystemService(AUDIO_SERVICE);
if (audioService.getRingerMode() != AudioManager.RINGER_MODE_NORMAL) {
playBeep =false;
}
initBeepSound();
vibrate =true;
}
initCamera
1 private void initCamera(SurfaceHolder surfaceHolder) {
2 try {
3 CameraManager.get().openDriver(surfaceHolder);
4 } catch (IOException ioe) {
5 return;
6 } catch (RuntimeException e) {
7 return;
8 }
9 if (handler ==null) {
10 handler =new CaptureActivityHandler(this, decodeFormats,
11 characterSet);
12 }
13 }
SurfaceHolder接口实现
@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width,
int height) {
}
@Override
public void surfaceCreated(SurfaceHolder holder) {
if (!hasSurface) {
hasSurface =true;
initCamera(holder);
}
}
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
hasSurface =false;
}
initCamera () 方法用于初始化摄像头,如果排除了所有的error ,运行项目时就可以看到大致扫描界面了。 surfaceHolder.addCallback(this);表示让CaptureActivity实现其callback接口。 handler = new CaptureActivityHandler(this, decodeFormats,characterSet) 用于进行扫描解码处理。 解码 上面的步骤主要都是用于对摄像头的控制,而解码的真正工作入口是在CaptureActivityHandler 里面的。新建一个Decoding包把以下文件拷入包中:
- CaptureActivityHandler
- DecodeFormatManager
- DecodeHandler
- DecodeThread
- FinishListener
- InactivityTimer
- Intents
由于我们的包结构和Zxing 项目的有所不同所以需要注意一下类的可访问性 同样开始ctrl+B 编译一下,然后开始修正错误。 在CaptureActivityHandler 里 把 handleMessage 里的部分方法先注释掉如:“decode_succeeded ”分支,这是解码成功时调用 CaptureActivity 展示解码的结果。 在DecodeThread 类里,修改部分涉及Preference配置的代码:
DecodeThread(CaptureActivity activity,
Vector<BarcodeFormat> decodeFormats,
String characterSet,
ResultPointCallback resultPointCallback) {
this.activity = activity;
handlerInitLatch =new CountDownLatch(1);
hints =new Hashtable<DecodeHintType, Object>(3);
//// The prefs can't change while the thread is running, so pick them up once here.
// if (decodeFormats == null || decodeFormats.isEmpty()) {
// SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(activity);
// decodeFormats = new Vector<BarcodeFormat>();
// if (prefs.getBoolean(PreferencesActivity.KEY_DECODE_1D, true)) {
// decodeFormats.addAll(DecodeFormatManager.ONE_D_FORMATS);
// }
// if (prefs.getBoolean(PreferencesActivity.KEY_DECODE_QR, true)) {
// decodeFormats.addAll(DecodeFormatManager.QR_CODE_FORMATS);
// }
// if (prefs.getBoolean(PreferencesActivity.KEY_DECODE_DATA_MATRIX, true)) {
// decodeFormats.addAll(DecodeFormatManager.DATA_MATRIX_FORMATS);
// }
// }
if (decodeFormats ==null|| decodeFormats.isEmpty()) {
decodeFormats =new Vector<BarcodeFormat>();
decodeFormats.addAll(DecodeFormatManager.ONE_D_FORMATS);
decodeFormats.addAll(DecodeFormatManager.QR_CODE_FORMATS);
decodeFormats.addAll(DecodeFormatManager.DATA_MATRIX_FORMATS);
}
hints.put(DecodeHintType.POSSIBLE_FORMATS, decodeFormats);
if (characterSet !=null) {
hints.put(DecodeHintType.CHARACTER_SET, characterSet);
}
hints.put(DecodeHintType.NEED_RESULT_POINT_CALLBACK, resultPointCallback);
}
这里是设置 解码的类型,我们现在默认将所有类型都加入。 错误类型基本上都是:包结构、PreferencesActivity 的配置 、类可访问性的问题。根据错误提示耐心把错误解决。 返回解码结果 还记得在 CaptureActivityHandler 的 messagehandler 里注销掉的Case分支吗?现在CaptureActivity 里实现它。
public void handleDecode(Result obj, Bitmap barcode) {
inactivityTimer.onActivity();
viewfinderView.drawResultBitmap(barcode);
playBeepSoundAndVibrate();
txtResult.setText(obj.getBarcodeFormat().toString() +":"
+ obj.getText());
}
最后 ZXing的简化已基本完成,有几位是可以运行成功的?呵呵。 下面是CaptureActivity的源码: CaputreActivity
publicclass CaptureActivity extends Activity implements Callback {
private CaptureActivityHandler handler;
private ViewfinderView viewfinderView;
privateboolean hasSurface;
private Vector<BarcodeFormat> decodeFormats;
private String characterSet;
private TextView txtResult;
private InactivityTimer inactivityTimer;
private MediaPlayer mediaPlayer;
privateboolean playBeep;
privatestaticfinalfloat BEEP_VOLUME =0.10f;
privateboolean vibrate;
/** Called when the activity is first created. */
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
//初始化 CameraManager
CameraManager.init(getApplication());
viewfinderView = (ViewfinderView) findViewById(R.id.viewfinder_view);
txtResult = (TextView) findViewById(R.id.txtResult);
hasSurface =false;
inactivityTimer =new InactivityTimer(this);
}
@Override
protectedvoid onResume() {
super.onResume();
SurfaceView surfaceView = (SurfaceView) findViewById(R.id.preview_view);
SurfaceHolder surfaceHolder = surfaceView.getHolder();
if (hasSurface) {
initCamera(surfaceHolder);
} else {
surfaceHolder.addCallback(this);
surfaceHolder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);
}
decodeFormats =null;
characterSet =null;
playBeep =true;
AudioManager audioService = (AudioManager) getSystemService(AUDIO_SERVICE);
if (audioService.getRingerMode() != AudioManager.RINGER_MODE_NORMAL) {
playBeep =false;
}
initBeepSound();
vibrate =true;
}
@Override
protectedvoid onPause() {
super.onPause();
if (handler !=null) {
handler.quitSynchronously();
handler =null;
}
CameraManager.get().closeDriver();
}
@Override
protectedvoid onDestroy() {
inactivityTimer.shutdown();
super.onDestroy();
}
privatevoid initCamera(SurfaceHolder surfaceHolder) {
try {
CameraManager.get().openDriver(surfaceHolder);
} catch (IOException ioe) {
return;
} catch (RuntimeException e) {
return;
}
if (handler ==null) {
handler =new CaptureActivityHandler(this, decodeFormats,
characterSet);
}
}
@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width,
int height) {
}
@Override
public void surfaceCreated(SurfaceHolder holder) {
if (!hasSurface) {
hasSurface =true;
initCamera(holder);
}
}
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
hasSurface =false;
}
public ViewfinderView getViewfinderView() {
return viewfinderView;
}
public Handler getHandler() {
return handler;
}
public void drawViewfinder() {
viewfinderView.drawViewfinder();
}
public void handleDecode(Result obj, Bitmap barcode) {
inactivityTimer.onActivity();
viewfinderView.drawResultBitmap(barcode);
playBeepSoundAndVibrate();
txtResult.setText(obj.getBarcodeFormat().toString() +":"
+ obj.getText());
}
private void initBeepSound() {
if (playBeep && mediaPlayer ==null) {
// The volume on STREAM_SYSTEM is not adjustable, and users found it
// too loud,
// so we now play on the music stream.
setVolumeControlStream(AudioManager.STREAM_MUSIC);
mediaPlayer =new MediaPlayer();
mediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
mediaPlayer.setOnCompletionListener(beepListener);
AssetFileDescriptor file = getResources().openRawResourceFd(
R.raw.beep);
try {
mediaPlayer.setDataSource(file.getFileDescriptor(),
file.getStartOffset(), file.getLength());
file.close();
mediaPlayer.setVolume(BEEP_VOLUME, BEEP_VOLUME);
mediaPlayer.prepare();
} catch (IOException e) {
mediaPlayer =null;
}
}
}
private static final long VIBRATE_DURATION =200L;
private void playBeepSoundAndVibrate() {
if (playBeep && mediaPlayer !=null) {
mediaPlayer.start();
}
if (vibrate) {
Vibrator vibrator = (Vibrator) getSystemService(VIBRATOR_SERVICE);
vibrator.vibrate(VIBRATE_DURATION);
}
}
/**
* When the beep has finished playing, rewind to queue up another one.
*/
private final OnCompletionListener beepListener =new OnCompletionListener() {
public void onCompletion(MediaPlayer mediaPlayer) {
mediaPlayer.seekTo(0);
}
};
简化过的包结构图:  简化后的ZXing 更加方便我们了解ZXing项目 是如何解码的。只要仔细查看源码,进行单点跟踪调试,相信大家很容易能理解。
------------------------------------------------------------------------------------
參考連結
Android Zxing条码扫描自定义控件(附代码)
2012-12-03 18:08:30
标签:Android 自定义控件 Zxing 条码扫描
原创作品,允许转载,转载时请务必以超链接形式标明文章 原始出处 、作者信息和本声明。否则将追究法律责任。http://littleleaf.blog.51cto.com/6259336/1077496
团队要做一个项目,里面要用到条码扫描,搜了一下,知道了Zxing。这是一个开源的条码扫描程序。官方网站有完整的Android程序可下载。但是,如果想将扫描功能融合在自己开发的程序里,则需要理清设计的思路,并去掉一些没有必要的代码。
为了让团队更方便使用,我将Zxing代码做了封装,做成了一个自定义View控件,并且生成了jar文件,可以在Android程序里直接引用。这个控件解决了不少网友询问的竖屏和横屏摆放的问题。
使用该控件很简单。首先要在项目目录下,建一个libs目录,将ZxingScanner.jar文件放进去。
然后建立项目引用,如下步骤:
1.通过Eclipse菜单Project->Properties->Java Build Path->Libraries,点击Add library;
2.选择User Library ,点击Next,点击User Library,点击New,输入随意的Library Name,比如ZxingScanner;
3.选择刚才创建的Library Name,点击Add JARs,找到libs目录下的ZxingScanner.jar,加入;
4.在Java Build Path的Libraries窗口中,就应该有这个带上了ZxingScanner.jar的library了。
因为扫描要用到相机,所以接着要设置AndroidManifest.xml,在里面加入使用相机的权限。
- <uses-permission
android:name="android.permission.CAMERA"></uses-permission>
<uses-permission
android:name="android.permission.WRITE_EXTERNAL_STORAGE"></uses-permission>
<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.FLASHLIGHT"/>
建议将控件放在一个横屏的Activity里,这样的话,由于扫描框的宽度大于高度,扫描条码时,可以将手机比较靠近条码。将Activity设为横屏大家都应该会了,但我还是附上代码吧,其实就是在AndroidManifest.xml的activity标签里加上下面这一句:
- <com.covics.zxingscanner.ScannerView
android:id="@+id/scanner_view"
android:layout_width="fill_parent"
android:layout_height="fill_parent"/>
这样,当activity打开时,会看到控件占据的区域内是相机影像,只要将中间的框对准条码,框中间的红线压住条码,就能识别。
别急,activity中还是要写一些代码的。
我定义的activity叫ZXingScannerActivity,它的作用是将控件识别的条码显示在一个TextView(txtResult)上。代码如下:
- package com.covics.zxingscanner;
- import android.app.Activity;
- import android.graphics.Bitmap;
- import android.os.Bundle;
- import android.widget.TextView;
- import android.widget.LinearLayout;
- import android.widget.FrameLayout.LayoutParams;
- import com.covics.zxingscanner.ScannerView;
- import com.covics.zxingscanner.R;
- //activity需要实现com.covics.zxingscanner.OnDecodeCompletionListener接口
- public
class ZXingScannerActivity extends Activity implements OnDecodeCompletionListener{
private ScannerView scannerView;
private TextView txtResult;
/** Called when the activity is first created. */
@Override
public
void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState); - setContentView(R.layout.main);
- scannerView=(ScannerView)findViewById(R.id.scanner_view);
- txtResult = (TextView) findViewById(R.id.txtResult);
//条码扫描后回调自己的onDecodeCompletion
- scannerView.setOnDecodeListener(this);
- }
//将条码扫描识别结果打印在textview上,barcodeFormat是条码格式,barcode是条码内容,bitmap是条码图像
@Override
public
void onDecodeCompletion(String barcodeFormat,String barcode,Bitmap bitmap){ - txtResult.setText("Barcode Format:"+barcodeFormat+" Barcode:"+barcode);
- }
@Override
protected
void onResume() {
super.onResume();
//onResume时才打开相机和闪光灯
- scannerView.onResume();
- }
@Override
protected
void onPause() {
super.onPause();
//onPause时关闭相机和闪光灯
- scannerView.onPause();
- }
@Override
protected
void onDestroy() {
super.onDestroy(); - }
- }
使用上就这么简单。
下面简单介绍一下ScannerView的实现原理。它扩展自FrameLayout,里面加入了一个SurfaceView和另一个扩展的View(ViewfinderView)。
SurfaceView用于显示相机的图像,通过将SurfaceView的surfaceholder传入相机对象,就能将相机的图像显示出来。
而ViewfinderView则是覆盖在SurfaceView上面,通过onDraw方法,画出中间一个框是全透明,其余区域是半透明的效果,让用户明白,通过中间全透明框取景。
扫描的原理:
CaptureActivityHandler 作为一个 handler,不断让相机自动对焦,并驱动相机获取preview图像,一旦获得preview图像,就传入给独立线程DecodeThread的 handler,有该handler负责将取景框部分的图像获取后,调用Zxing进行解码,如果解码成功,则通知ScannerView,将结果传给实现了OnDecodeCompletionListener接口的对象。
Zxing的简化代码是从这篇网文获取的:条码扫描二维码扫描——ZXing android 源码简化 感谢作者。
附件有两个内容:
1.ScannerView的项目源代码ZXingScanner
2.演示使用ScannerView的项目ZxingScanner Demo
本文出自 “平湖秋月的专栏” 博客,请务必保留此出处http://littleleaf.blog.51cto.com/6259336/1077496 |