1. 程式人生 > >Android Camera 相機開發詳解

Android Camera 相機開發詳解

在Android 5.0(SDK 21)中,Google使用Camera2替代了Camera介面。Camera2在介面和架構上做了巨大的變動,
但是基於眾所周知的原因,我們還必須基於 Android 4.+ 系統進行開發。本文介紹的是Camera介面開發及其使用方法,通過本文章,你將全面地學會Camera介面的開發流程。

YOOJIA-CAMERAS

呼叫系統相機/其它App完成拍攝操作

如果你的App的需求只是呼叫攝像頭拍照並拿到照片,老司機的建議是別自己實現拍照模組,這裡面坑多水深。你完全可以使用Intent來呼叫系統相機或第三方具備拍照功能的App來拍照並獲取返回照片資料。

建立一個Intent,指定兩個拍攝型別之一:

  • MediaStore.ACTION_IMAGE_CAPTURE 拍攝照片;
  • MediaStore.ACTION_VIDEO_CAPTURE 拍攝視訊;

Intent intent = new Intent(MediaStore.ACTION_IMAGE/VIDEO_CAPTURE);

通用流程startActivityForResult()onActivityResult()就不表述了。說說拍攝照片的Intent引數吧。

首先是設定拍攝後返回資料的地址:

intent.putExtra(MediaStore.EXTRA_OUTPUT, your-store-uri);

MediaStore.EXTRA_OUTPUT 引數用於指定拍攝完成後的照片/視訊的儲存路徑。你可以使用Android預設的儲存照片目錄來儲存:

Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURE)

也可以是其它任意你喜歡的儲存目錄。如果你使用了App內部目錄,某些臨時檔案如拍攝並上傳的頭像檔案,在處理完成後,要記得將它刪除。這樣做的好處是減少App佔用儲存空間,手機使用者特別喜歡對佔用大儲存空間的App下重手刪除和清理空間。如果你必須儲存大體積的檔案,可以使用公共空間來儲存,把包袱丟出去,私有空間僅儲存應用配置資料。

相機其它設定,如指定拍攝照片的尺寸大小,照片質量等,待以後文章更新吧。

// TODO 是程式界最大的謊言

使用Camera開發照相功能

使用Camera API來開發拍照模組需要費一番大功夫。下面是介紹我在開發NextQRCode專案中使用Camera API的方法和流程。

1.在 Android Manifest.xml 中宣告相機許可權

開發第一步是在 Android Manifest.xml 檔案中宣告使用相機的許可權:

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

有些同學在開發時忘了宣告許可權,執行時應用可能會崩潰掉。另外也要增加以下兩個特性宣告:

<uses-feature android:name="android.hardware.camera" android:required="true"/>
<uses-feature android:name="android.hardware.camera.autofocus" android:required="false"/>

required屬性是說明這個特性是否必須滿足。比方說示例的設定就是要求必須擁有相機裝置但可以沒有自動對焦功能。

這兩個宣告是可選的,它們用於應用商店(Google Play)過濾不支援相機和不支援自動對焦的裝置。

另外在儲存照片時需要寫入儲存器的許可權,也需要加上讀寫儲存器的許可權宣告:

<uses-permission android:name="android.permission.WEITE_EXTERNAL_STORAGE" />

2. 開啟相機裝置

現在市面上銷售的手機/平板等消費產品基本標配兩個攝像頭。如華為P9,更有前置雙攝像頭。講真,我很好奇開發雙攝像頭的App是怎樣的體驗。在開啟相機裝置前,先獲取當前裝置有多少個相機裝置。如果你的開發需求裡包含切換前後攝像頭功能,可以獲取攝像頭數量來判斷是否存在後置攝像頭。

int cameras = Camera.getNumberOfCameras();

這個介面返回值為攝像頭的數量:非負整數。對應地,攝像頭序號為: cameras - 1。例如在擁有前後攝像頭的手機裝置上,其返回結果是2,則第一個攝像頭的cameraId0,通常對應手機背後那個大攝像頭;第二個攝像頭的cameraId1,通常對應著手機的前置自拍攝像頭;

相機是一個硬體裝置資源,在使用裝置資源前需要將它開啟,可以通過介面Camera.open(cameraId)來開啟。參考以下程式碼:

public static Camera openCamera(int cameraId) {
    try{
        return Camera.open(cameraId);
    }catch(Exception e) {
        return null;
    }
}

注意

開啟相機裝置可能會失敗,你一定要檢查開啟操作是否成功。開啟失敗的可能原因有兩種:一是安裝App的裝置上根本沒有攝像頭,例如某些平板或特殊Android裝置;二是cameraId對應的攝像頭正被使用,可能某個App正在後臺使用它錄製視訊。

3. 配置相機引數

在開啟相機裝置後,你將獲得一個Camera物件,並獨佔相機裝置資源。
通過Camera.getParameters()介面可以獲取當前相機裝置的預設配置引數。下面列舉一些我能理解的引數:

閃光燈配置引數,可以通過Parameters.getFlashMode()介面獲取當前相機的閃光燈配置引數:

  • Camera.Parameters.FLASH_MODE_AUTO 自動模式,當光線較暗時自動開啟閃光燈;
  • Camera.Parameters.FLASH_MODE_OFF 關閉閃光燈;
  • Camera.Parameters.FLASH_MODE_ON 拍照時閃光燈;
  • Camera.Parameters.FLASH_MODE_RED_EYE 閃光燈引數,防紅眼模式,科普一下:防紅眼

對焦模式配置引數,可以通過Parameters.getFocusMode()介面獲取:

  • Camera.Parameters.FOCUS_MODE_AUTO 自動對焦模式,攝影小白專用模式;
  • Camera.Parameters.FOCUS_MODE_FIXED 固定焦距模式,拍攝老司機模式;
  • Camera.Parameters.FOCUS_MODE_EDOF 景深模式,文藝女青年最喜歡的模式;
  • Camera.Parameters.FOCUS_MODE_INFINITY 遠景模式,拍風景大場面的模式;
  • Camera.Parameters.FOCUS_MODE_MACRO 微焦模式,拍攝小花小草小螞蟻專用模式;

場景模式配置引數,可以通過Parameters.getSceneMode()介面獲取:

  • Camera.Parameters.SCENE_MODE_BARCODE 掃描條碼場景,NextQRCode專案會判斷並設定為這個場景;
  • Camera.Parameters.SCENE_MODE_ACTION 動作場景,就是抓拍跑得飛快的運動員、汽車等場景用的;
  • Camera.Parameters.SCENE_MODE_AUTO 自動選擇場景;
  • Camera.Parameters.SCENE_MODE_HDR 高動態對比度場景,通常用於拍攝晚霞等明暗分明的照片;
  • Camera.Parameters.SCENE_MODE_NIGHT 夜間場景;

Camera API提供了非常多的引數介面供開發者設定,有必要的話,可以翻閱相關API文件。

在NextQRCode專案中,需要使用到自動對焦的特性。在一些機型上可能是沒有的自動對焦(雖然比較少見),需要對這種情況進行處理。

4. 設定相機預覽方向

相機預覽圖需要設定正確的預覽方向才能正常地顯示預覽畫面,否則預覽畫面會被擠壓得很慘。
在通常情況下,如果我們需要知道裝置的螢幕方向,可以通過Resources.Configuration.orientation來獲取。Android螢幕方向有“豎屏”和“橫屏”兩種,對應的值分別是ORIENTATION_PORTRAITORIENTATION_LANDSCAPE。但相機裝置的方向卻有些特別,設定預覽方向的介面Camera.setDisplayOrientaion(int)的引數是以角度為單位的,而且只能是0,90,180,270其中之一,預設為0,是指手機的左側為攝像頭頂部畫面。記得只能是[0、90、180、270]其中之一,輸入其它角度數值會報錯。

如果你想讓相機跟隨裝置的方向,預覽介面頂部一直保持正上方,以下程式碼供參考:

public static void followScreenOrientation(Context context, Camera camera){
    final int orientation = context.getResources().getConfiguration().orientation;
    if(orientation == Configuration.ORIENTATION_LANDSCAPE) {
        camera.setDisplayOrientation(180);
    }else if(orientation == Configuration.ORIENTATION_PORTRAIT) {
        camera.setDisplayOrientation(90);
    }
}

5. 預覽View與拍照

我們一般使用SurfaceView作為相機預覽View,你也可以使用Texture。在SurfaceView中獲取得SurfaceHolder,並通過setPreviewDisplay()介面設定預覽。在設定預覽View後,一定要記得以下兩點:

  • 呼叫startPreview()方法啟動預覽,否則預覽View不會顯示任何內容;
  • 拍照操作需要在startPreview()方法執行之後呼叫;
  • 每次拍照後,預覽View會停止預覽。所以連續拍照,需要重新呼叫startPreview()來恢復預覽;

Camera接受一個SurfaceHolder介面,這個介面可以通過SurfaceHolder.Callback獲得。我們可以通過繼承SurfaceView來實現相機預覽效果。在NextQRCode專案中,實現了LiveCameraView類,它內部已實現了相機預覽所需要的處理過程,很簡潔的類,以下是它的全部原始碼:

public class LiveCameraView extends SurfaceView implements SurfaceHolder.Callback {
    private final static String TAG = LiveCameraView.class.getSimpleName();
    private Camera mCamera;
    private SurfaceHolder mSurfaceHolder;

    public LiveCameraView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        mSurfaceHolder = this.getHolder();
        mSurfaceHolder.addCallback(this);
    }

    @Override
    public void surfaceCreated(SurfaceHolder holder) {
        Log.d(TAG, "Start preview display[SURFACE-CREATED]");
        startPreviewDisplay(holder);
    }

    @Override
    public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
        if (mSurfaceHolder.getSurface() == null){
            return;
        }
        Cameras.followScreenOrientation(getContext(), mCamera);
        Log.d(TAG, "Restart preview display[SURFACE-CHANGED]");
        stopPreviewDisplay();
        startPreviewDisplay(mSurfaceHolder);
    }

    public void setCamera(Camera camera) {
        mCamera = camera;
        final Camera.Parameters params = mCamera.getParameters();
        params.setFocusMode(Camera.Parameters.FOCUS_MODE_AUTO);
        params.setSceneMode(Camera.Parameters.SCENE_MODE_BARCODE);
    }

    private void startPreviewDisplay(SurfaceHolder holder){
        checkCamera();
        try {
            mCamera.setPreviewDisplay(holder);
            mCamera.startPreview();
        } catch (IOException e) {
            Log.e(TAG, "Error while START preview for camera", e);
        }
    }

    private void stopPreviewDisplay(){
        checkCamera();
        try {
            mCamera.stopPreview();
        } catch (Exception e){
            Log.e(TAG, "Error while STOP preview for camera", e);
        }
    }

    private void checkCamera(){
        if(mCamera == null) {
            throw new IllegalStateException("Camera must be set when start/stop preview, call <setCamera(Camera)> to set");
        }
    }

    @Override
    public void surfaceDestroyed(SurfaceHolder holder) {
        Log.d(TAG, "Stop preview display[SURFACE-DESTROYED]");
        stopPreviewDisplay();
    }
}

從上面程式碼可以看出LiveCameraView的核心程式碼是SurfaceHolder.Callback的回撥:在建立/銷燬時啟動/停止預覽動作。在LiveCameraView類中,我們利用了View的生命週期回撥來實現自動管理預覽生命週期控制:

  • 當SurfaceView被建立後自動開啟預覽;
  • 當SurfaceView被銷燬時關閉預覽;
  • 當SurfaceView尺寸被改變時重置預覽;

預覽View需要注意預覽輸出畫面的尺寸。相機輸出畫面只支援部分尺寸。關於尺寸部分,後面再更新。

在啟用預覽View後,就可以通過Camera.takePicture()方法拍攝一張照片,返回的照片資料通過Callback介面獲取。takePicture()介面可以獲取三個型別的照片:

  • 第一個,ShutterCallback介面,在拍攝瞬間瞬間被回撥,通常用於播放“咔嚓”這樣的音效;
  • 第二個,PictureCallback介面,返回未經壓縮的RAW型別照片;
  • 第三個,PictureCallback介面,返回經過壓縮的JPEG型別照片;

我們使用第三個引數,JPEG型別的照片的圖片精度即可滿足識別二維碼的需求。在NextQRCode專案中,ZXing識別二維碼的資料格式為Bitmap,通過BitmapFactory可以很方便方便地將byte陣列轉換成Bitmap。

public abstract class BitmapCallback implements Camera.PictureCallback {
    @Override
    public void onPictureTaken(byte[] data, Camera camera) {
        onPictureTaken(BitmapFactory.decodeByteArray(data, 0, data.length));
    }
    public abstract void onPictureTaken(Bitmap bitmap);
}

6. 釋放相機裝置

在開啟一個相機裝置後,意味著你的App就獨佔了這個裝置,其它App將無法使用它。因此在你不需要相機裝置時,記得呼叫release()方法釋放裝置,再使用時可以重新開啟,這並不需要多大的成本。可以選擇在stopPreview()後即釋放相機裝置。

附加工具性程式碼實現

1 - 判斷手機裝置是否有相機裝置

public static boolean hasCameraDevice(Context ctx) {
    return ctx.getPackageManager()
            .hasSystemFeature(PackageManager.FEATURE_CAMERA);
}

2 - 判斷是否支援自動對焦

public static boolean isAutoFocusSupported(Camera.Parameters params) {
   List<String> modes = params.getSupportedFocusModes();
   return modes.contains(Camera.Parameters.FOCUS_MODE_AUTO);
}

如何正確地使用Camera來開發視訊拍攝功能

抱歉,這個我真沒研究過。

關於Camera2

後續再更新Camera2的開發教程