前言

        接触Android开发有一段时间了。一开始时纯粹是出于自己的兴趣,空闲时写几个小软件自娱自乐。刚好暑假时老板布置的任务跟Android相关,所以这段时间又继续进行了Android的开发学习。现在的Android开发水平仅属于菜鸟级别,之所以写这系列博客,一来是对这段时间的学习做一些总结,二来是分享,希望能帮助到有需要的人。最近状态很差,干什么都不上劲,也希望能通过动手写这个博客来改变一下自己的状态,迎接后面的挑战。

        好了,言归正传。Android要实现拍照功能,有两种方法。一是直接利用Intent调用系统自带的相机进行拍照,这适用于不想自己DIY相机,而仅仅是拍照分享或者拍照后进行处理的场合,简单方便。而是利用Camera相关的API自己从头实现一个相机,这个好处是可以自定义相机的行为,能最大程度满足自己的开发需求。这篇文章主要是介绍怎么实现预览和拍照保存的功能,其实这些基本功能的实现在Android文档中给出了很详细的介绍->http://developer.android.com/guide/topics/media/camera.html

正文


一、声明权限

为了让手机能够正常使用相机功能,必须在AndroidManifest.xml文件中声明所需的权限。

<span class="tag" style="color: rgb(0, 0, 136);"><uses-permission</span><span class="pln" style="color: rgb(0, 0, 0);"> </span><span class="atn" style="color: rgb(136, 34, 136);">android:name</span><span class="pun" style="color: rgb(102, 102, 0);">=</span><span class="atv" style="color: rgb(0, 136, 0);">"android.permission.CAMERA"</span><span class="pln" style="color: rgb(0, 0, 0);"> </span><span class="tag" style="color: rgb(0, 0, 136);">/></span>


另外,还需要加入写入存储卡的权限,否则无法保存图片。整个AndroidManifest.xml文件的内容如下:

1. <?xml version="1.0" encoding="utf-8"?>  
2. <manifest xmlns:android="http://schemas.android.com/apk/res/android"  
3. package="com.example.supercamera"  
4. "1"  
5. "1.0" >  
6.   
7.     <uses-sdk  
8. "8"  
9. "17" />  
10.       
11. "android.permission.CAMERA" />  
12. "android.hardware.camera" />  
13. "android.hardware.camera" android:required="false" />  
14. "android.permission.WRITE_EXTERNAL_STORAGE" />  
15.       
16.     <application  
17. "true"  
18. "@drawable/ic_launcher"  
19. "@string/app_name"  
20. "@style/AppTheme" >  
21.         <activity  
22. "com.example.supercamera.MainActivity"  
23. "@string/app_name" >  
24.             <intent-filter>  
25. "android.intent.action.MAIN" />  
26. "android.intent.category.LAUNCHER" />  
27.             </intent-filter>  
28.         </activity>  
29.     </application>  
30.   
31. </manifest>


其中,<uses-feature android:name="android.hardware.camera" />的作用是表明该程序必须安装在支持相机功能的设备中,Google Play会阻止不支持相机的设备下载该程序。

二、检测并打开相机

        在打开相机之前,先利用PackageManager.hasSystemFeature()方法检查相机是否可用。然后用 Camera.open()方法获取相机,注意一定要捕捉异常。Camera.open()方法还有一种重载形式Camera.open(int),参数为相机的id,这样就可以通过指定id来获取前摄像头或者后摄像头。

1. // 检查设备是否提供摄像头  
2. private boolean checkCameraHardware(Context context) {   
3. if (context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_CAMERA)){   
4. // 摄像头存在   
5. return true;   
6. else {   
7. // 摄像头不存在   
8. return false;   
9.     }   
10. }  
11.   
12. // 安全获取Camera对象实例的方法*/   
13. public static Camera getCameraInstance(){   
14. null;   
15. try {   
16. // 试图获取Camera实例  
17.     }   
18. catch (Exception e){   
19. // 摄像头不可用(正被占用或不存在)  
20.     }   
21. return c; // 不可用则返回null  
22. }



三、预览

        为了呈现要拍摄的内容,必须创建预览窗口,实时显示摄像头获取的内容。摄像头预览类需要继承 SurfaceView 类并且实现 SurfaceHolder.Callback接口,如下面的代码所示:

1. // 基本的摄像头预览类  
2. public class CameraPreview extends SurfaceView implements SurfaceHolder.Callback {   
3.   
4. private SurfaceHolder mHolder;   
5. private Camera mCamera;   
6.   
7. public CameraPreview(Context context, Camera camera) {   
8. super(context);   
9.         mCamera = camera;   
10. // 安装一个SurfaceHolder.Callback,  
11. // 这样创建和销毁底层surface时能够获得通知。  
12.         mHolder = getHolder();   
13. this);   
14. // 已过期的设置,但版本低于3.0的Android还需要  
15.         mHolder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);   
16.     }   
17.   
18. public void startPreview()  
19.     {  
20.         mCamera.startPreview();  
21.     }  
22.       
23. public void surfaceCreated(SurfaceHolder holder) {   
24. // surface已被创建,现在把预览画面的位置通知摄像头  
25. try {   
26.             mCamera.setPreviewDisplay(holder);   
27.             mCamera.startPreview();   
28. catch (IOException e) {   
29. "Error setting camera preview: " + e.getMessage());   
30.         }   
31.     }   
32.   
33. public void surfaceDestroyed(SurfaceHolder holder) {   
34. // 空代码。注意在activity中释放摄像头预览对象     
35.     }   
36.   
37. public void surfaceChanged(SurfaceHolder holder, int format, int w, int h) {   
38. // 如果预览无法更改或旋转,注意此处的事件  
39. // 确保在缩放或重排时停止预览  
40. if (mHolder.getSurface() == null){   
41. // 预览surface不存在  
42. return;   
43.         }   
44. // 更改时停止预览   
45. try {   
46.             mCamera.stopPreview();   
47. catch (Exception e){   
48. // 忽略:试图停止不存在的预览  
49.         }   
50. // 在此进行缩放、旋转和重新组织格式  
51. // 以新的设置启动预  
52. try {   
53.             mCamera.setPreviewDisplay(mHolder);   
54. 90);   
55.             mCamera.startPreview();   
56. catch (Exception e){   
57. "Error starting camera preview: " + e.getMessage());   
58.         }   
59.     }   
60.       
61. public void setCamera(Camera camera)  
62.     {  
63. try {  
64.             mCamera = camera;  
65.             camera.setPreviewDisplay(mHolder);  
66. catch (IOException e) {  
67.             e.printStackTrace();  
68.         }  
69.     }  
70. }

布局的xml文件如下所示:

1. <?xml version="1.0" encoding="utf-8"?>   
2. <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"  
3. "fill_parent"  
4. "fill_parent"  
5. "vertical" >  
6.    
7.  <FrameLayout  
8. "@+id/camera_preview"  
9. "fill_parent"  
10. "wrap_content"  
11. "1" >  
12.   
13.  </FrameLayout>  
14.   
15.  <RelativeLayout  
16. "match_parent"  
17. "wrap_content"  
18. "#7f7f7f" >  
19.   
20.      <Button  
21. "@+id/button_capture"  
22. "98dp"  
23. "96dp"  
24. "true"  
25. "true"  
26. "@drawable/button_capture"  
27. "32dp"  
28. "24dip"  
29. "24dip"  
30. "32dp" />  
31.   
32.      <ImageButton  
33. "@+id/imgBtnOpenPic"  
34. "48dp"  
35. "48dp"  
36. "true"  
37. "28dp"  
38. "@+id/button_capture"  
39. "false"  
40. "#0000"  
41. "48dp"  
42. "48dp"  
43. "32dp"  
44. "32dp"  
45. "centerCrop"  
46. "@drawable/gallery_pic" />  
47.   
48.  </RelativeLayout>  
49.    
50. </LinearLayout>


其中,camera_preview是一个FrameLayout布局,把CameraPreview添加到这个布局中即可。

1. // 创建Preview view并将其设为activity中的内容  
2. new CameraPreview(this, mCamera);   
3.         FrameLayout preview = (FrameLayout) findViewById(id.camera_preview);   
4.         preview.addView(mPreview);


在预览时,可能会出现画面旋转了90度的情况,这时候camera.setDisplayOrientation(90);来设置一下角度即可。


四、拍照保存

void takePicture (Camera.ShutterCallback shutter, Camera.PictureCallback raw, Camera.PictureCallback jpeg)方法即可实现拍照功能,其中shutter为拍照瞬间的回调函数,在该函数中可以用来播放快门声等,raw为原始图像数据的回调函数,jpeg为Jpeg格式数据的回调函数。要保存图像,重载 Camera.PictureCallback jpeg即可。

1. private PictureCallback mPicture = new PictureCallback() {  
2. @Override   
3. public void onPictureTaken(byte[] data, Camera camera) {   
4.         File pictureFile = getOutputMediaFile(MEDIA_TYPE_IMAGE);   
5. if (pictureFile == null){   
6. "Error creating media file, check storage permissions: ");   
7. return;   
8.         }  
9. try {   
10. new FileOutputStream(pictureFile);   
11.             fos.write(data);   
12.             fos.close();   
13.             mPreview.startPreview();  
14. // 更新上一张图片路径,更新ImgBtnOpenPic缩略图  
15.         lastPicPath = pictureFile.getAbsolutePath();  
16.         updateOpenPicImgBtn(lastPicPath);  
17. catch (FileNotFoundException e) {   
18. "File not found: " + e.getMessage());   
19. catch (IOException e) {   
20. "Error accessing file: " + e.getMessage());   
21.         }   
22.   
23.     }   
24. };


其中,getOutputMediaFile函数返回File类型的变量,指定了文件的保存位置。

在保存图像时,可能会出现保存的图像被旋转了90度的情况,这里可以利用以下方法解决:

1. // 设置摄像头参数  
2. protected void setCameraParams(Camera camera)  
3. {  
4. 90);     
5.     Camera.Parameters params = camera.getParameters();  
6. 90);  
7.     camera.setParameters(params);  
8. }

通过设置Camera.Parameters的setRotation方法,把图像旋转90度即可。当然这种方法也会存在一定的问题,后面将会提供另外一种解决思路。



五、打开图像

        要实现这个功能,首先添加一个Button,点击这个Button后,通过图库打开上一次拍摄的照片,这就需要每次拍照时保存上一张图片的文件路径lastPicPath。给该按钮添加监听事件:

1. public class OpenPictureListener implements View.OnClickListener  
2. {  
3. @Override  
4. public void onClick(View v) {  
5. new File(lastPicPath);   
6. new Intent(Intent.ACTION_VIEW);   
7. "image/*");  
8.         startActivity(intent);  
9.     }     
10. };


其中intent.setDataAndType(Uri.fromFile(file), "image/*");这个语句指定了要查看的文件类型是图像文件,并且给定了文件路径。

        另一个很重要的问题是更新缩略图,这能让用户在打开图像之前能够大致知道图像是什么样子的。所以实际上打开图像按钮使用的imageButton类型。

1. // 更新打开图像按钮的缩略图  
2. public void updateOpenPicImgBtn(String path)  
3. {  
4. new BitmapFactory.Options();  
5. true;                     //表明只获取图像大小  
6. //由于inJustDecodeBounds为true,此时bm为null  
7. 64;  
8. false;  
9.     bm = BitmapFactory.decodeFile(path, options);  
10.     ImageButton openPicBtn = (ImageButton)findViewById(id.imgBtnOpenPic);  
11.     openPicBtn.setImageBitmap(bm);  
12. }


用BitmapFactory.decodeFile方法可以读取本地图像,但是因为我们这里仅仅需要的是缩略图,如果按照原始尺寸读入,不但浪费内存,还会使得等待时间变长。可以通过BitmapFactory.Options来控制decodeFile的行为。如果把options.inJustDecodeBounds设为true,则表明decodeFile返回null,但是可以得到图像的大小,这使得不用为图像像素分配内存,提高效率。然后通过指定采样率options.inSampleSize,同时把options.inJustDecodeBounds设为false,再次调用decodeFile方法来得到一个经过采样的小尺寸图像,提高响应时间并且节省内存。然后利用ImageButton的setImageBitmap方法就可以设置缩略图。

        还有一个需要注意的问题是,当程序第一次启动的时候,lastPicPath还没被赋予任何有意义的值。这样的话,就无法查看以前拍的照片了。这时候有两种解决思路,一是把lastPicPath保存到本地,然后程序启动的时候读取这个值,刷新缩略图;而是遍历保存目录,找出拍摄时间最晚的图像文件。这里采用的是第二种思路。

1. // 读取保存目录中最新的图像文件  
2. String getLastCaptureFile()  
3. {  
4. new File(Environment.getExternalStoragePublicDirectory(   
5. "SuperCamera");    
6. if(mediaStorageDir.exists() == false)  
7.     {  
8. return "";  
9.     }  
10.     File [] fs = mediaStorageDir.listFiles();    
11. if(fs.length <= 0)  
12. return "";  
13. new MainActivity.CompratorByLastModified());  
14. return fs[fs.length-1].getPath();  
15. }  
16.   
17. // 排序器,按修改时间从新到旧排序  
18. static class CompratorByLastModified implements Comparator<File>    
19. {    
20. public int compare(File f1, File f2)   
21.   {    
22. long diff = f1.lastModified()-f2.lastModified();    
23. if(diff>0)    
24. return 1;    
25. else if(diff==0)    
26. return 0;    
27. else    
28. return -1;    
29.  }  
30.     
31. public boolean equals(Object obj)  
32.  {    
33. return true;    
34.  }    
35. }


首先,利用File的listFiles方法返回所有文件的文件名,然后利用Arrays.sort()方法来按修改时间对文件进行排序,得到上次拍摄的文件。当然,严格起见,这里应该加上对该文件类型的判断,剔除非图像文件。

注:在测试时发现了一个问题,系统并不会及时把所有包含图像的文件添加到图库中,我保存的目录是sdcard/Pictures/SuperCamera,但是在图库中有时候能看到这个文件夹,有时候看不到。当图库不包括这个目录的时候,用上述方法打开图像后,不能通过左右滑动查看上一张或者下一张图像。这个问题挺困惑的,但是没有找出原因和解决方法。


六、Pause and resume
        在安卓程序中,这是一个很重要的问题。因为在使用你的程序时,用户随时可能切换到其他应用中,这时候如果程序不做相应处理,就会影响其他应用的使用(比如摄像头),或者导致程序崩溃。

1. @Override  
2. protected void onResume() {  
3. super.onResume();  
4. if(mCamera == null)  
5.     {  
6.         mCamera = getCameraInstance();  
7.         setCameraParams(mCamera);  
8.         mPreview.setCamera(mCamera);  
9.         mCamera.startPreview();       
10.     }  
11.       
12. }<pre name="code" class="java">    </pre><pre name="code" class="java">    @Override  
13. protected void onPause() {  
14. super.onPause();  
15.     releaseCamera();  
16. }</pre>    private void releaseCamera(){ <br><span style="white-space:pre">    </span>if (mCamera != null){ <br><span style="white-space:pre">    </span>    mCamera.release();        // 为其它应用释放摄像头<br><span >  </span>        mCamera = null; <br><span >  </span>    } <br><span >  </span>}     }


在onPause中,必须调用Camera.release方法释放相机,否则会影响其他应用对相机得访问。在onResume中,重新打开相机并且设置参数即可。


参考

http://developer.android.com/guide/topics/media/camera.html

注:在写程序的时候还参考了其他的博客,但是当时并没有一一记录下来。