Android Camera SurfaceView 預覽拍照

Android使用 Camera API + SurfaceView 方式進行預覽拍照。

SurfaceView 的創建以及回調

創建一個SurfaceView,並實現SurfaceHolder的回調。

由於Camera在SurfaceView中是通過SurfaceHolder 使得Surfaceview能夠預覽Camera返回的數據,因此我們需要實現SurfaceHolder 的回調,實現代碼如下:

public class CameraSurfaceView extends SurfaceView implements SurfaceHolder.Callback {    private static final String TAG = CameraSurfaceView.class.getSimpleName();    private SurfaceHolder mSurfaceHolder;    public CameraSurfaceView(Context context) {        super(context);        init();    }    public CameraSurfaceView(Context context, AttributeSet attrs) {        super(context, attrs);        init();    }    public CameraSurfaceView(Context context, AttributeSet attrs, int defStyleAttr) {        super(context, attrs, defStyleAttr);        init();    }    private void init() {        mSurfaceHolder = getHolder();        mSurfaceHolder.addCallback(this);    }    @Override    public void surfaceCreated(SurfaceHolder holder) {        CameraUtils.openFrontalCamera(CameraUtils.DESIRED_PREVIEW_FPS);    }    @Override    public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {        CameraUtils.startPreviewDisplay(holder);    }    @Override    public void surfaceDestroyed(SurfaceHolder holder) {        CameraUtils.releaseCamera();    }}

Camera 操作的輔助類

CameraUtils 輔助類主要是Camera API 的一些操作,比如打開相機、開始預覽、停止預覽、切換相機、設置預覽參數等操作,具體實現如下:

public class CameraUtils {    // 相機默認寬高,相機的寬度和高度跟屏幕座標不一樣,手機屏幕的寬度和高度是反過來的。    public static final int DEFAULT_WIDTH = 1280;    public static final int DEFAULT_HEIGHT = 720;    public static final int DESIRED_PREVIEW_FPS = 30;    private static int mCameraID = Camera.CameraInfo.CAMERA_FACING_FRONT;    private static Camera mCamera;    private static int mCameraPreviewFps;    private static int mOrientation = 0;    /**     * 打開相機,默認打開前置相機     * @param expectFps     */    public static void openFrontalCamera(int expectFps) {        if (mCamera != null) {            throw new RuntimeException("camera already initialized!");        }        Camera.CameraInfo info = new Camera.CameraInfo();        int numCameras = Camera.getNumberOfCameras();        for (int i = 0; i < numCameras; i++) {            Camera.getCameraInfo(i, info);            if (info.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) {                mCamera = Camera.open(i);                mCameraID = info.facing;                break;            }        }        // 如果沒有前置攝像頭,則打開默認的後置攝像頭        if (mCamera == null) {            mCamera = Camera.open();            mCameraID = Camera.CameraInfo.CAMERA_FACING_BACK;        }        // 沒有攝像頭時,拋出異常        if (mCamera == null) {            throw new RuntimeException("Unable to open camera");        }        Camera.Parameters parameters = mCamera.getParameters();        mCameraPreviewFps = CameraUtils.chooseFixedPreviewFps(parameters, expectFps * 1000);        parameters.setRecordingHint(true);        mCamera.setParameters(parameters);        setPreviewSize(mCamera, CameraUtils.DEFAULT_WIDTH, CameraUtils.DEFAULT_HEIGHT);        setPictureSize(mCamera, CameraUtils.DEFAULT_WIDTH, CameraUtils.DEFAULT_HEIGHT);        mCamera.setDisplayOrientation(mOrientation);    }    /**     * 根據ID打開相機     * @param cameraID     * @param expectFps     */    public static void openCamera(int cameraID, int expectFps) {        if (mCamera != null) {            throw new RuntimeException("camera already initialized!");        }        mCamera = Camera.open(cameraID);        if (mCamera == null) {            throw new RuntimeException("Unable to open camera");        }        mCameraID = cameraID;        Camera.Parameters parameters = mCamera.getParameters();        mCameraPreviewFps = CameraUtils.chooseFixedPreviewFps(parameters, expectFps * 1000);        parameters.setRecordingHint(true);        mCamera.setParameters(parameters);        setPreviewSize(mCamera, CameraUtils.DEFAULT_WIDTH, CameraUtils.DEFAULT_HEIGHT);        setPictureSize(mCamera, CameraUtils.DEFAULT_WIDTH, CameraUtils.DEFAULT_HEIGHT);        mCamera.setDisplayOrientation(mOrientation);    }    /**     * 開始預覽     * @param holder     */    public static void startPreviewDisplay(SurfaceHolder holder) {        if (mCamera == null) {            throw new IllegalStateException("Camera must be set when start preview");        }        try {            mCamera.setPreviewDisplay(holder);            mCamera.startPreview();        } catch (IOException e) {            e.printStackTrace();        }    }    /**     * 切換相機     * @param cameraID     */    public static void switchCamera(int cameraID, SurfaceHolder holder) {        if (mCameraID == cameraID) {            return;        }        mCameraID = cameraID;        // 釋放原來的相機        releaseCamera();        // 打開相機        openCamera(cameraID, CameraUtils.DESIRED_PREVIEW_FPS);        // 打開預覽        startPreviewDisplay(holder);    }    /**     * 釋放相機     */    public static void releaseCamera() {        if (mCamera != null) {            mCamera.stopPreview();            mCamera.release();            mCamera = null;        }    }    /**     * 開始預覽     */    public static void startPreview() {        if (mCamera != null) {            mCamera.startPreview();        }    }    /**     * 停止預覽     */    public static void stopPreview() {        if (mCamera != null) {            mCamera.stopPreview();        }    }    /**     * 拍照     */    public static void takePicture(Camera.ShutterCallback shutterCallback,                                   Camera.PictureCallback rawCallback,                                   Camera.PictureCallback pictureCallback) {        if (mCamera != null) {            mCamera.takePicture(shutterCallback, rawCallback, pictureCallback);        }    }    /**     * 設置預覽大小     * @param camera     * @param expectWidth     * @param expectHeight     */    public static void setPreviewSize(Camera camera, int expectWidth, int expectHeight) {        Camera.Parameters parameters = camera.getParameters();        Camera.Size size = calculatePerfectSize(parameters.getSupportedPreviewSizes(),                expectWidth, expectHeight);        parameters.setPreviewSize(size.width, size.height);        camera.setParameters(parameters);    }    /**     * 獲取預覽大小     * @return     */    public static Camera.Size getPreviewSize() {        if (mCamera != null) {            return mCamera.getParameters().getPreviewSize();        }        return null;    }    /**     * 設置拍攝的照片大小     * @param camera     * @param expectWidth     * @param expectHeight     */    public static void setPictureSize(Camera camera, int expectWidth, int expectHeight) {        Camera.Parameters parameters = camera.getParameters();        Camera.Size size = calculatePerfectSize(parameters.getSupportedPictureSizes(),                expectWidth, expectHeight);        parameters.setPictureSize(size.width, size.height);        camera.setParameters(parameters);    }    /**     * 獲取照片大小     * @return     */    public static Camera.Size getPictureSize() {        if (mCamera != null) {            return mCamera.getParameters().getPictureSize();        }        return null;    }    /**     * 計算最完美的Size     * @param sizes     * @param expectWidth     * @param expectHeight     * @return     */    public static Camera.Size calculatePerfectSize(List<Camera.Size> sizes, int expectWidth,                                                   int expectHeight) {        sortList(sizes); // 根據寬度進行排序        Camera.Size result = sizes.get(0);        boolean widthOrHeight = false; // 判斷存在寬或高相等的Size        // 輾轉計算寬高最接近的值        for (Camera.Size size: sizes) {            // 如果寬高相等,則直接返回            if (size.width == expectWidth && size.height == expectHeight) {                result = size;                break;            }            // 僅僅是寬度相等,計算高度最接近的size            if (size.width == expectWidth) {                widthOrHeight = true;                if (Math.abs(result.height - expectHeight)                        > Math.abs(size.height - expectHeight)) {                    result = size;                }            }            // 高度相等,則計算寬度最接近的Size            else if (size.height == expectHeight) {                widthOrHeight = true;                if (Math.abs(result.width - expectWidth)                        > Math.abs(size.width - expectWidth)) {                    result = size;                }            }            // 如果之前的查找不存在寬或高相等的情況,則計算寬度和高度都最接近的期望值的Size            else if (!widthOrHeight) {                if (Math.abs(result.width - expectWidth)                        > Math.abs(size.width - expectWidth)                        && Math.abs(result.height - expectHeight)                        > Math.abs(size.height - expectHeight)) {                    result = size;                }            }        }        return result;    }    /**     * 排序     * @param list     */    private static void sortList(List<Camera.Size> list) {        Collections.sort(list, new Comparator<Camera.Size>() {            @Override            public int compare(Camera.Size pre, Camera.Size after) {                if (pre.width > after.width) {                    return 1;                } else if (pre.width < after.width) {                    return -1;                }                return 0;            }        });    }    /**     * 選擇合適的FPS     * @param parameters     * @param expectedThoudandFps 期望的FPS     * @return     */    public static int chooseFixedPreviewFps(Camera.Parameters parameters, int expectedThoudandFps) {        List<int[]> supportedFps = parameters.getSupportedPreviewFpsRange();        for (int[] entry : supportedFps) {            if (entry[0] == entry[1] && entry[0] == expectedThoudandFps) {                parameters.setPreviewFpsRange(entry[0], entry[1]);                return entry[0];            }        }        int[] temp = new int[2];        int guess;        parameters.getPreviewFpsRange(temp);        if (temp[0] == temp[1]) {            guess = temp[0];        } else {            guess = temp[1] / 2;        }        return guess;    }    /**     * 設置預覽角度,setDisplayOrientation本身只能改變預覽的角度     * previewFrameCallback以及拍攝出來的照片是不會發生改變的,拍攝出來的照片角度依舊不正常的     * 拍攝的照片需要自行處理     * 這裏Nexus5X的相機簡直沒法吐槽,後置攝像頭倒置了,切換攝像頭之後就出現問題了。     * @param activity     */    public static int calculateCameraPreviewOrientation(Activity activity) {        Camera.CameraInfo info = new Camera.CameraInfo();        Camera.getCameraInfo(mCameraID, info);        int rotation = activity.getWindowManager().getDefaultDisplay()                .getRotation();        int degrees = 0;        switch (rotation) {            case Surface.ROTATION_0:                degrees = 0;                break;            case Surface.ROTATION_90:                degrees = 90;                break;            case Surface.ROTATION_180:                degrees = 180;                break;            case Surface.ROTATION_270:                degrees = 270;                break;        }        int result;        if (info.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) {            result = (info.orientation + degrees) % 360;            result = (360 - result) % 360;        } else {            result = (info.orientation - degrees + 360) % 360;        }        mOrientation = result;        return result;    }    /**     * 獲取當前的Camera ID     * @return     */    public static int getCameraID() {        return mCameraID;    }    /**     * 獲取當前預覽的角度     * @return     */    public static int getPreviewOrientation() {        return mOrientation;    }    /**     * 獲取FPS(千秒值)     * @return     */    public static int getCameraPreviewThousandFps() {        return mCameraPreviewFps;    }}

權限申請

在Activity中使用CameraSurfaceview,有Android6.0動態權限申請問題,需要我們判斷相機和存儲權限是否申請了:

public class CameraSurfaceViewActivity extends AppCompatActivity implements View.OnClickListener {    private static final int REQUEST_CAMERA = 0x01;    private CameraSurfaceView mCameraSurfaceView;    private Button mBtnTake;    private Button mBtnSwitch;    private int mOrientation;    // CameraSurfaceView 容器包裝類    private FrameLayout mAspectLayout;    private boolean mCameraRequested;    @Override    protected void onCreate(Bundle savedInstanceState) {        super.onCreate(savedInstanceState);        requestWindowFeature(Window.FEATURE_NO_TITLE);        getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN,                WindowManager.LayoutParams.FLAG_FULLSCREEN);        setContentView(R.layout.activity_camera_surface);        // Android 6.0相機動態權限檢查        if (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA)                == PackageManager.PERMISSION_GRANTED) {            initView();        } else {            ActivityCompat.requestPermissions(this,                    new String[]{                            Manifest.permission.CAMERA,                            Manifest.permission.WRITE_EXTERNAL_STORAGE                    }, REQUEST_CAMERA);        }    }    /**     * 初始化View     */    private void initView() {        mAspectLayout = (FrameLayout) findViewById(R.id.layout_aspect);;        mCameraSurfaceView = new CameraSurfaceView(this);        mAspectLayout.addView(mCameraSurfaceView);        mOrientation = CameraUtils.calculateCameraPreviewOrientation(CameraSurfaceViewActivity.this);        mBtnTake = (Button) findViewById(R.id.btn_take);        mBtnTake.setOnClickListener(this);        mBtnSwitch = (Button) findViewById(R.id.btn_switch);        mBtnSwitch.setOnClickListener(this);    }    @Override    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {        super.onRequestPermissionsResult(requestCode, permissions, grantResults);        switch (requestCode) {            // 相機權限            case REQUEST_CAMERA:                if (grantResults.length > 0                        && grantResults[0] == PackageManager.PERMISSION_GRANTED) {                    mCameraRequested = true;                    initView();                }                break;        }    }    @Override    protected void onResume() {        super.onResume();        if (mCameraRequested) {            CameraUtils.startPreview();        }    }    @Override    protected void onPause() {        super.onPause();        CameraUtils.stopPreview();    }    @Override    public void onClick(View v) {        switch (v.getId()) {            case R.id.btn_take:                takePicture();                break;            case R.id.btn_switch:                switchCamera();                break;        }    }    /**     * 拍照     */    private void takePicture() {        CameraUtils.takePicture(new Camera.ShutterCallback() {            @Override            public void onShutter() {            }        }, null, new Camera.PictureCallback() {            @Override            public void onPictureTaken(byte[] data, Camera camera) {                CameraUtils.startPreview();                Bitmap bitmap = BitmapFactory.decodeByteArray(data, 0, data.length);                if (bitmap != null) {                    bitmap = ImageUtils.getRotatedBitmap(bitmap, mOrientation);                    String path = Environment.getExternalStorageDirectory() + "/DCIM/Camera/"                            + System.currentTimeMillis() + ".jpg";                    try {                        FileOutputStream fout = new FileOutputStream(path);                        BufferedOutputStream bos = new BufferedOutputStream(fout);                        bitmap.compress(Bitmap.CompressFormat.JPEG, 100, bos);                        bos.flush();                        bos.close();                        fout.close();                    } catch (IOException e) {                        e.printStackTrace();                    }                }                CameraUtils.startPreview();            }        });    }    /**     * 切換相機     */    private void switchCamera() {        if (mCameraSurfaceView != null) {            CameraUtils.switchCamera(1 - CameraUtils.getCameraID(), mCameraSurfaceView.getHolder());            // 切換相機後需要重新計算旋轉角度            mOrientation = CameraUtils.calculateCameraPreviewOrientation(CameraSurfaceViewActivity.this);        }    }}

由於用到了相機和存儲權限,我們需要在manifest中註冊相機和存儲權限,這裏要說明的是,manifest用use-permission只是聲明瞭需要使用哪些權限,而我們實際項目中在使用到這兩項權限時,需要你檢查權限是否已經被授權,如果沒授權,則需要請求授權:

<!-- 存儲權限 -->    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />    <!-- 相機權限 -->    <uses-permission android:name="android.permission.CAMERA" />    <uses-permission android:name="android.permission.WAKE_LOCK" />    <uses-permission android:name="android.permission.FLASHLIGHT" />

圖像處理的輔助類

另外,ImageUtils類的實現如下:

public class ImageUtils {    /**     * 旋轉圖片     * @param bitmap     * @param rotation     * @Return     */    public static Bitmap getRotatedBitmap(Bitmap bitmap, int rotation) {        Matrix matrix = new Matrix();        matrix.postRotate(rotation);        return Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(),                bitmap.getHeight(), matrix, false);    }    /**     * 鏡像翻轉圖片     * @param bitmap     * @Return     */    public static Bitmap getFlipBitmap(Bitmap bitmap) {        Matrix matrix = new Matrix();        matrix.setScale(-1, 1);        matrix.postTranslate(bitmap.getWidth(), 0);        return Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(),                bitmap.getHeight(), matrix, false);    }}

layout如下:

<?xml version="1.0" encoding="utf-8"?><FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"    xmlns:tools="http://schemas.android.com/tools"    android:layout_width="match_parent"    android:layout_height="match_parent"    tools:context="com.cgfay.camerasample.CameraSurfaceViewActivity">    <FrameLayout        android:id="@+id/layout_aspect"        android:layout_width="match_parent"        android:layout_height="wrap_content">    </FrameLayout>    <LinearLayout        android:layout_width="match_parent"        android:layout_height="wrap_content"        android:orientation="horizontal"        android:layout_gravity="bottom"        android:gravity="center">        <Button            android:id="@+id/btn_take"            android:layout_width="wrap_content"            android:layout_height="wrap_content"            android:text="拍照" />        <Button            android:id="@+id/btn_switch"            android:layout_width="wrap_content"            android:layout_height="wrap_content"            android:text="切換相機" />    </LinearLayout></FrameLayout>

至此,通過SurfaceView + Camera API 預覽拍照功能已經實現。

備註

Camera API 在打開相機是在哪個線程,那麼onPreviewFrame回調執行就在哪個線程。

因此,如果要通過onPreviewFrame回調使用預覽數據,則可以通過HandlerThread 異步調用Camera進行操作。
另外一個問題,onPreviewFrame方法中不要執行過於複雜的邏輯操作,這樣會阻塞Camera,無法獲取新的Frame,導致幀率下降。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章