根據官方教程,翻譯而來。
創建一個Camera App:
& 一般步驟
- 檢查並訪問Camera —— 寫代碼檢查是否存在cameras並請求使用
- 創建一個Preview類 —— 創建一個preview類,繼承自SurfaceView 並實現SurfaceHolder 接口。這個類是用來預覽在拍照時的影像
- 創建一個Preview布局 —— 接下去創建一個跟Preview類對應的布局文件,里面可以放一些你想要的交互控件
- 為Capture(拍照)設置監聽(Listener) —— 為你的交互控件設置監聽,比如按下一個Button
- 拍照並保存文件 —— 為拍照或錄像寫代碼,並保存到輸出流(output)
- 釋放Camera —— 在使用完后,必須要合理地釋放,以供其他應用使用。
注意:要及時地釋放Camera資源,通過調用Camera.release() 來實現。否則其他應用,包括你自己的應用,如果想要使用Camera,都會被關閉。
* 檢查(detecting)並訪問(access)Camera:
如果應用沒有在Manifest文件里聲明要使用Camera硬件,那就應該在runtime時檢查Camera是否可用。通過PackageManager.hasSystemFeature()檢查。整體代碼如下:
/** Check if this device has a camera */ private boolean checkCameraHardware(Context context) { if (context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_CAMERA)){ // this device has a camera return true; } else { // no camera on this device return false; } }
可能會有多個攝像頭,在API9之后,可以通過Camera.getNumberOfCameras()得到有幾個可用的攝像頭。
* 訪問Cameras:
確定有設備之后,要通過得到一個Camera實例來訪問它。(除非使用intent 來訪問Camera),通過Camera.open()方法,可以得到主攝像頭。代碼如下:
/** A safe way to get an instance of the Camera object. */ public static Camera getCameraInstance(){ Camera c = null; try { c = Camera.open(); // attempt to get a Camera instance } catch (Exception e){ // Camera is not available (in use or does not exist) } return c; // returns null if camera is unavailable }
注意:要用try/catch Camera.open()這個方法,防止其他應用在使用Camera,而導致本應用被系統關閉。在API Level 9以上,可以通過Camera.open(int)來打開特定的攝像頭。
* 檢查Camera特征
可以通過Camera.getParameters()方法來得到更多的關於它的功能。在API Level 9及以上版本中,使用Camera.getCameraInfo()來確定這個攝像頭是前面的還是后面的,以及這個圖像的朝向(orientation)。
* 創建一個預覽類(preview class)
下面的代碼演示了如何創建一個基本的可以包含在一個View 布局里的預覽圖,這個類實現了SurfaceHolder.Callback接口,來捕獲創建/刪除這個view的回調事件,這些事件是被安排Camera預覽所需要的。
/** A basic Camera preview class */ public class CameraPreview extends SurfaceView implements SurfaceHolder.Callback { private SurfaceHolder mHolder; private Camera mCamera; public CameraPreview(Context context, Camera camera) { super(context); mCamera = camera; // 把這個監聽添加進去,這樣可以監聽到創建和銷毀的事件了
mHolder = getHolder(); mHolder.addCallback(this); // deprecated setting, but required on Android versions prior to 3.0 mHolder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS); } public void surfaceCreated(SurfaceHolder holder) { // The Surface has been created, now tell the camera where to draw the preview. try { mCamera.setPreviewDisplay(holder); mCamera.startPreview(); } catch (IOException e) { Log.d(TAG, "Error setting camera preview: " + e.getMessage()); } } public void surfaceDestroyed(SurfaceHolder holder) { // empty. Take care of releasing the Camera preview in your activity. } public void surfaceChanged(SurfaceHolder holder, int format, int w, int h) { // If your preview can change or rotate, take care of those events here. // Make sure to stop the preview before resizing or reformatting it. if (mHolder.getSurface() == null){ // preview surface does not exist return; } // stop preview before making changes try { mCamera.stopPreview(); } catch (Exception e){ // ignore: tried to stop a non-existent preview } // set preview size and make any resize, rotate or // reformatting changes here // start preview with new settings try { mCamera.setPreviewDisplay(mHolder); mCamera.startPreview(); } catch (Exception e){ Log.d(TAG, "Error starting camera preview: " + e.getMessage()); } } }
如果自己想指定大小,在surfaceChanged()方法里可以寫。設置之前,必須使用從getSupportedPreviewSizes()得到的值,不要在setPreviewSize()里使用合意值。注:supportedPreviewSize()是在camera.getParameters()后再得到的。
* 把預覽圖放在一個布局里
一個Preview類,要放在一個布局里,這個布局還要包括其他用來控制拍照等行為的交互界面。這部分展示如何為預覽圖構建一個布局以及Activity。在這個例子里,FrameLayout元素用來包含預覽圖。使用這種布局,可以把多余的信息或控制覆蓋到活動的Camera預覽圖上。
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="horizontal" android:layout_width="fill_parent" android:layout_height="fill_parent" > <FrameLayout android:id="@+id/camera_preview" android:layout_width="fill_parent" android:layout_height="fill_parent" android:layout_weight="1" /> <Button android:id="@+id/button_capture" android:text="Capture" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center" /> </LinearLayout>
在多數的設備上,默認的朝向是橫向的。這個例子的布局指定一個水平的布局,下面的代碼固定應用的朝向是橫向的。在Manifest里如下指定,就可以簡單地讓應用保持橫向。
<activity android:name=".CameraActivity" android:label="@string/app_name" android:screenOrientation="landscape"> <!-- configure this activity to use landscape orientation --> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity>
note:Preview沒有必要一定是Landscape(橫向)模式。從API Level 8開始,可以使用setDiaplayOrientation()方法來設置預覽圖的旋轉。如果要改變朝向,在surfaceChanged()方法里,先用Camera.stopPreview()方法stop這個Preview,改變朝向,然后再用Camera.startPreview()來開始Preview。
在你的CameraActivity中,把Preview類加入到這個Framelayout中。你的CameraActivity必須要保證在停止或關閉時釋放這個camera。下面代碼說明一切。
public class CameraActivity extends Activity { private Camera mCamera; private CameraPreview mPreview; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); // Create an instance of Camera mCamera = getCameraInstance(); // Create our Preview view and set it as the content of our activity. mPreview = new CameraPreview(this, mCamera); FrameLayout preview = (FrameLayout) findViewById(R.id.camera_preview); preview.addView(mPreview); } }
* 拍照
一旦設置好Preview,就可以使用它去拍照了。在應用代碼里,必須要為你的交互界面設置拍照的響應。
為了得到圖,要使用Camera.takePicture()方法。這個方法帶了三個參數,用來接收來自Camera的數據。為了接收JPEG格式的數據,必須要實現Camera.PictureCallback接口,來接收圖像數據,並寫入到一個文件。下例展示了一個基本的實現,用來保存來自Camera的圖片。
private PictureCallback mPicture = new PictureCallback() { @Override public void onPictureTaken(byte[] data, Camera camera) { File pictureFile = getOutputMediaFile(MEDIA_TYPE_IMAGE); if (pictureFile == null){ Log.d(TAG, "Error creating media file, check storage permissions: " + e.getMessage()); return; } try { FileOutputStream fos = new FileOutputStream(pictureFile); fos.write(data); fos.close(); } catch (FileNotFoundException e) { Log.d(TAG, "File not found: " + e.getMessage()); } catch (IOException e) { Log.d(TAG, "Error accessing file: " + e.getMessage()); } } };
“getOutputMediaFile(MEDIA_TYPE_IMAGE)”這個方法及這個常量會在之后“保存文件”一節中說到
調用Camera.takePicture()方法來觸發拍照事件。
// Add a listener to the Capture button Button captureButton = (Button) findViewById(id.button_capture); captureButton.setOnClickListener( new View.OnClickListener() { @Override public void onClick(View v) { // get an image from the camera mCamera.takePicture(null, null, mPicture); } } );
* 釋放Camera
當停止使用Camera時要及時把它釋放掉。在Activity.onPause()方法中也要釋放。通過Camera.release()方法來釋放。代碼如下:
public class CameraActivity extends Activity { private Camera mCamera; private SurfaceView mPreview; private MediaRecorder mMediaRecorder; ... @Override protected void onPause() { super.onPause(); releaseMediaRecorder(); // if you are using MediaRecorder, release it first releaseCamera(); // release the camera immediately on pause event } private void releaseMediaRecorder(){ if (mMediaRecorder != null) { mMediaRecorder.reset(); // clear recorder configuration mMediaRecorder.release(); // release the recorder object mMediaRecorder = null; mCamera.lock(); // lock camera for later use } } private void releaseCamera(){ if (mCamera != null){ mCamera.release(); // release the camera for other applications mCamera = null; } } }
* 保存文件
Media文件應該被保存到設備的外部存儲目錄(SD Card),以此來節約系統空間。有很多可以存放文件的地方,但作為開發者,有兩個標准的目錄可以使用:
Environment.getExternalStoragePublicDirectory
(Environment.DIRECTORY_PICTURES
) 這個方法返回一個標准的,分享的,並被推薦的目錄,用來存放圖片和Video。如果被用戶卸載了,文件也會存在。為了防止與用戶已存在的文件沖突,你應該再創建一個子目錄用來存放自己應用的圖片。如下面的例子。這個方法在API Level 8以上可以使用,更早的設備,可以查看其他方法。Context.getExternalFilesDir
(Environment.DIRECTORY_PICTURES
) ,這個方法返回一個標准的用來存放你的應用的圖片和Video的地方。如果應用被卸載,這里的文件也會被卸載。其他應用也可以操作這里的文件。
如下代碼展示了如何創建一個File或者一個Uri,用來保存文件。適用於通過Intent或者自己構建的應用。
public static final int MEDIA_TYPE_IMAGE = 1; public static final int MEDIA_TYPE_VIDEO = 2; /** Create a file Uri for saving an image or video */ private static Uri getOutputMediaFileUri(int type){ return Uri.fromFile(getOutputMediaFile(type)); } /** Create a File for saving an image or video */ private static File getOutputMediaFile(int type){ // To be safe, you should check that the SDCard is mounted // using Environment.getExternalStorageState() before doing this. File mediaStorageDir = new File(Environment.getExternalStoragePublicDirectory( Environment.DIRECTORY_PICTURES), "MyCameraApp"); // This location works best if you want the created images to be shared // between applications and persist after your app has been uninstalled. // Create the storage directory if it does not exist if (! mediaStorageDir.exists()){ if (! mediaStorageDir.mkdirs()){ Log.d("MyCameraApp", "failed to create directory"); return null; } } // Create a media file name String timeStamp = new SimpleDateFormat("yyyyMMdd_HHmmss").format(new Date()); File mediaFile; if (type == MEDIA_TYPE_IMAGE){ mediaFile = new File(mediaStorageDir.getPath() + File.separator + "IMG_"+ timeStamp + ".jpg"); } else if(type == MEDIA_TYPE_VIDEO) { mediaFile = new File(mediaStorageDir.getPath() + File.separator + "VID_"+ timeStamp + ".mp4"); } else { return null; } return mediaFile; }
* Camera Features
可以設置很多的特性,比如圖片格式,閃光模式,焦點,還有其他。本節列出幾個常用的特性,簡單介紹如何使用它們。很多的特性可以通過訪問Camera.Parameters對象來得到。但還是有一些重要的特性需要更多的設置。概括為以下幾個部分:
- 計量和焦點區域
- 面部識別
- 定時攝影
關於如何使用這些由Camera.Parameters對象控制的特性,可以查看"使用Camera特性"一節。從API Level1到14,有很多的特性,便並不是所有的都可以被設備使用,使用前要先檢查一下。
& 檢測特性是否可用
要明確使用哪些特性,以及是哪個版本的,之后可以在代碼里檢查設備硬件是否支持這個特性,如果不行的話,要合理地處理。
可以通過得到一個Camera Parameters對象還檢查特性是否可用,以及相應的方法。下例演示如何得到一個Camera.Parameters對象,並檢查是否支持自動對焦功能:
// get Camera parameters Camera.Parameters params = mCamera.getParameters(); List<String> focusModes = params.getSupportedFocusModes(); if (focusModes.contains(Camera.Parameters.FOCUS_MODE_AUTO)) { // Autofocus mode is supported }
有些功能是要在Manifest里進行聲明,比如閃光和自動對焦。可以在Manifest里的“Features Reference”中查到。
& 使用特性
從camera中getParameters();然后進行setXXX();之后再setParameters()進Camera。
重要:有些參數的設置,可能需要先stop preview,變換preview size ,然后再重啟preview。從4.0開始之后就不用再重啟preview了。
上文提到的三個部分,需要寫一些更多的代碼,下面會說到。
& Metering和對焦區域
跟其他的調用方法差不多
& 面部識別
大多數包括人的照片里,臉部很重要,拍照時應該被焦點或者白平衡。4.0提供了API來確定臉部,並利用面部識別來捕捉照片。
注意:當使用面部識別時,setWhiteBalance(String),setFocusAreas(List)以及setMeteringAreas(List)就沒有用了。
使用這個通常需要幾個步驟:
-
- 檢查這個功能是是否被設備支持
- 創建一個面部監測的監聽
- 把這個監聽添加到Camera對象里
- 在preview之后開始面部監測
面部識別不被所有設備支持,通過調用getMaxNumDetectedFaces()來檢查是否可以使用。在下面的startFAceDetection()例子中,展示一個這個檢測。
為了能被提醒並且響應面部識別的監測,需要對面部檢測加一個監聽。創建一個實現了Camera.FaceDetectionListener接口的監聽。如下代碼所示:
class MyFaceDetectionListener implements Camera.FaceDetectionListener { @Override public void onFaceDetection(Face[] faces, Camera camera) { if (faces.length > 0){ Log.d("FaceDetection", "face detected: "+ faces.length + " Face 1 Location X: " + faces[0].rect.centerX() + "Y: " + faces[0].rect.centerY() ); } } }
創建完之后,把它加入到Camera對象里。
mCamera.setFaceDetectionListener(new MyFaceDetectionListener()
應用應該在每次開始(或重啟)Camera Preview時開啟這個監聽方法。創建一個用來開啟面部識別的方法,如下所示:
public void startFaceDetection(){ // Try starting Face Detection Camera.Parameters params = mCamera.getParameters(); // start face detection only *after* preview has started if (params.getMaxNumDetectedFaces() > 0){ // camera supports face detection, so can start it: mCamera.startFaceDetection(); } }
必須在每次打開(或重啟)preview時,開啟面部監測。把上面這個方法添加到你的Preview類里的surfaceCreated()和surfaceChanged()方法中。如下所示:
public void surfaceCreated(SurfaceHolder holder) { try { mCamera.setPreviewDisplay(holder); mCamera.startPreview(); startFaceDetection(); // start face detection feature } catch (IOException e) { Log.d(TAG, "Error setting camera preview: " + e.getMessage()); } } public void surfaceChanged(SurfaceHolder holder, int format, int w, int h) { if (mHolder.getSurface() == null){ // preview surface does not exist Log.d(TAG, "mHolder.getSurface() == null"); return; } try { mCamera.stopPreview(); } catch (Exception e){ // ignore: tried to stop a non-existent preview Log.d(TAG, "Error stopping camera preview: " + e.getMessage()); } try { mCamera.setPreviewDisplay(mHolder); mCamera.startPreview(); startFaceDetection(); // re-start face detection feature } catch (Exception e){ // ignore: tried to stop a non-existent preview Log.d(TAG, "Error starting camera preview: " + e.getMessage()); } }
注意:記住要在調用startPreview()之后調用這個方法。不要嘗試在mainActivity 的onCreate()方法里啟動面部識別。