Android開發技巧之Camera拍照功能

來源:互聯網
上載者:User

Android開發技巧之Camera拍照功能

本篇是我對開發項目的拍照功能過程中,對Camera拍照使用的總結。由於camera2是在api level 21(5.0.1)才引入的,而Camera到6.0仍可使用,所以暫未考慮camera2。

文檔中的Camera

相對於其他絕大多數類,文檔對Camera的介紹還是比較詳盡的,包含了使用過程中所需要的步驟說明,當然,這也表明了它在實際使用中的繁瑣。
首先,需要在AndroidManifest.xml中聲明以下許可權和特性:

   

然後,拍照的話,需要以下十步:
1. 通過open(int)方法得到一個執行個體
2. 通過getParameters()方法得到預設的設定
3. 如果有必要,修改上面所返回的Camera.Parameters對象,並調用setParameters(Camera.Parameters) 進行設定
4. 如果有需要,調用setDisplayOrientation(int)設定顯示的方向
5. 這一步很重要,通過setPreviewDisplay(SurfaceHolder)傳入一個已經初始化了的SurfaceHolder,否則無法進行預覽。
6. 這一步也很重要,通過startPreview()開始更新你的預覽介面,在你拍照之前,它必須開始。
7. 調用takePicture(Camera.ShutterCallback, Camera.PictureCallback, Camera.PictureCallback, Camera.PictureCallback)進行拍照,等待它的回調
8. 拍照之後,預覽的展示會停止。如果想繼續拍照,需要先再調用startPreview()
9. 調用stopPreview()停止預覽。
10. 非常重要,調用release()釋放Camera,以使其他應用也能夠使用相機。你的應用應該在onPause()被調用時就進行釋放,在onResume()時再重新open()

上面就是文檔中關於使用Camera進行拍照的介紹了。接下來說一下我的使用情境。

我的使用情境


這是項目的介面需求。下面一個圓的拍照按鈕,然後是一個取消按鈕,上面是預覽介面(SurfaceView)加個取景框。再上面就是一塊黑的了。點拍照,拍照之後,跳到一個裁剪圖片的介面,所以不會有連續拍多次照片的情境。
取景框什麼的這裡略過不談,布局檔案也相對比較簡單,下面直接看Java代碼裡對Camera的使用。

實際使用及填坑SurfaceHolder的回調

我在Activity中實現SurfaceHolder.Callback介面。然後在onCreate(Bundle)方法中,添加SurfaceHolder的回調。

        SurfaceHolder holder = mSurfaceView.getHolder();        holder.addCallback(this);

它的回調方法有3個,分別是surface被建立時的回調surfaceCreated(SurfaceHolder),surface被銷毀時的回調surfaceDestroyed(SurfaceHolder)以及surface改變時的回調surfaceChanged(SurfaceHolder holder, int, int, int)。這裡我們只關注建立和銷毀時的回調,定義一個變數用於標誌它的狀態。

    private boolean mIsSurfaceReady;    @Override    public void surfaceCreated(SurfaceHolder holder) {        mIsSurfaceReady = true;        startPreview();    }    @Override    public void surfaceDestroyed(SurfaceHolder holder) {        mIsSurfaceReady = false;    }

其中的startPreview()方法將在下面講到。

開啟相機

然後是開啟相機。這些代碼在我定義的openCamera方法中。

        if (mCamera == null) {            try {                mCamera = Camera.open();            } catch (RuntimeException e) {                if ("Fail to connect to camera service".equals(e.getMessage())) {                    //提示無法開啟相機,請檢查是否已經開啟許可權                } else if ("Camera initialization failed".equals(e.getMessage())) {                    //提示相機初始化失敗,無法開啟                } else {                    //提示相機發生未知錯誤,無法開啟                }                finish();                return;            }        }

開啟相機失敗的話,我們無法進行下一步操作,所以在提示之後會直接把介面關掉。

拍照參數
        final Camera.Parameters cameraParams = mCamera.getParameters();        cameraParams.setPictureFormat(ImageFormat.JPEG);        cameraParams.setFocusMode(Camera.Parameters.FOCUS_MODE_AUTO);

分別設定圖片格式,以及對焦模式。然後因為我這裡是豎屏拍照,所以還需要對Camera旋轉90度。

cameraParams.setRotation(90);

注意:涉及到旋轉的有兩個方法,一個是旋轉相機,一個是旋轉預覽。這裡設定的是對相機的旋轉。
繼續注意:由於機型相容的問題,這裡設定旋轉之後,有些手機照片來的照片就是豎的了,但是有些手機(比如萬惡的三星)拍出來的照片還是橫的,但是它們在照片的Exif資訊中有相關的角度屬性。所以對於拍出來的照片還是橫著的,我們在裁剪時再繼續處理。關於照片的旋轉處理,後續部落格中會講到。

尺寸參數

這裡還是Camera的參數設定,但是我把它單獨抽出來是因為,它不像上面設定的參數那樣簡單直接,而需要進行計算。下面是我們需要注意的問題:

首先,相機的寬高比例主要有兩種,一種是16:9,一種是4:3。其次,我們需要SurfaceView的比例與Camera預覽尺寸的比例一樣,才不會導致預覽出來的結果是變形的。由於機型解析度的問題,再加上我們的SurfaceView不是滿屏的(即使滿屏,還要考慮一些虛擬導覽列和各種奇葩解析度的機型),16:9的比例我們需要上是不會用到的了,我們會讓Camera預覽的尺寸比例與SurfaceView的大小比例一樣。要特別注意,一些手機,如果設定預覽的大小與設定的圖片大小相差太大(但寬高比例相同)的話,拍出來的照片可能範圍也不一樣。比如你拍的時候明明是一幅畫包括畫框,儲存的圖片卻只有畫框裡的內容。

下面的代碼還是寫在我們的openCamera()方法中。由於我們需要能夠擷取到SurfaceView的大小,所以openCamera()是這樣調用的:

    @Override    protected void onResume() {        super.onResume();        mSurfaceView.post(new Runnable() {            @Override            public void run() {                openCamera();            }        });    }

它可以保證在openCamera()被調用時surfaceView一定是繪製完成了的。
然後在openCamera()的後續代碼中,先擷取surfaceView的寬高比例。注意,對於surfaceView我開始在布局上寫的是高度佔滿剩下的空間。

    

這時候得到的寬高比就是我們所能接受的最小比例了。

        // 短邊比長邊        final float ratio = (float) mSurfaceView.getWidth() / mSurfaceView.getHeight();

然後擷取相機支援的圖片尺寸,找出最適合的尺寸。

        // 設定pictureSize        List pictureSizes = cameraParams.getSupportedPictureSizes();        if (mBestPictureSize == null) {            mBestPictureSize =findBestPictureSize(pictureSizes, cameraParams.getPictureSize(), ratio);        }        cameraParams.setPictureSize(mBestPictureSize.width, mBestPictureSize.height);

findBestPictureSize的代碼如下。注意,因為我們是旋轉了相機的,所以計算的時候,對surfaceView的比例是寬除以高,而對Camera.Size則是高除以寬。

    /**     * 找到短邊比長邊大于于所接受的最小比例的最大尺寸     *     * @param sizes       支援的尺寸列表     * @param defaultSize 預設大小     * @param minRatio    相機圖片短邊比長邊所接受的最小比例     * @return 返回計算之後的尺寸     */    private Camera.Size findBestPictureSize(List sizes, Camera.Size defaultSize, float minRatio) {        final int MIN_PIXELS = 320 * 480;        sortSizes(sizes);        Iterator it = sizes.iterator();        while (it.hasNext()) {            Camera.Size size = it.next();            //移除不滿足比例的尺寸            if ((float) size.height / size.width <= minRatio) {                it.remove();                continue;            }            //移除太小的尺寸            if (size.width * size.height < MIN_PIXELS) {                it.remove();            }        }        // 返回符合條件中最大尺寸的一個        if (!sizes.isEmpty()) {            return sizes.get(0);        }        // 沒得選,預設吧        return defaultSize;    }

接下來是設定預覽圖片的尺寸:

        // 設定previewSize        List previewSizes = cameraParams.getSupportedPreviewSizes();        if (mBestPreviewSize == null) {            mBestPreviewSize = findBestPreviewSize(previewSizes, cameraParams.getPreviewSize(),                    mBestPictureSize, ratio);        }        cameraParams.setPreviewSize(mBestPreviewSize.width, mBestPreviewSize.height);

根據圖片尺寸,以及SurfaceView的比例來計算preview的尺寸。

    /**     * @param sizes     * @param defaultSize     * @param pictureSize 圖片的大小     * @param minRatio preview短邊比長邊所接受的最小比例     * @return     */    private Camera.Size findBestPreviewSize(List sizes, Camera.Size defaultSize,                                            Camera.Size pictureSize, float minRatio) {        final int pictureWidth = pictureSize.width;        final int pictureHeight = pictureSize.height;        boolean isBestSize = (pictureHeight / (float)pictureWidth) > minRatio;        sortSizes(sizes);        Iterator it = sizes.iterator();        while (it.hasNext()) {            Camera.Size size = it.next();            if ((float) size.height / size.width <= minRatio) {                it.remove();                continue;            }            // 找到同樣的比例,直接返回            if (isBestSize && size.width * pictureHeight == size.height * pictureWidth) {                return size;            }        }        // 未找到同樣的比例的,返回尺寸最大的        if (!sizes.isEmpty()) {            return sizes.get(0);        }        // 沒得選,預設吧        return defaultSize;    }

上面的兩個findBestxxx方法,可以自己根據業務需要進行調整。整體思路就是先對尺寸排序,然後遍曆排除掉不滿足條件的尺寸,如果找到比例一樣的,則直接返回。如果遍曆完了仍沒找到,則返回最大的尺寸,如果發現都排除完了,只能返回預設的那一個了。
然後,我們還要再根據previewSize來重新設定我們的surfaceView的大小,以使它們的比例完全一樣,才不會導致預覽時變形。

        ViewGroup.LayoutParams params = mSurfaceView.getLayoutParams();        params.height = mSurfaceView.getWidth() * mBestPreviewSize.width / mBestPreviewSize.height;        mSurfaceView.setLayoutParams(params);

再下來就是把參數設定過去:

mCamera.setParameters(cameraParams);

然後預覽。

預覽

由於相機開啟會需要一些時間,而surfaceHolder的回調也需要一些時間。我希望的是當相機準備完成可以回調並且surface也建立完畢的時候,就可以馬上預覽(盡量減小進入介面後可能會有的黑一下的時間),所以這裡My Code如下:

        if (mIsSurfaceReady) {            startPreview();        }

同時在surface被建立的時候,也會調用一下這個startPreview()方法。
startPreview()代碼如下,在camera初始化之後,首先設定SurfaceHolder對象,然後對預覽旋轉90度,然後開始預覽。

    private void startPreview() {        if (mCamera == null) {            return;        }        try {            mCamera.setPreviewDisplay(mSurfaceView.getHolder());            mCamera.setDisplayOrientation(90);            mCamera.startPreview();        } catch (IOException e) {            e.printStackTrace();            BugReport.report(e);        }    }
自動對焦

我希望在點擊預覽圖的時候能夠進行自動對焦。由於在介面上我在surfaceview之上放了一個取景框View,所以我直接對這個View設定一個點擊事件,進行觸發自動對焦。
自動對焦的代碼如下:

    /**     * 請求自動對焦     */    private void requestFocus() {        if (mCamera == null || mWaitForTakePhoto) {            return;        }        mCamera.autoFocus(null);    }

這裡我只需要相機能夠對焦,並不是要在對焦成功之後才進行拍照,所以回調我傳了一個null。
之所以這樣使用是因為,之前我寫的是對焦成功之後才拍照,但是會有兩個問題:一是對焦會有一個過程,這樣對完焦之後才拍照會慢,二是可能在點拍照的時候預覽的介面正是我們想要的,但是一對焦,可能對焦失敗,導致沒有拍照或者是拍出來的是模糊的。

拍照

拍照也是非同步回調,並且會需要點時間,所以這裡我定義了一個mWaitForTakePhoto變數,表示正在拍照,還沒完成。在拍照的過程中,不允許重新對焦或重新拍照。

    private void takePhoto() {        if (mCamera == null || mWaitForTakePhoto) {            return;        }        mWaitForTakePhoto = true;        mCamera.takePicture(null, null, new Camera.PictureCallback() {            @Override            public void onPictureTaken(byte[] data, Camera camera) {                onTakePhoto(data);                mWaitForTakePhoto = false;            }        });    }

儲存照片。這裡返回的data可以直接寫入檔案,就是一張jpg圖了。

    private void onTakePhoto(byte[] data) {        final String tempPath = mOutput + "_";        FileOutputStream fos = null;        try {            fos = new FileOutputStream(tempPath);            fos.write(data);            fos.flush();            //啟動我的裁剪介面           } catch (Exception e) {            BugReport.report(e);        } finally {            IOUtils.close(fos);        }    }
相機的開啟與關閉以及Activity的生命週期
    @Override    protected void onResume() {        super.onResume();        mSurfaceView.post(new Runnable() {            @Override            public void run() {                openCamera();            }        });    }    @Override    protected void onPause() {        super.onPause();        closeCamera();    }

關閉相機時,首先要取消掉自動對焦,否則如果正好在自動對焦,又關掉相機,會引發異常。接著停止preview,然後再釋放:

    private void closeCamera() {        if (mCamera == null) {            return;        }        mCamera.cancelAutoFocus();        stopPreview();        mCamera.release();        mCamera = null;    }
總結

1,該類的全部代碼見:https://gist.github.com/msdx/f8ca0fabf0092f67d829 。沒有Demo項目,沒有Demo項目,沒有Demo項目。
2,文檔很重要。
3,我不保證My Code完全沒問題,至少我現在沒發現。如果有出現什麼問題,歡迎提出。
4,注意相機開啟和釋放。
5,注意不同機型的相機旋轉設定。特別是三星。
6,尺寸計算,previewSize的比例一定要和surfaceView可以顯示的比例一樣,才不會變形。
 

 

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.