目录[-]
下载zxing2.3.0后,与android相关的有三个目录:
android:就是Barcode Scanner,中文名"条码扫描器"。下文中简写为BS。
android-integration:提供一种简单的途径将Barcode Scanner整合到调用方app中
androidtest:模拟调用方app,通过android-integration整合Barcode Scanner
如果将androidtest看做你的app的话,那么上面三者的关系将会如下图:
zxing项目的本意是将BS作为一个独立的app,可以单独使用,同时亦可简单被其他app调用,在调用的过程中给用户的感觉它们是一个整体。在这中间起到”简单整合”作用的便是android-integration。
正如android-integration的文档中所述,android-integration的作用在于为调用方提供一个简单的方法实现扫描并接受扫描的结果,调用方完全不必了解BS的代码原理(不必为了整合到调用方app中而学习BS代码),只需按照简单的几个步骤即可轻松实现编解码。
但是这样看起来简单的整合存在一个问题:作为独立应用的BS必须与调用方app一起被安装,否则android-integration会要求用户到应用市场下载BS。而BS同时包含了编解码功能,如果只需要解码功能,那多余的那部分编码功能只是徒增了app的大小却没有任何用处。研究BS源码是势在必行。
integration的使用方法只需简单的两步,具体使用方法可参考IntentIntegrator类的注释。
这是一个完整的应用Barcode Scanner的实例,从主界面上即可看出可用的操作,其实大部分操作都是在BS中完成的:
该例子需要引入android-support-v4.jar,core-2.3.0.jar,还依赖android-integration。
androidtest代码比较简单,基本上没什么需要解释的。第一个Run benchmark是一个用来解码本地图片的功能,该功能会扫描${sdcard}/zxingbenchmark目录下的条码图片,并解码,例子中仅仅打印了图片的编码格式,可以通过com.google.zxing.Result.getText()获取被编码的文本。
该功能的核心类就是com.google.zxing.MultiFormatReader和com.google.zxing.Result。后面我们在分析Barcode Scanner的时候会再次遇到MultiFormatReader。
是一个功能强大的条码扫描器,不仅支持多种类型的条码,还支持多国语言,分享二维码,查看扫描历史,反向扫描等功能。
包结构
android:与CaptureActivity直接相关的核心组件。包含了发生震动管理器,闪光灯等等,后面介绍
book:如果查询的结果是图书信息,用户可以选择查询该书的更进一步的详细信息,该包即包含了搜索与展示书籍的相关类。
camera/camera.open:摄像头相关组件,核心类是CameraManager
clipboard:剪贴板
encode:编码功能的各个组件集合。核心类为QRCodeEncoder,最终实施编码的是MultiFormatWriter类
history:扫描历史管理,核心类是HistoryManager
result:条码扫描的结果被分为不同的类型,所有的类型都定义在com.google.zxing.client.result.ParsedResultType中,对于不同的类型都有对应的处理方法:xxxResultHandler,所有的ResultHandler都包含在此包中。不同的xxxResultHandler还提供了扫描结果页面要展示几个button,每个button的文本以及需要绑定的事件等等。
result.supplement:对已经扫描并解码的结果做额外处理的工具集。比如扫描出来的是isbn号,如果在设置中选择了“检索更多信息”则会在扫描出isbn号之后自动去网上查询该书的信息,最后将书的信息展示出来,而如果没选中,则只会将isbn号码展示。
share:分享二维码功能,亦是编码功能的入口所在。
wifi:是WifiResultHandler的辅助类集合。如果扫描到的二维码是对wifi信息的编码,那么最终扫描结果页会展示一个“连接到网络”的按钮,点击此按钮就会自动尝试连接。该包中所包含的类则是链接网络所需的工具类。
按照功能来分,其实BS是由:扫描 、历史和分享(生成二维码) 组成。
## 新增内容(2014/3/16) ##
在较为详细地对代码进行全面分析之前,我把扫码+解码的流程说的更简单一些,因为很多功能我们并不需要太关注,比如声音、对焦、闪光灯等等,我们关注的只是这一套动作是怎么实现的:
* 打开预览界面。zxing借助的肯定是camera的功能,通过分析和了解camera的工作原理可知,调用Camera.startPreview()后,就可以从屏幕中看到预览界面。
* 捕捉画面。处在previewing状态的画面,随时都可以捕获(即使不对焦也可以,只是模糊一点而已)。调用Camera.setOneShotPreviewCallback(),该方法会在启动preview之后,预览界面展示出来后调用(只调用一次便自动解绑),向回调函数Camera.PreviewCallback.onPreviewFrame()中传回当前画面的字节数组。
* 解析。拿到字节数组就可以解码了。这部分逻辑是包含在core中的,zxing的应用层无需关注的逻辑。
## 新增内容 ##
在分析这三部分功能之前,先来看下配置。在界面上对应“设置”,在代码中则是利用了PreferenceFragment类,将用户对Camera以及扫描行为的设置保存在SharePreferences中。后文提到的“配置”均指用户在设置中进行的设置。
进入扫描界面后大致做了如下的事情:配置Camera并启动Camera、构建preview与扫描窗口、捕捉画面并解码、将解码结果交给不同ResultHandler去处理。下面逐一进行分析。
1. 配置Camera并启动Camera
启动Camera是在CaptureActivity.initCamera中进行的,最重要的两句代码是:
1
2
3
4
5
|
cameraManager.openDriver(surfaceHolder);
...
handler =
new
CaptureActivityHandler(
this
, decodeFormats, decodeHints,
characterSet, cameraManager);
...
|
CameraManager是相机管理类,是BS中唯一与Camera打交道的类,CameraManager.openDriver主要做了三件事:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
|
/**
* Opens the camera driver and initializes the hardware parameters.
*
* @param holder The surface object which the camera will draw preview frames into.
* @throws IOException Indicates the camera driver failed to open.
*/
public
synchronized
void
openDriver(SurfaceHolder holder)
throws
IOException {
Camera theCamera = camera;
if
(theCamera ==
null
) {
// 1. 获取手机背面的摄像头
theCamera = OpenCameraInterface.open();
if
(theCamera ==
null
) {
throw
new
IOException();
}
camera = theCamera;
}
// 2. 设置摄像头预览view
theCamera.setPreviewDisplay(holder);
if
(!initialized) {
initialized =
true
;
configManager.initFromCameraParameters(theCamera);
if
(requestedFramingRectWidth >
0
&& requestedFramingRectHeight >
0
) {
setManualFramingRect(requestedFramingRectWidth, requestedFramingRectHeight);
requestedFramingRectWidth =
0
;
requestedFramingRectHeight =
0
;
}
}
Camera.Parameters parameters = theCamera.getParameters();
String parametersFlattened = parameters ==
null
?
null
: parameters.flatten();
// Save
// these,
// temporarily
try
{
// 3. 读取配置并设置相机参数
configManager.setDesiredCameraParameters(theCamera,
false
);
}
catch
(RuntimeException re) {
// Driver failed
Log.w(TAG,
"Camera rejected parameters. Setting only minimal safe-mode parameters"
);
Log.i(TAG,
"Resetting to saved camera params: "
+ parametersFlattened);
// Reset:
if
(parametersFlattened !=
null
) {
parameters = theCamera.getParameters();
parameters.unflatten(parametersFlattened);
try
{
theCamera.setParameters(parameters);
configManager.setDesiredCameraParameters(theCamera,
true
);
}
catch
(RuntimeException re2) {
// Well, darn. Give up
Log.w(TAG,
"Camera rejected even safe-mode parameters! No configuration"
);
}
}
}
}
|
CameraConfigurationManager是相机辅助类,主要用于设置相机的各类参数。核心方法有两个:
initFromCameraParameters:计算了屏幕分辨率和当前最适合的相机像素
setDesiredCameraParameters:读取配置设置相机的对焦模式、闪光灯模式等等
CaptureActivityHandler类是一个针对扫描任务的Handler,可接收的message有启动扫描(restart_preview)、扫描成功(decode_succeeded)、扫描失败(decode_failed)等等。
在创建一个CaptureActivityHandler对象的时候也做了三件事:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
CaptureActivityHandler(CaptureActivity activity, Collection<BarcodeFormat> decodeFormats,
Map<DecodeHintType, ?> baseHints, String characterSet, CameraManager cameraManager) {
this
.activity = activity;
// 1. 启动扫描线程
decodeThread =
new
DecodeThread(activity, decodeFormats, baseHints, characterSet,
new
ViewfinderResultPointCallback(activity.getViewfinderView()));
decodeThread.start();
state = State.SUCCESS;
// Start ourselves capturing previews and decoding.
this
.cameraManager = cameraManager;
// 2. 开启相机预览界面
cameraManager.startPreview();
// 3. 将preview回调函数与decodeHandler绑定、调用viewfinderView
restartPreviewAndDecode();
}
|
restartPreviewAndDecode方法又调用了CameraManager.requestPreviewFrame:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
/**
* A single preview frame will be returned to the handler supplied. The data
* will arrive as byte[] in the message.obj field, with width and height
* encoded as message.arg1 and message.arg2, respectively.
* <br/>
*
* 1:将handler与preview回调函数绑定;<br/>
* 2:注册preview回调函数<br/>
* 综上,该函数的作用是当相机的预览界面准备就绪后就会调用hander向其发送传入的message
*
* @param handler The handler to send the message to.
* @param message The what field of the message to be sent.
*/
public
synchronized
void
requestPreviewFrame(Handler handler,
int
message) {
Camera theCamera = camera;
if
(theCamera !=
null
&& previewing) {
previewCallback.setHandler(handler, message);
// 绑定相机回调函数,当预览界面准备就绪后会回调Camera.PreviewCallback.onPreviewFrame
theCamera.setOneShotPreviewCallback(previewCallback);
}
}
|
2. 构建preview与扫描窗口
首先相机有自己的preview界面,然后我们需要构造一个扫描窗口,引导用户将条码置于窗口中完成扫描。
构造扫描窗口是在CaptureActivityHandler.restartPreviewAndDecode中,通过调用activity.drawViewfinder()来实现的。这里有个画扫描窗口的类叫ViewfinderView,该类也是想要改变扫描窗口风格所必须重构的一个类。
由于网上已经有很多重构ViewfinderView的帖子,本文不再赘述,可以参考《基于google Zxing实现二维码、条形码扫描,仿微信二维码扫描效果》
相机的preview界面显示出来后即可开始扫描,所以需要监听preview是否已经显示这个事件,这就是Camera.PreviewCallback的作用。PreviewCallback.onPreviewFrame做的事便是当preview界面展示出来的时候向DecodeHandler发送一个decode消息,DecodeHandler收到该消息后会执行decode方法来解码。
注意,检测并触发捕获画面动作的,是Camera.setOneShotPreviewCallback()这个方法。该函数被调用后,如果预览界面已经打开,就会将包含当前preview frame的byte数组传给回调函数,此时再向DecodeHandler发送decode消息。
3. 捕捉画面并解码
具体参考DecodeHandler.decode方法。(本文只从宏观上对zxing进行分析,对于解码的原理将会另开博文进行介绍)
4. 将解码结果交给不同ResultHandler去处理
当DecodeHandler.decode完成解码后会向CaptureActivityHandler发消息。如果编码成功则调用CaptureActivity.handleDecode方法对扫描到的结果进行分类处理。
该方法中首先获取ResultHandler:
1
2
|
// 解析rawResult,根据不同类型result生成对应的ResultHandler
ResultHandler resultHandler = ResultHandlerFactory.makeResultHandler(
this
, rawResult);
|
然后调用handleDecodeInternally和handleDecodeExternally对ResultHandler进行处理。谈到这两个方法,就不得不再分析一下IntentSource。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
enum
IntentSource {
/**
* 本地app向BS(Barcode Scanner)发起的启动指令
* 比如在androidtest项目中,利用整合的android-integration对BS发起调用指令:com.google.zxing.client.android.SCAN
* BS中该启动命令对应的Source类型便是NATIVE_APP_INTENT
*/
NATIVE_APP_INTENT,
/**
* 打开BS的时候传入查询商品的url,与最终扫描到的product id结合进行查询
* 两种url的形式不同
*/
PRODUCT_SEARCH_LINK,
ZXING_LINK,
/**
* 直接打开BS
*/
NONE
}
|
结合CaptureActivity.onResume中的部分代码来理解:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
}
else
if
(dataString !=
null
&& dataString.contains(
"http://www.google"
)
&& dataString.contains(
"/m/products/scan"
)) {
// Scan only products and send the result to mobile Product
// Search.
source = IntentSource.PRODUCT_SEARCH_LINK;
sourceUrl = dataString;
decodeFormats = DecodeFormatManager.PRODUCT_FORMATS;
}
else
if
(isZXingURL(dataString)) {
// Scan formats requested in query string (all formats if none
// specified).
// If a return URL is specified, send the results there.
// Otherwise, handle it ourselves.
source = IntentSource.ZXING_LINK;
sourceUrl = dataString;
Uri inputUri = Uri.parse(dataString);
scanFromWebPageManager =
new
ScanFromWebPageManager(inputUri);
decodeFormats = DecodeFormatManager.parseDecodeFormats(inputUri);
// Allow a sub-set of the hints to be specified by the caller.
decodeHints = DecodeHintManager.parseDecodeHints(inputUri);
}
|
NATIVE_APP_INTENT和NONE很好理解,而PRODUCT_SEARCH_LINK和ZXING_LINK是指定查询商品的url(而不是交给zxing分析后再决定去哪里查询),将扫描出来的内容拼凑到url中,然后在浏览器中展示结果。
理解了IntentSource,就容易看懂handleDecodeInternally其实就是将结果展示到界面上。handleDecodeExternally稍复杂些,当source == IntentSource.NATIVE_APP_INTENT时,BS会将扫描分析的结果存到Intent中返回给调用方app,因此调用方app在启动BS的时候一定要使用startActivityForResult。这一点可以在androidtest的IntentIntegrator.initiateScan方法的最后看到。
每扫描成功一次会有一次记录,逻辑相对简单,不再花更多时间分析。
对于分享,其实我们更关注的是其编码的实现。这部分代码就比扫描少得多,精简过后,核心包已经减少到2个:
点击分享会进入到分享界面,每一种分享方式最终会生成一个二维码,二维码界面是EncodeActivity,核心代码只需参考onResume中的这一句:
1
|
Bitmap bitmap = qrCodeEncoder.encodeAsBitmap();
|
进入encodeAsBitmap中,最终看到的是与MultiFormatReader相对应的方法MultiFormatWriter,此方法是生成二维码的关键。
以上是对zxing项目android部分的代码简单介绍。
Barcode Scanner的代码我已经加了较为完整的注释,代码上传至git.oschina.net:Zxing-Simplification