Androd自定義控件(五)打造自己的Camera

寫在前面的話

前一陣子負責一個自定義相機進行拍照並在另一個頁面進行人臉識別的模塊,相機部分需求並不怎麼複雜,可以切換前後攝像頭,可以拍照並把照片返回上一個頁面。由於沒有怎麼接觸過自定義相機的部分,而網上的一些資料又不全,踩了不少坑。故在這裏總結一下,希望對大家有所幫助,同時把自定義控件系列的最後一個坑填上(surfaceview)。效果圖如下:

這裏寫圖片描述

Android中開發相機的兩種方式

Android系統提供了兩種使用手機相機資源實現拍攝功能的方法,一種是直接通過Intent調用系統相機組件,這種方法快速方便,適用於直接獲得照片的場景,如上傳相冊,微博、朋友圈發照片等。另一種是使用相機API來定製自定義相機,這種方法適用於需要定製相機界面或者開發特殊相機功能的場景,如需要對照片做裁剪、濾鏡處理,添加貼紙,表情,地點標籤等。

調用系統自帶相機

關於系統自帶相機的調用非常簡單,這裏我就不過多敘述了,具體可以參考谷歌的Training。我只說容易被大家忽視的幾個點:

  • 如果我們的應用使用相機,但相機並不是應用的正常運行所必不可少的組件,可以將權限聲明中的android:required設置爲”false”。這樣的話,Google Play 也會允許沒有相機的設備下載該應用。當然我們有必要在使用相機之前通過調用hasSystemFeature(PackageManager.FEATURE_CAMERA)方法來檢查設備上是否有相機。如果沒有,我們應該禁用和相機相關的功能!

  • 在調用startActivityForResult()方法之前,先調用resolveActivity(),這個方法會返回能處理該Intent的第一個Activity(譯註:即檢查有沒有能處理這個Intent的Activity)。執行這個檢查非常重要,因爲如果在調用startActivityForResult()時,沒有應用能處理你的Intent,應用將會崩潰。所以只要返回結果不爲null,使用該Intent就是安全的。

使用Android框架所提供的API來直接控制相機硬件

使用API來控制相機我們需要用到關鍵類和接口:

  • 使用Camera對象來控制相機
  • 使用SurfaceView來展現照相機採集的圖像
  • 通過surfaceholder來控制surfac的尺寸和格式,修改surface的像素,監視surface的變化等等
  • 通過SurfaceHolder.Callback 接口,監聽surface狀態變化

接下來我們分爲以下三部分來介紹:關鍵類以及接口的作用和方法,Camera控制拍照步驟,自定義相機容易踩到的坑以及解決辦法。

API說明

Camera :最主要的類,用於管理和操作camera資源。它提供了完整的相機底層接口,支持相機資源切換,設置預覽/拍攝尺寸,設定光圈、曝光、聚焦等相關參數,獲取預覽/拍攝幀數據等功能,主要方法有以下這些:

  • open():獲取camera實例。
  • setPreviewDisplay(SurfaceHolder):綁定繪製預覽圖像的surface。surface是指向屏幕窗口原始圖像緩衝區(raw buffer)的一個句柄,通過它可以獲得這塊屏幕上對應的canvas,進而完成在屏幕上繪製View的工作。通過surfaceHolder可以將Camera和surface連接起來,當camera和surface連接後,camera獲得的預覽幀數據就可以通過surface顯示在屏幕上了。
  • setPrameters設置相機參數,包括前後攝像頭,閃光燈模式、聚焦模式、預覽和拍照尺寸等。
  • startPreview():開始預覽,將camera底層硬件傳來的預覽幀數據顯示在綁定的surface上。
  • stopPreview():停止預覽,關閉camra底層的幀數據傳遞以及surface上的繪製。
  • release():釋放Camera實例
  • takePicture(Camera.ShutterCallback shutter, Camera.PictureCallback raw, Camera.PictureCallback jpeg):這個是實現相機拍照的主要方法,包含了三個回調參數。shutter是快門按下時的回調,raw是獲取拍照原始數據的回調,jpeg是獲取經過壓縮成jpg格式的圖像數據的回調。

SurfaceView :用於繪製相機預覽圖像的類,提供給用戶實時的預覽圖像。普通的view以及派生類都是共享同一個surface的,所有的繪製都必須在UI線程中進行。而surfaceview是一種比較特殊的view,它並不與其他普通view共享surface,而是在內部持有了一個獨立的surface,surfaceview負責管理這個surface的格式、尺寸以及顯示位置。由於UI線程還要同時處理其他交互邏輯,因此對view的更新速度和幀率無法保證,而surfaceview由於持有一個獨立的surface,因而可以在獨立的線程中進行繪製,因此可以提供更高的幀率。自定義相機的預覽圖像由於對更新速度和幀率要求比較高,所以比較適合用surfaceview來顯示。

SurfaceHolder :surfaceholder是控制surface的一個抽象接口,它能夠控制surface的尺寸和格式,修改surface的像素,監視surface的變化等等,surfaceholder的典型應用就是用於surfaceview中。surfaceview通過getHolder()方法獲得surfaceholder 實例,通過後者管理監聽surface 的狀態。

SurfaceHolder.Callback 接口 :負責監聽surface狀態變化的接口,有三個方法:

  • surfaceCreated(SurfaceHolder holder):在surface創建後立即被調用。在開發自定義相機時,可以通過重載這個函數調用camera.open()、camera.setPreviewDisplay(),來實現獲取相機資源、連接camera和surface等操作。
  • surfaceChanged(SurfaceHolder holder, int format, int width, int height):在surface發生format或size變化時調用。在開發自定義相機時,可以通過重載這個函數調用camera.startPreview來開啓相機預覽,使得camera預覽幀數據可以傳遞給surface,從而實時顯示相機預覽圖像。
  • surfaceDestroyed(SurfaceHolder holder):在surface銷燬之前被調用。在開發自定義相機時,可以通過重載這個函數調用camera.stopPreview(),camera.release()來實現停止相機預覽及釋放相機資源等操作。

Camera控制拍照的過程

  1. 調用Camera的open()方法打開相機。
  2. 調用Camera的getParameters()獲取拍照參數,該方法返回一個Cmera.Parameters對象。
  3. 調用Camera.Parameters對象對照相的參數進行設置。
  4. 調用Camera的setParameters(),並將Camera.Parameters對象作爲參數傳入,這樣就可以對拍照進行參數控制,Android2.3.3以後不用設置。
  5. 調用Camerade的startPreview()的方法開始預覽取景,在之前需要調用Camera的setPreviewDisplay(SurfaceHolder holder)設置使用哪個SurfaceView來顯示取得的圖片。
  6. 調用Camera的takePicture()方法進行拍照。
  7. 程序結束時,要調用Camera的stopPreview()方法停止預覽,並且通過Camera.release()來釋放資源。

具體實現代碼戳後面鏈接

踩坑與填坑

預覽方向

先看下官方文檔的說明

Most camera applications lock the display into landscape mode because that is the natural orientation of the camera sensor. This setting does not prevent you from taking portrait-mode photos, because the orientation of the device is recorded in the EXIF header. The setCameraDisplayOrientation() method lets you change how the preview is displayed without affecting how the image is recorded. However, in Android prior to API level 14, you must stop your preview before changing the orientation and then restart it.

大多數相機程序會鎖定預覽爲橫屏狀態,因爲該方向是相機傳感器的自然方向。當然這一設定並不會阻止我們去拍豎屏的照片,因爲設備的方向信息會被記錄在EXIF頭中。setCameraDisplayOrientation()方法可以讓你在不影響照片拍攝過程的情況下,改變預覽的方向。然而,對於Android API Level 14及以下版本的系統,在改變方向之前,我們必須先停止預覽,然後再去重啓它。

SurfaceView預覽圖像拉伸變形,拍攝照片尺寸不對

說明這個問題之前,同樣先說一下幾個跟相機有關的尺寸。

  • SurfaceView尺寸 :即自定義相機應用中用於顯示相機預覽圖像的View的尺寸,當它鋪滿全屏時就是屏幕的大小。這裏surfaceview顯示的預覽圖像暫且稱作手機預覽圖像。

  • Previewsize :相機硬件提供的預覽幀數據尺寸。預覽幀數據傳遞給SurfaceView,實現預覽圖像的顯示。這裏預覽幀數據對應的預覽圖像暫且稱作相機預覽圖像。

  • Picturesize :相機硬件提供的拍攝幀數據尺寸。拍攝幀數據可以生成位圖文件,最終保存成.jpg或者.png等格式的圖片。這裏拍攝幀數據對應的圖像稱作相機拍攝圖像。圖4說明了以上幾種圖像及照片之間的關係。手機預覽圖像是直接提供給用戶看的圖像,它由相機預覽圖像生成,拍攝照片的數據則來自於相機拍攝圖像。

原因是沒有正確設置比例 parameter.setPictureSize(width,height),這個比例不是你決定的,要先通過camera.getParameters().getSupportedPictureSizes()獲得手機支持的尺寸。

/**
     * 設置照片格式
     */
    private void setParameter() {
        Camera.Parameters parameters = camera.getParameters(); // 獲取各項參數
        parameters.setPictureFormat(PixelFormat.JPEG); // 設置圖片格式
        parameters.setJpegQuality(100); // 設置照片質量
        //獲得相機支持的照片尺寸,選擇合適的尺寸
        List<Camera.Size> sizes = parameters.getSupportedPictureSizes();
        int maxSize = Math.max(display.getWidth(), display.getHeight());
        int length = sizes.size();
        if (maxSize > 0) {
            for (int i = 0; i < length; i++) {
                if (maxSize <= Math.max(sizes.get(i).width, sizes.get(i).height)) {
                    parameters.setPictureSize(sizes.get(i).width, sizes.get(i).height);
                    break;
                }
            }
        }
        List<Camera.Size> ShowSizes = parameters.getSupportedPreviewSizes();
        int showLength = ShowSizes.size();
        if (maxSize > 0) {
            for (int i = 0; i < showLength; i++) {
                if (maxSize <= Math.max(ShowSizes.get(i).width, ShowSizes.get(i).height)) {
                    parameters.setPreviewSize(ShowSizes.get(i).width, ShowSizes.get(i).height);
                    break;
                }
            }
        }
        camera.setParameters(parameters);
    }

前置攝像頭的鏡像效果

  • Android 相機硬件有個特殊設定,就是對於前置攝像頭,在展示預覽視圖時採用類似鏡面的效果,顯示的是攝像頭成像的鏡像。而拍攝出的照片則仍採用攝像頭成像。看到這裏,大家可能會有些懷疑,不妨現在就試試自己 Android 手機上的前置攝像頭,對比下預覽圖像和拍攝出照片的區別。這是由於底層相機在傳遞前置攝像頭預覽數據時做了水平翻轉變換,即將x方向鏡像翻轉180度。這個變化對之前豎屏預覽的方向也會造成影響,本來對於後置攝像頭旋轉90度即可使預覽視圖正確,而對前置攝像頭,如果也旋轉90度的話,看到的預覽圖像則是上下顛倒的(因爲x方向翻轉了180度),因此必須再旋轉180度,才能顯示正確。
    解決方案,在保存圖片的時候根據選擇的攝像頭做對應的翻轉。
//將照片改爲豎直方向
                    Bitmap bitmap = BitmapFactory.decodeByteArray(data, 0, data.length);
                    Matrix matrix = new Matrix();
                    switch (cameraPosition) {
                        case 0://前
                            matrix.preRotate(270);
                            break;
                        case 1:
                            matrix.preRotate(90);
                            break;
                    }
                    bitmap = Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), matrix, true);
  • 同時在開發的過程中發現了一個有趣的東西,我們用前置攝像頭拍出來的照片其實是左右翻轉的。但我用小米自帶的相機測試發現,當攝像頭中有人臉出現的時候,相機會做左右翻轉的操作,以給用戶更好的體驗。

源碼戳這裏。

發佈了40 篇原創文章 · 獲贊 143 · 訪問量 21萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章