從5.0開始(API Level 21),可以完全控制Android設備相機的新api Camera2(android.hardware.Camera2)被引入了進來。在以前的Camera api(android.hardware.Camera)中,對相機的手動控制需要更改系統才能實現,而且api也不友好。不過老的Camera API在5.0上已經過時,在未來的app開發中推薦的是Camera2 API。
1、Camera2介紹
在Camera類中我們多是使用這個類的對象去調用方法,而Camera2則是使用多個類去設置,功能更加強大。
Camera2架構:
Google採用了pipeline(管道)的概念,將Camera Device相機設備和Android Device安卓設備連接起來, Android Device通過管道發送CaptureRequest拍照請求給Camera Device,Camera Device通過管道返回CameraMetadata數據給Android Device,這一切建立在一個叫作CameraCaptureSession的會話中。
基本上我們需要使用的就是這些類啦。其中CameraManager是所有相機設備(CameraDevice)的管理者,要枚舉,查詢和打開可用的相機設備,就獲取CameraManager實例。
單個CameraDevices提供一組靜態屬性信息,描述硬體設備以及設備的可用設置和輸出參數。該信息通過CameraCharacteristics對象提供,可通過getCameraCharacteristics(String)獲得。
CameraCharacteristics是CameraDevice的屬性描述類,在CameraCharacteristics中可以進行相機設備功能的詳細設定(當然了,首先你得確定你的相機設備支持這些功能才行)。
要從相機設備捕獲或流式傳輸圖像,應用程式必須首先使用createCaptureSession(List,CameraCaptureSession.StateCallback,Handler)與相機設備一起使用一組輸出Surfaces創建攝像機捕獲會話。每個Surface必須預先配置適當的大小和格式(如果適用)以匹配相機設備可用的大小和格式。目標Surface可以從各種類獲得。
CameraCaptureSession:這是一個非常重要的API,當程序需要預覽、拍照時,都需要先通過該類的實例創建Session。而且不管預覽還是拍照,也都是由該對象的方法進行控制的,其中控制預覽的方法為setRepeatingRequest();控制拍照的方法為capture()。
通常,相機預覽圖像將發送到SurfaceView或TextureView(通過其SurfaceTexture)。
然後,應用程式需要構建一個CaptureRequest,它定義了相機設備捕獲單個映像所需的所有捕獲參數。該請求還列出了哪些配置的輸出表面應該用作此捕獲的目標。 CameraDevice具有用於為給定用例創建請求構建器的工廠方法,針對應用程式正在運行的Android設備進行了優化。
CameraRequest和CameraRequest.Builder:當程序調用setRepeatingRequest()方法進行預覽時,或調用capture()方法進行拍照時,都需要傳入CameraRequest參數。CameraRequest代表了一次捕獲請求,用於描述捕獲圖片的各種參數設置,比如對焦模式、曝光模式……總之,程序需要對照片所做的各種控制,都通過CameraRequest參數進行設置。CameraRequest.Builder則負責生成CameraRequest對象。
一旦請求被建立,它可以交給主動捕獲會話進行單次捕獲或無休止地重複使用。處理請求後,相機設備將產生一個TotalCaptureResult對象,該對象包含有關拍攝時相機設備狀態的信息以及使用的最終設置。如果需要捨入或解決矛盾的參數,這些請求可能會有所不同。相機設備還會將圖像數據幀發送到請求中包括的每個輸出表面。這些相對於輸出CaptureResult是異步產生的,有時候稍後會產生。類圖中有著三個重要的callback,其中CameraCaptureSession.CaptureCallback將處理預覽和拍照圖片的工作,需要重點對待。
這兩幅對Camera2接口使用的流程介紹我們綜合起來看會有更深的理解。
1.可以看出調用openCamera方法後會回調CameraDevice.StateCallback這個方法,在該方法裡重寫onOpened函數。
2.在onOpened方法中調用createCaptureSession,該方法又回調CameraCaptureSession.StateCallback方法。
3.在CameraCaptureSession.StateCallback中重寫onConfigured方法,設置setRepeatingRequest方法(也就是開啟預覽)。
4.setRepeatingRequest又會回調 CameraCaptureSession.CaptureCallback方法。
5.重寫CameraCaptureSession.CaptureCallback中的onCaptureCompleted方法,result就是未經過處理的元數據了。
順便提一下CameraCaptureSession.CaptureCallback中的onCaptureProgressed方法很明顯是在Capture過程中的,也就是在onCaptureCompleted之前,所以,在這之前想對圖像幹什麼就看你的了,像美顏等操作就可以在這個方法中實現了。
可以看出Camera2相機使用的邏輯還是比較簡單的,其實就是3個Callback函數的回調,先說一下:setRepeatingRequest和capture方法其實都是向相機設備發送獲取圖像的請求,但是capture就獲取那麼一次,而setRepeatingRequest就是不停的獲取圖像數據,所以呢,使用capture就想拍照一樣,圖像就停在那裡了,但是setRepeatingRequest一直在發送和獲取,所以需要連拍的時候就調用它,然後在onCaptureCompleted中保存圖像就行了。(注意了,圖像的預覽也是用的setRepeatingRequest,只是你不處理數據就行了)。
通過上面對Camera2的API的分析,我們可以知道控制拍照的大致步驟為:
調用CameraManager的openCamera(String cameraId, CameraDevice.StateCallback callback, Handler handler)方法打開指定攝像頭。該方法的第一個參數代表要打開的攝像頭ID;第二個參數用於監聽攝像頭的狀態;第三個參數代表執行callback的Handler,如果程序希望直接在當前線程中執行callback,則可將handler參數設為null。
當攝像頭被打開之後,程序即可獲取CameraDevice—即根據攝像頭ID獲取了指定攝像頭設備,然後調用CameraDevice的createCaptureSession(List outputs, CameraCaptureSession. StateCallback callback,Handler handler)方法來創建CameraCaptureSession。該方法的第一個參數是一個List集合,封裝了所有需要從該攝像頭獲取圖片的Surface,第二個參數用於監聽CameraCaptureSession的創建過程;第三個參數代表執行callback的Handler,如果程序希望直接在當前線程中執行callback,則可將handler參數設為null。
不管預覽還是拍照,程序都調用CameraDevice的createCaptureRequest(int templateType)方法創建CaptureRequest.Builder,該方法支持TEMPLATE_PREVIEW(預覽)、TEMPLATE_RECORD(拍攝視頻)、TEMPLATE_STILL_CAPTURE(拍照)等參數。
通過第3步所調用方法返回的CaptureRequest.Builder設置拍照的各種參數,比如對焦模式、曝光模式等。
調用CaptureRequest.Builder的build()方法即可得到CaptureRequest對象,接下來程序可通過CameraCaptureSession的setRepeatingRequest()方法開始預覽,或調用capture()方法拍照。
2、自定義相機
經過上面的說明,相信大家對Camera2的接口已經有了一定的了解,不是很清楚不要緊,實踐出真知,我們就開始上代碼啦。
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextureView
android:id="@+id/textureView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:layout_alignParentTop="true" />
<FrameLayout
android:id="@+id/control"
android:layout_width="match_parent"
android:layout_height="112dp"
android:layout_alignParentBottom="true"
android:layout_alignParentStart="true" //true
android:background="@color/control_background">
<Button
android:id="@+id/picture"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:text="@string/picture" />
</FrameLayout>
</RelativeLayout>
既然只是示例,我們的布局就簡單一些就好,就下來我們先為TextureView設置好它的回調:
@Override
public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) {
setupCamera();
openCamera();
}
@Override //1
public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height) {
} // 3
@Override //2
public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) {
return false;
} //4
@Override //3
public void onSurfaceTextureUpdated(SurfaceTexture surface) {
} //12
我們這個案例主要是為了介紹如何用Camera2實現拍照,所以關於尺寸大小適配的處理就不多做了,所以我們就在onSurfaceTextureSizeChanged()中設置並打開Camera。
private void setupCamera() {
//獲取攝像頭的管理者CameraManager
CameraManager manager = (CameraManager) getSystemService(Context.CAMERA_SERVICE);
try {
//遍歷所有攝像頭
for (String id : manager.getCameraIdList()) {
CameraCharacteristics characteristics = manager.getCameraCharacteristics(id);
//默認打開後置攝像頭
if (characteristics.get(CameraCharacteristics.LENS_FACING) == CameraCharacteristics.LENS_FACING_FRONT)
continue;
//獲取StreamConfigurationMap,它是管理攝像頭支持的所有輸出格式和尺寸
StreamConfigurationMap map = characteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP);
// 對於靜態圖像捕獲,我們使用最大的可用尺寸。
mPreviewSize = 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());
}
});
mCameraId = id;
break;
}
} catch (CameraAccessException e) {
e.printStackTrace();
}
} // 1
我們這裡就啟用後置攝像頭,setupCamera()我們就是設置圖像尺寸並獲得攝像頭ID,方便我們在openCamera()中使用。
private void openCamera() {
//獲取攝像頭的管理者CameraManager 1
CameraManager manager = (CameraManager) getSystemService(Context.CAMERA_SERVICE); // 2
//檢查權限
try { // try 1
if (ActivityCompat.checkSelfPermission(this, Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED) {
return;
}//6666
//打開相機,第一個參數指示打開哪個攝像頭,第二個參數stateCallback為相機的狀態回調接口,第三個參數用來確定Callback在哪個線程執行,為null的話就在當前線程執行
manager.openCamera(mCameraId, stateCallback, null);
} catch (CameraAccessException e) {//1001
e.printStackTrace();//rr
}//8888
} //2
這樣我們算是完成了第一步,按照流程圖接下來就是啟用我們設備的回調開始預覽:
private final CameraDevice.StateCallback stateCallback = new CameraDevice.StateCallback() {
@Override
public void onOpened(CameraDevice camera) {
mCameraDevice = camera;
//開啟預覽
startPreview();
}//9999
@Override //111
public void onDisconnected(CameraDevice camera) {
}//11111
@Override //22222
public void onError(CameraDevice camera, int error) {
}//22222
};
mCameraDevice是我設置的CameraDevice對象,現在給它初始化,我們知道CameraDevice相當於舊的Camera,所以我們就得到了這個攝像頭。
private void startPreview() {
SurfaceTexture mSurfaceTexture = mPreviewView.getSurfaceTexture();
//設置TextureView的緩衝區大小
mSurfaceTexture.setDefaultBufferSize(mPreviewSize.getWidth(), mPreviewSize.getHeight());
//獲取Surface顯示預覽數據
Surface mSurface = new Surface(mSurfaceTexture);
setupImageReader();
//獲取ImageReader的Surface
Surface imageReaderSurface = mImageReader.getSurface();
try {//1111
//創建CaptureRequestBuilder,TEMPLATE_PREVIEW比表示預覽請求
mPreviewBuilder = mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);
//設置Surface作為預覽數據的顯示界面
mPreviewBuilder.addTarget(mSurface);
//創建相機捕獲會話,第一個參數是捕獲數據的輸出Surface列表,第二個參數是CameraCaptureSession的狀態回調接口,當它創建好後會回調onConfigured方法,第三個參數用來確定Callback在哪個線程執行,為null的話就在當前線程執行
mCameraDevice.createCaptureSession(Arrays.asList(mSurface, imageReaderSurface), mSessionStateCallback, null);
} catch (CameraAccessException e) {//ww
e.printStackTrace();//gg
}//33333
} //31
這個方法就是我們實現預覽的關鍵,我們設置好了Surface就把它與CaptureRequestBuilder對象關聯,然後就是設置會話開始捕獲畫面。
private CameraCaptureSession.StateCallback mSessionStateCallback = new CameraCaptureSession.StateCallback() {
@Override //3333
public void onConfigured(CameraCaptureSession session) {
try {
//創建捕獲請求
mCaptureRequest = mPreviewBuilder.build();
mPreviewSession = session;
//設置反覆捕獲數據的請求,這樣預覽界面就會一直有數據顯示
mPreviewSession.setRepeatingRequest(mCaptureRequest, mSessionCaptureCallback, mHandler);
} catch (CameraAccessException e) {
e.printStackTrace();
} // 501
}//44444
@Override //4444
public void onConfigureFailed(CameraCaptureSession session) {
}//55555
};//666666
最後的回調CameraCaptureSession.CaptureCallback就給我們設置預覽完成的邏輯處理:
private CameraCaptureSession.CaptureCallback mSessionCaptureCallback = new CameraCaptureSession.CaptureCallback() {
@Override ///555
public void onCaptureCompleted(CameraCaptureSession session, CaptureRequest request, TotalCaptureResult result) {
super.onCaptureCompleted(session, request, result);
//重啟預覽
restartPreview();
}//66666
};//1212
private void restartPreview() {
try {//2222
//執行setRepeatingRequest方法就行了,注意mCaptureRequest是之前開啟預覽設置的請求
mPreviewSession.setRepeatingRequest(mCaptureRequest, null, mHandler);
} catch (CameraAccessException e) {//ff
e.printStackTrace();//7777
}//77777
} //401
這樣就創建好了,但是要注意的是因為Camera2沒有onPictureTaken()方法,所以我們不能直接獲得圖像數據,這裡我們要用的是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) {
mHandler.post(new ImageSaver(reader.acquireNextImage()));
}//601
}, mHandler);
} // 7
在處理ImageReader我們可以用handler來做:
public class ImageSaver implements Runnable {
private Image mImage;
private File mFile;
public ImageSaver(Image image) {
this.mImage = image;
}//701
@Override //1313
public void run() {
ByteBuffer buffer = mImage.getPlanes()[0].getBuffer();
byte[] bytes = new byte[buffer.remaining()];
buffer.get(bytes);
FileOutputStream output = null;
SimpleDateFormat sdf = new SimpleDateFormat(
"yyyyMMdd_HHmmss",
Locale.US);
String fname = "IMG_" +
sdf.format(new Date())
+ ".jpg";
mFile = new File(getApplication().getExternalFilesDir(null), fname);
try {
output = new FileOutputStream(mFile);
output.write(bytes);
} catch (IOException e) {
e.printStackTrace();
}
finally {
mImage.close();
if (null != output) {
try {
output.close();
} catch (IOException e) {
e.printStackTrace();
}
}//5555
}/1717
}//801
} // 8
這個Run()方法裡做的就是把從Image中獲得的幀數據輸出到指定的文件裡,文件名我們用當前時間來生成。
這樣我們就做好所有的拍照前的設置了,現在只要處理點擊按鈕時進行拍照即可。
private HandlerThread mThreadHandler;
private TextureView mPreviewView;
private Handler mHandler = new Handler();
private CaptureRequest.Builder mPreviewBuilder;
private Button mButton;
private ImageReader mImageReader;
private String mCameraId;
private Size mPreviewSize;
private CameraDevice mCameraDevice;
private CaptureRequest mCaptureRequest;
private CameraCaptureSession mPreviewSession;
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);
} // 9
@Override //4
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_test);
requestCameraPermission();
mThreadHandler = new HandlerThread("CAMERA2");
mThreadHandler.start();
mHandler = new Handler(mThreadHandler.getLooper());
mPreviewView = (TextureView) findViewById(textureView);
mPreviewView.setSurfaceTextureListener(this);
mButton = (Button) findViewById(R.id.picture);
mButton.setOnClickListener(new View.OnClickListener() {
@Override //1414
public void onClick(View v) {
try {//1515
//獲取屏幕方向
int rotation = getWindowManager().getDefaultDisplay().getRotation();
//設置CaptureRequest輸出到mImageReader
//CaptureRequest添加imageReaderSurface,不加的話就會導致ImageReader的onImageAvailable()方法不會回調
mPreviewBuilder.addTarget(mImageReader.getSurface());
//設置拍照方向
mPreviewBuilder.set(CaptureRequest.JPEG_ORIENTATION, ORIENTATION.get(rotation));
//聚焦
mPreviewBuilder.set(CaptureRequest.CONTROL_AF_MODE,
CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE);
//停止預覽
mPreviewSession.stopRepeating();
//開始拍照,然後回調上面的接口重啟預覽,因為mPreviewBuilder設置ImageReader作為target,所以會自動回調ImageReader的onImageAvailable()方法保存圖片
mPreviewSession.capture(mPreviewBuilder.build(), mSessionCaptureCallback, null);
} catch (CameraAccessException e) {
e.printStackTrace();//1616
}//1818
}//901
});
} //10
這裡要注意的是給ImageReader的surface的設置必須放在拍照這裡,否則再預覽的時候就會不斷的執行handler,將圖像保存下來。
Camera2還有很多的功能,谷歌在給我們提供強大類的時候也讓我們的學習量增大了,所以大家不要認為基本了解Camera2的工作流程就是掌握了Camera2,只有能將其運用到我們的開發中去才算掌握了,這只是你的第一步而已。