对于刚开始学习android开发的童鞋们来说,若有一个简单而又全面的android工程能来剖析,那就是再好不过了,zxing就是不错得例子。
    zxing的源码可以到google code上下载,整个源码check out 下来,里面有各个平台的源码,ios的,android的。当然我们需要的就是android代码。
    将android的工程导入到eclipse中,导入完成后,eclipse会显示各种错误,这是缺少core文件夹里面的核心库文件所致,在project中创建文件夹core,再将zxing源码中得core文件夹下得代码导入进来,这样就可以了。
    如果遇到unable resolved target-X,则是你的avd版本问题,可以在project.propertities修改target值。clean下就ok。
    如上的都是zxing android代码分析的准备,下面的则是正式开始。


    如上图:为整个android工程的代码,android入门就重这些代码着手。其中主要关注的是android,camera,encode,result文件夹。
   程序启动的流程:加载main activity,在此类中创建CaptureActivityHandler对象,该对象启动相机,实现自动聚焦,创建DecodeThread线程,DecodeThread创建Decodehandler,这个对象就获取从相机得到的原始byte数据,开始解码的第一步工作,从获取的byte中解析qr图来,并解析出qr图中的字符,将这块没有分析的字符抛送到CaptureActivityHandler中handle,该类调用main activity的decode函数完成对字符的分析,最后显示在界面上(刷新UI,最好在UI线程里完成)。这样一个解析qr图的过程并完成。
   下面具体分析整个过程。重点之处有main activity,camera.
   程序启动的第一个activity便是:CaptureActivity,有点类似于c中的main函数,在此是main activity。这个acitvity做的主要的事便是:加载扫描各种条形码,二维码的一个界面,启动一个处理获取一维码二维码信息的线程,完成对于获取的图像信息进行解码,最后再将解码的信息显示在界面上。

   完成界面的加载主要在于onCreate,和onResume函数中,这涉及到了一个activity的生命周期,以后再具体分析。首先调用onCreate,再调用onResume,在onResume中会判断这个activity是由什么启动的,可能是其他的app触发了,也可能是用户直接启动的。这样就初始化了三个变量,一是source,便是启动activity的源,一是decodeFormats,指出解码的方式,是qr,还是其他的等等,最后一个是:charactreset,即是对于这些生成qr图的字符的编码方式。若没有对core中得代码修改,用该程序解析GB2312编码的字符则会乱码。乱码的解决后面将提到。
   界面的加载中有两个很关键的类。surfaceview 和 ViewFinderView,前面的是用来加载从底层硬件获取的相机取景的图像,后面的是自定义的view,实现了扫描时的界面,不停的刷新,并将识别的一些数据,如定位的点回调显示在界面上。

上面介绍了zxing扫描二维码的过程,刚开始看这份代码时,不怎么明白,很多细节都不清楚,到后来又了更深的理解后,发现这代码设计的就是好,质量高。整个扫描二维码和一维码的过程是非常迅速的,效率很高。最近发现微博上有个二维坊的ID,发得qr码图形都非常的Q,不知道怎么弄出来的,程序员可以借这个可爱的qr码浪漫下。
   在整个zxing的android代码部分,很重要的两点是main activity 和 camera。在这一篇,就主要介绍下android camera的使用。打开zxing下的Barcode scanner,并会有如下的界面。为了更好的理解camera,先介绍这个界面。

    刚开始接触到android时,对此界面一点不熟悉。后面认真看了其中的代码,明白了一点点。 这个界面的定义主要在ViewfinderView.java这个类中,这个类继承了View类,实现了自定义的View。View就是对应于屏幕的一个画布,可以在这个屏幕上任意绘制你想要的设计。最重要的重载onDraw函数,在其中实现绘制。就来看下ViewfinderView是如何实现界面上的感觉的。
    画面中一共分为两块:外边半透明的一片,中间全透明的一片。外面半透明的画面是由四个矩形组成。


paint.setColor(resultBitmap != null ? resultColor : maskColor);
canvas.drawRect(0, 0, width, frame.top, paint);
canvas.drawRect(0, frame.top, frame.left, frame.bottom + 1, paint);
canvas.drawRect(frame.right + 1, frame.top, width, frame.bottom + 1, paint);
canvas.drawRect(0, frame.bottom + 1, width, height, paint);

drawRect函数有五个参数,前四个参数构成两个坐标,组成一个矩形,后面一个画笔相关的。
   中间的全透明一块,也是由四个矩形组成,只是每个矩形很窄,才一两个像素,成了一条直线。



paint.setColor(frameColor);
canvas.drawRect(frame.left, frame.top, frame.right + 1, frame.top + 2, paint);
canvas.drawRect(frame.left, frame.top + 2, frame.left + 2, frame.bottom - 1, paint);
canvas.drawRect(frame.right - 1, frame.top, frame.right + 1, frame.bottom - 1, paint);
canvas.drawRect(frame.left, frame.bottom - 1, frame.right + 1, frame.bottom + 1, paint);

最中间的一条红色扫描线亦如此。
   onDraw()函数的最后一句是:


postInvalidateDelayed(ANIMATION_DELAY, frame.left, frame.top, frame.right, frame.bottom);

这一句很关键,postInvalidateDelayed函数主要用来在非UI线程中刷新UI界面,每个ANIMATION_DELAY时间,刷新指定的范围。所以会不停得调用onDraw函数,并在界面上添加绿色的特征点。在刚开始看这份代码时,没明白是如何添加绿色的标记点的。现在再看了一遍,大致明白了。在camera聚焦获取图片后,再使用core中的库进行解析,会得出特征点的坐标,最后通过ViewfinderResultPointCallback类回调,将特征点添加到ViewfinderView中的ArrayList容器中。


public void foundPossibleResultPoint(ResultPoint point) {
viewfinderView.addPossibleResultPoint(point);
}

这个函数特征点加入到possibleResultPoints中,由于对java不熟悉,不知道 “=” 的赋值对于List来说是浅拷贝,总在想possibleResultPoints对象没有被赋值,如何获取这些特征点了。后面才知道,这个“=”赋值,只是个浅拷贝。若要对这种预定义的集合实现深拷贝,可以使用构造函数,
如:List<ResultPoint> points = new List<ResultPoint>(possibleResultPoints);


public void addPossibleResultPoint(ResultPoint point) {
   List<ResultPoint> points = possibleResultPoints;
   synchronized (point) {
       points.add(point);
       int size = points.size();
       if (size > MAX_RESULT_POINTS) {
         // trim it
         points.subList(0, size - MAX_RESULT_POINTS / 2).clear();
       }
   }
   }

ViewfinderView自定义了view,实现了一个简洁的扫描界面。这一篇记录我再看代码过程中对于Android Camera 的理解。由于才开始写技术类博客,前两篇有很多不足
之处,都是自己随性而写,估计大家很难对我写的有一个清晰的了解。这篇尝试改变下风格,争取好好的表达我的浅薄理解,也让大家能够看懂。
      在看Barcode Scanner中关于camera代码前,先对android camera开发做个简单的介绍,算是入门。
      首先是使用camera需要用到的权限。



<uses-permission android:name="android.permission.CAMERA"/>
< uses-feature android:name="android.hardware.camera"/>

如下是一个很简单的camera示例,简单到只能取景,即打开相机,将景象显示在屏幕上,仅此而已。

import java.io.IOException;
import android.app.Activity;
import android.hardware.Camera;
import android.os.Bundle;
import android.view.SurfaceHolder;
import android.view.SurfaceView;

public class CameraTestActivity extends Activity implements SurfaceHolder.Callback {
   private SurfaceHolder surfaceHolder;
   private Camera camera;
   /** Called when the activity is first created. */
   @Override
   public void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
         setContentView(R.layout.main);
         SurfaceView surfaceView = (SurfaceView) findViewById(R.id.preview_view);
         surfaceHolder = surfaceView.getHolder();
         surfaceHolder.addCallback(this);
         surfaceHolder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);
   }
   @Override
   public void surfaceChanged(SurfaceHolder arg0, int arg1, int arg2, int arg3) {
         // TODO Auto-generated method stub
   }
   @Override
   public void surfaceCreated(SurfaceHolder arg0) {
         // TODO Auto-generated method stub
         camera = Camera.open();

         Camera.Parameters parameters = camera.getParameters();
         parameters.setPreviewSize(480, 320); // 设置
         camera.setParameters(parameters);
         try {
             camera.setPreviewDisplay(surfaceHolder);
         } catch (IOException e) {
             System.out.println(e.getMessage());
         }
         camera.startPreview();
   }
   @Override
   public void surfaceDestroyed(SurfaceHolder arg0) {
         // TODO Auto-generated method stub
         if (camera != null) {
             camera.stopPreview();
         }
         camera.release();
         camera = null;
   }
}

其中的R.id.preview_view如下:


<SurfaceView
android:id="@+id/preview_view"
android:layout_width="fill_parent"
android:layout_height="fill_parent" />

首先这个activity实现了SurfaceHolder.Callback接口,并重写了这个接口的三个方法。

surfaceview总之能够获相机硬件捕捉到的数据并显示出来,在上面的代码中,先初始化了surfaceholder对象。并重写了surfaceCreated函数,在这个函数中,完成了对相机打开取景的基本操作。首先是Camera.open()获取一个Camera对象,在初始化一些camera参数,如图像格式,图像预览大小,刷新率等等。在设置预览显示,最后别忘了startPreview,则完成了取景。由于刚开始开发的工程需要将相机的取景设置为竖屏的,Barcode Scanner设置的是横屏的,开始再尝试调整图片显示方向时,我以为是再manifest中重新设置,

android:screenOrientation="landscape"

将landscape该为portrait,结果却很意外,屏幕是竖着显示了,但是取景后的内容与显示却是横竖相反的,手机竖着取景,显示的却是横着的。不可以简单的通过调整这个参数值来改变方向。后面调用下面这个函数,重新设置了预览照片的显示方向。

camera.setDisplayOrientation(90);

调整显示方向后,取景终于正常了。但是在后面预览拍照结果时,发现这都是假象,相机底层取景还是横屏的,只是在预览时进行了方向调整,这样还存在一个显示照片拉伸的问题。这个没有深入查看了。

Camera取景后显示于屏幕上,是个挺简单的过程,但这会出现各种意料不到的问题,例如之前说的屏幕横竖屏与预览图片之间的方向,图片拉伸,还有在Barcode Scanner中,简单的旋转了图片预览方向后,会出现特征点标记错位,等等。
   第三篇简单的完成了相机的取景,还没有将取景的图片拍照存储下来。若想实现拍照的效果,则需要实现回调函数:Camera.PreviewCallback接口。接上一篇的代码,在此实现拍照的功能,将图片显示出来。之前一直在看Barcode Scanner的源码,并只是在其代码上修剪。当昨天自己来实现Camera的自动聚焦时,并遇到比较纠结的问题。在不出意外的情况下,Camera的使用还是挺简单的。
   先在此贴出代码,最简单,代码经过了测试,正常运行,测试机是HTC MyTouch 3G slide。
   需要的权限:

<uses-permission android:name="android.permission.CAMERA" /><uses-feature android:name="android.hardware.camera" /><uses-feature android:name="android.hardware.camera.autofocus" />

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.Timer;
import java.util.TimerTask;

import android.app.Activity;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.ImageFormat;
import android.graphics.Rect;
import android.graphics.YuvImage;
import android.hardware.Camera;
import android.os.Bundle;
import android.util.Log;
import android.view.Display;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
import android.view.WindowManager;
import android.widget.ImageView;

public class CameraTestActivity extends Activity implements SurfaceHolder.Callback {
   private static String TAG = CameraTestActivity.class.getSimpleName();
   private SurfaceHolder surfaceHolder;
   private Camera camera;
   private ImageView imageView;
   private Timer mTimer;
   private TimerTask mTimerTask;

   private Camera.AutoFocusCallback mAutoFocusCallBack;
   private Camera.PreviewCallback previewCallback;

   /** Called when the activity is first created. */
   @Override
   public void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
         setContentView(R.layout.main);
         SurfaceView surfaceView = (SurfaceView) findViewById(R.id.preview_view);
         imageView = (ImageView) findViewById(R.id.image_view);
         surfaceHolder = surfaceView.getHolder();
         surfaceHolder.addCallback(this);
         surfaceHolder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);
         mAutoFocusCallBack = new Camera.AutoFocusCallback() {
             @Override
             public void onAutoFocus(boolean success, Camera camera) {
               if (success) {
                     // isAutoFocus = true;
                     camera.setOneShotPreviewCallback(previewCallback);
                     Log.d(TAG, "onAutoFocus success");
               }
             }
         };

         previewCallback = new Camera.PreviewCallback() {
             @Override
             public void onPreviewFrame(byte[] data, Camera arg1) {
               if (data != null)
               {
                     Camera.Parameters parameters = camera.getParameters();
                     int imageFormat = parameters.getPreviewFormat();
                     Log.i("map", "Image Format: " + imageFormat);

                     Log.i("CameraPreviewCallback", "data length:" + data.length);
                     if (imageFormat == ImageFormat.NV21)
                     {
                         // get full picture
                         Bitmap image = null;
                         int w = parameters.getPreviewSize().width;
                         int h = parameters.getPreviewSize().height;
                           
                         Rect rect = new Rect(0, 0, w, h);
                         YuvImage img = new YuvImage(data, ImageFormat.NV21, w, h, null);
                         ByteArrayOutputStream baos = new ByteArrayOutputStream();
                         if (img.compressToJpeg(rect, 100, baos))
                         {
                           image =BitmapFactory.decodeByteArray(baos.toByteArray(), 0, baos.size());
                           imageView.setImageBitmap(image);
                         }
               
                     }
               }
             }
         };

         mTimer = new Timer();
         mTimerTask = new CameraTimerTask();
         mTimer.schedule(mTimerTask, 0, 500);
   }

   @Override
   public void surfaceChanged(SurfaceHolder arg0, int arg1, int arg2, int arg3) {
         // TODO Auto-generated method stub
   }

   @Override
   public void surfaceCreated(SurfaceHolder arg0) {
         // TODO Auto-generated method stub
         initCamera();
   }

   @Override
   public void surfaceDestroyed(SurfaceHolder arg0) {
         // TODO Auto-generated method stub
         if (camera != null) {
             camera.stopPreview();
             camera.release();
             camera = null;
         }
         previewCallback = null;
         mAutoFocusCallBack = null;
   }

   public void initCamera() {
         camera = Camera.open();
         Camera.Parameters parameters = camera.getParameters();
         WindowManager wm = (WindowManager) getSystemService(Context.WINDOW_SERVICE); // 获取当前屏幕管理器对象
         Display display = wm.getDefaultDisplay(); // 获取屏幕信息的描述类
         parameters.setPreviewSize(display.getWidth(), display.getHeight());
         camera.setParameters(parameters);
         try {
             camera.setPreviewDisplay(surfaceHolder);
         } catch (IOException e) {
             System.out.println(e.getMessage());
         }
         camera.startPreview();
   }

   class CameraTimerTask extends TimerTask {
         @Override
         public void run() {
             if (camera != null) {
               camera.autoFocus(mAutoFocusCallBack);
             }
         }
   }
}

与上一篇的简单预览相比,这篇增加了两个内容,一个是自动聚焦,一个是拍照。代码看上去很简单,没多少内容。但不亲自测试下,还会发现不少。
   刚开始在Samsung S5570 galaxy mini上测试,总是不能成功的拍照。调试跟踪后,发现自动聚焦总是失败,聚焦失败就没有进行拍照操作。后面并尝试将自动聚焦代码注释掉,直接拍照,发现也是无法显示拍照的结果。之前的PreviewCallback的代码如下:


Bitmap bitmap = BitmapFactory.decodeByteArray(data, 0, data.length);

这行代码返回的总是null,即bitmap没有成功生成。对这些代码本来就是拿来用,功能实现了,就行,对这些都只是简单的了解,当遇到bug后并百思不得其解。后来在网上几经查找发现原来是BitmapFactory.decodeByteArray只支持一定的格式,camara支持的previewformat格式为NV21,所以在获得bitmap时,需要进行转换。通过YuvImage类来转换成JPEG格式,再显示出来。

这行代码返回的总是null,即bitmap没有成功生成。对这些代码本来就是拿来用,功能实现了,就行,对这些都只是简单的了解,当遇到bug后并百思不得其解。后来在网上几经查找发现原来是BitmapFactory.decodeByteArray只支持一定的格式,camara支持的previewformat格式为NV21,所以在获得bitmap时,需要进行转换。通过YuvImage类来转换成JPEG格式,再显示出来。

CameraManager.get().requestPreviewFrame(decodeThread.getHandler(), R.id.decode);//实现拍照
CameraManager.get().requestAutoFocus(this, R.id.auto_focus);//实现聚焦

首先实现拍照,再是实现聚焦,并且重载的聚焦回调函数是隔一段时间再次发出聚焦的请求,实现不断的聚焦。

public void onAutoFocus(boolean success, Camera camera) {
   if (autoFocusHandler != null) {
       Message message = autoFocusHandler.obtainMessage(autoFocusMessage, success);
       // Simulate continuous autofocus by sending a focus request every
// AUTOFOCUS_INTERVAL_MS milliseconds.
//Log.d(TAG, "Got auto-focus callback; requesting another");
       autoFocusHandler.sendMessageDelayed(message, AUTOFOCUS_INTERVAL_MS);
       autoFocusHandler = null;
   } else {
       Log.d(TAG, "Got auto-focus callback, but no handler for it");
   }
   }

聚焦于拍照之前没有先后的逻辑关系,聚焦为了拍照更清晰。这样,关于camera取景聚焦拍照的简单过程并如上了。
    还有一个关键的点幷是回调函数。以前没有接触java代码,在看到很多接口监听处理的代码时,总是很困惑。譬如一段简单的button:



private final Button.OnClickListener addCardListener = new TextView.OnClickListener() {
@Override
public void onClick(View v) {
//在此实现button点击后的操作
}
};

如上的代码实现了点击监听,通过回调函数,当有点击操作时,并执行onClick函数。这就是一个简单的回调函数的使用。