1. 程式人生 > >Android實戰開發:自定義照相機

Android實戰開發:自定義照相機

參考資料:

感謝以上大神們的的無私分享!

之前在公司寫了一個自定義CameraView,年代久遠,回頭看程式碼時居然有點看不懂了。。。
真是好記性不如爛筆頭啊~

趁著年底不忙有時間,再次重寫下Camera,話不多說,開始擼程式碼。

1.許可權

首先需要在AndroidManifest檔案中配置許可權:

    <!-- 許可權 -->
    <!-- 攝像頭許可權 -->
    <uses-permission android:name="android.permission.CAMERA" />
    <!-- 閃光燈許可權 -->
<uses-permission android:name="android.permission.FLASHLIGHT" /> <!-- 在SDCard中建立與刪除檔案許可權 --> <uses-permission android:name="android.permission.MOUNT_UNMOUNT_FILESYSTEMS" /> <!-- 寫入SD卡許可權 --> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
/>
<!-- 功能 --> <!-- 攝像頭功能 --> <uses-feature android:name="android.hardware.camera" /> <!-- 攝像頭自動對焦功能 --> <uses-feature android:name="android.hardware.camera.autofocus" />

2.預覽影象

2.1 SurfaceView和SurfaceHolder

2.1.1 SurfaceView

因為我們需要預覽照相機中的影象,而這個影象又是動態變化的,所以必須用到這個SurfaceView。
SurfaceView繼承自View,能夠在非UI執行緒中在螢幕上繪圖,所以我們可以在預覽影象的同時進行一些別的操作。
我們把它寫在佈局檔案中,使用findViewById獲得它的例項即可。

    mSurfaceView = (SurfaceView) findViewById(R.id.surfaceView);

2.1.2 SurfaceHolder

SurfaceHolder相當於是SurfaceView的控制器,用來操縱surface。處理它的Canvas上畫的效果和動畫,控制表面,大小,畫素等。

    mSurfaceHolder = mSurfaceView.getHolder();//通過getHolder方法獲取例項

實現SurfaceView需要實現SurfaceHolder.Callback介面,可以自定義一個類繼承SurfaceView並實現這個介面,也可以讓Activity直接實現這個介面,我們這裡使用第二種。

實現SurfaceHolder.Callback介面需要實現三個方法:

    public class CameraActivity extends AppCompatActivity implements SurfaceHolder.Callback {
        ...
        mSurfaceHolder.addCallback(this);
        ...
        @Override
        public void surfaceCreated(SurfaceHolder holder) {
            //建立時觸發,surfaceView生命週期的開始,在這裡開啟相機
        }

        @Override
        public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
            //surface的大小發生改變時觸發,在這裡預覽影象
        }

        @Override
        public void surfaceDestroyed(SurfaceHolder holder) {
            //銷燬時觸發,surfaceView生命週期的結束,在這裡關閉相機
        }
    }

有了預覽影象的容器,下面就真正開始使用Camera了。

2.2 Camera

2.2.1 import注意事項

匯入包的時候注意是android.hardware.Camera,而不是android.graphics.Camera,不要搞錯了;
hardware中的Camera是控制裝置攝像頭的,graphics中的Camera是對影象進行處理的。

PS:在android5.0以上,android.hardware.Camera已過時,推薦使用的是android.hardware.Camera2類;但由於Camera2類不向下相容,而且目前安卓手機5.0以上的不多,所以我們還是使用過時的android.hardware.Camera。

2.2.2 生成預覽影象

在重寫的surfaceCreated方法中初始化camera,並開啟預覽

    @Override
    public void surfaceCreated(SurfaceHolder holder) {
        mCamera = Camera.open();//使用靜態方法open初始化camera物件,預設開啟的是後置攝像頭
        try {
            mCamera.setPreviewDisplay(mSurfaceHolder);//設定在surfaceView上顯示預覽
            mCamera.startPreview();//開始預覽
        } catch (IOException e) {
            //在異常處理裡釋放camera並置為null
            mCamera.release();
            mCamera = null;
            e.printStackTrace();
        }
    }

可以直接把預覽寫在surfaceCreated裡,也可以寫在surfaceChanged裡

    @Override
    public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
        //也可以把預覽寫在這裡
    }

在surfaceView被銷燬時,停止預覽並釋放camera物件並置為null

    @Override
    public void surfaceDestroyed(SurfaceHolder holder) {
        //停止預覽並釋放camera物件並置為null
        mCamera.stopPreview();
        mCamera.release();
        mCamera = null;
    }

這時候執行程式,點選同意呼叫攝像頭,我們會發現有影象了,
可是,為什麼是歪的?
。。。whta the hell。。。

2.2.3 設定預覽方向

之所以是歪的,是因為攝像頭預設捕獲的畫面byte[]是根據橫向來的,而我們的應用是豎向的,
解決辦法是呼叫setDisplayOrientation來設定PreviewDisplay的方向,效果就是將捕獲的畫面旋轉多少度顯示。
詳情點這裡:http://blog.sina.com.cn/s/blog_777c69930100y7nv.html

所以我們只要在預覽前呼叫下setDisplayOrientation這個方法就好了

    @Override
    public void surfaceCreated(SurfaceHolder holder) {
        mCamera = Camera.open();
        try {
            mCamera.setPreviewDisplay(mSurfaceHolder);

            //設定預覽偏移90度,一般的裝置都是90,但某些裝置會偏移180
            mCamera.setDisplayOrientation(90);

            mCamera.startPreview();
        } catch (IOException e) {
            mCamera.release();
            mCamera = null;
            e.printStackTrace();
        }
    }

2.2.4 Camera.Parameters 相機引數類

Camera.Parameters是相機引數類,在這裡可以給camera物件設定解析度,圖片方向,閃光燈模式等等一些引數,用以實現更豐富的功能。
使用方式如下:

    @Override
    public void surfaceCreated(SurfaceHolder holder) {
        mCamera = Camera.open();
        try {
            mCamera.setPreviewDisplay(mSurfaceHolder);
            mCamera.setDisplayOrientation(90);

            /**Camera.Parameters**/
            Camera.Parameters parameters = mCamera.getParameters();//得到一個已有的(預設的)引數
            parameters.setPreviewSize(1920, 1080);//設定解析度,後面有詳細說明
            parameters.setRotation(90);//設定照相生成的圖片的方向,後面有詳細說明
            parameters.setFlashMode(Camera.Parameters.FLASH_MODE_OFF);//設定閃光燈模式為關
            mCamera.setParameters(parameters);//將引數賦給camera

            mCamera.startPreview();
        } catch (IOException e) {
            mCamera.release();
            mCamera = null;
            e.printStackTrace();
        }
    }

注意:這裡設定解析度是不能隨便設定的,因為每個裝置所支援的解析度不一樣,如果設定了裝置不支援的解析度程式就會崩潰,所以上面我把解析度寫死是非常不可取的
可以通過getSupportedPreviewSizes()獲得裝置支援的解析度list。

    List<Camera.Size> sizeList = parameters.getSupportedPreviewSizes();//獲得裝置所支援的解析度列表
        for (int i = 0; i < sizeList.size(); i++) {
            //這裡的TAG是一個常量字串,用來標識log
            Log.i(TAG, "width:" + sizeList.get(i).width + ",height:" + sizeList.get(i).height);
        }

執行這行程式碼,我們可以看到如下log

01-09 09:23:09.540 11014-11014/com.waka.workspace.wakacamera I/CameraActivity: width:176,height:144
01-09 09:23:09.540 11014-11014/com.waka.workspace.wakacamera I/CameraActivity: width:320,height:240
01-09 09:23:09.540 11014-11014/com.waka.workspace.wakacamera I/CameraActivity: width:352,height:288
01-09 09:23:09.540 11014-11014/com.waka.workspace.wakacamera I/CameraActivity: width:480,height:320
01-09 09:23:09.540 11014-11014/com.waka.workspace.wakacamera I/CameraActivity: width:480,height:368
01-09 09:23:09.541 11014-11014/com.waka.workspace.wakacamera I/CameraActivity: width:640,height:480
01-09 09:23:09.541 11014-11014/com.waka.workspace.wakacamera I/CameraActivity: width:720,height:480
01-09 09:23:09.541 11014-11014/com.waka.workspace.wakacamera I/CameraActivity: width:800,height:480
01-09 09:23:09.541 11014-11014/com.waka.workspace.wakacamera I/CameraActivity: width:800,height:600
01-09 09:23:09.541 11014-11014/com.waka.workspace.wakacamera I/CameraActivity: width:864,height:480
01-09 09:23:09.541 11014-11014/com.waka.workspace.wakacamera I/CameraActivity: width:960,height:540
01-09 09:23:09.541 11014-11014/com.waka.workspace.wakacamera I/CameraActivity: width:1280,height:720
01-09 09:23:09.541 11014-11014/com.waka.workspace.wakacamera I/CameraActivity: width:1088,height:1088
01-09 09:23:09.541 11014-11014/com.waka.workspace.wakacamera I/CameraActivity: width:1440,height:1080
01-09 09:23:09.541 11014-11014/com.waka.workspace.wakacamera I/CameraActivity: width:1920,height:1080
01-09 09:23:09.541 11014-11014/com.waka.workspace.wakacamera I/CameraActivity: width:1920,height:1088
01-09 09:23:09.541 11014-11014/com.waka.workspace.wakacamera I/CameraActivity: width:1680,height:1248

在這裡面我們可以獲得所有的支援的解析度;
但是注意,這裡面的width都比height大,這是因為系統預設的影象捕捉是橫屏的,而非我們所熟悉的豎屏。

2.2.5 獲得最佳解析度

我們發現,sizeList的解析度大小雖然有一定的規律,但是並不是最後一個就是螢幕的解析度;
比如我測試的手機螢幕解析度為1920x1080,但是sizeList的最後一個是1680x1248,
所以我們還要對這些資料進行一下篩選,找到最適合螢幕的解析度。

程式碼如下:

    /**
     * 獲得最佳解析度
     * 注意:因為相機預設是橫屏的,所以傳參的時候要注意,width和height都是橫屏下的
     *
     * @param parameters 相機引數物件
     * @param width      期望寬度
     * @param height     期望高度
     * @return
     */
    private int[] getBestResolution(Camera.Parameters parameters, int width, int height) {
        int[] bestResolution = new int[2];//int陣列,用來儲存最佳寬度和最佳高度
        int bestResolutionWidth = -1;//最佳寬度
        int bestResolutionHeight = -1;//最佳高度

        List<Camera.Size> sizeList = parameters.getSupportedPreviewSizes();//獲得裝置所支援的解析度列表
        int difference = 99999;//最小差值,初始化市需要設定成一個很大的數

        //遍歷sizeList,找出與期望解析度差值最小的解析度
        for (int i = 0; i < sizeList.size(); i++) {
            int differenceWidth = Math.abs(width - sizeList.get(i).width);//求出寬的差值
            int differenceHeight = Math.abs(height - sizeList.get(i).height);//求出高的差值

            //如果它們兩的和,小於最小差值
            if ((differenceWidth + differenceHeight) < difference) {
                difference = (differenceWidth + differenceHeight);//更新最小差值
                bestResolutionWidth = sizeList.get(i).width;//賦值給最佳寬度
                bestResolutionHeight = sizeList.get(i).height;//賦值給最佳高度
            }
        }

        //最後將最佳寬度和最佳高度新增到陣列中
        bestResolution[0] = bestResolutionWidth;
        bestResolution[1] = bestResolutionHeight;
        return bestResolution;//返回最佳解析度陣列
    }

注意:這裡期望寬度和期望高度是為了擴充套件功能,
一般來說照相機都全屏,直接傳入手機解析度就可以了
但是如果不全屏呢?

所以就可以在這裡傳入希望的高度和寬度,保證預覽影象不變形。

修改surfaceCreate方法中的程式碼:

    @Override
    public void surfaceCreated(SurfaceHolder holder) {
        mCamera = Camera.open();
        try {
            mCamera.setPreviewDisplay(mSurfaceHolder);
            mCamera.setDisplayOrientation(90);
            Camera.Parameters parameters = mCamera.getParameters();

            /**獲得螢幕解析度**/
            Display display = this.getWindowManager().getDefaultDisplay();
            Point size = new Point();
            display.getSize(size);
            int screenWidth = size.x;
            int screenHeight = size.y;

            /**獲得最佳解析度,注意此時要傳的width和height是指橫屏時的,所以要顛倒一下**/
            int[] bestResolution = Utils.getBestResolution(parameters, screenHeight, screenWidth);//Utils是一個工具類,我習慣把操作的方法放在一個工具類中,作為靜態方法使用
            parameters.setPreviewSize(bestResolution[0], bestResolution[1]);

            parameters.setRotation(90);
            parameters.setFlashMode(Camera.Parameters.FLASH_MODE_OFF);
            mCamera.setParameters(parameters);
            mCamera.startPreview();
        } catch (IOException e) {
            mCamera.release();
            mCamera = null;
            e.printStackTrace();
        }
    }

好了,現在應該就可以看到不變形的影象了~
如果還變形,請隱藏通知欄,ActionBar和底部的虛擬導航鍵欄,等等等等等。。。

所以這個時候期望寬度和期望高度就有用了,我們可以計算出除去這些系統欄的高度,然後傳入我們算好的值,就可以自動匹配出最佳解析度~

2.2.6 設定自動對焦

現在預覽出來的影象終於是正常大小了,可還是模模糊糊的,這咋拍?這坑定不能拍啊
所以我們需要實現相機的自動對焦功能,在這裡Android已經給我們封裝的很好了,我們只需要簡單的呼叫一下方法就行。
好多手機裡自帶的那種停下來就自動進行對焦的方式我還不太會,就先寫了個觸控自動對焦

自動對焦需要配置相關許可權,和實現Camera.AutoFocusCallback介面

程式碼如下:

    //重寫onTouchEvent方法
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        mCamera.autoFocus(this);//讓Activity實現介面
        return true;
    }

    //實現介面中的方法,自動對焦完成時的回撥
    @Override
    public void onAutoFocus(boolean success, Camera camera) {
        //在這裡可以判斷對焦是否成功,進行一些操作
    }

自動對焦已經完成,呼叫Google的東西真是很簡單,我們可以在這裡加個提示框啊什麼的,後面有時間再說。

2.2.7 照相

終於要照相了,T^T
照相呼叫camera.takePicture方法,
程式碼如下:

“`
private Camera.PictureCallback pictureCallback = new Camera.PictureCallback() {//照相動作回撥用的pictureCallback

    //在這裡可以獲得拍照後的圖片資料
    @Override
    public void onPictureTaken(byte[] data, Camera camera) {

        //byte[]陣列data就是圖片資料,可以在這裡對圖片進行處理
        mCamera.startPreview();//恢復預覽
    }
};

//點選事件
@Override
public void onClick(View v) {
    switch (v.getId()) {
        case R.id.imgbtnTakePhoto:
            mCamera.takePicture(null, null, pictureCallback);//拍照會停止預覽
            break;
        default:
            break;
    }
}

就先寫到這裡吧,有精力再補充。。