Android Camera 程式設計從入門到精通

仰簡發表於2018-12-24

一、前言

想通過一篇文章就讓我們精通 Android 的 Camera 那肯定是不可能的事情。但通過對 Android 中相機拍照的所有的方式的梳理和理解,包括直接調起相機拍照,Camera API 1 以及 Camera API 2 的分析與理解,為我們指明一條通往精通 Android Camera 的路還是有可能的。文章將先對 Android Camera 有一個全域性的認知,然後再分析拍照的各個關鍵路徑及相關知識點。在實際開發過程中碰到問題再深入去了解 API 及其相關引數,應該就能解決我們在 Android Camera 程式設計中的大部分問題了。

二、相機基本使用以及 Camra API 1

這裡主要涉及到的是如何直接調起系統相機拍照以及基於 Camra API 1 實現拍照。如下的思維導圖是一個基本的導讀。

Android  相機.jpg

1.許可權及需求說明

要使用相機必須宣告 CAMERA 許可權以及告訴系統你要使用這個功能。

<
uses-permission android:name="android.permission.CAMERA" />
<
uses-feature android:name="android.hardware.camera" />
複製程式碼

上面這是最基本的,但如果你需要寫檔案,錄音,定位等還需要下面的許可權

<
uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<
uses-permission android:name="android.permission.RECORD_AUDIO" />
<
uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
...<
!-- Needed only if your app targets Android 5.0 (API level 21) or higher. -->
<
uses-feature android:name="android.hardware.location.gps" />
複製程式碼

2.調起系統或者三方相機直接拍照

  • 拍照
takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT, photoURI);
複製程式碼
  • 獲取拍照後的照片
Uri contentUri = FileProvider.getUriForFile(this, "com.example.android.fileprovider", photoFile);
複製程式碼

3.通過 Camera API 1 進行拍照

  • 相機裝置檢測
(context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_CAMERA))複製程式碼
  • 開啟相機
Camera.open();
// Camera.open(0)// Camera.open(1)複製程式碼
  • 建立預覽介面
/** A basic Camera preview class */public class CameraPreview extends SurfaceView implements SurfaceHolder.Callback { 
...... public CameraPreview(Context context, Camera camera) {
...... mHolder.addCallback(this);

} public void surfaceCreated(SurfaceHolder holder) {
...... mCamera.setPreviewDisplay(holder);
mCamera.startPreview();
......
} public void surfaceDestroyed(SurfaceHolder holder) {
...... mCamera.stopPreview();

} public void surfaceChanged(SurfaceHolder holder, int format, int w, int h) {
...... mCamera.setPreviewDisplay(mHolder);
mCamera.startPreview();
......
}
}...
}複製程式碼
  • 設定相機引數
public void setCamera(Camera camera) { 
...... if (mCamera != null) {
List<
Size>
localSizes = mCamera.getParameters().getSupportedPreviewSizes();
mSupportedPreviewSizes = localSizes;
requestLayout();
...... // 相機引數設定完成後,需要重新啟動預覽介面 mCamera.setPreviewDisplay(mHolder);
mCamera.startPreview();
......
}
}複製程式碼
  • 停止預覽及釋放相機

這個建議放在 onDestroy() 中呼叫

private void stopPreviewAndFreeCamera() { 
...... mCamera.stopPreview();
...... mCamera.release();
mCamera = null;

}
}複製程式碼

以上就是如何通過調起系統或者三方相機以及通過呼叫 Camera API 1 來進行拍照的講解,相對來說還是比較簡單的。一般來說掌握 Camera API 1 的用法基本能滿足常規開發了,但當我們需要獲取更多相機裝置的特性時,顯然需要通過 Camera API 2 所提供的更加豐富的功能來達到目的了。對於基本的拍照以及 API 1 的講解這裡只是簡單過一下,重點在 API 2 的介紹。

三、全新 Camera API 2

Camera API 2 是從 Android 5.0 L 版本開始引入的。官網對相機介紹的引導文件裡是沒有涉及到 API 2 的講解的,都是基於 API 1 的。能找到的是其推薦的一篇部落格Detecting camera features with Camera2 以及官方的 API 文件。通過文件大概瞭解到其比較重要的優點如下:

  • 改進了新硬體的效能。
  • 以更快的間隔拍攝影像。
  • 顯示來自多個攝像頭的預覽。
  • 直接應用效果和過濾器。

看起來很爽,但是用起來那就是酸爽了,如下是梳理的一個思維導圖。看看就知道有多麻煩了。

Camera API 2 拍照.jpg

四、官方 demo 分析

正是由於 Camera 的 API 從 1 到 2 發生了架構上的變化,而且使用難度也是大大地增加了好幾倍,加上 Android 的碎片化又是如斯的嚴重。因此官方考慮到大家掌握不好,推出了其官方的 demo 供我們參考和學習——cameraview。這裡也將基於官方的 demo 來深入掌握 Android 相機 API 2 的使用。

1. 主要類圖

先來看看工程中主要的類圖及其關係,好對整個工程以及 Camera2 中的相關類有一個基本的認知。

工程主類圖

(1) 類圖結構上封裝了 CameraView 用於給 Activity 直接呼叫。

(2) 抽象了相機類 CameraViewImpl 和預覽類 PreviewImpl。根據不同的版本由其具體實現類來解決版本之間的差異以及相容。

(3) 用於預覽的既可以是 SurfaceView 也可以是 TextureView,框架內根據不同版本做了相應的適配。

(4) Camera1 即使用的舊版 Camera 及其相關的 API。而 Camera2 使用了新的 Camera2 API,這裡簡要介紹一下這幾個類的作用。

序號 說明
1 CameraManager 這是一個系統服務,主要用於管理相機裝置的,如相機的開啟。與 AlarmManager 同等級。
2 CameraDevice 這個就是相機裝置了,與 Camra1 中的 Camera 同等級。
3 ImageReader 用於從相機開啟的通道中讀取需要的格式的原始影像資料,理論上一個裝置可以連線 N 多個 ImageReader。在這裡可以看成是和 Preview 同等級。
4 CaptureRequest.Builder CaptureRequest 構造器,主要給相機設定引數的類。Builder 設計模式真好用。
5 CameraCharacteristics 與 CaptureRequest 反過來,主要是獲取相機引數的。
6 CameraCaptureSession 請求抓取相機影像幀的會話,會話的建立主要會建立起一個通道。源端是相機,另一端是 Target,Target 可以是 Preview,也可以是 ImageReader。

2.CameraView 初始化

先看一看 CameraView 初始化的時序圖,大概一共做了 13 事情。當然,初始化做的事情其實都是簡單的,主要就是初始化必要的物件且設定一些監聽。

CameraView 初始化.jpg
  • CameraView 的構建方法
public CameraView(Context context, AttributeSet attrs, int defStyleAttr) { 
...... // 建立預覽檢視 final PreviewImpl preview = createPreviewImpl(context);
// Callback 橋接器,將相機內部的回撥轉發給呼叫層 mCallbacks = new CallbackBridge();
// 根據不同的 SDK 版本選擇不同的 Camera 實現,這裡假設選擇了 Camera2 if (Build.VERSION.SDK_INT <
21) {
mImpl = new Camera1(mCallbacks, preview);

} else if (Build.VERSION.SDK_INT <
23) {
mImpl = new Camera2(mCallbacks, preview, context);

} else {
mImpl = new Camera2Api23(mCallbacks, preview, context);

} ...... // 設定相機 ID,如前置或者後置 setFacing(a.getInt(R.styleable.CameraView_facing, FACING_BACK));
...... // 設定預覽介面的比例,如 4:3 或者 16:9 setAspectRatio(AspectRatio.parse(aspectRatio));
// 設定對焦方式 setAutoFocus(a.getBoolean(R.styleable.CameraView_autoFocus, true));
// 設定閃光燈 setFlash(a.getInt(R.styleable.CameraView_flash, Constants.FLASH_AUTO));
...... // 初始化顯示裝置(主要指手機螢幕)的旋轉監聽,主要用來設定相機的旋轉方向 mDisplayOrientationDetector = new DisplayOrientationDetector(context) {
@Override public void onDisplayOrientationChanged(int displayOrientation) {
mImpl.setDisplayOrientation(displayOrientation);

}
};

}複製程式碼

構造方法中所做的事情都在註釋裡進行說明,沒有需要展開的。下面來看 createPreviewImpl()。

  • createPreviewImpl() 的實現
private PreviewImpl createPreviewImpl(Context context) { 
PreviewImpl preview;
if (Build.VERSION.SDK_INT <
14) {
preview = new SurfaceViewPreview(context, this);

} else {
preview = new TextureViewPreview(context, this);

} return preview;

}複製程式碼

這裡的 SurfaceViewPreview 以及 TextureViewPreview 都是一個包裝類,從名字上就可以知道其內部分別包裝了 SurfaceView 和 TextureView 例項來實現相機的預覽介面的。關於 SurfaceView 以及 TextureView 的區別,這裡也再簡單提一下,詳細的可以參考其他大神的文章說明:

SurfaceView:是一個獨立的 Window,由系統 WMS 直接管理,可支援硬體加速,也可以不支援硬體加速。

TextureView:可以看成是一個普通的 View,屬於所於應用的檢視層級樹中,屬於 ViewRootImpl 管理,只支援硬體加速。

儘管 SurfaceView 和 TextureView 有區別,但本質上它們都是對 Surface 的一個封裝實現。

這裡假設選擇的是 TextureViewPreview。TextureViewPreview 的構造方法很簡單,就是從 xml 裡獲取 TextureView 的例項,並且同時設定 TextureView 的監聽 TextureView.SurfaceTextureListener,這個後面會再詳細講。

接下來是根據不同的版本選擇 Camera,這裡假設選擇的是 Camera2,主線上我們也只分析它就可以了。那麼就來看一看 Camera2 的實現吧。

  • 初始化 Camera2
Camera2(Callback callback, PreviewImpl preview, Context context) { 
super(callback, preview);
mCameraManager = (CameraManager) context.getSystemService(Context.CAMERA_SERVICE);
mPreview.setCallback(new PreviewImpl.Callback() {
@Override public void onSurfaceChanged() {
startCaptureSession();

}
});

}複製程式碼

首先是初始化 CameraManager 的例項,這是相比 Camera1 多出來的步驟,這麼說 Camera 有一個專業的管理者了。其次可以看到這裡是向 Context 獲取一個系統 Service “CAMERA_SERVICE” 來初始化 CameraManager 的,這也說明了其被上升到了一個系統服務的高度了。

然後就是向 Preview 新增回撥,監聽其 Surface 的變化來作進一步的事情。

  • 關於 Camera 與 Preview 的選擇這裡 github 首頁給出了 Android 的推薦選擇。
API Level Camera API Preview View
9-13 Camera1 SurfaceView
14-20 Camera1 TextureView
21-23 Camera2 TextureView
24 Camera2 SurfaceView

API 20 以下用 Camera 1,20 以上用 Camera 2,這個沒有爭議。但是對於 Preview 的選擇也根據 API 來選擇, 這個就不應該了。看過其他相應的實現,除了 SDK API 的檢查,應用 TextureView 前還應該要判斷一下當前的執行環境是否支援硬體加速。

而讓我有疑問的是這裡的 24 以上推薦使用 SurfaceView,這個是為什麼呢?而其裡面的程式碼實際實現,看上面createPreviewImpl() 的實現可知又不是這樣的,也是選擇了 TextureView。

  • setFacing、setAspectRatio、setAutoFocus、setFlash這些都是設定引數,其實際生效的方法在 Camera2 中,而這個時候相機都還沒有開啟,對於它們的設定目前來說是不會立即生效的,只是記錄下它們的值而已。後面我們分析時已預設值來分析即可。

當然,這裡只是給出了 4 個引數,其實還有很多,後面還會講到。

小結


到這裡就分析完了 CameraView 的初始化了,其主要做了以下幾件事情:

(1) 通過 getSystemService 初始化了 CameraManager。

(2) 準備好了 Preview ,用於相機的預覽。

(3) 設定好了相機要用的引數。

3.開啟相機

同樣,先來看一看開啟相機的時序圖。概括了有 15 個步驟,但其實關鍵步驟沒有這麼多。

CameraView 開啟相機.jpg
  • CameraView.start()
/**     * Open a camera device and start showing camera preview. This is typically called from     * {@link Activity#onResume()
}.
*/ public void start() {
if (!mImpl.start()) {
//store the state ,and restore this state after fall back o Camera1 Parcelable state=onSaveInstanceState();
// Camera2 uses legacy hardware layer;
fall back to Camera1 mImpl = new Camera1(mCallbacks, createPreviewImpl(getContext()));
onRestoreInstanceState(state);
mImpl.start();

}
}複製程式碼

這裡給了幾個關鍵的資訊:

(1) 此方法推薦的是在 Activity#onResume() 方法裡面進行呼叫,這個是很重要的,告訴了我們開啟相機的最適合時機。

(2) 按照前面的場景,這裡呼叫了 Camera2#start()。這是要進一步分析的。

(3) 如果開啟 Camera2 失敗了,則降級到 Camera1。做了回退保護,考慮的確實比較周到。

  • Camera2.start()
boolean start() { 
if (!chooseCameraIdByFacing()) {
return false;

} collectCameraInfo();
prepareImageReader();
startOpeningCamera();
return true;

}複製程式碼

都是內部呼叫,下面逐個分析這些方法的實現。

  • chooseCameraIdByFacing()
private boolean chooseCameraIdByFacing() { 
try {
// 1.根據 mFacing 選擇相機 int internalFacing = INTERNAL_FACINGS.get(mFacing);
// 2.獲取所有的可用相機 ID 列表,注意相機的 ID 是 字串型別了 final String[] ids = mCameraManager.getCameraIdList();
...... for (String id : ids) {
// 根據相機的 ID 獲取相機特徵 CameraCharacteristics characteristics = mCameraManager.getCameraCharacteristics(id);
// 查詢其支援的硬體級別 Integer level = characteristics.get( CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL);
if (level == null || level == CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY) {
continue;

} // 查詢相機的方向(前置,後置或者外接),也可以同等看成是其整型的 ID Integer internal = characteristics.get(CameraCharacteristics.LENS_FACING);
if (internal == null) {
throw new NullPointerException("Unexpected state: LENS_FACING null");

} // 查出來的與所期望的相等,則認為就是要找到的相機裝置 if (internal == internalFacing) {
// 儲存相機的 ID mCameraId = id;
// 儲存相機的特徵引數 mCameraCharacteristics = characteristics;
return true;

}
} // 如果沒找到就取第 0 個。後面的過程就跟上面是一樣的。這裡就省略了。一般來說第 0 個就是 ID 為 "1" 的相機,其方向為後置。 mCameraId = ids[0];
...... return true;

} catch (CameraAccessException e) {
throw new RuntimeException("Failed to get a list of camera devices", e);

}
}複製程式碼

這段程式碼確實有點長,並且資訊量也多。其主要的目的是根據 mFacing 指定的相機方向選擇一個正確的相機,但如果沒有的話就預設選擇後置相機。這個過程涉及到了幾個比較重要的相機引數及其 API 呼叫。

(1) 關於選擇相機方向

相機方向主要是相對於手機螢幕而言的,系統可取的值有 LENS_FACING_FRONT(前置),LENS_FACING_BACK(後置),LENS_FACING_EXTERNAL(外接)。但工程裡只給我們定義了前置與後置。

static { 
INTERNAL_FACINGS.put(Constants.FACING_BACK, CameraCharacteristics.LENS_FACING_BACK);
INTERNAL_FACINGS.put(Constants.FACING_FRONT, CameraCharacteristics.LENS_FACING_FRONT);

}複製程式碼

(2)關於CameraCharacteristics

這裡是查詢出了所有的相機 ID ,然後來逐個遍歷看是否與所期望的相機方向相符合的相機裝置。這裡要注意的是相機的 ID 是實際是字串,這個需要記住並且很重要,後面的相機操作,如開啟裝置、查詢或者設定引數等都是需要這個 ID 的。通過 CameraManager. getCameraCharacteristics(ID) 查詢出了相關裝置的特徵資訊,特徵資訊都被封裝在了 CameraCharacteristics 中。它以 Key-Value 的形式儲存了所有的相機裝置的引數資訊。注意這個 Key ,它又是一個泛型,這說明了 Key 也是可以以不同的形式存在的。這樣的擴充套件性就強了。特別是對於現在一些特殊攝像頭的發展,如3D 攝像頭,那麼廠商就可自行新增引數支援而不用新增私有 API 了。這也是主要需要理解的部分。

(3)關於支援的硬體級別

瞭解了第(2)點,其他的就都只是引數查詢的問題了。這裡摘抄官網了。

  • LEGACY 對於較舊的Android裝置,裝置以向後相容模式執行,並且功能非常有限。
  • LIMITED裝置代表基線功能集,還可能包括作為子集的附加功能FULL。
  • FULL 裝置還支援感測器,閃光燈,鏡頭和後處理設定的每幀手動控制,以及高速率的影像捕獲。
  • LEVEL_3 裝置還支援YUV重新處理和RAW影像捕獲,以及其他輸出流配置。
  • EXTERNAL裝置類似於LIMITED裝置,例如某些感測器或鏡頭資訊未重新排列或不太穩定的幀速率。

CameraCharacteristics 中還有非常多的引數,這裡僅列出其所提及到的,其他的引數如果你真的實際會在開發中用到,建議還是過一遍。這樣一來,相機能做什麼,具備什麼特性就會有一個整體感知了。

  • collectCameraInfo()
private void collectCameraInfo() { 
// 獲取此攝像機裝置支援的可用流配置,其包括格式、大小、持續時間和停頓持續時間等 StreamConfigurationMap map = mCameraCharacteristics.get( CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP);
if (map == null) {
throw new IllegalStateException("Failed to get configuration map: " + mCameraId);

} mPreviewSizes.clear();
// 根據需要渲染到的目標型別選擇合適的輸出大小 for (android.util.Size size : map.getOutputSizes(mPreview.getOutputClass())) {
int width = size.getWidth();
int height = size.getHeight();
if (width <
= MAX_PREVIEW_WIDTH &
&
height <
= MAX_PREVIEW_HEIGHT) {
mPreviewSizes.add(new Size(width, height));

}
} // 根據圖片格式選擇圖片大小 mPictureSizes.clear();
collectPictureSizes(mPictureSizes, map);
// 把預覽中所支援的大小比例,但在圖片大小比例不支援的 比例 移除掉 for (AspectRatio ratio : mPreviewSizes.ratios()) {
if (!mPictureSizes.ratios().contains(ratio)) {
mPreviewSizes.remove(ratio);

}
} // 如果設定的比例不完全相符合,那選擇一個接近的。 if (!mPreviewSizes.ratios().contains(mAspectRatio)) {
mAspectRatio = mPreviewSizes.ratios().iterator().next();

}
}複製程式碼

這段程式碼相對來說要簡單一些,主要完成的是獲取預覽尺寸,圖片尺寸以及合適的顯示比例。

  • prepareImageReader()
private void prepareImageReader() { 
if (mImageReader != null) {
mImageReader.close();

} Size largest = mPictureSizes.sizes(mAspectRatio).last();
mImageReader = ImageReader.newInstance(largest.getWidth(), largest.getHeight(), ImageFormat.JPEG, /* maxImages */ 2);
mImageReader.setOnImageAvailableListener(mOnImageAvailableListener, null);

}複製程式碼

根據合適的圖片尺寸初始化 ImageReader,主要是用於接收圖片的原始資料資訊,且這裡的原始資料資訊為 ImageFormat.JPEG。當然你也可以指定為 YUV 等更原始的資料資訊。這樣一來除了除了讓影像顯示在預覽介面上,我們還可以同時獲取原始資料資訊做進一步處理,如增加濾鏡效果後再儲存等。

而要獲取到原始資料資訊,就需要向 ImageReader 註冊相應的監聽器 ImageReader.OnImageAvailableListener,當有相機的影像幀後會通過onImageAvailable 進行回撥。這裡展開看一下它的實現。

public void onImageAvailable(ImageReader reader) { 
// 獲取 Image try (Image image = reader.acquireNextImage()) {
// 獲取 Image 的平面 Image.Plane[] planes = image.getPlanes();
if (planes.length >
0) {
// 獲取平面 0 的 ByteBuffer,並從 ByteBuffer 中獲取 byte[] ByteBuffer buffer = planes[0].getBuffer();
byte[] data = new byte[buffer.remaining()];
buffer.get(data);
mCallback.onPictureTaken(data);

}
}
}複製程式碼

這裡涉及到了影像格式的知識, 這裡就不細述了,感興趣的同學可以自己去查一下資料。

  • startOpeningCamera()
private void startOpeningCamera() { 
try {
mCameraManager.openCamera(mCameraId, mCameraDeviceCallback, null);

} catch (CameraAccessException e) {
throw new RuntimeException("Failed to open camera: " + mCameraId, e);

}
}複製程式碼

最後一步就是開啟相機了,開啟相機需要傳遞前面所確定的 CameraID,注意它是個字串。還傳入了一個 mCameraDeviceCallback,它的型別是 CameraDevice.StateCallback。看一看它的實現。

private final CameraDevice.StateCallback mCameraDeviceCallback            = new CameraDevice.StateCallback() { 
// 相機開啟 @Override public void onOpened(@NonNull CameraDevice camera) {
mCamera = camera;
mCallback.onCameraOpened();
startCaptureSession();

} // 相機關閉 @Override public void onClosed(@NonNull CameraDevice camera) {
mCallback.onCameraClosed();

} // 相機斷開連線 @Override public void onDisconnected(@NonNull CameraDevice camera) {
mCamera = null;

} // 開啟相機出錯 @Override public void onError(@NonNull CameraDevice camera, int error) {
Log.e(TAG, "onError: " + camera.getId() + " (" + error + ")");
mCamera = null;

}
};
複製程式碼

這裡就是開啟相機狀態的回撥監聽,主要關注的是 onOpened()。在這個回撥方法中返回了 CameraDevice ,也就是實際的相機裝置。關於 CameraDevice 再來看一個類圖。

CameraDevice.jpg

看出來了吧,CameraDevice 的實現類 CameraDeviceImpl 是持有了一個 Binder 端的代理。這裡不看原始碼,只憑推測可知,實際的相機裝置物件應該被放到了系統程式 SystemServer 或者別的程式中去了。這和 Camera 1 就有本質上的區別了。

然後就是通知呼叫者,再然後就是一個 startCaptureSession() 呼叫。這個呼叫非常重要,它建立起了相機與 Target(這裡是 Preview 以及 ImageReader) 的通道連線。

  • startCaptureSession()
void startCaptureSession() { 
if (!isCameraOpened() || !mPreview.isReady() || mImageReader == null) {
return;

} // 根據 Preivew 的大小從 mPreviewSize 中選擇一個最佳的。 Size previewSize = chooseOptimalSize();
// 設定 Preview Buffer 的大小 mPreview.setBufferSize(previewSize.getWidth(), previewSize.getHeight());
// 獲取 Preview 的 Surface,將被用來作用相機實際預覽的 Surface Surface surface = mPreview.getSurface();
try {
// 構建一個預覽請求 mPreviewRequestBuilder = mCamera.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);
// 新增 Target ,通道的輸出端之一,這裡只新增了 preview mPreviewRequestBuilder.addTarget(surface);
// 建立 capture 會話,打通通道。設定輸出列表,並且還設定了回撥 SessionCallback mCamera.createCaptureSession(Arrays.asList(surface, mImageReader.getSurface()), mSessionCallback, null);

} catch (CameraAccessException e) {
throw new RuntimeException("Failed to start camera session");

}
}複製程式碼

該方法總的來說就是設定 Surface 的 Buffer 大小,建立請求引數,建立會話,打通通道。而關於建立請求引數,這裡用了 CameraDevice.TEMPLATE_PREVIEW。其主要支援的引數有TEMPLATE_PREVIEW(預覽)、TEMPLATE_RECORD(拍攝視訊)、TEMPLATE_STILL_CAPTURE(拍照)等引數。接下來是呼叫了 createCaptureSession()。

在 createCaptureSession 時設定了輸出端列表,還設定了回撥 mSessionCallback,它是CameraCaptureSession.StateCallback型別。

細心的讀者可能會發現,在這裡,mPreivewRequestBuilder 並沒有用上,在 createCaptureSeesion 的引數中並沒有它。並且你應該還注意到,mPreviewRequestBuilder 通過 addTarget() 新增了輸出端,而 createCaptureSeesion 也新增新增了輸出列表。它們之間應該存在著某種關係。

先來說 createCaptureSeeson 的輸出列表。這個輸出列表決定了 CameraDevices 將根據列表的不同 Surface 將建立不同的影像資料,比如這裡的 preview surface 以及 ImageReader 的 Surface。而 PreviewRequestBuilder 中的 addTarget() 表示的是針對 CaptureRequest 應該將影像資料輸出到哪裡去,並且要求這裡被新增到 target 的 Surface 必須是 createCaptureSession 的輸出列表的其中之一。那針對這段程式碼來說,被建立的影像資料有 2 種,一種是用於 preview 顯示的,一種是用於 ImageReader 的 jpeg。要想在預覽中也獲取 jpeg 資料,則把 ImageReader 的 surface 新增到 PreviewRequestBuilder 的 target 中去即中。

這裡理清了這 2 個列表的關係,接下來看看 createCaptureSeesion 時的第 2 引數 mSessionCallback,它是 CameraCaptureSession.StateCallback 型別的。會話一旦被建立,它的回撥方法便會被呼叫,這裡主要關注 onConfigured() 的實現,在這裡將關聯起 PreviewRequestBuilder 和會話。

       @Override        public void onConfigured(@NonNull CameraCaptureSession session) { 
if (mCamera == null) {
return;

} mCaptureSession = session;
// 設定對焦模式 updateAutoFocus();
// 設定閃光燈模式 updateFlash();
try {
// 設定引數,並請求此捕獲會話不斷重複捕獲影像,這樣就能連續不斷的得到影像幀輸出到預覽介面 mCaptureSession.setRepeatingRequest(mPreviewRequestBuilder.build(), mCaptureCallback, null);

} catch (CameraAccessException e) {
Log.e(TAG, "Failed to start camera preview because it couldn't access camera", e);

} catch (IllegalStateException e) {
Log.e(TAG, "Failed to start camera preview.", e);

}
}複製程式碼

會話建立好之後,我們還要告訴會話該怎麼用。檢視 API 可知,接下來可以進行的是 capture, captureBurst, setRepeatingRequest,或 setRepeatingBurst 的提交。其中 capture 會在後面拍照章節中講述,***Burst 是用於連拍的。這裡所呼叫的便是 setRepeatingRequest。通過 setRepeatingRequest 請求就將 mPreivewRequestBuilder 提交給了會話,而該提交就是請求此捕獲會話不斷重複捕獲影像,這樣就能連續不斷的得到影像幀輸出到預覽介面。

提交 setRepeatingRequest 請求時,還設定了一個引數 mCaptureCallback,它是 PictureCaptureCallback 型別的,而 PictureCaptureCallback 又是繼承自 CameraCaptureSession.CaptureCallback。捕獲到影像後會同時呼叫 CaptureCallback 相應的回撥方法,然而對於預覽模式下在這裡並沒有什麼處理。

關於 updateAutoFocus() 和 updateFlash() 看下面進一步的展開說明。

void updateAutoFocus() { 
if (mAutoFocus) {
int[] modes = mCameraCharacteristics.get( CameraCharacteristics.CONTROL_AF_AVAILABLE_MODES);
// Auto focus is not supported if (modes == null || modes.length == 0 || (modes.length == 1 &
&
modes[0] == CameraCharacteristics.CONTROL_AF_MODE_OFF)) {
mAutoFocus = false;
mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_OFF);

} else {
mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE);

}
} else {
mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_OFF);

}
}複製程式碼

這段程式碼的目的是如果設定了並且支援自動對焦,則 CONTROL_AF_MODE(auto-focus) 就設定為 CONTROL_AF_MODE_CONTINUOUS_PICTURE,否則就為 CONTROL_AF_MODE_OFF。有關 auto-focus 的值的含義概述如下。

value 說明
CONTROL_AF_MODE_AUTO 基本自動對焦模式
CONTROL_AF_MODE_CONTINUOUS_PICTURE 圖片模式下的連續對焦
CONTROL_AF_MODE_CONTINUOUS_VIDEO 視訊模式下的連續對焦
CONTROL_AF_MODE_EDOF 擴充套件景深(數字對焦)模式
CONTROL_AF_MODE_MACRO 特寫聚焦模式
CONTROL_AF_MODE_OFF 無自動對焦

這個表格中的每個 value 我也並不是每個都熟悉,因此,只作瞭解即可。

void updateFlash() { 
switch (mFlash) {
case Constants.FLASH_OFF: mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON);
mPreviewRequestBuilder.set(CaptureRequest.FLASH_MODE, CaptureRequest.FLASH_MODE_OFF);
break;
case Constants.FLASH_ON: mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON_ALWAYS_FLASH);
mPreviewRequestBuilder.set(CaptureRequest.FLASH_MODE, CaptureRequest.FLASH_MODE_OFF);
break;
case Constants.FLASH_TORCH: mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON);
mPreviewRequestBuilder.set(CaptureRequest.FLASH_MODE, CaptureRequest.FLASH_MODE_TORCH);
break;
case Constants.FLASH_AUTO: mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON_AUTO_FLASH);
mPreviewRequestBuilder.set(CaptureRequest.FLASH_MODE, CaptureRequest.FLASH_MODE_OFF);
break;
case Constants.FLASH_RED_EYE: mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON_AUTO_FLASH_REDEYE);
mPreviewRequestBuilder.set(CaptureRequest.FLASH_MODE, CaptureRequest.FLASH_MODE_OFF);
break;

}
}複製程式碼

通過 PreviewRequestBuilder 設定閃光燈的模式,其需要同時設定 CONTROL_AE_MODE 和 FLASH_MODE。

(1) FLASH_MODE,對應是控制閃光燈。

引數 說明
FLASH_MODE_OFF 關閉模式
FLASH_MODE_SINGLE 閃一下模式
FLASH_MODE_TORCH 長亮模式

(2) CONTROL_AE_MODE,對應是曝光,即 auto-exposure。

引數 說明
CONTROL_AE_MODE_ON_AUTO_FLASH 自動曝光
CONTROL_AE_MODE_ON_ALWAYS_FLASH 強制曝光
CONTROL_AE_MODE_ON_AUTO_FLASH_REDEYE 不閃光

到這裡,基本上就成功開啟相機了,然後就能看到相機的畫面了。歷經磨難,終於開啟相機了。而關於相機引數設定,在 Camera 2 中則更加豐富,工程裡沒有涉及到的這裡就不做詳細講解,在實際開發中再去慢慢消化,慢慢理解。

接下來,終於可以進行愉快的拍照了。

4.拍照

分析之前也先來看一看拍照的時序圖。梳理了 16 個步驟,但其實拍照的關鍵步驟就 2 步:通過 CameraDevice 建立一個 TEMPLATE_STILL_CAPTURE 的 CaptureRequest,然後通過 CaptureSession 的 capture 方法提交請求即是拍照的主要步驟。

CameraView 拍照.jpg

CameraView 的 takePicture 就是進一步呼叫 Camera2 的 takePicture,所以直接從 takePicture() 開始吧。

  • takePicture()
void takePicture() { 
if (mAutoFocus) {
lockFocus();

} else {
captureStillPicture();

}
}複製程式碼

CameraView 初始化時預設是自動對焦,因此這裡是走入 lockFocus(),時序圖也是依據此來繪製的。

  • lockFocus()
private void lockFocus() { 
// 設定當前立刻觸發自動對焦 mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AF_TRIGGER, CaptureRequest.CONTROL_AF_TRIGGER_START);
try {
// 這裡是修改了 PictureCaptureCallback 的狀態為 STATE_LOCKING mCaptureCallback.setState(PictureCaptureCallback.STATE_LOCKING);
// 向會話提交 capture 請求,以鎖定自動對焦 mCaptureSession.capture(mPreviewRequestBuilder.build(), mCaptureCallback, null);

} catch (CameraAccessException e) {
Log.e(TAG, "Failed to lock focus.", e);

}
}複製程式碼

設定了立刻觸發自動對焦,修改了 PictureCaptureCallback 狀態為 STATE_LOCKING。接下來就是等待 PictureCaptureCallback 的 onCaptureCompleted() 被系統回撥。在 onCaptureCompleted() 中進步呼叫了 process(),而在 process() 中以不同的狀態進行不同的處理。這裡根據前面的設定處理的是 STATE_LOCKING。

 private void process(@NonNull CaptureResult result) { 
switch (mState) {
case STATE_LOCKING: {
......if (af == CaptureResult.CONTROL_AF_STATE_FOCUSED_LOCKED || af == CaptureResult.CONTROL_AF_STATE_NOT_FOCUSED_LOCKED) {
setState(STATE_CAPTURING);
onReady();
...... break;

} case STATE_PRECAPTURE: {
...... setState(STATE_WAITING);
break;

} case STATE_WAITING: {
...... setState(STATE_CAPTURING);
onReady();
break;

}
}
}複製程式碼

為了避免不必要的麻煩,在不影響對程式碼理解的情況下,這裡省略了其他狀態的處理。這裡假設自動對焦成功了且達到了一個很好的狀態下,那麼當前的自動對對焦就會進入被鎖定的狀態,即 CONTROL_AF_STATE_FOCUSED_LOCKED。而自動對焦前面在 updateAutoFocus() 中已經設定為 CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE 了。接下來就會進入真正的抓取圖片的處理了。這裡先設定了狀態為 STATE_CAPTURING,然後呼叫了自已擴充套件的 onReady()。onReady() 的實現很簡單,就是呼叫 captureStillPicture()。

  • captureStillPicture()
void captureStillPicture() { 
try {
//1. 建立一個新的CaptureRequest.Builder,且其引數為 TEMPLATE_STILL_CAPTURE CaptureRequest.Builder captureRequestBuilder = mCamera.createCaptureRequest( CameraDevice.TEMPLATE_STILL_CAPTURE);
//2. 新增它的 target 為 ImageReader 的 Surface captureRequestBuilder.addTarget(mImageReader.getSurface());
//3. 設定自動對焦模式為預覽的自動對焦模式 captureRequestBuilder.set(CaptureRequest.CONTROL_AF_MODE, mPreviewRequestBuilder.get(CaptureRequest.CONTROL_AF_MODE));
//4. 設定閃光燈與曝光引數 switch (mFlash) {
case Constants.FLASH_OFF: captureRequestBuilder.set(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON);
captureRequestBuilder.set(CaptureRequest.FLASH_MODE, CaptureRequest.FLASH_MODE_OFF);
break;
case Constants.FLASH_ON: captureRequestBuilder.set(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON_ALWAYS_FLASH);
break;
case Constants.FLASH_TORCH: captureRequestBuilder.set(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON);
captureRequestBuilder.set(CaptureRequest.FLASH_MODE, CaptureRequest.FLASH_MODE_TORCH);
break;
case Constants.FLASH_AUTO: captureRequestBuilder.set(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON_AUTO_FLASH);
break;
case Constants.FLASH_RED_EYE: captureRequestBuilder.set(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON_AUTO_FLASH);
break;

} // 5. 計算 JPEG 的旋轉角度 @SuppressWarnings("ConstantConditions") int sensorOrientation = mCameraCharacteristics.get( CameraCharacteristics.SENSOR_ORIENTATION);
captureRequestBuilder.set(CaptureRequest.JPEG_ORIENTATION, (sensorOrientation + mDisplayOrientation * (mFacing == Constants.FACING_FRONT ? 1 : -1) + 360) % 360);
// 6.停止預覽 mCaptureSession.stopRepeating();
// 7.抓取當前圖片 mCaptureSession.capture(captureRequestBuilder.build(), new CameraCaptureSession.CaptureCallback() {
@Override public void onCaptureCompleted(@NonNull CameraCaptureSession session, @NonNull CaptureRequest request, @NonNull TotalCaptureResult result) {
// 8.解鎖對自動對焦的鎖定 unlockFocus();

}
}, null);

} catch (CameraAccessException e) {
Log.e(TAG, "Cannot capture a still picture.", e);

}
}複製程式碼

這是拍照的關鍵實現,程式碼有點長,但通過增加了帶時序的註釋,邏輯上看起來也就並不複雜了。這裡只強調 3 個點,其他的看一看註釋即可,而關於設定閃光燈和曝光這裡就省略了。

(1) 這裡建立了一個新的 CaptureRequest.Builder ,且其引數為TEMPLATE_STILL_CAPTURE。相應的其 CallBack 也是新的。

(2) 請求的 Target 只有 ImageReader 的 Surface,因此獲取到圖片後會輸出到 ImageReader。最後會在 ImageReader.OnImageAvailableListener 的 onImageAvailable 得到回撥。

(3) 拍照前先停止了預覽請求,從這裡可以看出拍照就是捕獲預覽模式下自動對焦成功鎖定後的影像資料。

接下來就是等待 onCaptureCompleted 被系統回撥,然後進一步呼叫 unlockFocus()。

  • unlockFocus()
void unlockFocus() { 
// 取消了立即自動對焦的觸發 mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AF_TRIGGER, CaptureRequest.CONTROL_AF_TRIGGER_CANCEL);
try {
mCaptureSession.capture(mPreviewRequestBuilder.build(), mCaptureCallback, null);
updateAutoFocus();
updateFlash();
mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AF_TRIGGER, CaptureRequest.CONTROL_AF_TRIGGER_IDLE);
// 重新開啟預覽 mCaptureSession.setRepeatingRequest(mPreviewRequestBuilder.build(), mCaptureCallback, null);
mCaptureCallback.setState(PictureCaptureCallback.STATE_PREVIEW);

} catch (CameraAccessException e) {
Log.e(TAG, "Failed to restart camera preview.", e);

}
}複製程式碼

該方法主要做的事情就是重新開啟預覽,並且取消了立即自動對焦,同時將其設定為 CONTROL_AF_TRIGGER_IDLE,這將會解除自動對焦的狀態,即其狀態不再是 CaptureResult.CONTROL_AF_STATE_FOCUSED_LOCKED。

系統組織好 ImageReader 需要的影像資料後,就會回撥其監聽 ImageReader.OnImageAvailableListener 的 onImageAvailable()。

  • onImageAvailable()
public void onImageAvailable(ImageReader reader) { 
try (Image image = reader.acquireNextImage()) {
Image.Plane[] planes = image.getPlanes();
if (planes.length >
0) {
ByteBuffer buffer = planes[0].getBuffer();
byte[] data = new byte[buffer.remaining()];
buffer.get(data);
mCallback.onPictureTaken(data);

}
}
}複製程式碼

從 ImageReader 中獲取到 Image,Image 相比 Bitmap 就要複雜的多了,這裡簡單說明一下。ImageReader 封裝了影像的資料平面,而每個平面又封裝了 ByteBuffer 來儲存原始資料。關於影像的資料平面這個相對於影像的格式來說的,比如 rgb 就只一個平面,而 YUV 一般就有 3 個平面。從 ByteBuffer 中獲取的資料都是最原始的資料,對於 rgb 格式的資料,就可以直接將其轉換成 Bitmap 然後給 ImageView 顯示。

到這裡就分析完了拍照的過程了。

5.關閉相機

void stop() { 
if (mCaptureSession != null) {
mCaptureSession.close();
mCaptureSession = null;

} if (mCamera != null) {
mCamera.close();
mCamera = null;

} if (mImageReader != null) {
mImageReader.close();
mImageReader = null;

}
}複製程式碼

全場最簡單,關閉會話,關閉相機,關閉 ImageReader,Game voer !!!

五、總結

文章對 Android Camera 程式設計進行了一個較為詳細的概括,尤其是對於偏難的 Camera 2 的 API 的理解,結合了官方的 Demo 對 API 及其引數進行了詳細的分析,以使得對 API 的理解更加透徹。

另外,如果你的專案需要整合 Camera,又不想自己去封裝,同時又覺得官方的 demo 還不夠,這裡另外推薦一個 github 開源專案 camerakit-android。其也是從官方 demo fork 出來的,自動支援 camera api 1 以及 camera api 2。

最後,感謝你能讀到並讀完此文章。受限於作者水平有限,如果分析的過程中存在錯誤或者疑問都歡迎留言討論。如果我的分享能夠幫助到你,也請記得幫忙點個贊吧,鼓勵我繼續寫下去,謝謝。

來源:https://juejin.im/post/5c20cdc9e51d45262629638c

相關文章