接著上一篇接口介紹繼續(xù),這一片主要是看一下代碼結(jié)構(gòu),本身自己缺乏這方面的能力,借著看zxing調(diào)用Camera組件的源碼,也寫了一個小demo,下面介紹一下代碼中如何調(diào)用Camera進行拍照。最后會貼上demo地址。
目錄:
- 功能介紹
- 根據(jù)功能設(shè)計類
- 流程分析
1. 功能介紹
這個demo主要實現(xiàn)了調(diào)用Camera組件進行拍照,并保存圖片的功能。一下詳細的功能拆分:
- 調(diào)用相機預(yù)覽;
- 預(yù)覽過程中相機自動對焦;
- 調(diào)用相機拍照;
- 保存圖片;
- 退出Activity,釋放相機。
2. 根據(jù)功能設(shè)計類
-
顯示界面——CameraActivity,用于相機預(yù)覽,根據(jù)上一篇。。。我們知道,相機預(yù)覽需要使用到SurfaceView,所以,CameraActivity中會有一個SurfaceView變量。同時,我們需要監(jiān)聽SurfaceView中的Surface的生命周期,保證在其生命周期之內(nèi),調(diào)用Camera組件的接口
setPreviewDisplay(holder)
(設(shè)置顯示預(yù)覽界面的Surface),startPreview()
(開啟預(yù)覽),stopPreview()
(停止預(yù)覽)進行相應(yīng)的操作,所以CameraActivity還需要實現(xiàn)SurfaceHolder.Callback接口。 -
管理Camera的各個接口——CameraManager,上一篇。。。介紹過調(diào)用Camera的接口,CameraManager需要對Camera接口進行封裝,提供給Activity使用,主要包括一下成員函數(shù):
openDriver(SurfaceHolder holder)
:連接相機,并設(shè)置相機基礎(chǔ)參數(shù),保證預(yù)覽畫面正常。
closeDriver()
:釋放相機。
setFlashLight(boolean open)
:打開或關(guān)閉閃關(guān)燈。
startPreview()
:開始預(yù)覽,告訴相機硬件將預(yù)覽幀數(shù)據(jù)顯示到屏幕上。
stopPreview()
:停止預(yù)覽,告訴相機硬件停止繪制預(yù)覽幀數(shù)據(jù)。
requestPreviewFrame(Handler handler, int message)
:獲取下一次預(yù)覽幀數(shù)據(jù),為什么說“下一次”呢,因為預(yù)覽數(shù)據(jù)是在不斷更新的,這樣才能保證我們在移動手機的時候,屏幕能顯示你移動之后畫面。這里調(diào)用的是mCamera.setOneShotPreviewCallback(PreviewCallback)
,這里只是設(shè)置了一個回調(diào)接口PreviewCallback,當我們調(diào)用這個方法設(shè)置回調(diào)接口之后,下一次預(yù)覽幀數(shù)據(jù)顯示到屏幕上的時候,就會回調(diào)PreviewCallback接口的onPreviewFrame(byte[] data, Camera camera)方法,我們會獲取到一個byte[]類型的數(shù)據(jù),這就是預(yù)覽的幀數(shù)據(jù)。為什么會傳入Handler和Message呢,就是為了在回調(diào)接口被調(diào)用的時候,用這里傳入的Handler處理byte[]數(shù)據(jù)。
requestAutoFocus(Handler handler, int message)
:自動對焦,這里需要調(diào)用Camera的autoFocus(AutoFocusCallback cb)
方法,該方法會設(shè)置自動對焦的回調(diào)接口AutoFocusCallback,然后調(diào)用native_autoFocus()
,告訴相機硬件自動對焦。底層硬件對焦之后,會回調(diào)AutoFocusCallback
接口的onAutoFocus(boolean success, Camera camera)
方法,返回對焦知否成功,這時傳入Handler就可以針對對焦狀態(tài)進行下一步操作。需要注意的是,調(diào)用autoFocus(AutoFocusCallback cb)
方法才會自動對焦一次,但是我們經(jīng)常會移動手機,改變物體和攝像頭之間的距離,所以需要隔一段時間請求一次自動對焦,這樣才能保證相機一直都能自動對焦。當然,也可以提供手動對角的方法。
requestCapture(Handler handler, int message)
:告訴相機硬件,要拍照,這里會調(diào)用takePicture(ShutterCallback shutter, PictureCallback raw,PictureCallback jpeg)
方法,ShutterCallback接口會在拍照的瞬間回調(diào),可以在這里設(shè)置拍照的聲音,第二個參數(shù)PictureCallback接口的回調(diào)方法,會返回未壓縮處理的原生數(shù)據(jù)byte[],可以在這里對照片的原生數(shù)據(jù)進行壓縮處理,轉(zhuǎn)換成我們需要的格式,第三個參數(shù)也是PictureCallback類型,這里的回調(diào)接口返回的數(shù)據(jù)也是byte[]類型的,但是將原生的數(shù)據(jù)壓縮成了jepg格式。這里我們主要關(guān)注第三個參數(shù),傳入的Handler和Message可以在PictureCallback的回調(diào)方法onPictureTaken(byte[] data, Camera camera)
進行下一步處理。 -
預(yù)覽回調(diào)類——PreviewCallback,需要實現(xiàn)接口
Camera.PreviewCallback
,當然,如果程序中不需要對預(yù)覽數(shù)據(jù)進一步操作,就不用自定義這個回調(diào)接口了。如果需要對預(yù)覽數(shù)據(jù)進行處理(比如掃描識別二維碼,就是獲取預(yù)覽幀數(shù)據(jù)進行識別),就可以創(chuàng)建自己的預(yù)覽回調(diào)類,實現(xiàn)函數(shù)onPreviewFrame(byte[] data, Camera camera)
,在這里獲取data進行處理。 -
自動對焦的回調(diào)類——AutoFocusCallback,實現(xiàn)接口
Camera.AutoFocusCallback
,可以在onAutoFocus(boolean success, Camera camera)
方法中獲取對焦的結(jié)果。 -
拍照的回調(diào)類——PictureCallback,實現(xiàn)接口
Camera.PictureCallback
,然后在方法onPictureTaken(byte[] data, Camera camera)
中獲取拍照的圖片數(shù)據(jù),將其保存成圖片。 -
輔助Activity,處理各個回調(diào)方法返回的結(jié)果——CaptureHandler,以上幾個回調(diào)接口的結(jié)果會交有CaptureHandler進行處理,這樣
CameraActivity
就可以只負責界面顯示,CaptureHandler
可以處理業(yè)務(wù)邏輯。
以下各個類之間的靜態(tài)結(jié)構(gòu):
CameraDemo類圖.png
3. 流程分析
為了幫助理解,我們看一下類之間的調(diào)用流程:
CameraDemo時序圖.png
最后看一下代碼中可能需要注意的幾個地方吧。
1. 在CameraActivity的onResume中初始化相機。
protected void onResume() {
super.onResume();
if (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED) {
//初始化Camera
initCamera();
} else {
ActivityCompat.requestPermissions(this, new String[] { Manifest.permission.CAMERA }, REQUEST_PERMISSIONS);
}
}
這里主要是,申請權(quán)限,然后初始化相機。下面看一下initCamera()
.
private void initCamera() {
//1:初始化SurfaceView
if (mSurfaceView == null) {
mSurfaceView = new SurfaceView(this);
mSurfaceView.setLayoutParams(new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
mContainer.addView(mSurfaceView, 0);
}
//2.獲取SurfaceHolder,判斷Surface是否可用
SurfaceHolder surfaceHolder = mSurfaceView.getHolder();
Surface surface = surfaceHolder.getSurface();
boolean isValid = surface == null ? false:surface.isValid();
if (mHasSurface && isValid) {
//2.1 SurfaceV可用,直接調(diào)用initCamera(surfaceHolder)初始化相機
initCamera(surfaceHolder);
} else {
//2.2 Surface不可用,通過SurfaceHolder.Callback監(jiān)聽Surface的生命周期
surfaceHolder.addCallback(this);
//設(shè)置Surface的type,該參數(shù)表示,由Camera為Surface提供幀數(shù)據(jù)。
surfaceHolder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);
}
}
- 首先,初始化
SurfaceView
,SurfaceView
在這里可以當做一個普通View
進行初始化。 - 獲取
SurfaceHolder
,目的是判斷Surface
是否可用,如果可用,直接調(diào)用initCamera(surfaceHolder)
初始化相機,如果不可用,需要通過SurfaceHolder.Callback
監(jiān)聽Surface
的生命周期,確保在surfaceCreated(SurfaceHolder holder)
和surfaceDestroyed(SurfaceHolder holder)
之內(nèi),使用Surface
,這里如果不清楚的,可以看上一篇 Camera使用指南(一)。這里的mHasSurface
在onCreate()
中設(shè)置成false,在surfaceCreated(SurfaceHolder holder)中設(shè)置為true,在surfaceDestroyed(SurfaceHolder holder)
設(shè)置成false,這樣就不需要用isValid
來判斷了,這里其實比較多余。
這里看一下initCamera(surfaceHolder)
:
private void initCamera(SurfaceHolder surfaceHolder) {
try {
//1. 連接相機
if (!CameraManager.get().openDriver(surfaceHolder)) {
Log.d(TAG, "Camera被占用");
}
} catch (IOException e) {
e.printStackTrace();
}
if (mCaptureHandler == null) {
mCaptureHandler = new CaptureHandler(this);
}
}
這里主要步驟就是調(diào)用CameraManager連接相機,一般情況下不會有什么問題,但是在多個調(diào)用相機的應(yīng)用切換的時候,就很有可能會出問題,但是你的應(yīng)用里可能也不會出現(xiàn)多個調(diào)用相機應(yīng)用切換的時候,比較有的時候,切換是需要先按home鍵或者back鍵返回的,所以這里我沒有進行處理。這里有兩個需要注意的地方,可能會用到:
-
多個調(diào)用Camera的進程切換,爭搶Camera,導(dǎo)致應(yīng)用預(yù)覽失敗,預(yù)覽界面黑屏/定屏。
這種情況可以在調(diào)用openDriver(surfaceHolder)
之后,判斷結(jié)果,結(jié)果為false,一般就是openDriver()
內(nèi)部發(fā)生了異常,這時,可以等待2S左右,類似這樣:
long oldTime = System.currentTimeMillis();
boolean isRunning = !CameraManager.get().openDriver(surfaceHolder);
while (isRunning && (System.currentTimeMillis() - oldTime) < 2500) {
try {
Thread.sleep(50);
} catch (Exception e) {
}
isRunning = !CameraManager.get().openDriver(surfaceHolder);
}
if (isRunning)
showToast("相機被占用");
- 此外,連接相機是一個耗時操作,可以放到子線程中進行,這樣的話,釋放相機也最好放到子線程中。
2. 在CameraActivity的onPause中釋放相機。
protected void onPause() {
super.onPause();
//斷開Camera連接
if (mCaptureHandler != null) {
try {
//停止預(yù)覽
mCaptureHandler.quitSynchronously();
mCaptureHandler = null;//下次進入重新初始化
//釋放相機
CameraManager.get().closeDriver();
} catch (Exception e) {
//關(guān)閉攝像頭失敗情況下,最好退出Activity,否則下次初始化相機的時候,會提示相機占用
finish();
}
}
}
其它的內(nèi)容,可以看一下demo,地址如下,CameraDemo.