1. 程式人生 > >自定義Camera系列之:SurfaceView + Camera

自定義Camera系列之:SurfaceView + Camera

一、前言

之前一直想把 Camera 系列的寫一下,拖了很久,現在慢慢填坑吧。

首先介紹 SurfaceView + Camera 的組合。雖然從 Android 5.0 後推薦使用 Camera2 了,不過某些舊工程或者需要適配低版本的場景還是用得著舊的 Camer API 的。

為什麼選擇 SurfaceView

SurfaceView 在自己獨立的執行緒中繪製,不會影響到主執行緒,內部使用雙緩衝機制,畫面更流暢。相比於 TextureView,它記憶體佔用低,繪製更及時,耗時也更低,但不支援動畫和截圖。

下面是該應用的簡要截圖:
在這裡插入圖片描述

二、相機開發步驟

我們選擇將 Camera 和 View 分開,Camera 的相關操作由 CameraProxy

類完成,而 View 持有一個 CameraProxy 物件。這樣 CameraProxy 也是可以重複利用的。

1. 開啟相機

開啟相機需要傳入一個 cameraId,舊的 API 中只有 CAMERA_FACING_BACKCAMERA_FACING_FRONT 兩個值可以選擇,返回一個 Camera 物件。

    public void openCamera() {
        mCamera = Camera.open(mCameraId); // 開啟相機
        Camera.getCameraInfo(mCameraId, mCameraInfo);
// 獲取相機資訊 initConfig(); // 初始化相機配置 setDisplayOrientation(); // 設定相機顯示方向 }

2. 初始化相機配置

在舊 Camera API 中,相機的配置都是通過 Parameters 類完成。

我們可以設定 閃光模式聚焦模式曝光強度預覽圖片格式和大小拍照圖片格式和大小 等等資訊。

    private void initConfig() {
        try {
            mParameters = mCamera.getParameters();
            // 如果攝像頭不支援這些引數都會出錯的,所以設定的時候一定要判斷是否支援
List<String> supportedFlashModes = mParameters.getSupportedFlashModes(); if (supportedFlashModes != null && supportedFlashModes.contains(Parameters.FLASH_MODE_OFF)) { mParameters.setFlashMode(Parameters.FLASH_MODE_OFF); // 設定閃光模式(關閉) } List<String> supportedFocusModes = mParameters.getSupportedFocusModes(); if (supportedFocusModes != null && supportedFocusModes.contains(Parameters.FOCUS_MODE_AUTO)) { mParameters.setFocusMode(Parameters.FOCUS_MODE_AUTO); // 設定聚焦模式(自動) } mParameters.setPreviewFormat(ImageFormat.NV21); // 設定預覽圖片格式 mParameters.setPictureFormat(ImageFormat.JPEG); // 設定拍照圖片格式 mParameters.setExposureCompensation(0); // 設定曝光強度 Size previewSize = getSuitableSize(mParameters.getSupportedPreviewSizes()); mPreviewWidth = previewSize.width; mPreviewHeight = previewSize.height; mParameters.setPreviewSize(mPreviewWidth, mPreviewHeight); // 設定預覽圖片大小 Log.d(TAG, "previewWidth: " + mPreviewWidth + ", previewHeight: " + mPreviewHeight); Size pictureSize = getSuitableSize(mParameters.getSupportedPictureSizes()); mParameters.setPictureSize(pictureSize.width, pictureSize.height); Log.d(TAG, "pictureWidth: " + pictureSize.width + ", pictureHeight: " + pictureSize.height); mCamera.setParameters(mParameters); // 將設定好的parameters新增到相機裡 } catch (Exception e) { e.printStackTrace(); } }

這裡用到的一個 getSuitableSize 方法獲取合數的 預覽/拍照 尺寸。(後面貼完整程式碼)

3. 設定相機預覽時的顯示方向

這個方法很重要,關乎著你預覽畫面是否正常。其實相機底層的預覽畫面全都是寬度大於高度的,但是豎屏時畫面要顯示正常,都是通過這個方法設定了一定的顯示方向。

    private void setDisplayOrientation() {
        int rotation = mActivity.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 (mCameraInfo.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) {
            result = (mCameraInfo.orientation + degrees) % 360;
            result = (360 - result) % 360;  // compensate the mirror
        } else {  // back-facing
            result = (mCameraInfo.orientation - degrees + 360) % 360;
        }
        mCamera.setDisplayOrientation(result);
    }

4. 開始預覽、停止預覽

可以通過 SurfaceViewgetHolder 方法獲取一個 SurfaceHolder,設定給 Camera 系統即可幫你完成一系列複雜的繫結。

    public void startPreview(SurfaceHolder holder) {
        if (mCamera != null) {
            try {
                mCamera.setPreviewDisplay(holder); // 先繫結顯示的畫面
            } catch (IOException e) {
                e.printStackTrace();
            }
            mCamera.startPreview(); // 這裡才是開始預覽
        }
    }

    public void stopPreview() {
        if (mCamera != null) {
            mCamera.stopPreview(); // 停止預覽
        }
    }

5. 釋放相機

相機是很耗費系統資源的東西,用完一定要釋放。對應於 openCamera

    public void releaseCamera() {
        if (mCamera != null) {
            mCamera.setPreviewCallback(null);
            mCamera.stopPreview();
            mCamera.release();
            mCamera = null;
        }
    }

6. 點選聚焦

簡單的說,就是根據使用者在 view 上的觸控點,使相機對該點進行一次對焦操作。

詳細看我的這篇部落格介紹的把:Android自定義相機定點聚焦

完整程式碼後面會貼的。

7. 雙指放大縮小

我們也只實現放大縮小的邏輯,至於 View 的觸控交給 View 類去完成。

    public void handleZoom(boolean isZoomIn) {
        if (mParameters.isZoomSupported()) { // 首先還是要判斷是否支援
            int maxZoom = mParameters.getMaxZoom();
            int zoom = mParameters.getZoom();
            if (isZoomIn && zoom < maxZoom) {
                zoom++;
            } else if (zoom > 0) {
                zoom--;
            }
            mParameters.setZoom(zoom); // 通過這個方法設定放大縮小
            mCamera.setParameters(mParameters);
        } else {
            Log.w(TAG, "zoom not supported");
        }
    }

8. 拍照

拍照的邏輯我交給上層去完成了,後面再詳細介紹把。這裡我們只是簡單的封裝了一下元介面,一般常用的是 Camera.PictureCallback,會返回可用的 jpeg 給我們。

    public void takePicture(Camera.PictureCallback pictureCallback) {
        mCamera.takePicture(null, null, pictureCallback);
    }

9. 其它

諸如設定預覽回撥、切換前後攝像頭的操作等直接看下面的實現把。另外對於 聚焦模式閃光燈模式 等沒有詳細去介紹了,感興趣的可以另外搜尋相關模組。畢竟相機要介紹完全的話還是一塊很大的東西。

10. CameraProxy 類

下面程式碼還用到了 OrientationEventListener,這裡之前沒介紹,是通過感測器來獲取當前手機的方向的,用於 拍照 的時候設定圖片的選擇使用,後面會介紹。

package com.afei.camerademo.camera;

import android.app.Activity;
import android.graphics.ImageFormat;
import android.graphics.Rect;
import android.graphics.SurfaceTexture;
import android.hardware.Camera;
import android.hardware.Camera.CameraInfo;
import android.hardware.Camera.Parameters;
import android.hardware.Camera.PreviewCallback;
import android.hardware.Camera.Size;
import android.util.Log;
import android.view.OrientationEventListener;
import android.view.Surface;
import android.view.SurfaceHolder;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

@SuppressWarnings("deprecation")
public class CameraProxy implements Camera.AutoFocusCallback {

    private static final String TAG = "CameraProxy";

    private Activity mActivity;
    private Camera mCamera;
    private Parameters mParameters;
    private CameraInfo mCameraInfo = new CameraInfo();
    private int mCameraId = CameraInfo.CAMERA_FACING_BACK;
    private int mPreviewWidth = 1440; // default 1440
    private int mPreviewHeight = 1080; // default 1080
    private float mPreviewScale = mPreviewHeight * 1f / mPreviewWidth;
    private PreviewCallback mPreviewCallback; // 相機預覽的資料回撥
    private OrientationEventListener mOrientationEventListener;
    private int mLatestRotation = 0;

    public byte[] mPreviewBuffer;

    public CameraProxy(Activity activity) {
        mActivity = activity;
        mOrientationEventListener = new OrientationEventListener(mActivity) {
            @Override
            public void onOrientationChanged(int orientation) {
                Log.d(TAG, "onOrientationChanged: orientation: " + orientation);
                setPictureRotate(orientation);
            }
        };
    }

    public void openCamera() {
        Log.d(TAG, "openCamera cameraId: " + mCameraId);
        mCamera = Camera.open(mCameraId);
        Camera.getCameraInfo(mCameraId, mCameraInfo);
        initConfig();
        setDisplayOrientation();
        Log.d(TAG, "openCamera enable mOrientationEventListener");
        mOrientationEventListener.enable();
    }

    public void releaseCamera() {
        if (mCamera != null) {
            Log.v(TAG, "releaseCamera");
            mCamera.setPreviewCallback(null);
            mCamera.stopPreview();
            mCamera.release();
            mCamera = null;
        }
        mOrientationEventListener.disable();
    }

    public void startPreview(SurfaceHolder holder) {
        if (mCamera != null) {
            Log.v(TAG, "startPreview");
            try {
                mCamera.setPreviewDisplay(holder);
            } catch (IOException e) {
                e.printStackTrace();
            }
            mCamera.startPreview();
        }
    }

    public void startPreview(SurfaceTexture surface) {
        if (mCamera != null) {
            Log.v(TAG, "startPreview");
            try {
                mCamera.setPreviewTexture(surface);
            } catch (IOException e) {
                e.printStackTrace();
            }
            mCamera.startPreview();
        }
    }

    public void stopPreview() {
        if (mCamera != null) {
            Log.v(TAG, "stopPreview");
            mCamera.stopPreview();
        }
    }

    public boolean isFrontCamera() {
        return mCameraInfo.facing == Camera.CameraInfo.CAMERA_FACING_FRONT;
    }

    private void initConfig() {
        Log.v(TAG, "initConfig");
        try {
            mParameters = mCamera.getParameters();
            // 如果攝像頭不支援這些引數都會出錯的,所以設定的時候一定要判斷是否支援
            List<String> supportedFlashModes = mParameters.getSupportedFlashModes();
            if (supportedFlashModes != null && supportedFlashModes.contains(Parameters.FLASH_MODE_OFF)) {
                mParameters.setFlashMode(Parameters.FLASH_MODE_OFF); // 設定閃光模式
            }
            List<String> supportedFocusModes = mParameters.getSupportedFocusModes();
            if (supportedFocusModes != null && supportedFocusModes.contains(Parameters.FOCUS_MODE_AUTO)) {
                mParameters.setFocusMode(Parameters.FOCUS_MODE_AUTO); // 設定聚焦模式
            }
            mParameters.setPreviewFormat(ImageFormat.NV21); // 設定預覽圖片格式
            mParameters.setPictureFormat(ImageFormat.JPEG); // 設定拍照圖片格式
            mParameters.setExposureCompensation(0); // 設定曝光強度
            Size previewSize = getSuitableSize(mParameters.getSupportedPreviewSizes());
            mPreviewWidth = previewSize.width;
            mPreviewHeight = previewSize.height;
            mParameters.setPreviewSize(mPreviewWidth, mPreviewHeight); // 設定預覽圖片大小
            Log.d(TAG, "previewWidth: " + mPreviewWidth + ", previewHeight: " + mPreviewHeight);
            Size pictureSize = getSuitableSize(mParameters.getSupportedPictureSizes());
            mParameters.setPictureSize(pictureSize.width, pictureSize.height);
            Log.d(TAG, "pictureWidth: " + pictureSize.width + ", pictureHeight: " + pictureSize.height);
            mCamera.setParameters(mParameters); // 將設定好的parameters新增到相機裡
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    private Size getSuitableSize(List<Size> sizes) {
        int minDelta = Integer.MAX_VALUE; // 最小的差值,初始值應該設定大點保證之後的計算中會被重置
        int index = 0; // 最小的差值對應的索引座標
        for (int i = 0; i < sizes.size(); i++) {
            Size previewSize = sizes.get(i);
            Log.v(TAG, "SupportedPreviewSize, width: " + previewSize.width + ", height: " + previewSize.height);
            // 找到一個與設定的解析度差值最小的相機支援的解析度大小
            if (previewSize.width * mPreviewScale == previewSize.height) {
                int delta = Math.abs(mPreviewWidth - previewSize.width);
                if (delta == 0) {
                    return previewSize;
                }
                if (minDelta > delta) {
                    minDelta = delta;
                    index = i;
                }
            }
        }
        return sizes.get(index); // 預設返回與設定的解析度最接近的預覽尺寸
    }

    /**
     * 設定相機顯示的方向,必須設定,否則顯示的影象方向會錯誤
     */
    private void setDisplayOrientation() {
        int rotation = mActivity.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 (mCameraInfo.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) {
            result = (mCameraInfo.orientation + degrees) % 360;
            result = (360 - result) % 360;  // compensate the mirror
        } else {  // back-facing
            result = (mCameraInfo.orientation - degrees + 360) % 360;
        }
        mCamera.setDisplayOrientation(result);
    }

    private void setPictureRotate(int orientation) {
        if (orientation == OrientationEventListener.ORIENTATION_UNKNOWN) return;