Android Camera1 教程 · 第二章 · 預(yù)覽

Android Camera

注意:本教程已經(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):

  1. 如何獲取相機(jī)支持的參數(shù)
  2. 如何配置預(yù)覽尺寸
  3. 如何配置預(yù)覽的 Surface
  4. 如何開(kāi)啟和關(guān)閉預(yù)覽
  5. 設(shè)備方向的概念
  6. 局部坐標(biāo)系的概念
  7. 屏幕方向的概念
  8. 攝像頭傳感器方向的概念
  9. 如何矯正預(yù)覽畫(huà)面的方向
  10. 如何適配預(yù)覽畫(huà)面的比例
  11. 如何獲取預(yù)覽數(shù)據(jù)
  12. 如何切換前后置攝像頭

你可以在 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è)步驟:

  1. 通過(guò) Camera.getParameters() 獲取 Camera.Parameters 實(shí)例野来。
  2. 通過(guò) Camera.Parameters.getSupportedXXX 獲取某個(gè)參數(shù)的支持情況恼除。
  3. 通過(guò) Camera.Parameters.set() 方法設(shè)置參數(shù)。
  4. 通過(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:

  1. 通過(guò) Camera.setPreviewDisplay() 方法設(shè)置 SurfaceHolder 給相機(jī)红柱,通常是在你使用 SurfaceView 作為預(yù)覽控件時(shí)會(huì)使用該方法承匣。
  2. 通過(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)系示意圖:

局部坐標(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 軸正方向還是指向屏幕外面,如下圖所示:

設(shè)備方向 90°

屏幕方向

屏幕方向指的是屏幕上顯示畫(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ù)覽比例的適配有兩種方式:

  1. 根據(jù)預(yù)覽比例修改 Surface 的比例,這個(gè)是我們實(shí)際業(yè)務(wù)中經(jīng)常用的方式庄吼,比如用戶選擇了 4:3 的預(yù)覽比例缎除,這個(gè)時(shí)候我們會(huì)選取 4:3 的預(yù)覽尺寸并且把 Surface 修改成 4:3 的比例,從而讓畫(huà)面不會(huì)變形总寻。
  2. 根據(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è)步驟如下:

  1. 根據(jù)需求確定預(yù)覽尺寸
  2. 根據(jù)需求確定預(yù)覽數(shù)據(jù)格式
  3. 根據(jù)預(yù)覽尺寸和數(shù)據(jù)格式計(jì)算出每一幀畫(huà)面要占用的內(nèi)存大小
  4. 通過(guò) addCallbackBuffer() 方法提前添加若干個(gè)創(chuàng)建好的 byte 數(shù)組對(duì)象作為緩沖對(duì)象供回調(diào)預(yù)覽數(shù)據(jù)使用
  5. 通過(guò) setPreviewCallbackWithBuffer() 注冊(cè)預(yù)覽回調(diào)
  6. 使用完緩沖對(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)的切換前后置攝像頭流程了:

  1. 停止預(yù)覽
  2. 關(guān)閉當(dāng)前攝像頭
  3. 開(kāi)啟新的攝像頭
  4. 配置預(yù)覽尺寸
  5. 配置預(yù)覽 Surface
  6. 開(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ù)覽
        }
    }
}

到這里辜梳,本章就介紹完了,謝謝泳叠。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末作瞄,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子危纫,更是在濱河造成了極大的恐慌宗挥,老刑警劉巖,帶你破解...
    沈念sama閱讀 222,183評(píng)論 6 516
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件种蝶,死亡現(xiàn)場(chǎng)離奇詭異契耿,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)螃征,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,850評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門(mén)搪桂,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人会傲,你說(shuō)我怎么就攤上這事∽驹螅” “怎么了淌山?”我有些...
    開(kāi)封第一講書(shū)人閱讀 168,766評(píng)論 0 361
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)顾瞻。 經(jīng)常有香客問(wèn)我泼疑,道長(zhǎng),這世上最難降的妖魔是什么荷荤? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 59,854評(píng)論 1 299
  • 正文 為了忘掉前任退渗,我火速辦了婚禮,結(jié)果婚禮上蕴纳,老公的妹妹穿的比我還像新娘会油。我一直安慰自己,他們只是感情好古毛,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,871評(píng)論 6 398
  • 文/花漫 我一把揭開(kāi)白布翻翩。 她就那樣靜靜地躺著都许,像睡著了一般。 火紅的嫁衣襯著肌膚如雪嫂冻。 梳的紋絲不亂的頭發(fā)上胶征,一...
    開(kāi)封第一講書(shū)人閱讀 52,457評(píng)論 1 311
  • 那天,我揣著相機(jī)與錄音桨仿,去河邊找鬼睛低。 笑死,一個(gè)胖子當(dāng)著我的面吹牛服傍,可吹牛的內(nèi)容都是我干的钱雷。 我是一名探鬼主播,決...
    沈念sama閱讀 40,999評(píng)論 3 422
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼伴嗡,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼急波!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起瘪校,我...
    開(kāi)封第一講書(shū)人閱讀 39,914評(píng)論 0 277
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤澄暮,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后阱扬,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體泣懊,經(jīng)...
    沈念sama閱讀 46,465評(píng)論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,543評(píng)論 3 342
  • 正文 我和宋清朗相戀三年麻惶,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了馍刮。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,675評(píng)論 1 353
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡窃蹋,死狀恐怖另玖,靈堂內(nèi)的尸體忽然破棺而出增蹭,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 36,354評(píng)論 5 351
  • 正文 年R本政府宣布锄俄,位于F島的核電站硝训,受9級(jí)特大地震影響泼掠,放射性物質(zhì)發(fā)生泄漏校镐。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,029評(píng)論 3 335
  • 文/蒙蒙 一树酪、第九天 我趴在偏房一處隱蔽的房頂上張望浅碾。 院中可真熱鬧,春花似錦续语、人聲如沸垂谢。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 32,514評(píng)論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)埂陆。三九已至苛白,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間焚虱,已是汗流浹背购裙。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,616評(píng)論 1 274
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留鹃栽,地道東北人躏率。 一個(gè)月前我還...
    沈念sama閱讀 49,091評(píng)論 3 378
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像民鼓,于是被迫代替她去往敵國(guó)和親薇芝。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,685評(píng)論 2 360