該博客主要記錄在開發(fā)過程中所運用到的Api 和部分技術(shù)調(diào)用的記錄,閱讀時間:15分鐘+ ,該博客記錄內(nèi)容相對簡單,僅以用于開發(fā)過程記錄。
本文已獨家授權(quán)微信公眾號:鴻洋(hongyangAndroid)在微信公眾號平臺原創(chuàng)首發(fā)
說明
在我們前一篇文章中提到Camera,在開發(fā)中發(fā)現(xiàn)很多api 都已經(jīng)不推薦使用,google給出的替代方案則就是我們今天的主角 Camera2 ,從5.0開始(API Level 21),可以完全控制Android設備相機的新api 。當然如果產(chǎn)品覆蓋的還是有4.0版本的Android 用戶的話,還是建議 使用Camera。但是在以前的Camera 中,對相機的手動控制都是通過更改系統(tǒng)才能實現(xiàn),而且api也不友好。Camera2 這時針對這一點進行了管理的分離,Api會更加的友好,分工明確。
文章對應 Demo 地址
google demo 地址
文章參考地址
很感謝該篇文章作者的描述。
效果圖
1. camera2 概念
相對于camera ,camera2 在Api上將拍照對象進行了獨立,camera2采用pipeline的方式,將Camera 設備和 Android 設備連接起來,Android Device通過管道發(fā)送CaptureRequest拍照請求給Camera Device,Camera Device通過管道返回CameraMetadata數(shù)據(jù)給Android Device,這一切都發(fā)生在CameraCaptureSession的會話中。
2. camera2 API簡要說明
CameraCaptureSession:這是一個非常重要的API,當程序需要預覽、拍照時,都通過該類的實例創(chuàng)建Session,控制預覽的方法setRepeatingRequest();控制拍照的方法為capture()。
CameraDevices:提供一組靜態(tài)屬性信息,描述硬件設備以及設備的可用設置和輸出參數(shù)。通過getCameraCharacteristics獲得。
CameraManager:所有相機設備(CameraDevice)的管理者,要枚舉,查詢和打開可用的相機設備,用于打開和關(guān)閉系統(tǒng)攝像頭,就獲取CameraManager實例。
CaptureRequest:定義了相機設備捕獲單個映像所需的所有捕獲參數(shù)。該請求還列出了哪些配置的輸出表面應該用作此捕獲的目標。
CameraDevice:具有用于為給定用例創(chuàng)建請求構(gòu)建器的工廠方法,針對應用程序正在運行的Android設備進行了優(yōu)化,描述系統(tǒng)攝像頭,類似于早期的Camera。
CameraRequest CameraRequest.Builder:當程序調(diào)用setRepeatingRequest()方法進行預覽時,或調(diào)用capture()方法進行拍照時,都需要傳入CameraRequest參數(shù)。CameraRequest代表了一次捕獲請求,用于描述捕獲圖片的各種參數(shù)設置,程序?qū)φ掌龅母鞣N控制,都通過CameraRequest參數(shù)進行設置。CameraRequest.Builder則負責生成CameraRequest對象。
CameraCharacteristics:描述攝像頭的各種特性,我們可以通過CameraManager的getCameraCharacteristics(@NonNull String cameraId)方法來獲取。
CaptureResult:描述拍照完成后的結(jié)果。
ImageReader :通過添加 PreviewRequestBuilder.addTarget(mImageReader.getSurface()); 可以在 OnImageAvailableListener 接口中實時獲取 yuv 數(shù)據(jù)。
3.Camera2接口使用的流程
1.調(diào)用openCamera方法后會回調(diào)CameraDevice.StateCallback這個方法,在該方法里重寫onOpened函數(shù)。
2.在onOpened方法中調(diào)用createCaptureSession,該方法又回調(diào)CameraCaptureSession.StateCallback方法。
3.CameraCaptureSession.StateCallback中重寫onConfigured方法,設置setRepeatingRequest方法(也就是開啟預覽)。
4.setRepeatingRequest又會回調(diào) CameraCaptureSession.CaptureCallback方法。
5.重寫CameraCaptureSession.CaptureCallback中的onCaptureCompleted方法,result就是未經(jīng)過處理的幀數(shù)據(jù)了。
4. 自定義Camera2
Camera2與Camera一樣也有cameraId的概念,我們通過mCameraManager.getCameraIdList()來獲取cameraId列表,然后通過mCameraManager.getCameraCharacteristics(id) 獲取每個id對應攝像頭的參數(shù)。
關(guān)于CameraCharacteristics里面的參數(shù),主要用到的有以下幾個:
LENS_FACING:前置攝像頭(LENS_FACING_FRONT)還是后置攝像頭(LENS_FACING_BACK)。
SENSOR_ORIENTATION:攝像頭拍照方向。
FLASH_INFO_AVAILABLE:是否支持閃光燈。
CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL:獲取當前設備支持的相機特性。
注:事實上,在各個廠商的的Android設備上,Camera2的各種特性并不都是可用的,需要通過characteristics.get(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL)方法 來根據(jù)返回值來獲取支持的級別,具體說來:
INFO_SUPPORTED_HARDWARE_LEVEL_FULL:全方位的硬件支持,允許手動控制全高清的攝像、支持連拍模式以及其他新特性。
INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED:有限支持,這個需要單獨查詢。
INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY:所有設備都會支持,也就是和過時的Camera API支持的特性是一致的。
利用這個INFO_SUPPORTED_HARDWARE_LEVEL參數(shù),我們可以來判斷是使用Camera還是使用Camera
通過上面的 原理的說明,大致流程可能還會有點模糊,我們直接對應上述邏輯開始代碼。
4.1 界面布局
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout 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"
android:background="@android:color/black"
tools:context="cn.tongue.tonguecamera.ui.CameraActivity">
<FrameLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
// 自定義 TextureView
<cn.tongue.tonguecamera.view.AutoFitTextureView
android:id="@+id/textureView_g"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</FrameLayout>
<RelativeLayout
android:id="@+id/homecamera_bottom_relative2"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#00ffffff"
android:layout_alignParentBottom="true">
// 返回按鈕
<ImageView
android:id="@+id/iv_back_g"
android:layout_width="40dp"
android:layout_height="30dp"
android:scaleType="centerInside"
android:layout_marginBottom="20dp"
android:layout_marginStart="20dp"
android:layout_centerVertical="true"
android:background="@drawable/icon_back" />
// 拍照按鈕
<ImageView
android:id="@+id/img_camera_g"
android:layout_width="80dp"
android:layout_height="80dp"
android:scaleType="centerInside"
android:layout_marginBottom="20dp"
android:layout_centerInParent="true"
android:background="@drawable/camera" />
</RelativeLayout>
</RelativeLayout>
通過上述布局我們會發(fā)現(xiàn) camera2 中我們并沒有繼續(xù)選擇 SurfaceView作為呈現(xiàn)圖片的載體,這里選擇的TextureView。
@RequiresApi(api = Build.VERSION_CODES.M)
@Override
public void onResume() {
super.onResume();
// 啟動 HandlerThread ,后臺維護一個 handler
startBackgroundThread();
// 存在關(guān)聯(lián)則打開相機,沒有則綁定事件
if (mTextureView.isAvailable()) {
openCamera(mTextureView.getWidth(), mTextureView.getHeight());
} else {
// 綁定 載體監(jiān)聽事件
mTextureView.setSurfaceTextureListener(mSurfaceTextureListener);
}
}
如果是第一次進入 就需要配置 相機相關(guān)配置
/**
* 打開相機
*
* @param width 寬度
* @param height 長度
*/
@RequiresApi(api = Build.VERSION_CODES.M)
private void openCamera(int width, int height) {
// 判斷相機權(quán)限 6.0 以上的動態(tài)權(quán)限
if (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA)
!= PackageManager.PERMISSION_GRANTED) {
requestPermissions(new String[]{Manifest.permission.CAMERA}, REQUEST_CAMERA_PERMISSION);
return;
}
// 配置 相機的 預覽尺寸
setUpCameraOutputs(width, height);
// Matrix 轉(zhuǎn)換配置為 mTextureView
configureTransform(width, height);
CameraManager manager = (CameraManager) getSystemService(Context.CAMERA_SERVICE);
try {
if (!mCameraOpenCloseLock.tryAcquire(2500, TimeUnit.MILLISECONDS)) {
throw new RuntimeException("Time out waiting to lock camera opening.");
}
// 都配置完成后 打開相機 并綁定回調(diào)接口
manager.openCamera(mCameraId, mStateCallback, mBackgroundHandler);
} catch (CameraAccessException e) {
e.printStackTrace();
} catch (InterruptedException e) {
throw new RuntimeException("Interrupted while trying to lock camera opening.", e);
}
}
上述代碼中,setUpCameraOutputs() 方法主要進行 相機參數(shù)的設置(前后攝像頭,閃光燈的配置),設置mImageReader 的成像格式及數(shù)據(jù)流的回調(diào)監(jiān)聽事件 OnImageAvailableListener,并且根據(jù)硬件數(shù)據(jù) 查看是否需要交換尺寸以獲得相對于傳感器的預覽尺寸。
/**
* CameraDevice 改變狀態(tài)時候 調(diào)用
*/
private final CameraDevice.StateCallback mStateCallback = new CameraDevice.StateCallback() {
// 打開事件監(jiān)聽
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
@Override
public void onOpened(@NonNull CameraDevice cameraDevice) {
//打開相機時會調(diào)用此方法。 我們在這里開始相機預覽。
mCameraOpenCloseLock.release();
mCameraDevice = cameraDevice;
createCameraPreviewSession();
}
// 關(guān)閉監(jiān)聽
@Override
public void onDisconnected(@NonNull CameraDevice cameraDevice) {
mCameraOpenCloseLock.release();
cameraDevice.close();
mCameraDevice = null;
}
@Override
public void onError(@NonNull CameraDevice cameraDevice, int error) {
mCameraOpenCloseLock.release();
cameraDevice.close();
mCameraDevice = null;
finish();
}
};
從回調(diào)方法中,我們 通過 CameraCaptureSession 開始 預覽圖像,createCameraPreviewSession() 則是創(chuàng)建一個 相機預覽。
/**
* 創(chuàng)建一個新的相機預覽
*/
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
private void createCameraPreviewSession() {
try {
SurfaceTexture texture = mTextureView.getSurfaceTexture();
//將默認緩沖區(qū)的大小配置為相機預覽的大小。
texture.setDefaultBufferSize(mPreviewSize.getWidth(), mPreviewSize.getHeight());
Surface surface = new Surface(texture);
// Camera2都是通過創(chuàng)建請求會話的方式進行調(diào)用的
mPreviewRequestBuilder = mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);
// 使用Surface設置CaptureRequest.Builder
mPreviewRequestBuilder.addTarget(surface);
// 方法創(chuàng)建CaptureSession。
mCameraDevice.createCaptureSession(Arrays.asList(surface, mImageReader.getSurface()),
new CameraCaptureSession.StateCallback() {
@Override
public void onConfigured(@NonNull CameraCaptureSession cameraCaptureSession) {
if (null == mCameraDevice) {
return;
}
mCaptureSession = cameraCaptureSession;
try {
// 自動變焦是連續(xù)的
mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AF_MODE,
CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE);
setAutoFlash(mPreviewRequestBuilder);
// 顯示相機預覽
mPreviewRequest = mPreviewRequestBuilder.build();
//設置反復捕獲數(shù)據(jù)的請求,這樣預覽界面就會一直有數(shù)據(jù)顯示
mCaptureSession.setRepeatingRequest(mPreviewRequest,
mCaptureCallback, mBackgroundHandler);
} catch (CameraAccessException e) {
e.printStackTrace();
}
}
@Override
public void onConfigureFailed(
@NonNull CameraCaptureSession cameraCaptureSession) {
}
}, null
);
} catch (CameraAccessException e) {
e.printStackTrace();
}
}
createCaptureRequest()方法里參數(shù)templateType代表了請求類型,請求類型一共分為六種,分別為:
TEMPLATE_PREVIEW:創(chuàng)建預覽的請求
TEMPLATE_STILL_CAPTURE:創(chuàng)建一個適合于靜態(tài)圖像捕獲的請求,圖像質(zhì)量優(yōu)先于幀速率。
TEMPLATE_RECORD:創(chuàng)建視頻錄制的請求
TEMPLATE_VIDEO_SNAPSHOT:創(chuàng)建視視頻錄制時截屏的請求
TEMPLATE_ZERO_SHUTTER_LAG:創(chuàng)建一個適用于零快門延遲的請求。在不影響預覽幀率的情況下最大化圖像質(zhì)量。
TEMPLATE_MANUAL:創(chuàng)建一個基本捕獲請求,這種請求中所有的自動控制都是禁用的(自動曝光,自動白平衡、自動焦點)。
createCaptureSession()方法一共包含三個參數(shù):
1.List outputs:我們需要輸出到的Surface列表。
2.CameraCaptureSession.StateCallback callback:會話狀態(tài)相關(guān)回調(diào)。
3.Handler handler:callback可以有多個(來自不同線程),這個handler用來區(qū)別那個callback應該被回調(diào),一般寫當前線程的Handler即可。
關(guān)于CameraCaptureSession.StateCallback里的回調(diào)方法:
1.onConfigured(@NonNull CameraCaptureSession session); 攝像頭完成配置,可以處理Capture請求了。
2.onConfigureFailed(@NonNull CameraCaptureSession session); 攝像頭配置失敗
3.onReady(@NonNull CameraCaptureSession session); 攝像頭處于就緒狀態(tài),當前沒有請求需要處理。
onActive(@NonNull CameraCaptureSession session); 攝像頭正在處理請求。
onClosed(@NonNull CameraCaptureSession session); 會話被關(guān)閉
onSurfacePrepared(@NonNull CameraCaptureSession session, @NonNull Surface surface); Surface準備就緒
理解了這些東西,創(chuàng)建預覽請求就十分簡單了。
設置預覽界面尺寸信息,Surface就把它與CaptureRequestBuilder對象關(guān)聯(lián),然后就是設置會話開始捕獲畫面。最后的回調(diào)CameraCaptureSession.CaptureCallback就給我們設置預覽完成的邏輯處理
/**
* ImageReader 的回調(diào)對象。 靜止圖像已準備好保存。
*/
private final ImageReader.OnImageAvailableListener mOnImageAvailableListener
= new ImageReader.OnImageAvailableListener() {
@Override
public void onImageAvailable(ImageReader reader) {
Log.e(TAG, "onImageAvailable:-------------------");
Image image = reader.acquireLatestImage();
//我們可以將這幀數(shù)據(jù)轉(zhuǎn)成字節(jié)數(shù)組,類似于Camera1的PreviewCallback回調(diào)的預覽幀數(shù)據(jù)
ByteBuffer buffer = image.getPlanes()[0].getBuffer();
byte[] data = new byte[buffer.remaining()];
buffer.get(data);
image.close();
// mBackgroundHandler.post(new ImageSaver(reader.acquireNextImage(), mFile));
}
};
最后這里的方法 就是同 camera 中的 實時幀數(shù)據(jù),這里我們可以在這里獲取 靜止圖片的 幀數(shù)據(jù),進行一些 圖層 水印的處理。google 源碼中 一直維系了一個mBackgroundHandler ,我們可以在這里發(fā)送 子線程任務reade.acquireNextImage方法獲取 靜止圖片信息,將圖片保存到本地文件夾中,基本流程完成后,我們只需要看一下如何觸發(fā) 拍照即可。
/**
* 鎖定焦點設置
*/
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
private void lockFocus() {
try {
// 相機鎖定的方法 (設置相機對焦)
mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AF_TRIGGER,
CameraMetadata.CONTROL_AF_TRIGGER_START);
// mCaptureCallback 等待鎖定 //修改狀態(tài)
mState = STATE_WAITING_LOCK;
//發(fā)送對焦請求
mCaptureSession.capture(mPreviewRequestBuilder.build(), mCaptureCallback,
mBackgroundHandler);
} catch (CameraAccessException e) {
e.printStackTrace();
}
}
通過點擊事件,調(diào)用相機鎖定 ,設置 mCaptureSession.capture,
/**
* 處理與jpg文件捕捉的事件監(jiān)聽(預覽)
*/
private CameraCaptureSession.CaptureCallback mCaptureCallback
= new CameraCaptureSession.CaptureCallback() {
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
private void process(CaptureResult result) {
switch (mState) {
case STATE_PREVIEW: {
// 預覽正常
break;
}
//等待對焦
case STATE_WAITING_LOCK: {
Integer afState = result.get(CaptureResult.CONTROL_AF_STATE);
if (afState == null) {
// 對焦失敗 直接拍照
captureStillPicture();
} else if (CaptureResult.CONTROL_AF_STATE_FOCUSED_LOCKED == afState ||
CaptureResult.CONTROL_AF_STATE_NOT_FOCUSED_LOCKED == afState) {
Integer aeState = result.get(CaptureResult.CONTROL_AE_STATE);
if (aeState == null ||
aeState == CaptureResult.CONTROL_AE_STATE_CONVERGED) {
mState = STATE_PICTURE_TAKEN;
// 對焦完成,進行拍照
captureStillPicture();
} else {
runPrecaptureSequence();
}
}
break;
}
case STATE_WAITING_PRECAPTURE: {
Integer aeState = result.get(CaptureResult.CONTROL_AE_STATE);
if (aeState == null ||
aeState == CaptureResult.CONTROL_AE_STATE_PRECAPTURE ||
aeState == CaptureRequest.CONTROL_AE_STATE_FLASH_REQUIRED) {
mState = STATE_WAITING_NON_PRECAPTURE;
}
break;
}
case STATE_WAITING_NON_PRECAPTURE: {
Integer aeState = result.get(CaptureResult.CONTROL_AE_STATE);
if (aeState == null || aeState != CaptureResult.CONTROL_AE_STATE_PRECAPTURE) {
mState = STATE_PICTURE_TAKEN;
captureStillPicture();
}
break;
}
default:
break;
}
}
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
@Override
public void onCaptureProgressed(@NonNull CameraCaptureSession session,
@NonNull CaptureRequest request,
@NonNull CaptureResult partialResult) {
process(partialResult);
}
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
@Override
public void onCaptureCompleted(@NonNull CameraCaptureSession session,
@NonNull CaptureRequest request,
@NonNull TotalCaptureResult result) {
process(result);
}
};
/**
* 拍攝靜止圖片。 當我們得到響應時,應該調(diào)用此方法
*/
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
private void captureStillPicture() {
try {
if (null == activity || null == mCameraDevice) {
return;
}
final CaptureRequest.Builder captureBuilder =
mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_STILL_CAPTURE);
captureBuilder.addTarget(mImageReader.getSurface());
// 使用與預覽相同的AE和AF模式。
captureBuilder.set(CaptureRequest.CONTROL_AF_MODE,
CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE);
setAutoFlash(captureBuilder);
int rotation = getWindowManager().getDefaultDisplay().getRotation();
// 設置 方向
captureBuilder.set(CaptureRequest.JPEG_ORIENTATION, getOrientation(rotation));
//創(chuàng)建會話
CameraCaptureSession.CaptureCallback CaptureCallback
= new CameraCaptureSession.CaptureCallback() {
@Override
public void onCaptureCompleted(@NonNull CameraCaptureSession session,
@NonNull CaptureRequest request,
@NonNull TotalCaptureResult result) {
Log.e(TAG, mFile.toString());
unlockFocus();
}
};
mCaptureSession.stopRepeating();
mCaptureSession.abortCaptures();
mCaptureSession.capture(captureBuilder.build(), CaptureCallback, mBackgroundHandler);
} catch (CameraAccessException e) {
e.printStackTrace();
}
}
此篇文章主要記錄 camera2 自定義相機的簡單流程,代碼基本都來自于 Google 提供的代碼,當然 如果有 Kotlin 的朋友,也可以在文章 開頭進入Google github 地址查看Kotlin 版本。