一、前言
想通過一篇文章就讓我們精通 Android 的 Camera 那肯定是不可能的事情。但通過對 Android 中相機拍照的所有的方式的梳理和理解,包括直接調(diào)起相機拍照,Camera API 1 以及 Camera API 2 的分析與理解,為我們指明一條通往精通 Android Camera 的路還是有可能的。文章將先對 Android Camera 有一個全局的認知,然后再分析拍照的各個關(guān)鍵路徑及相關(guān)知識點。在實際開發(fā)過程中碰到問題再深入去了解 API 及其相關(guān)參數(shù),應(yīng)該就能解決我們在 Android Camera 編程中的大部分問題了。
二、相機基本使用以及 Camra API 1
這里主要涉及到的是如何直接調(diào)起系統(tǒng)相機拍照以及基于 Camra API 1 實現(xiàn)拍照。如下的思維導(dǎo)圖是一個基本的導(dǎo)讀。

1.權(quán)限及需求說明
要使用相機必須聲明 CAMERA 權(quán)限以及告訴系統(tǒng)你要使用這個功能。
<uses-permission android:name="android.permission.CAMERA" />
<uses-feature android:name="android.hardware.camera" />
上面這是最基本的,但如果你需要寫文件,錄音,定位等還需要下面的權(quán)限
<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.調(diào)起系統(tǒng)或者三方相機直接拍照
- 拍照
takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT, photoURI);
- 獲取拍照后的照片
Uri contentUri = FileProvider.getUriForFile(this, "com.example.android.fileprovider", photoFile);
3.通過 Camera API 1 進行拍照
- 相機設(shè)備檢測
(context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_CAMERA))
- 打開相機
Camera.open();
// Camera.open(0)
// Camera.open(1)
- 創(chuàng)建預(yù)覽界面
/** 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();
......
}
}
...
}
- 設(shè)置相機參數(shù)
public void setCamera(Camera camera) {
......
if (mCamera != null) {
List<Size> localSizes = mCamera.getParameters().getSupportedPreviewSizes();
mSupportedPreviewSizes = localSizes;
requestLayout();
......
// 相機參數(shù)設(shè)置完成后,需要重新啟動預(yù)覽界面
mCamera.setPreviewDisplay(mHolder);
mCamera.startPreview();
......
}
}
-
停止預(yù)覽及釋放相機
這個建議放在 onDestroy() 中調(diào)用
private void stopPreviewAndFreeCamera() {
......
mCamera.stopPreview();
......
mCamera.release();
mCamera = null;
}
}
以上就是如何通過調(diào)起系統(tǒng)或者三方相機以及通過調(diào)用 Camera API 1 來進行拍照的講解,相對來說還是比較簡單的。一般來說掌握 Camera API 1 的用法基本能滿足常規(guī)開發(fā)了,但當(dāng)我們需要獲取更多相機設(shè)備的特性時,顯然需要通過 Camera API 2 所提供的更加豐富的功能來達到目的了。對于基本的拍照以及 API 1 的講解這里只是簡單過一下,重點在 API 2 的介紹。
三、全新 Camera API 2
Camera API 2 是從 Android 5.0 L 版本開始引入的。官網(wǎng)對相機介紹的引導(dǎo)文檔里是沒有涉及到 API 2 的講解的,都是基于 API 1 的。能找到的是其推薦的一篇博客Detecting camera features with Camera2 以及官方的 API 文檔。通過文檔大概了解到其比較重要的優(yōu)點如下:
- 改進了新硬件的性能。
- 以更快的間隔拍攝圖像。
- 顯示來自多個攝像頭的預(yù)覽。
- 直接應(yīng)用效果和過濾器。
看起來很爽,但是用起來那就是酸爽了,如下是梳理的一個思維導(dǎo)圖??纯淳椭烙卸嗦闊┝?。

四、官方 demo 分析
正是由于 Camera 的 API 從 1 到 2 發(fā)生了架構(gòu)上的變化,而且使用難度也是大大地增加了好幾倍,加上 Android 的碎片化又是如斯的嚴重。因此官方考慮到大家掌握不好,推出了其官方的 demo 供我們參考和學(xué)習(xí)——cameraview。這里也將基于官方的 demo 來深入掌握 Android 相機 API 2 的使用。
1. 主要類圖
先來看看工程中主要的類圖及其關(guān)系,好對整個工程以及 Camera2 中的相關(guān)類有一個基本的認知。

(1) 類圖結(jié)構(gòu)上封裝了 CameraView 用于給 Activity 直接調(diào)用。
(2) 抽象了相機類 CameraViewImpl 和預(yù)覽類 PreviewImpl。根據(jù)不同的版本由其具體實現(xiàn)類來解決版本之間的差異以及兼容。
(3) 用于預(yù)覽的既可以是 SurfaceView 也可以是 TextureView,框架內(nèi)根據(jù)不同版本做了相應(yīng)的適配。
(4) Camera1 即使用的舊版 Camera 及其相關(guān)的 API。而 Camera2 使用了新的 Camera2 API,這里簡要介紹一下這幾個類的作用。
| 序號 | 類 | 說明 |
|---|---|---|
| 1 | CameraManager | 這是一個系統(tǒng)服務(wù),主要用于管理相機設(shè)備的,如相機的打開。與 AlarmManager 同等級。 |
| 2 | CameraDevice | 這個就是相機設(shè)備了,與 Camra1 中的 Camera 同等級。 |
| 3 | ImageReader | 用于從相機打開的通道中讀取需要的格式的原始圖像數(shù)據(jù),理論上一個設(shè)備可以連接 N 多個 ImageReader。在這里可以看成是和 Preview 同等級。 |
| 4 | CaptureRequest.Builder | CaptureRequest 構(gòu)造器,主要給相機設(shè)置參數(shù)的類。Builder 設(shè)計模式真好用。 |
| 5 | CameraCharacteristics | 與 CaptureRequest 反過來,主要是獲取相機參數(shù)的。 |
| 6 | CameraCaptureSession | 請求抓取相機圖像幀的會話,會話的建立主要會建立起一個通道。源端是相機,另一端是 Target,Target 可以是 Preview,也可以是 ImageReader。 |
2.CameraView 初始化
先看一看 CameraView 初始化的時序圖,大概一共做了 13 事情。當(dāng)然,初始化做的事情其實都是簡單的,主要就是初始化必要的對象且設(shè)置一些監(jiān)聽。

- CameraView 的構(gòu)建方法
public CameraView(Context context, AttributeSet attrs, int defStyleAttr) {
......
// 創(chuàng)建預(yù)覽視圖
final PreviewImpl preview = createPreviewImpl(context);
// Callback 橋接器,將相機內(nèi)部的回調(diào)轉(zhuǎn)發(fā)給調(diào)用層
mCallbacks = new CallbackBridge();
// 根據(jù)不同的 SDK 版本選擇不同的 Camera 實現(xiàn),這里假設(shè)選擇了 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);
}
......
// 設(shè)置相機 ID,如前置或者后置
setFacing(a.getInt(R.styleable.CameraView_facing, FACING_BACK));
......
// 設(shè)置預(yù)覽界面的比例,如 4:3 或者 16:9
setAspectRatio(AspectRatio.parse(aspectRatio));
// 設(shè)置對焦方式
setAutoFocus(a.getBoolean(R.styleable.CameraView_autoFocus, true));
// 設(shè)置閃光燈
setFlash(a.getInt(R.styleable.CameraView_flash, Constants.FLASH_AUTO));
......
// 初始化顯示設(shè)備(主要指手機屏幕)的旋轉(zhuǎn)監(jiān)聽,主要用來設(shè)置相機的旋轉(zhuǎn)方向
mDisplayOrientationDetector = new DisplayOrientationDetector(context) {
@Override
public void onDisplayOrientationChanged(int displayOrientation) {
mImpl.setDisplayOrientation(displayOrientation);
}
};
}
構(gòu)造方法中所做的事情都在注釋里進行說明,沒有需要展開的。下面來看 createPreviewImpl()。
- createPreviewImpl() 的實現(xiàn)
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 都是一個包裝類,從名字上就可以知道其內(nèi)部分別包裝了 SurfaceView 和 TextureView 實例來實現(xiàn)相機的預(yù)覽界面的。關(guān)于 SurfaceView 以及 TextureView 的區(qū)別,這里也再簡單提一下,詳細的可以參考其他大神的文章說明:
SurfaceView:是一個獨立的 Window,由系統(tǒng) WMS 直接管理,可支持硬件加速,也可以不支持硬件加速。
TextureView:可以看成是一個普通的 View,屬于所于應(yīng)用的視圖層級樹中,屬于 ViewRootImpl 管理,只支持硬件加速。
盡管 SurfaceView 和 TextureView 有區(qū)別,但本質(zhì)上它們都是對 Surface 的一個封裝實現(xiàn)。
這里假設(shè)選擇的是 TextureViewPreview。TextureViewPreview 的構(gòu)造方法很簡單,就是從 xml 里獲取 TextureView 的實例,并且同時設(shè)置 TextureView 的監(jiān)聽 TextureView.SurfaceTextureListener,這個后面會再詳細講。
接下來是根據(jù)不同的版本選擇 Camera,這里假設(shè)選擇的是 Camera2,主線上我們也只分析它就可以了。那么就來看一看 Camera2 的實現(xiàn)吧。
- 初始化 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 有一個專業(yè)的管理者了。其次可以看到這里是向 Context 獲取一個系統(tǒng) Service "CAMERA_SERVICE" 來初始化 CameraManager 的,這也說明了其被上升到了一個系統(tǒng)服務(wù)的高度了。
然后就是向 Preview 添加回調(diào),監(jiān)聽其 Surface 的變化來作進一步的事情。
-
關(guān)于 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 的選擇也根據(jù) API 來選擇, 這個就不應(yīng)該了??催^其他相應(yīng)的實現(xiàn),除了 SDK API 的檢查,應(yīng)用 TextureView 前還應(yīng)該要判斷一下當(dāng)前的運行環(huán)境是否支持硬件加速。
而讓我有疑問的是這里的 24 以上推薦使用 SurfaceView,這個是為什么呢?而其里面的代碼實際實現(xiàn),看上面createPreviewImpl() 的實現(xiàn)可知又不是這樣的,也是選擇了 TextureView。
-
setFacing、setAspectRatio、setAutoFocus、setFlash
這些都是設(shè)置參數(shù),其實際生效的方法在 Camera2 中,而這個時候相機都還沒有打開,對于它們的設(shè)置目前來說是不會立即生效的,只是記錄下它們的值而已。后面我們分析時已默認值來分析即可。
當(dāng)然,這里只是給出了 4 個參數(shù),其實還有很多,后面還會講到。
小結(jié)
到這里就分析完了 CameraView 的初始化了,其主要做了以下幾件事情:
(1) 通過 getSystemService 初始化了 CameraManager。
(2) 準(zhǔn)備好了 Preview ,用于相機的預(yù)覽
(3) 設(shè)定好了相機要用的參數(shù)
3.打開相機
同樣,先來看一看打開相機的時序圖。概括了有 15 個步驟,但其實關(guān)鍵步驟沒有這么多。

- 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();
}
}
這里給了幾個關(guān)鍵的信息:
(1) 此方法推薦的是在 Activity#onResume() 方法里面進行調(diào)用,這個是很重要的,告訴了我們打開相機的最適合時機。
(2) 按照前面的場景,這里調(diào)用了 Camera2#start()。這是要進一步分析的。
(3) 如果打開 Camera2 失敗了,則降級到 Camera1。做了回退保護,考慮的確實比較周到。
- Camera2.start()
boolean start() {
if (!chooseCameraIdByFacing()) {
return false;
}
collectCameraInfo();
prepareImageReader();
startOpeningCamera();
return true;
}
都是內(nèi)部調(diào)用,下面逐個分析這些方法的實現(xiàn)。
- chooseCameraIdByFacing()
private boolean chooseCameraIdByFacing() {
try {
// 1.根據(jù) mFacing 選擇相機
int internalFacing = INTERNAL_FACINGS.get(mFacing);
// 2.獲取所有的可用相機 ID 列表,注意相機的 ID 是 字串類型了
final String[] ids = mCameraManager.getCameraIdList();
......
for (String id : ids) {
// 根據(jù)相機的 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");
}
// 查出來的與所期望的相等,則認為就是要找到的相機設(shè)備
if (internal == internalFacing) {
// 保存相機的 ID
mCameraId = id;
// 保存相機的特征參數(shù)
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);
}
}
這段代碼確實有點長,并且信息量也多。其主要的目的是根據(jù) mFacing 指定的相機方向選擇一個正確的相機,但如果沒有的話就默認選擇后置相機。這個過程涉及到了幾個比較重要的相機參數(shù)及其 API 調(diào)用。
(1) 關(guān)于選擇相機方向
相機方向主要是相對于手機屏幕而言的,系統(tǒng)可取的值有 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)關(guān)于CameraCharacteristics
這里是查詢出了所有的相機 ID ,然后來逐個遍歷看是否與所期望的相機方向相符合的相機設(shè)備。這里要注意的是相機的 ID 是實際是字符串,這個需要記住并且很重要,后面的相機操作,如打開設(shè)備、查詢或者設(shè)置參數(shù)等都是需要這個 ID 的。
通過 CameraManager. getCameraCharacteristics(ID) 查詢出了相關(guān)設(shè)備的特征信息,特征信息都被封裝在了 CameraCharacteristics 中。它以 Key<?>-Value 的形式儲存了所有的相機設(shè)備的參數(shù)信息。注意這個 Key<?> ,它又是一個泛型,這說明了 Key 也是可以以不同的形式存在的。這樣的擴展性就強了。特別是對于現(xiàn)在一些特殊攝像頭的發(fā)展,如3D 攝像頭,那么廠商就可自行添加參數(shù)支持而不用添加私有 API 了。這也是主要需要理解的部分。
(3)關(guān)于支持的硬件級別
了解了第(2)點,其他的就都只是參數(shù)查詢的問題了。這里摘抄官網(wǎng)了。
- LEGACY 對于較舊的Android設(shè)備,設(shè)備以向后兼容模式運行,并且功能非常有限。
- LIMITED設(shè)備代表基線功能集,還可能包括作為子集的附加功能FULL。
- FULL 設(shè)備還支持傳感器,閃光燈,鏡頭和后處理設(shè)置的每幀手動控制,以及高速率的圖像捕獲。
- LEVEL_3 設(shè)備還支持YUV重新處理和RAW圖像捕獲,以及其他輸出流配置。
- EXTERNAL設(shè)備類似于LIMITED設(shè)備,例如某些傳感器或鏡頭信息未重新排列或不太穩(wěn)定的幀速率。
CameraCharacteristics 中還有非常多的參數(shù),這里僅列出其所提及到的,其他的參數(shù)如果你真的實際會在開發(fā)中用到,建議還是過一遍。這樣一來,相機能做什么,具備什么特性就會有一個整體感知了。
- collectCameraInfo()
private void collectCameraInfo() {
// 獲取此攝像機設(shè)備支持的可用流配置,其包括格式、大小、持續(xù)時間和停頓持續(xù)時間等
StreamConfigurationMap map = mCameraCharacteristics.get(
CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP);
if (map == null) {
throw new IllegalStateException("Failed to get configuration map: " + mCameraId);
}
mPreviewSizes.clear();
// 根據(jù)需要渲染到的目標(biāo)類型選擇合適的輸出大小
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));
}
}
// 根據(jù)圖片格式選擇圖片大小
mPictureSizes.clear();
collectPictureSizes(mPictureSizes, map);
// 把預(yù)覽中所支持的大小比例,但在圖片大小比例不支持的 比例 移除掉
for (AspectRatio ratio : mPreviewSizes.ratios()) {
if (!mPictureSizes.ratios().contains(ratio)) {
mPreviewSizes.remove(ratio);
}
}
// 如果設(shè)置的比例不完全相符合,那選擇一個接近的。
if (!mPreviewSizes.ratios().contains(mAspectRatio)) {
mAspectRatio = mPreviewSizes.ratios().iterator().next();
}
}
這段代碼相對來說要簡單一些,主要完成的是獲取預(yù)覽尺寸,圖片尺寸以及合適的顯示比例。
- 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);
}
根據(jù)合適的圖片尺寸初始化 ImageReader,主要是用于接收圖片的原始數(shù)據(jù)信息,且這里的原始數(shù)據(jù)信息為 ImageFormat.JPEG。當(dāng)然你也可以指定為 YUV 等更原始的數(shù)據(jù)信息。這樣一來除了除了讓圖像顯示在預(yù)覽界面上,我們還可以同時獲取原始數(shù)據(jù)信息做進一步處理,如增加濾鏡效果后再保存等。
而要獲取到原始數(shù)據(jù)信息,就需要向 ImageReader 注冊相應(yīng)的監(jiān)聽器 ImageReader.OnImageAvailableListener,當(dāng)有相機的圖像幀后會通過onImageAvailable 進行回調(diào)。這里展開看一下它的實現(xiàn)。
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);
}
}
}
這里涉及到了圖像格式的知識, 這里就不細述了,感興趣的同學(xué)可以自己去查一下資料。
- 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??匆豢此膶崿F(xiàn)。
private final CameraDevice.StateCallback mCameraDeviceCallback
= new CameraDevice.StateCallback() {
// 相機打開
@Override
public void onOpened(@NonNull CameraDevice camera) {
mCamera = camera;
mCallback.onCameraOpened();
startCaptureSession();
}
// 相機關(guān)閉
@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;
}
};
這里就是打開相機狀態(tài)的回調(diào)監(jiān)聽,主要關(guān)注的是 onOpened()。在這個回調(diào)方法中返回了 CameraDevice ,也就是實際的相機設(shè)備。關(guān)于 CameraDevice 再來看一個類圖。

看出來了吧,CameraDevice 的實現(xiàn)類 CameraDeviceImpl 是持有了一個 Binder 端的代理。這里不看源碼,只憑推測可知,實際的相機設(shè)備對象應(yīng)該被放到了系統(tǒng)進程 SystemServer 或者別的進程中去了。這和 Camera 1 就有本質(zhì)上的區(qū)別了。
然后就是通知調(diào)用者,再然后就是一個 startCaptureSession() 調(diào)用。這個調(diào)用非常重要,它建立起了相機與 Target(這里是 Preview 以及 ImageReader) 的通道連接。
- startCaptureSession()
void startCaptureSession() {
if (!isCameraOpened() || !mPreview.isReady() || mImageReader == null) {
return;
}
// 根據(jù) Preivew 的大小從 mPreviewSize 中選擇一個最佳的。
Size previewSize = chooseOptimalSize();
// 設(shè)置 Preview Buffer 的大小
mPreview.setBufferSize(previewSize.getWidth(), previewSize.getHeight());
// 獲取 Preview 的 Surface,將被用來作用相機實際預(yù)覽的 Surface
Surface surface = mPreview.getSurface();
try {
// 構(gòu)建一個預(yù)覽請求
mPreviewRequestBuilder = mCamera.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);
// 添加 Target ,通道的輸出端之一,這里只添加了 preview
mPreviewRequestBuilder.addTarget(surface);
// 建立 capture 會話,打通通道。設(shè)置輸出列表,并且還設(shè)置了回調(diào) SessionCallback
mCamera.createCaptureSession(Arrays.asList(surface, mImageReader.getSurface()),
mSessionCallback, null);
} catch (CameraAccessException e) {
throw new RuntimeException("Failed to start camera session");
}
}
該方法總的來說就是設(shè)置 Surface 的 Buffer 大小,創(chuàng)建請求參數(shù),建立會話,打通通道。而關(guān)于創(chuàng)建請求參數(shù),這里用了 CameraDevice.TEMPLATE_PREVIEW。其主要支持的參數(shù)有TEMPLATE_PREVIEW(預(yù)覽)、TEMPLATE_RECORD(拍攝視頻)、TEMPLATE_STILL_CAPTURE(拍照)等參數(shù)。接下來是調(diào)用了 createCaptureSession()。
在 createCaptureSession 時設(shè)置了輸出端列表,還設(shè)置了回調(diào) mSessionCallback,它是CameraCaptureSession.StateCallback類型。
細心的讀者可能會發(fā)現(xiàn),在這里,mPreivewRequestBuilder 并沒有用上,在 createCaptureSeesion 的參數(shù)中并沒有它。并且你應(yīng)該還注意到,mPreviewRequestBuilder 通過 addTarget() 添加了輸出端,而 createCaptureSeesion 也添加添加了輸出列表。它們之間應(yīng)該存在著某種關(guān)系。
先來說 createCaptureSeeson 的輸出列表。這個輸出列表決定了 CameraDevices 將根據(jù)列表的不同 Surface 將創(chuàng)建不同的圖像數(shù)據(jù),比如這里的 preview surface 以及 ImageReader 的 Surface。而 PreviewRequestBuilder 中的 addTarget() 表示的是針對 CaptureRequest 應(yīng)該將圖像數(shù)據(jù)輸出到哪里去,并且要求這里被添加到 target 的 Surface 必須是 createCaptureSession 的輸出列表的其中之一。那針對這段代碼來說,被創(chuàng)建的圖像數(shù)據(jù)有 2 種,一種是用于 preview 顯示的,一種是用于 ImageReader 的 jpeg。要想在預(yù)覽中也獲取 jpeg 數(shù)據(jù),則把 ImageReader 的 surface 添加到 PreviewRequestBuilder 的 target 中去即中。
這里理清了這 2 個列表的關(guān)系,接下來看看 createCaptureSeesion 時的第 2 參數(shù) mSessionCallback,它是 CameraCaptureSession.StateCallback 類型的。會話一旦被創(chuàng)建,它的回調(diào)方法便會被調(diào)用,這里主要關(guān)注 onConfigured() 的實現(xiàn),在這里將關(guān)聯(lián)起 PreviewRequestBuilder 和會話。
@Override
public void onConfigured(@NonNull CameraCaptureSession session) {
if (mCamera == null) {
return;
}
mCaptureSession = session;
// 設(shè)置對焦模式
updateAutoFocus();
// 設(shè)置閃光燈模式
updateFlash();
try {
// 設(shè)定參數(shù),并請求此捕獲會話不斷重復(fù)捕獲圖像,這樣就能連續(xù)不斷的得到圖像幀輸出到預(yù)覽界面
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);
}
}
會話創(chuàng)建好之后,我們還要告訴會話該怎么用。查看 API 可知,接下來可以進行的是 capture, captureBurst, setRepeatingRequest,或 setRepeatingBurst 的提交。其中 capture 會在后面拍照章節(jié)中講述,***Burst 是用于連拍的。這里所調(diào)用的便是 setRepeatingRequest。通過 setRepeatingRequest 請求就將 mPreivewRequestBuilder 提交給了會話,而該提交就是請求此捕獲會話不斷重復(fù)捕獲圖像,這樣就能連續(xù)不斷的得到圖像幀輸出到預(yù)覽界面。
提交 setRepeatingRequest 請求時,還設(shè)置了一個參數(shù) mCaptureCallback,它是 PictureCaptureCallback 類型的,而 PictureCaptureCallback 又是繼承自 CameraCaptureSession.CaptureCallback。捕獲到圖像后會同時調(diào)用 CaptureCallback 相應(yīng)的回調(diào)方法,然而對于預(yù)覽模式下在這里并沒有什么處理。
關(guān)于 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);
}
}
這段代碼的目的是如果設(shè)置了并且支持自動對焦,則 CONTROL_AF_MODE(auto-focus) 就設(shè)置為 CONTROL_AF_MODE_CONTINUOUS_PICTURE,否則就為 CONTROL_AF_MODE_OFF。有關(guān) auto-focus 的值的含義概述如下。
| value | 說明 |
|---|---|
| CONTROL_AF_MODE_AUTO | 基本自動對焦模式 |
| CONTROL_AF_MODE_CONTINUOUS_PICTURE | 圖片模式下的連續(xù)對焦 |
| CONTROL_AF_MODE_CONTINUOUS_VIDEO | 視頻模式下的連續(xù)對焦 |
| CONTROL_AF_MODE_EDOF | 擴展景深(數(shù)字對焦)模式 |
| 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 設(shè)定閃光燈的模式,其需要同時設(shè)定 CONTROL_AE_MODE 和 FLASH_MODE。
(1) FLASH_MODE,對應(yīng)是控制閃光燈。
| 參數(shù) | 說明 |
|---|---|
| FLASH_MODE_OFF | 關(guān)閉模式 |
| FLASH_MODE_SINGLE | 閃一下模式 |
| FLASH_MODE_TORCH | 長亮模式 |
(2) CONTROL_AE_MODE,對應(yīng)是曝光,即 auto-exposure。
| 參數(shù) | 說明 |
|---|---|
| CONTROL_AE_MODE_ON_AUTO_FLASH | 自動曝光 |
| CONTROL_AE_MODE_ON_ALWAYS_FLASH | 強制曝光 |
| CONTROL_AE_MODE_ON_AUTO_FLASH_REDEYE | 不閃光 |
到這里,基本上就成功打開相機了,然后就能看到相機的畫面了。歷經(jīng)磨難,終于打開相機了。而關(guān)于相機參數(shù)設(shè)置,在 Camera 2 中則更加豐富,工程里沒有涉及到的這里就不做詳細講解,在實際開發(fā)中再去慢慢消化,慢慢理解。
接下來,終于可以進行愉快的拍照了。
4.拍照
分析之前也先來看一看拍照的時序圖。梳理了 16 個步驟,但其實拍照的關(guān)鍵步驟就 2 步:通過 CameraDevice 創(chuàng)建一個 TEMPLATE_STILL_CAPTURE 的 CaptureRequest,然后通過 CaptureSession 的 capture 方法提交請求即是拍照的主要步驟。

CameraView 的 takePicture 就是進一步調(diào)用 Camera2 的 takePicture,所以直接從 takePicture() 開始吧。
- takePicture()
void takePicture() {
if (mAutoFocus) {
lockFocus();
} else {
captureStillPicture();
}
}
CameraView 初始化時默認是自動對焦,因此這里是走入 lockFocus(),時序圖也是依據(jù)此來繪制的。
- lockFocus()
private void lockFocus() {
// 設(shè)置當(dāng)前立刻觸發(fā)自動對焦
mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AF_TRIGGER,
CaptureRequest.CONTROL_AF_TRIGGER_START);
try {
// 這里是修改了 PictureCaptureCallback 的狀態(tài)為 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);
}
}
設(shè)置了立刻觸發(fā)自動對焦,修改了 PictureCaptureCallback 狀態(tài)為 STATE_LOCKING。接下來就是等待 PictureCaptureCallback 的 onCaptureCompleted() 被系統(tǒng)回調(diào)。在 onCaptureCompleted() 中進步調(diào)用了 process(),而在 process() 中以不同的狀態(tài)進行不同的處理。這里根據(jù)前面的設(shè)置處理的是 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;
}
}
}
為了避免不必要的麻煩,在不影響對代碼理解的情況下,這里省略了其他狀態(tài)的處理。這里假設(shè)自動對焦成功了且達到了一個很好的狀態(tài)下,那么當(dāng)前的自動對對焦就會進入被鎖定的狀態(tài),即 CONTROL_AF_STATE_FOCUSED_LOCKED。而自動對焦前面在 updateAutoFocus() 中已經(jīng)設(shè)置為 CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE 了。接下來就會進入真正的抓取圖片的處理了。這里先設(shè)置了狀態(tài)為 STATE_CAPTURING,然后調(diào)用了自已擴展的 onReady()。onReady() 的實現(xiàn)很簡單,就是調(diào)用 captureStillPicture()。
- captureStillPicture()
void captureStillPicture() {
try {
//1. 創(chuàng)建一個新的CaptureRequest.Builder,且其參數(shù)為 TEMPLATE_STILL_CAPTURE
CaptureRequest.Builder captureRequestBuilder = mCamera.createCaptureRequest(
CameraDevice.TEMPLATE_STILL_CAPTURE);
//2. 添加它的 target 為 ImageReader 的 Surface
captureRequestBuilder.addTarget(mImageReader.getSurface());
//3. 設(shè)置自動對焦模式為預(yù)覽的自動對焦模式
captureRequestBuilder.set(CaptureRequest.CONTROL_AF_MODE,
mPreviewRequestBuilder.get(CaptureRequest.CONTROL_AF_MODE));
//4. 設(shè)置閃光燈與曝光參數(shù)
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 的旋轉(zhuǎn)角度
@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.停止預(yù)覽
mCaptureSession.stopRepeating();
// 7.抓取當(dāng)前圖片
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);
}
}
這是拍照的關(guān)鍵實現(xiàn),代碼有點長,但通過增加了帶時序的注釋,邏輯上看起來也就并不復(fù)雜了。這里只強調(diào) 3 個點,其他的看一看注釋即可,而關(guān)于設(shè)置閃光燈和曝光這里就省略了。
(1) 這里創(chuàng)建了一個新的 CaptureRequest.Builder ,且其參數(shù)為TEMPLATE_STILL_CAPTURE。相應(yīng)的其 CallBack 也是新的。
(2) 請求的 Target 只有 ImageReader 的 Surface,因此獲取到圖片后會輸出到 ImageReader。最后會在 ImageReader.OnImageAvailableListener 的 onImageAvailable 得到回調(diào)。
(3) 拍照前先停止了預(yù)覽請求,從這里可以看出拍照就是捕獲預(yù)覽模式下自動對焦成功鎖定后的圖像數(shù)據(jù)。
接下來就是等待 onCaptureCompleted 被系統(tǒng)回調(diào),然后進一步調(diào)用 unlockFocus()。
- unlockFocus()
void unlockFocus() {
// 取消了立即自動對焦的觸發(fā)
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);
// 重新打開預(yù)覽
mCaptureSession.setRepeatingRequest(mPreviewRequestBuilder.build(), mCaptureCallback,
null);
mCaptureCallback.setState(PictureCaptureCallback.STATE_PREVIEW);
} catch (CameraAccessException e) {
Log.e(TAG, "Failed to restart camera preview.", e);
}
}
該方法主要做的事情就是重新打開預(yù)覽,并且取消了立即自動對焦,同時將其設(shè)置為 CONTROL_AF_TRIGGER_IDLE,這將會解除自動對焦的狀態(tài),即其狀態(tài)不再是 CaptureResult.CONTROL_AF_STATE_FOCUSED_LOCKED。
系統(tǒng)組織好 ImageReader 需要的圖像數(shù)據(jù)后,就會回調(diào)其監(jiān)聽 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 就要復(fù)雜的多了,這里簡單說明一下。ImageReader 封裝了圖像的數(shù)據(jù)平面,而每個平面又封裝了 ByteBuffer 來保存原始數(shù)據(jù)。關(guān)于圖像的數(shù)據(jù)平面這個相對于圖像的格式來說的,比如 rgb 就只一個平面,而 YUV 一般就有 3 個平面。從 ByteBuffer 中獲取的數(shù)據(jù)都是最原始的數(shù)據(jù),對于 rgb 格式的數(shù)據(jù),就可以直接將其轉(zhuǎn)換成 Bitmap 然后給 ImageView 顯示。
到這里就分析完了拍照的過程了。
5.關(guān)閉相機
void stop() {
if (mCaptureSession != null) {
mCaptureSession.close();
mCaptureSession = null;
}
if (mCamera != null) {
mCamera.close();
mCamera = null;
}
if (mImageReader != null) {
mImageReader.close();
mImageReader = null;
}
}
全場最簡單,關(guān)閉會話,關(guān)閉相機,關(guān)閉 ImageReader,Game voer ?。?!
五、總結(jié)
文章對 Android Camera 編程進行了一個較為詳細的概括,尤其是對于偏難的 Camera 2 的 API 的理解,結(jié)合了官方的 Demo 對 API 及其參數(shù)進行了詳細的分析,以使得對 API 的理解更加透徹。
另外,如果你的項目需要集成 Camera,又不想自己去封裝,同時又覺得官方的 demo 還不夠,這里另外推薦一個 github 開源項目 camerakit-android。其也是從官方 demo fork 出來的,自動支持 camera api 1 以及 camera api 2。
最后,感謝你能讀到并讀完此文章。受限于作者水平有限,如果分析的過程中存在錯誤或者疑問都歡迎留言討論。如果我的分享能夠幫助到你,也請記得幫忙點個贊吧,鼓勵我繼續(xù)寫下去,謝謝。