注意:本教程已經(jīng)停更男翰,《Camera2 教程》持續(xù)更新中传轰。
上一章《Camera 教程 · 第一章 · 開(kāi)啟相機(jī)》 我們介紹了如何開(kāi)啟相機(jī)和關(guān)閉相機(jī)绳慎,但是還沒(méi)讓預(yù)覽畫(huà)面顯示出來(lái)丁存,這一章我們就來(lái)介紹下如何讓相機(jī)開(kāi)啟預(yù)覽敷存。
閱讀完本章墓造,你將會(huì)學(xué)到以下幾個(gè)知識(shí)點(diǎn):
- 如何獲取相機(jī)支持的參數(shù)
- 如何配置預(yù)覽尺寸
- 如何配置預(yù)覽的 Surface
- 如何開(kāi)啟和關(guān)閉預(yù)覽
- 設(shè)備方向的概念
- 局部坐標(biāo)系的概念
- 屏幕方向的概念
- 攝像頭傳感器方向的概念
- 如何矯正預(yù)覽畫(huà)面的方向
- 如何適配預(yù)覽畫(huà)面的比例
- 如何獲取預(yù)覽數(shù)據(jù)
- 如何切換前后置攝像頭
你可以在 https://github.com/darylgo/Camera1Sample 下載相關(guān)的源碼。
1 認(rèn)識(shí) Parameters
相機(jī)功能的強(qiáng)大與否完全取決于各手機(jī)廠商的底層實(shí)現(xiàn)锚烦,在基于相機(jī)開(kāi)發(fā)任何功能之前觅闽,你都需要通過(guò)某些手段判斷當(dāng)前設(shè)備相機(jī)的能力是否足以支撐你要開(kāi)發(fā)的功能,而 Camera.Parameters 就是我們判斷相機(jī)能力大小的手段挽牢,在 Camera.Parameters 里提供了大量形如 getSupportedXXX 的方法谱煤,通過(guò)這些方法你就可以判斷相機(jī)某方面的功能是否達(dá)到你的要求,例如通過(guò) getSupportedPreviewSizes() 可以獲取相機(jī)支持的預(yù)覽尺寸列表禽拔,進(jìn)而從這個(gè)列表中查詢是否有滿足你需求的尺寸刘离。
除了通過(guò) Camera.Parameters 判斷相機(jī)功能的支持情況之外,我們還通過(guò) Camera.Parameters 設(shè)置絕大部分相機(jī)參數(shù)睹栖,并且通過(guò) Camera.setParameters() 方法將設(shè)置好的參數(shù)傳給底層硫惕,讓這些參數(shù)生效。所以相機(jī)參數(shù)的配置流程基本就是以下三個(gè)步驟:
- 通過(guò) Camera.getParameters() 獲取 Camera.Parameters 實(shí)例野来。
- 通過(guò) Camera.Parameters.getSupportedXXX 獲取某個(gè)參數(shù)的支持情況恼除。
- 通過(guò) Camera.Parameters.set() 方法設(shè)置參數(shù)。
- 通過(guò) Camera.setParameters() 方法將參數(shù)應(yīng)用到底層曼氛。
注意:Camera.getParameters() 是一個(gè)比較耗時(shí)的操作豁辉,實(shí)測(cè) 20ms 到 100ms不等,所以盡可能地一次性設(shè)置所有必要的參數(shù)舀患,然后通過(guò) Camera.setParameters() 一次性應(yīng)用到底層徽级。
2 設(shè)置預(yù)覽尺寸
上面我們簡(jiǎn)單介紹了 Camera.Parameters,這一節(jié)我們就要通過(guò)它來(lái)配置相機(jī)的預(yù)覽尺寸聊浅。所謂的預(yù)覽尺寸餐抢,指的就是相機(jī)把畫(huà)面輸出到手機(jī)屏幕上供用戶預(yù)覽的尺寸,通常來(lái)說(shuō)我們希望預(yù)覽尺寸在不超過(guò)手機(jī)屏幕分辨率的情況下低匙,越大越好旷痕。另外,出于業(yè)務(wù)需求顽冶,我們的相機(jī)可能需要支持多種不同的預(yù)覽比例供用戶選擇欺抗,例如 4:3 和 16:9 的比例。由于不同廠商對(duì)相機(jī)的實(shí)現(xiàn)都會(huì)有差異强重,所以很多參數(shù)在不同的手機(jī)上支持的情況也不一樣绞呈,相機(jī)的預(yù)覽尺寸也是团滥。所以在設(shè)置相機(jī)預(yù)覽尺寸之前,我們先通過(guò) Camera.Parameters.getSupportedPreviewSizes() 獲取該設(shè)備支持的所有預(yù)覽尺寸:
Camera.Parameters parameters = mCamera.getParameters();
List<Camera.Size> supportedPreviewSizes = parameters.getSupportedPreviewSizes();
如果我們把所有的預(yù)覽尺寸都打印出來(lái)看時(shí)报强,會(huì)發(fā)現(xiàn)一個(gè)比較特別的情況,就是預(yù)覽尺寸的寬是長(zhǎng)邊拱燃,高是短邊秉溉,例如 1920x1080,而不是 1080x1920碗誉,這一點(diǎn)大家需要特別注意召嘶。
在獲取到預(yù)覽尺寸列表之后,我們要根據(jù)自己的實(shí)際需求過(guò)濾出其中一個(gè)最符合要求的尺寸哮缺,并且把它設(shè)置給相機(jī)弄跌,在我們的 Demo 里,只有當(dāng)預(yù)覽尺寸的比例和大小都滿足要求時(shí)才能被設(shè)置給相機(jī)尝苇,如下所示:
/**
* 根據(jù)指定的尺寸要求設(shè)置預(yù)覽尺寸铛只,我們會(huì)同時(shí)考慮指定尺寸的比例和大小。
*
* @param shortSide 短邊長(zhǎng)度
* @param longSide 長(zhǎng)邊長(zhǎng)度
*/
@WorkerThread
private void setPreviewSize(int shortSide, int longSide) {
if (mCamera != null && shortSide != 0 && longSide != 0) {
float aspectRatio = (float) longSide / shortSide;
Camera.Parameters parameters = mCamera.getParameters();
List<Camera.Size> supportedPreviewSizes = parameters.getSupportedPreviewSizes();
for (Camera.Size previewSize : supportedPreviewSizes) {
if ((float) previewSize.width / previewSize.height == aspectRatio && previewSize.height <= shortSide && previewSize.width <= longSide) {
parameters.setPreviewSize(previewSize.width, previewSize.height);
mCamera.setParameters(parameters);
break;
}
}
}
}
4 添加預(yù)覽 Surface
相機(jī)輸出的預(yù)覽畫(huà)面最終都是繪制到指定的 Surface 上糠溜,這個(gè) Surface 可以來(lái)自 SurfaceHolder 或者 SurfaceTexture淳玩,至于什么是 Surface 這里就不過(guò)多解釋,大家可以自行了解非竿。所以在開(kāi)啟預(yù)覽之前蜕着,我們還要告訴相機(jī)把畫(huà)面輸出到哪個(gè) Surface 上,Camera 支持兩種方式設(shè)置預(yù)覽的 Surface:
- 通過(guò) Camera.setPreviewDisplay() 方法設(shè)置 SurfaceHolder 給相機(jī)红柱,通常是在你使用 SurfaceView 作為預(yù)覽控件時(shí)會(huì)使用該方法承匣。
- 通過(guò) Camera.setPreviewTexture() 方法設(shè)置 SurfaceTexture 給相機(jī),通常是在你使用 TextureView 作為預(yù)覽控件或者自己創(chuàng)建 SurfaceTexture 時(shí)使用該方法锤悄。
在我們的 Demo 里韧骗,使用的 SurfaceView,所以會(huì)通過(guò) Camera.setPreviewDisplay() 方法設(shè)置預(yù)覽的 Surface铁蹈,代碼片段如下所示:
/**
* 設(shè)置預(yù)覽 Surface宽闲。
*/
@WorkerThread
private void setPreviewSurface(SurfaceHolder previewSurface) {
if (mCamera != null && previewSurface != null) {
try {
mCamera.setPreviewDisplay(previewSurface);
} catch (IOException e) {
e.printStackTrace();
}
}
}
5 開(kāi)啟和關(guān)閉預(yù)覽
接下來(lái),我們就要正式開(kāi)啟相機(jī)預(yù)覽了握牧,相關(guān)的方法就下面兩個(gè):
- Camera.startPreview():開(kāi)啟預(yù)覽
- Camera.stopPreview():關(guān)閉預(yù)覽
在 Demo 的代碼中容诬,我們做了一些邏輯處理,代碼如下:
/**
* 開(kāi)始預(yù)覽沿腰。
*/
@WorkerThread
private void startPreview() {
if (mCamera != null) {
mCamera.startPreview();
Log.d(TAG, "startPreview() called");
}
}
/**
* 停止預(yù)覽览徒。
*/
@WorkerThread
private void stopPreview() {
if (mCamera != null) {
mCamera.stopPreview();
Log.d(TAG, "stopPreview() called");
}
}
6 校正預(yù)覽畫(huà)面方向
如果沒(méi)有做任何畫(huà)面方向的校正,我們看到的畫(huà)面很可能是橫向的颂龙,這是因?yàn)槭謾C(jī)上的攝像頭傳感器方向不一定是垂直的习蓬。在做預(yù)覽畫(huà)面方向的校正之前我們先來(lái)了解五個(gè)概念纽什,分別是自然方向、設(shè)備方向躲叼、局部坐標(biāo)系芦缰、屏幕方向和攝像頭傳感器方向。
自然方向
當(dāng)我們談?wù)摲较虻臅r(shí)候枫慷,實(shí)際上都是相對(duì)于某一個(gè) 0° 方向的角度让蕾,這個(gè) 0° 方向被稱作自然方向,例如人站立的時(shí)候就是自然方向或听,你總不會(huì)認(rèn)為一個(gè)人要倒立的時(shí)候才是自然方向吧探孝,而接下來(lái)我們要談?wù)摰脑O(shè)備方向就有的自然方向的定義。
設(shè)備方向
設(shè)備方向指的是硬件設(shè)備在空間中的方向與其自然方向的順時(shí)針夾角誉裆。這里提到的自然方向指的就是我們手持一個(gè)設(shè)備的時(shí)候最習(xí)慣的方向顿颅,比如手機(jī)我們習(xí)慣豎著拿,而平板我們則習(xí)慣橫著拿足丢,所以通常情況下手機(jī)的自然方向就是豎著的時(shí)候粱腻,平板的自然方向就是橫著的時(shí)候。
以手機(jī)為例霎桅,我們可以有以下四個(gè)比較常見(jiàn)的設(shè)備方向:
- 當(dāng)我們把手機(jī)垂直放置且屏幕朝向我們的時(shí)候栖疑,設(shè)備方向?yàn)?0°,即設(shè)備自然方向
- 當(dāng)我們把手機(jī)向右橫放且屏幕朝向我們的時(shí)候滔驶,設(shè)備方向?yàn)?90°
- 當(dāng)我們把手機(jī)倒著放置且屏幕朝向我們的時(shí)候遇革,設(shè)備方向?yàn)?180°
- 當(dāng)我們把手機(jī)向左橫放且屏幕朝向我們的時(shí)候,設(shè)備方向?yàn)?270°
了解了設(shè)備方向的概念之后揭糕,我們可以通過(guò) OrientationEventListener 監(jiān)聽(tīng)設(shè)備的方向萝快,進(jìn)而判斷設(shè)備當(dāng)前是否處于自然方向,當(dāng)設(shè)備的方向發(fā)生變化的時(shí)候會(huì)回調(diào) OrientationEventListener.onOrientationChanged(int) 方法著角,傳給我們一個(gè) 0° 到 359° 的方向值揪漩,其中 0° 就代表設(shè)備處于自然方向。
局部坐標(biāo)系
所謂的局部坐標(biāo)系指的是當(dāng)設(shè)備處于自然方向時(shí)吏口,相對(duì)于設(shè)備屏幕的坐標(biāo)系奄容,該坐標(biāo)系是固定不變的,不會(huì)因?yàn)樵O(shè)備方向的變化而改變产徊,下圖是基于手機(jī)的局部坐標(biāo)系示意圖:
- x 軸是當(dāng)手機(jī)處于自然方向時(shí)昂勒,和手機(jī)屏幕平行且指向右邊的坐標(biāo)軸。
- y 軸是當(dāng)手機(jī)處于自然方向時(shí)舟铜,和手機(jī)屏幕平行且指向上方的坐標(biāo)軸戈盈。
- z 軸是當(dāng)手機(jī)處于自然方向時(shí),和手機(jī)屏幕垂直且指向屏幕外面的坐標(biāo)軸。
為了進(jìn)一步解釋【坐標(biāo)系是固定不變的塘娶,不會(huì)因?yàn)樵O(shè)備方向的變化而改變】的概念归斤,這里舉個(gè)例子,當(dāng)我們把手機(jī)向右橫放且屏幕朝向我們的時(shí)候刁岸,此時(shí)設(shè)備方向?yàn)?90°脏里,局部坐標(biāo)系相對(duì)于手機(jī)屏幕是保持不變的,所以 y 軸正方向指向右邊虹曙,x 軸正方向指向下方膝宁,z 軸正方向還是指向屏幕外面,如下圖所示:
屏幕方向
屏幕方向指的是屏幕上顯示畫(huà)面與局部坐標(biāo)系 y 軸的順時(shí)針夾角根吁,注意這里實(shí)際上指的是顯示的畫(huà)面,而不是物理硬件上的屏幕合蔽,只是我們習(xí)慣上稱作屏幕方向而已击敌。
為了更清楚的說(shuō)明這個(gè)概念,我們舉一個(gè)例子拴事,假設(shè)我們將手機(jī)向右橫放看電影沃斤,此時(shí)畫(huà)面是朝上的,如下圖所示:
從上圖來(lái)看刃宵,手機(jī)向右橫放會(huì)導(dǎo)致設(shè)備方向變成了 90°衡瓶,但是屏幕方向卻是 270°,因?yàn)樗窍鄬?duì)局部坐標(biāo)系 y 軸的順時(shí)針夾角牲证,所以跟設(shè)備方向沒(méi)有任何關(guān)系哮针。如果把圖中的設(shè)備換成是平板,結(jié)果就不一樣了坦袍,因?yàn)槠桨鍣M放的時(shí)候就是它的設(shè)備自然方向十厢,y 軸朝上,屏幕畫(huà)面顯示的方向和 y 軸的夾角是 0°捂齐,設(shè)備方向也是 0°蛮放。
總結(jié)一下,設(shè)備方向和屏幕方向之間沒(méi)有任何關(guān)系奠宜,設(shè)備方向是相對(duì)于其現(xiàn)實(shí)空間中自然方向的角度包颁,而屏幕方向是相對(duì)局部坐標(biāo)系的角度。
攝像頭傳感器方向
攝像頭傳感器方向指的是傳感器采集到的畫(huà)面方向經(jīng)過(guò)順時(shí)針旋轉(zhuǎn)多少度之后才能和局部坐標(biāo)系的 y 軸正方向一致压真,也就是在上一章《Camera 教程 · 第一章 · 開(kāi)啟相機(jī)》 里娩嚼,我們提到的 Camera.CameraInfo.orientation 屬性。
例如 orientation 為 90° 時(shí)榴都,意味我們將攝像頭采集到的畫(huà)面順時(shí)針旋轉(zhuǎn) 90° 之后待锈,畫(huà)面的方向就和局部坐標(biāo)系的 y 軸正方向一致,換個(gè)說(shuō)法就是原始畫(huà)面的方向和 y 軸的夾角是逆時(shí)針 90°嘴高。
最后我們要考慮一個(gè)特殊情況竿音,就是前置攝像頭的畫(huà)面是做了鏡像處理的和屎,也就是所謂的前置鏡像操作,這個(gè)情況下春瞬, orientation 的值并不是實(shí)際我們要旋轉(zhuǎn)的角度柴信,我們需要取它的鏡像值才是我們真正要旋轉(zhuǎn)的角度,例如 orientation 為 270°宽气,實(shí)際我們要旋轉(zhuǎn)的角度是 90°随常。
注意:攝像頭傳感器方向在不同的手機(jī)上可能不一樣,大部分手機(jī)都是 90°萄涯,也有小部分是 0° 的绪氛,所以我們要通過(guò) Camera.CameraInfo.orientation 去判斷方向,而不是假設(shè)所有設(shè)備的攝像頭傳感器方向都是 90°涝影。
畫(huà)面方向校正
介紹完幾個(gè)方向的概念之后枣察,我們就來(lái)說(shuō)下如何校正相機(jī)的預(yù)覽畫(huà)面。我們會(huì)舉幾個(gè)例子燃逻,由簡(jiǎn)到繁逐步說(shuō)明預(yù)覽畫(huà)面校正過(guò)程中要注意的事項(xiàng)序目。
首先我們要知道的是攝像頭傳感器方向只有 0°、90°伯襟、180°猿涨、270° 四個(gè)可選值,并且這些值是相對(duì)于局部坐標(biāo)系 的 y 軸定義出來(lái)的姆怪,現(xiàn)在假設(shè)一個(gè)相機(jī) APP 的畫(huà)面在手機(jī)上是豎屏顯示叛赚,也就是屏幕方向是 0° ,并且假設(shè)攝像頭傳感器的方向是 90°稽揭,如果我們沒(méi)有校正畫(huà)面的話红伦,則顯示的畫(huà)面如下圖所示(忽略畫(huà)面變形):
很明顯,上面顯示的畫(huà)面內(nèi)容方向是錯(cuò)誤的淀衣,里面的人物應(yīng)該是垂直向上顯示才對(duì)昙读,所以我們應(yīng)該吧攝像頭采集到的畫(huà)面順時(shí)針旋轉(zhuǎn) 90°,才能得到正確的顯示結(jié)果膨桥,如下圖所示:
上面的例子是建立在我們的屏幕方向是 0° 的時(shí)候蛮浑,如果我們要求屏幕方向是 90°,也就是手機(jī)向左橫放的時(shí)候畫(huà)面才是正的只嚣,并且假設(shè)攝像頭傳感器的方向還是 90°沮稚,如果我們沒(méi)有校正畫(huà)面的話,則顯示的畫(huà)面如下圖所示(忽略畫(huà)面變形):
此時(shí)册舞,我們知道傳感器的方向是 90°蕴掏,如果我們將傳感器采集到的畫(huà)面順時(shí)針旋轉(zhuǎn) 90° 顯然是無(wú)法得到正確的畫(huà)面,因?yàn)樗窍鄬?duì)于局部坐標(biāo)系 y 軸的角度,而不是實(shí)際屏幕方向盛杰,所以在做畫(huà)面校正的時(shí)候我們還要把實(shí)際屏幕方向也考慮進(jìn)去挽荡,這里實(shí)際屏幕方向是 90°,所以我們應(yīng)該把傳感器采集到的畫(huà)面順時(shí)針旋轉(zhuǎn) 180°(攝像頭傳感器方向 + 實(shí)際屏幕方向) 才能得到正確的畫(huà)面即供,顯示的畫(huà)面如下圖所示(忽略畫(huà)面變形):
總結(jié)一下定拟,在校正畫(huà)面方向的時(shí)候要同時(shí)考慮兩個(gè)因素,即攝像頭傳感器方向和屏幕方向逗嫡。接下來(lái)我們要回到我們的相機(jī)應(yīng)用里青自,看看通過(guò)代碼是如何實(shí)現(xiàn)預(yù)覽畫(huà)面方向校正的。
如果你有自己看過(guò) Camera 的官方 API 文檔驱证,你會(huì)發(fā)現(xiàn)官方已經(jīng)給我們寫(xiě)好了一個(gè)同時(shí)考慮屏幕方向和攝像頭傳感器方向的方法:
private int getCameraDisplayOrientation(Camera.CameraInfo cameraInfo) {
int rotation = getWindowManager().getDefaultDisplay().getRotation();
int degrees = 0;
switch (rotation) {
case Surface.ROTATION_0:
degrees = 0;
break;
case Surface.ROTATION_90:
degrees = 90;
break;
case Surface.ROTATION_180:
degrees = 180;
break;
case Surface.ROTATION_270:
degrees = 270;
break;
}
int result;
if (cameraInfo.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) {
result = (cameraInfo.orientation + degrees) % 360;
result = (360 - result) % 360; // compensate the mirror
} else { // back-facing
result = (cameraInfo.orientation - degrees + 360) % 360;
}
return result;
}
如果你已經(jīng)完全理解前面介紹的那些角度的概念延窜,那你應(yīng)該很容易就能理解上面這段代碼,實(shí)際上就是通過(guò) WindowManager 獲取當(dāng)前的屏幕方向抹锄,然后再參照攝像頭傳感器方向以及是否是前后置需曾,最后計(jì)算出我們實(shí)際要旋轉(zhuǎn)的角度。
計(jì)算出要矯正的角度之后祈远,我們要通過(guò) Camera.setDisplayOrientation() 方法設(shè)置畫(huà)面的矯正方向,下面是 Demo 中開(kāi)啟相機(jī)之后商源,馬上配置畫(huà)面矯正方向的代碼:
private void openCamera(int cameraId) {
if (mCamera != null) {
throw new RuntimeException("You must close previous camera before open a new one.");
}
if (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED) {
mCamera = Camera.open(cameraId);
mCameraId = cameraId;
mCameraInfo = cameraId == mFrontCameraId ? mFrontCameraInfo : mBackCameraInfo;
Log.d(TAG, "Camera[" + cameraId + "] has been opened.");
assert mCamera != null;
mCamera.setDisplayOrientation(getCameraDisplayOrientation(mCameraInfo));
}
}
7 適配預(yù)覽比例
前面介紹矯正預(yù)覽畫(huà)面方向的時(shí)候车份,我們看到了畫(huà)面變形的情況,這因?yàn)檎故绢A(yù)覽畫(huà)面的 Surface 和預(yù)覽尺寸的比例不一致導(dǎo)致的牡彻,所以接下來(lái)我們要學(xué)習(xí)的是如何適配不同的預(yù)覽比例扫沼。實(shí)際上預(yù)覽比例的適配有兩種方式:
- 根據(jù)預(yù)覽比例修改 Surface 的比例,這個(gè)是我們實(shí)際業(yè)務(wù)中經(jīng)常用的方式庄吼,比如用戶選擇了 4:3 的預(yù)覽比例缎除,這個(gè)時(shí)候我們會(huì)選取 4:3 的預(yù)覽尺寸并且把 Surface 修改成 4:3 的比例,從而讓畫(huà)面不會(huì)變形总寻。
- 根據(jù) Surface 的比例修改預(yù)覽比例器罐,這種情況適用于 Surface 的比例是固定的,然后根據(jù) Surface 的比例去選取適合的預(yù)覽尺寸渐行。
在我們的 Demo 中轰坊,出于簡(jiǎn)化的目的,我們選擇了第二種方式適配比例祟印,因?yàn)檫@種方式實(shí)現(xiàn)起來(lái)比較簡(jiǎn)單肴沫,所以我們會(huì)寫(xiě)一個(gè)自定義的 SurfaceView,讓它的比例固定是 4:3蕴忆,它的寬度固定填滿父布局颤芬,高度根據(jù)比例動(dòng)態(tài)計(jì)算:
public class SurfaceView43 extends SurfaceView {
public SurfaceView43(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int width = MeasureSpec.getSize(widthMeasureSpec);
int height = width / 3 * 4;
setMeasuredDimension(width, height);
}
}
上面的 SurfaceView43 使我們自定義的 SurfaceView,它的比例固定為 4:3,所以在它的 surfaceChanged() 回調(diào)中拿到的寬高的比例固定是 4:3站蝠,我們根據(jù)這個(gè)寬高比去調(diào)用前面定義好的設(shè)置預(yù)覽尺寸方法就可以設(shè)置正確比例的預(yù)覽尺寸:
@WorkerThread
private void setPreviewSize(int shortSide, int longSide) {
if (mCamera != null && shortSide != 0 && longSide != 0) {
float aspectRatio = (float) longSide / shortSide;
Camera.Parameters parameters = mCamera.getParameters();
List<Camera.Size> supportedPreviewSizes = parameters.getSupportedPreviewSizes();
for (Camera.Size previewSize : supportedPreviewSizes) {
if ((float) previewSize.width / previewSize.height == aspectRatio && previewSize.height <= shortSide && previewSize.width <= longSide) {
parameters.setPreviewSize(previewSize.width, previewSize.height);
mCamera.setParameters(parameters);
Log.d(TAG, "setPreviewSize() called with: width = " + previewSize.width + "; height = " + previewSize.height);
}
}
}
}
經(jīng)過(guò)上面的比例適配之后汰具,相機(jī)的預(yù)覽畫(huà)面就應(yīng)該固定是 4:3 的比例并且不會(huì)變形了。
8 獲取預(yù)覽數(shù)據(jù)
開(kāi)啟相機(jī)預(yù)覽的時(shí)候我們可以通過(guò)回調(diào)方法獲取相機(jī)的預(yù)覽數(shù)據(jù)沉衣,并且可以配置預(yù)覽數(shù)據(jù)的數(shù)據(jù)格式郁副,拿到預(yù)覽數(shù)據(jù)之后進(jìn)而做一些算法處理什么的。首先我們要通過(guò) Parameters.getSupportedPreviewFormats() 方法獲取相機(jī)支持哪些預(yù)覽數(shù)據(jù)格式豌习,所以我們定義了下面的方法:
/**
* 判斷指定的預(yù)覽格式是否支持存谎。
*/
private boolean isPreviewFormatSupported(Camera.Parameters parameters, int format) {
List<Integer> supportedPreviewFormats = parameters.getSupportedPreviewFormats();
return supportedPreviewFormats != null && supportedPreviewFormats.contains(format);
}
確定了你要的數(shù)據(jù)格式是支持的之后,就可以通過(guò) Parameters.setPreviewFormat() 放配置預(yù)覽數(shù)據(jù)的格式了肥隆,代碼片段如下所示:
private static final int PREVIEW_FORMAT = ImageFormat.NV21;
if (isPreviewFormatSupported(parameters, PREVIEW_FORMAT)) {
parameters.setPreviewFormat(PREVIEW_FORMAT);
}
上面說(shuō)到我們是通過(guò)回調(diào)的方式獲取相機(jī)預(yù)覽數(shù)據(jù)的既荚,所以相機(jī)為我們提供了一個(gè)回調(diào)接口叫 Camera.PreviewCallback,我們只需實(shí)現(xiàn)該接口并且注冊(cè)給相機(jī)就可以在預(yù)覽的時(shí)候接收到數(shù)據(jù)了栋艳,注冊(cè)回調(diào)接口的方式有兩種:
- setPreviewCallback():注冊(cè)預(yù)覽回調(diào)
- setPreviewCallbackWithBuffer():注冊(cè)預(yù)覽回調(diào)恰聘,并且使用已經(jīng)配置好的緩沖池
使用 setPreviewCallback() 注冊(cè)預(yù)覽回調(diào)獲取預(yù)覽數(shù)據(jù)是最簡(jiǎn)單的,因?yàn)槟悴恍枰渌渲昧鞒涛迹苯幼?cè)即可晴叨,但是出于性能考慮,官方推薦我們使用 setPreviewCallbackWithBuffer()矾屯,因?yàn)樗鼤?huì)使用我們配置好的緩沖對(duì)象回調(diào)預(yù)覽數(shù)據(jù)兼蕊,避免重復(fù)創(chuàng)建內(nèi)存占用很大的對(duì)象。所以接下來(lái)我們重點(diǎn)介紹如何根據(jù)預(yù)覽尺寸配置對(duì)象池并注冊(cè)回調(diào)件蚕,整個(gè)步驟如下:
- 根據(jù)需求確定預(yù)覽尺寸
- 根據(jù)需求確定預(yù)覽數(shù)據(jù)格式
- 根據(jù)預(yù)覽尺寸和數(shù)據(jù)格式計(jì)算出每一幀畫(huà)面要占用的內(nèi)存大小
- 通過(guò) addCallbackBuffer() 方法提前添加若干個(gè)創(chuàng)建好的 byte 數(shù)組對(duì)象作為緩沖對(duì)象供回調(diào)預(yù)覽數(shù)據(jù)使用
- 通過(guò) setPreviewCallbackWithBuffer() 注冊(cè)預(yù)覽回調(diào)
- 使用完緩沖對(duì)象之后孙技,通過(guò) addCallbackBuffer() 方法回收緩沖對(duì)象
根據(jù)上述步驟,我們修改原來(lái)設(shè)置預(yù)覽尺寸的方法排作,在配置預(yù)覽尺寸的同時(shí)根據(jù)預(yù)覽尺寸和數(shù)據(jù)格式配置緩沖對(duì)象牵啦,代碼如下:
@WorkerThread
private void setPreviewSize(int shortSide, int longSide) {
Camera camera = mCamera;
if (camera != null && shortSide != 0 && longSide != 0) {
float aspectRatio = (float) longSide / shortSide;
Camera.Parameters parameters = camera.getParameters();
List<Camera.Size> supportedPreviewSizes = parameters.getSupportedPreviewSizes();
for (Camera.Size previewSize : supportedPreviewSizes) {
if ((float) previewSize.width / previewSize.height == aspectRatio && previewSize.height <= shortSide && previewSize.width <= longSide) {
parameters.setPreviewSize(previewSize.width, previewSize.height);
Log.d(TAG, "setPreviewSize() called with: width = " + previewSize.width + "; height = " + previewSize.height);
if (isPreviewFormatSupported(parameters, PREVIEW_FORMAT)) {
parameters.setPreviewFormat(PREVIEW_FORMAT);
int frameWidth = previewSize.width;
int frameHeight = previewSize.height;
int previewFormat = parameters.getPreviewFormat();
PixelFormat pixelFormat = new PixelFormat();
PixelFormat.getPixelFormatInfo(previewFormat, pixelFormat);
int bufferSize = (frameWidth * frameHeight * pixelFormat.bitsPerPixel) / 8;
camera.addCallbackBuffer(new byte[bufferSize]);
camera.addCallbackBuffer(new byte[bufferSize]);
camera.addCallbackBuffer(new byte[bufferSize]);
Log.d(TAG, "Add three callback buffers with size: " + bufferSize);
}
camera.setParameters(parameters);
break;
}
}
}
}
上面代碼中,我們使用 PixelFormat 工具類根據(jù)當(dāng)前的預(yù)覽尺寸和格式計(jì)算出每一個(gè)像素占用多少 Bit妄痪,進(jìn)而算出一幀畫(huà)面需要占用的內(nèi)存大小哈雏,最后創(chuàng)建三個(gè) Buffer 通過(guò) addCallbackBuffer() 添加給相機(jī)供相機(jī)循環(huán)使用。
當(dāng)面我們開(kāi)啟預(yù)覽的時(shí)候衫生,相機(jī)就會(huì)通過(guò) Camera.PreviewCallback 將每一幀畫(huà)面的數(shù)據(jù)填充到 Buffer 里傳遞給我們僧著,我們?cè)谑褂猛?Buffer 之后,必須通過(guò) addCallbackBuffer() 將用完的 Buffer 重新設(shè)置回去一遍相機(jī)繼續(xù)重復(fù)利用該緩沖障簿,代碼如下:
private class PreviewCallback implements Camera.PreviewCallback {
@Override
public void onPreviewFrame(byte[] data, Camera camera) {
// 在使用完 Buffer 之后記得回收復(fù)用盹愚。
camera.addCallbackBuffer(data);
}
}
注意:在預(yù)覽回調(diào)方法里使用完 Buffer 之后,記得一定要調(diào)用 addCallbackBuffer() 將 Buffer 重新添加到緩沖池里供相機(jī)使用站故。
9 切換前后置攝像頭
實(shí)際需求經(jīng)常要求 APP 能夠支持前后置攝像頭的切換皆怕,所以這里我們也介紹下如何實(shí)現(xiàn)前后置攝像頭的切換毅舆。大部分情況下我們?cè)谇袚Q前后置攝像頭的時(shí)候,都會(huì)直接復(fù)用同一個(gè) Surface愈腾,所以我們會(huì)在 surfaceChanged() 的時(shí)候把 Surface 保存下來(lái)憋活,如下所示:
private class PreviewSurfaceCallback implements SurfaceHolder.Callback {
@Override
public void surfaceCreated(SurfaceHolder holder) {
}
@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
mPreviewSurface = holder;
mPreviewSurfaceWidth = width;
mPreviewSurfaceHeight = height;
setupPreview(holder, width, height);
}
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
mPreviewSurface = null;
mPreviewSurfaceWidth = 0;
mPreviewSurfaceHeight = 0;
}
}
然后就是添加一個(gè)切換前后置的按鈕,當(dāng)點(diǎn)擊按鈕的時(shí)候回去獲取和當(dāng)前攝像頭 ID 相反方向的 ID虱黄,所以我們定義了一個(gè) switchCameraId() 方法悦即,如下所示:
/**
* 切換前后置時(shí)切換ID
*/
private int switchCameraId() {
if (mCameraId == mFrontCameraId && hasBackCamera()) {
return mBackCameraId;
} else if (mCameraId == mBackCameraId && hasFrontCamera()) {
return mFrontCameraId;
} else {
throw new RuntimeException("No available camera id to switch.");
}
}
最后就是走一個(gè)標(biāo)準(zhǔn)的切換前后置攝像頭流程了:
- 停止預(yù)覽
- 關(guān)閉當(dāng)前攝像頭
- 開(kāi)啟新的攝像頭
- 配置預(yù)覽尺寸
- 配置預(yù)覽 Surface
- 開(kāi)啟預(yù)覽
因?yàn)槲覀兊?Demo 中使用 HandlerThread 控制了相機(jī)的操作流程,所以你可以看到如下代碼橱乱,具體的實(shí)現(xiàn)請(qǐng)看 Demo:
private class OnSwitchCameraButtonClickListener implements View.OnClickListener {
@Override
public void onClick(View v) {
Handler cameraHandler = mCameraHandler;
SurfaceHolder previewSurface = mPreviewSurface;
int previewSurfaceWidth = mPreviewSurfaceWidth;
int previewSurfaceHeight = mPreviewSurfaceHeight;
if (cameraHandler != null && previewSurface != null) {
int cameraId = switchCameraId();// 切換攝像頭 ID
cameraHandler.sendEmptyMessage(MSG_STOP_PREVIEW);// 停止預(yù)覽
cameraHandler.sendEmptyMessage(MSG_CLOSE_CAMERA);// 關(guān)閉當(dāng)前的攝像頭
cameraHandler.obtainMessage(MSG_OPEN_CAMERA, cameraId, 0).sendToTarget();// 開(kāi)啟新的攝像頭
cameraHandler.obtainMessage(MSG_SET_PREVIEW_SIZE, previewSurfaceWidth, previewSurfaceHeight).sendToTarget();// 配置預(yù)覽尺寸
cameraHandler.obtainMessage(MSG_SET_PREVIEW_SURFACE, previewSurface).sendToTarget();// 配置預(yù)覽 Surface
cameraHandler.sendEmptyMessage(MSG_START_PREVIEW);// 開(kāi)啟預(yù)覽
}
}
}
到這里辜梳,本章就介紹完了,謝謝泳叠。