Android Camera2教程之開啟相機、開啟預覽、實現PreviewCallback、拍照
Android API 21新增了Camera2,這與之前的camera架構完全不同,使用起來也比較複雜,但是功能變得很強大。
在講解開啟預覽之前,首先需要了解camera2的幾個比較重要的類:
CameraManager: 管理手機上的所有攝像頭裝置,它的作用主要是獲取攝像頭列表和開啟指定的攝像頭
CameraDevice: 具體的攝像頭裝置,它有一系列引數(預覽尺寸、拍照尺寸等),可以通過CameraManager的getCameraCharacteristics()方法獲取。它的作用主要是建立CameraCaptureSession和CaptureRequest
CameraCaptureSession: 相機捕獲會話,用於處理拍照和預覽的工作(很重要)
CaptureRequest: 捕獲請求,定義輸出緩衝區以及顯示介面(TextureView或SurfaceView)等
1,定義TextureView作為預覽介面
在佈局檔案中加入TextureView控制元件,然後實現其監聽事件
textureView = (TextureView) findViewById(R.id.textureView);
1
然後我們可以在OnResume()方法中設定監聽SurefaceTexture的事件
textureView.setSurfaceTextureListener(textureListener);
1
當SurefaceTexture準備好後會回撥SurfaceTextureListener 的onSurfaceTextureAvailable()方法
TextureView.SurfaceTextureListener textureListener = new TextureView.SurfaceTextureListener() {
public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) {
//當SurefaceTexture可用的時候,設定相機引數並開啟相機
setupCamera(width, height);
openCamera();
}
};
2,設定相機引數
為了更好地預覽,我們根據TextureView的尺寸設定預覽尺寸,Camera2中使用CameraManager來管理攝像頭
private void setupCamera(int width, int height) {
//獲取攝像頭的管理者CameraManager
CameraManager manager = (CameraManager) getSystemService(Context.CAMERA_SERVICE);
try {
//遍歷所有攝像頭
for (String cameraId: manager.getCameraIdList()) {
CameraCharacteristics characteristics = manager.getCameraCharacteristics(cameraId);
//預設開啟後置攝像頭
if (characteristics.get(CameraCharacteristics.LENS_FACING) == CameraCharacteristics.LENS_FACING_FRONT)
continue;
//獲取StreamConfigurationMap,它是管理攝像頭支援的所有輸出格式和尺寸
StreamConfigurationMap map = characteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP);
//根據TextureView的尺寸設定預覽尺寸
mPreviewSize = getOptimalSize(map.getOutputSizes(SurfaceTexture.class), width, height);
mCameraId = cameraId;
break;
}
} catch (CameraAccessException e) {
e.printStackTrace();
}
}
3,開啟相機
Camera2中開啟相機也需要通過CameraManager類
private void openCamera() {
//獲取攝像頭的管理者CameraManager
CameraManager manager = (CameraManager) getSystemService(Context.CAMERA_SERVICE);
//檢查許可權
try {
if (ActivityCompat.checkSelfPermission(this, Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED) {
return;
}
//開啟相機,第一個引數指示開啟哪個攝像頭,第二個引數stateCallback為相機的狀態回撥介面,第三個引數用來確定Callback在哪個執行緒執行,為null的話就在當前執行緒執行
manager.openCamera(mCameraId, stateCallback, null);
} catch (CameraAccessException e) {
e.printStackTrace();
}
}
實現StateCallback 介面,當相機開啟後會回撥onOpened方法,在這個方法裡面開啟預覽
private final CameraDevice.StateCallback stateCallback = new CameraDevice.StateCallback() {
@Override
public void onOpened(CameraDevice camera) {
mCameraDevice = camera;
//開啟預覽
startPreview();
}
}
4,開啟相機預覽
我們使用TextureView顯示相機預覽資料,Camera2的預覽和拍照資料都是使用CameraCaptureSession會話來請求的
private void startPreview() {
SurfaceTexture mSurfaceTexture = mTextureView.getSurfaceTexture();
//設定TextureView的緩衝區大小
mSurfaceTexture.setDefaultBufferSize(mPreviewSize.getWidth(), mPreviewSize.getHeight());
//獲取Surface顯示預覽資料
Surface mSurface = new Surface(mSurfaceTexture);
try {
//建立CaptureRequestBuilder,TEMPLATE_PREVIEW比表示預覽請求
mCaptureRequestBuilder = mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);
//設定Surface作為預覽資料的顯示介面
mCaptureRequestBuilder.addTarget(mSurface);
//建立相機捕獲會話,第一個引數是捕獲資料的輸出Surface列表,第二個引數是CameraCaptureSession的狀態回撥介面,當它建立好後會回撥onConfigured方法,第三個引數用來確定Callback在哪個執行緒執行,為null的話就在當前執行緒執行
mCameraDevice.createCaptureSession(Arrays.asList(mSurface), new CameraCaptureSession.StateCallback() {
@Override
public void onConfigured(CameraCaptureSession session) {
try {
//建立捕獲請求
mCaptureRequest = mCaptureRequestBuilder.build();
mPreviewSession = session;
//設定反覆捕獲資料的請求,這樣預覽介面就會一直有資料顯示
mPreviewSession.setRepeatingRequest(mCaptureRequest, mSessionCaptureCallback, null);
} catch (CameraAccessException e) {
e.printStackTrace();
}
}
@Override
public void onConfigureFailed(CameraCaptureSession session) {
}
}, null);
} catch (CameraAccessException e) {
e.printStackTrace();
}
}
5,實現PreviewCallback
Camera2中並沒有Camera1中的PreviewCallback介面,那怎麼實現獲取預覽幀資料呢?答案就是使用ImageReader間接實現
首先建立一個ImageReader,並監聽它的事件
private void setupImageReader() {
//前三個引數分別是需要的尺寸和格式,最後一個引數代表每次最多獲取幾幀資料,本例的2代表ImageReader中最多可以獲取兩幀影象流
mImageReader = ImageReader.newInstance(mPreviewSize.getWidth(), mPreviewSize.getHeight(),
ImageFormat.JPEG, 2);
//監聽ImageReader的事件,當有影象流資料可用時會回撥onImageAvailable方法,它的引數就是預覽幀資料,可以對這幀資料進行處理
mImageReader.setOnImageAvailableListener(new ImageReader.OnImageAvailableListener() {
@Override
public void onImageAvailable(ImageReader reader) {
Image image = reader.acquireLatestImage();
//我們可以將這幀資料轉成位元組陣列,類似於Camera1的PreviewCallback回撥的預覽幀資料
ByteBuffer buffer = image.getPlanes()[0].getBuffer();
byte[] data = new byte[buffer.remaining()];
buffer.get(data);
image.close();
}
}, null);
}
注意:一定要呼叫reader.acquireLatestImage()和close()方法,否則畫面就會卡住
然後我們在開啟預覽之前,設定ImageReader為輸出Surface
setupImageReader();
//獲取ImageReader的Surface
Surface imageReaderSurface = mImageReader.getSurface();
//CaptureRequest新增imageReaderSurface,不加的話就會導致ImageReader的onImageAvailable()方法不會回撥
mCaptureRequestBuilder.addTarget(imageReaderSurface);
//建立CaptureSession時加上imageReaderSurface,如下,這樣預覽資料就會同時輸出到previewSurface和imageReaderSurface了
mCameraDevice.createCaptureSession(Arrays.asList(previewSurface, imageReaderSurface), new CameraCaptureSession.StateCallback() {
}
關閉相機時別忘了關閉ImageReader
6,拍照
Camera2拍照也是通過ImageReader來實現的
首先先做些準備工作,設定拍照引數,如方向、尺寸等
private static final SparseIntArray ORIENTATION = new SparseIntArray();
static {
ORIENTATION.append(Surface.ROTATION_0, 90);
ORIENTATION.append(Surface.ROTATION_90, 0);
ORIENTATION.append(Surface.ROTATION_180, 270);
ORIENTATION.append(Surface.ROTATION_270, 180);
}
設定拍照尺寸,可以跟預覽尺寸一起設定,然後ImageReader初始化使用此尺寸
mCaptureSize = Collections.max(Arrays.asList(map.getOutputSizes(ImageFormat.JPEG)), new Comparator<Size>() {
@Override
public int compare(Size lhs, Size rhs) {
return Long.signum(lhs.getWidth() * lhs.getHeight() - rhs.getHeight() * rhs.getWidth());
}
});
建立儲存圖片的執行緒
public static class imageSaver implements Runnable {
private Image mImage;
public imageSaver(Image image) {
mImage = image;
}
@Override
public void run() {
ByteBuffer buffer = mImage.getPlanes()[0].getBuffer();
byte[] data = new byte[buffer.remaining()];
buffer.get(data);
mImageFile = new File(Environment.getExternalStorageDirectory() + "/DCIM/myPicture.jpg");
FileOutputStream fos = null;
try {
fos = new FileOutputStream(mImageFile);
fos.write(data, 0 ,data.length);
} catch (IOException e) {
e.printStackTrace();
} finally {
mImageFile = null;
if (fos != null) {
try {
fos.close();
fos = null;
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
然後當ImageReader有資料時,通過此執行緒儲存圖片
//使用前面獲取的拍照尺寸
mImageReader = ImageReader.newInstance(mCaptureSize.getWidth(), mCaptureSize.getHeight(),
ImageFormat.JPEG, 2);
mImageReader.setOnImageAvailableListener(new ImageReader.OnImageAvailableListener() {
@Override
public void onImageAvailable(ImageReader reader) {
//執行影象儲存子執行緒
mCameraHandler.post(new imageSaver(reader.acquireNextImage()));
}
}, mCameraHandler);
然後開啟預覽建立CaptureSession時把ImageReader新增進去
mCameraDevice.createCaptureSession(Arrays.asList(previewSurface, mImageReader.getSurface()), new CameraCaptureSession.StateCallback() {
}
現在準備工作做好了,還需要響應點選拍照事件,我們設定點選拍照按鈕呼叫capture()方法,capture()方法即實現拍照
private void capture() {
try {
//首先我們建立請求拍照的CaptureRequest
final CaptureRequest.Builder mCaptureBuilder = mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_STILL_CAPTURE);
//獲取螢幕方向
int rotation = getWindowManager().getDefaultDisplay().getRotation();
//設定CaptureRequest輸出到mImageReader
mCaptureBuilder.addTarget(mImageReader.getSurface());
//設定拍照方向
mCaptureBuilder.set(CaptureRequest.JPEG_ORIENTATION, ORIENTATION.get(rotation));
//這個回撥介面用於拍照結束時重啟預覽,因為拍照會導致預覽停止
CameraCaptureSession.CaptureCallback mImageSavedCallback = new CameraCaptureSession.CaptureCallback() {
@Override
public void onCaptureCompleted(CameraCaptureSession session, CaptureRequest request, TotalCaptureResult result) {
Toast.makeText(getApplicationContext(), "Image Saved!", Toast.LENGTH_SHORT).show();
//重啟預覽
restartPreview();
}
};
//停止預覽
mCameraCaptureSession.stopRepeating();
//開始拍照,然後回撥上面的介面重啟預覽,因為mCaptureBuilder設定ImageReader作為target,所以會自動回撥ImageReader的onImageAvailable()方法儲存圖片
mCameraCaptureSession.capture(mCaptureBuilder.build(), mImageSavedCallback, null);
} catch (CameraAccessException e) {
e.printStackTrace();
}
}
重啟預覽的方法很簡單了
private void restartPreview() {
try {
//執行setRepeatingRequest方法就行了,注意mCaptureRequest是之前開啟預覽設定的請求
mCameraCaptureSession.setRepeatingRequest(mCaptureRequest, null, mCameraHandler);
} catch (CameraAccessException e) {
e.printStackTrace();
}