在企业应用中可能某一些情况,需要二维码扫码,大多数采用的开源组件zxing实现。简介
最近的Widget项目用到需要用摄像头来处理条形码,章师兄推荐了ZXing这个开源的项目,于是做一简单的调研。
ZXing是一个开源Java类库用于解析多种格式的1D/2D条形码。目标是能够对QR编码、Data Matrix、UPC的1D条形码进行解码。 其提供了多种平台下的客户端包括:J2ME、J2SE和Android。
当前,ZXing主要支持以下条形码:
UPC-A and UPC-E
EAN-8 and EAN-13
Code 39
Code 128
QR Code
Data Matrix ('alpha' quality)
ITF
首先,在http://code.google.com/p/zxing/上下载到ZXing的应用程序包,解压下载到的程序包后,我们可以看到整个应用程序分为一些组件。如下:
1.core: 核心包,是整个应用的主要组件组成部分。
2.javame: 为JavaME来定制的客户端工具。
3.javase: 为PC端定制的客户端工具。
4.android: 为Android端定制的客户端工具。
5.androidtest: Android测试应用程序,里面有在其它程序中使用此应用的示例。
基本测试
ZXing程序的运行需要我们使用Ant去打包编译之后才可。在这里,我主要调研了在PC端,在WTK上及android上的运行。
各种语言zxing的实现路径:
http://zxing.googlecode.com/svn
源代码svn路径:
http://code.google.com/p/zxing/downloads/list。
zxing APK路径:
http://zxing.googlecode.com/files/BarcodeScanner4.1.apk
zxing和应用整合官方代码:
http://zxing.googlecode.com/svn/trunk/android-integration
方法一:整合为一个项目:
前言
最近公司的Android项目需要用到摄像头做条码或二维码的扫描,Google一下,发现一个以Apache License 2.0 开源的 ZXing项目。Zxing项目里的Android实现太过复杂多余东西太多,得对其进行简化。
前提条件
下载源代码:点击这里
编译核心库:Zxing的主页上有介绍具体步骤,大家也可以参照这篇博文:android 条码识别软件开发全解析(续2详解绝杀!)
导入项目
打开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
public
class
CaptureActivity
extends
Activity
implements
Callback {
private
CaptureActivityHandler handler;
private
ViewfinderView viewfinderView;
private
boolean
hasSurface;
private
Vector
<
BarcodeFormat
>
decodeFormats;
private
String characterSet;
private
TextView txtResult;
private
InactivityTimer inactivityTimer;
private
MediaPlayer mediaPlayer;
private
boolean
playBeep;
private
static
final
float
BEEP_VOLUME
=
0.10f
;
private
boolean
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
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
;
}
@Override
protected
void
onPause() {
super
.onPause();
if
(handler
!=
null
) {
handler.quitSynchronously();
handler
=
null
;
}
CameraManager.get().closeDriver();
}
@Override
protected
void
onDestroy() {
inactivityTimer.shutdown();
super
.onDestroy();
}
private
void
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项目 是如何解码的。只要仔细查看源码,进行单点跟踪调试,相信大家很容易能理解。
顾客是上帝
很多人留言要源码, 其实我这不是什么源码,我只是把ZXing的东西简化了一下而已。事实上我也不喜欢直接放源码项目,这样大家就不想读ZXing的源码了。
下面是我简化的版本:Zxing简化
很多人需要Core 核心包(其实ZXing的源码里面就有),这里提供下我写文章时的版本 1.6的:
Zxing
方法二:分别为两个项目:
测试代码如下:
package com.easyway.android;
import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
import android.widget.TextView;
/**
* 二维码扫码应用实例
*
* 1.安装zxing客户端android apk。
* 2.调用相关的zxing
* 3.获取相关的执行的结果
*
* 如何扫描条形码
* 现在做条形码扫描,java一般用zxing。zxing有多种复用方式,最简单的方式是,
*安装zxing软件,通过google market。在自己的应用中,通过Intent调用zxing的扫描条码的Activity,
*然后将扫描到的结果返回自己的Activity。
*比较复杂的办法是,将zxing库包含在自己的程序中,这比较适合正式的应用。
* @Title:
* @Description: 实现TODO
* @Copyright:Copyright (c) 2011
* @Company:易程科技股份有限公司
* @Date:2012-5-9
* @author longgangbai
* @version 1.0
*/
public class AndroidScanBarcodeActivity extends Activity implements OnClickListener {
private Button button;
private TextView textView;
/** Called when the activity is first created. */
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
this.button = (Button) this.findViewById(R.id.btnscan);
this.button.setOnClickListener(this);
this.textView = (TextView) this.findViewById(R.id.txtScan);
}
@Override
public void onClick(View view) {
//调用对应的扫描方法
Intent intent = new Intent("com.google.zxing.client.android.SCAN");
this.startActivityForResult(intent, 0);
}
/**
* 获取扫描的结果
*/
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent intent) {
if (requestCode != 0) {
this.textView.setText(R.string.noresult);
return;
}
if(resultCode==RESULT_OK){
String contents = intent.getStringExtra("SCAN_RESULT");
String format = intent.getStringExtra("SCAN_RESULT_FORMAT");
textView.setText(" 条形码为:"+contents+" 条码类型为: "+format);//利用页面的textveiw显示扫描后的结果 } else if (resultCode == RESULT_CANCELED) {
}
}
}
源代码下载: