自定義Camera系列之:SurfaceView + Camera
一、前言
之前一直想把 Camera 系列的寫一下,拖了很久,現在慢慢填坑吧。
首先介紹 SurfaceView + Camera
的組合。雖然從 Android 5.0 後推薦使用 Camera2
了,不過某些舊工程或者需要適配低版本的場景還是用得著舊的 Camer API 的。
為什麼選擇 SurfaceView
?
SurfaceView
在自己獨立的執行緒中繪製,不會影響到主執行緒,內部使用雙緩衝機制,畫面更流暢。相比於 TextureView
,它記憶體佔用低,繪製更及時,耗時也更低,但不支援動畫和截圖。
下面是該應用的簡要截圖:
二、相機開發步驟
我們選擇將 Camera 和 View 分開,Camera 的相關操作由 CameraProxy
1. 開啟相機
開啟相機需要傳入一個 cameraId
,舊的 API 中只有 CAMERA_FACING_BACK
和 CAMERA_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. 開始預覽、停止預覽
可以通過 SurfaceView
的 getHolder
方法獲取一個 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;