CameraAPI1 使用說(shuō)明
目錄
一、 概覽
1.1 Pipeline
1.2 Supported Hardware Level
1.3 Capture
1.4 CameraManager
1.5 CameraCharacteristics
1.6 CameraDevice
1.7 Surface
1.8 CameraCaptureSession
1.9 CaptureRequest
1.10 CaptureResult
1.11 一些只有 Camera2 才支持的高級(jí)特性
1.12 一些從 Camera1 遷移到 Camera2 的建議
二、 開(kāi)關(guān)相機(jī)
2.1 創(chuàng)建相機(jī)項(xiàng)目
2.2 注冊(cè)相關(guān)權(quán)限
2.3 配置相機(jī)特性要求
2.4 獲取 CameraManager 實(shí)例
2.5 獲取相機(jī) ID 列表
2.6 根據(jù)相機(jī) ID 獲取 CameraCharacteristics
2.7 開(kāi)啟相機(jī)
2.8 關(guān)閉相機(jī)
三、預(yù)覽
3.1 獲取預(yù)覽尺寸
3.2 配置預(yù)覽尺寸
3.3 創(chuàng)建 CameraCaptureSession
3.4 創(chuàng)建 CaptureRequest
3.5 開(kāi)啟和停止預(yù)覽
3.6 適配預(yù)覽比例
3.7 認(rèn)識(shí) ImageReader
3.8 獲取預(yù)覽數(shù)據(jù)
3.9 如何矯正圖像數(shù)據(jù)的方向
設(shè)備方向
局部坐標(biāo)系
顯示方向
屏幕方向
攝像頭傳感器方向
矯正圖像數(shù)據(jù)的方向
四、 拍照
4.1 理解 Capture 工作流程
4.2 如何拍攝單張照片
4.2.1 定義回調(diào)接口
4.2.2 創(chuàng)建 ImageReader
4.2.3 創(chuàng)建 CaptureRequest
4.2.4 矯正 JPEG 圖片方向
4.2.5 設(shè)置縮略圖尺寸
4.2.6 設(shè)置定位信息
4.2.7 播放快門(mén)音效
4.2.8 拍照并保存圖片
4.2.9 前置攝像頭拍照的鏡像問(wèn)題
4.3 如何連續(xù)拍攝多張圖片
4.4 如何連拍
4.5 如何切換前后置攝像頭
4.6 總結(jié)
????????從 Android 5.0 開(kāi)始,Google 引入了一套全新的相機(jī)框架 Camera2(android.hardware.camera2)并且廢棄了舊的相機(jī)框架 Camera1(android.hardware.Camera)。Camera2 的出現(xiàn)給相機(jī)應(yīng)用程序帶來(lái)了巨大的變革,因?yàn)樗哪康氖菫榱私o應(yīng)用層提供更多的相機(jī)控制權(quán)限,從而構(gòu)建出更高質(zhì)量的相機(jī)應(yīng)用程序。本文是 Camera2 教程的開(kāi)篇作,本章將介紹以下幾個(gè)內(nèi)容:
-
一些 Camera2 的重要概念
-
一些只有 Camera2 才支持的高級(jí)特性
-
一些從 Camera1 遷移到 Camera2 的建議
本章涉及的代碼很少,因?yàn)槲覀儠?huì)在接下來(lái)的教程中深入介紹 Camera2 的 API。
一、 概覽
1.1 Pipeline
????????Camera2 的 API 模型被設(shè)計(jì)成一個(gè) Pipeline(管道),它按順序處理每一幀的請(qǐng)求并返回請(qǐng)求結(jié)果給客戶端。下面這張來(lái)自官方的圖展示了 Pipeline 的工作流程,我們會(huì)通過(guò)一個(gè)簡(jiǎn)單的例子詳細(xì)解釋這張圖。
Pipeline 示意圖
????????為了解釋上面的示意圖,假設(shè)我們想要同時(shí)拍攝兩張不同尺寸的圖片,并且在拍攝的過(guò)程中閃光燈必須亮起來(lái)。整個(gè)拍攝流程如下:
-
創(chuàng)建一個(gè)用于從 Pipeline 獲取圖片的 CaptureRequest。
-
修改 CaptureRequest 的閃光燈配置,讓閃光燈在拍照過(guò)程中亮起來(lái)。
-
創(chuàng)建兩個(gè)不同尺寸的 Surface 用于接收?qǐng)D片數(shù)據(jù),并且將它們添加到 CaptureRequest 中。
-
發(fā)送配置好的 CaptureRequest 到 Pipeline 中等待它返回拍照結(jié)果。
????????一個(gè)新的 CaptureRequest 會(huì)被放入一個(gè)被稱(chēng)作 Pending Request Queue 的隊(duì)列中等待被執(zhí)行,當(dāng) In-Flight Capture Queue 隊(duì)列空閑的時(shí)候就會(huì)從 Pending Request Queue 獲取若干個(gè)待處理的 CaptureRequest,并且根據(jù)每一個(gè) CaptureRequest 的配置進(jìn)行 Capture 操作。最后我們從不同尺寸的 Surface 中獲取圖片數(shù)據(jù)并且還會(huì)得到一個(gè)包含了很多與本次拍照相關(guān)的信息的 CaptureResult,流程結(jié)束。
1.2 Supported Hardware Level
????????相機(jī)功能的強(qiáng)大與否和硬件息息相關(guān),不同廠商對(duì) Camera2 的支持程度也不同,所以 Camera2 定義了一個(gè)叫做 Supported Hardware Level 的重要概念,其作用是將不同設(shè)備上的 Camera2 根據(jù)功能的支持情況劃分成多個(gè)不同級(jí)別以便開(kāi)發(fā)者能夠大概了解當(dāng)前設(shè)備上 Camera2 的支持情況。截止到 Android P 為止,從低到高一共有 LEGACY、LIMITED、FULL 和 LEVEL_3 四個(gè)級(jí)別:
-
LEGACY:向后兼容的級(jí)別,處于該級(jí)別的設(shè)備意味著它只支持 Camera1 的功能,不具備任何 Camera2 高級(jí)特性。
-
LIMITED:除了支持 Camera1 的基礎(chǔ)功能之外,還支持部分 Camera2 高級(jí)特性的級(jí)別。
-
FULL:支持所有 Camera2 的高級(jí)特性。
-
LEVEL_3:新增更多 Camera2 高級(jí)特性,例如 YUV 數(shù)據(jù)的后處理等。
1.3 Capture
????????相機(jī)的所有操作和參數(shù)配置最終都是服務(wù)于圖像捕獲,例如對(duì)焦是為了讓某一個(gè)區(qū)域的圖像更加清晰,調(diào)節(jié)曝光補(bǔ)償是為了調(diào)節(jié)圖像的亮度。因此,在 Camera2 里面所有的相機(jī)操作和參數(shù)配置都被抽象成 Capture(捕獲),所以不要簡(jiǎn)單的把 Capture 直接理解成是拍照,因?yàn)?Capture 操作可能僅僅是為了讓預(yù)覽畫(huà)面更清晰而進(jìn)行對(duì)焦而已。如果你熟悉 Camera1,那你可能會(huì)問(wèn) setFlashMode()
在哪?setFocusMode()
在哪?takePicture()
在哪?告訴你,它們都是通過(guò) Capture 來(lái)實(shí)現(xiàn)的。
Capture 從執(zhí)行方式上又被細(xì)分為【單次模式】、【多次模式】和【重復(fù)模式】三種,我們來(lái)一一解釋下:
-
單次模式(One-shot):指的是只執(zhí)行一次的 Capture 操作,例如設(shè)置閃光燈模式、對(duì)焦模式和拍一張照片等。多個(gè)一次性模式的 Capture 會(huì)進(jìn)入隊(duì)列按順序執(zhí)行。
-
多次模式(Burst):指的是連續(xù)多次執(zhí)行指定的 Capture 操作,該模式和多次執(zhí)行單次模式的最大區(qū)別是連續(xù)多次 Capture 期間不允許插入其他任何 Capture 操作,例如連續(xù)拍攝 100 張照片,在拍攝這 100 張照片期間任何新的 Capture 請(qǐng)求都會(huì)排隊(duì)等待,直到拍完 100 張照片。多組多次模式的 Capture 會(huì)進(jìn)入隊(duì)列按順序執(zhí)行。
-
重復(fù)模式(Repeating):指的是不斷重復(fù)執(zhí)行指定的 Capture 操作,當(dāng)有其他模式的 Capture 提交時(shí)會(huì)暫停該模式,轉(zhuǎn)而執(zhí)行其他被模式的 Capture,當(dāng)其他模式的 Capture 執(zhí)行完畢后又會(huì)自動(dòng)恢復(fù)繼續(xù)執(zhí)行該模式的 Capture,例如顯示預(yù)覽畫(huà)面就是不斷 Capture 獲取每一幀畫(huà)面。該模式的 Capture 是全局唯一的,也就是新提交的重復(fù)模式 Capture 會(huì)覆蓋舊的重復(fù)模式 Capture。
1.4 CameraManager
????????CameraManager 是一個(gè)負(fù)責(zé)查詢(xún)和建立相機(jī)連接的系統(tǒng)服務(wù),它的功能不多,這里列出幾個(gè) CameraManager 的關(guān)鍵功能:
-
將相機(jī)信息封裝到 CameraCharacteristics 中,并提獲取 CameraCharacteristics 實(shí)例的方式。
-
根據(jù)指定的相機(jī) ID 連接相機(jī)設(shè)備。
-
提供將閃光燈設(shè)置成手電筒模式的快捷方式。
1.5 CameraCharacteristics
????????????????CameraCharacteristics 是一個(gè)只讀的相機(jī)信息提供者,其內(nèi)部攜帶大量的相機(jī)信息,包括代表相機(jī)朝向的 LENS_FACING
;判斷閃光燈是否可用的 FLASH_INFO_AVAILABLE
;獲取所有可用 AE 模式的 CONTROL_AE_AVAILABLE_MODES
等等。如果你對(duì) Camera1 比較熟悉,那么 CameraCharacteristics 有點(diǎn)像 Camera1 的 Camera.CameraInfo
或者 Camera.Parameters
。
1.6 CameraDevice
CameraDevice 代表當(dāng)前連接的相機(jī)設(shè)備,它的職責(zé)有以下四個(gè):
-
根據(jù)指定的參數(shù)創(chuàng)建 CameraCaptureSession。
-
根據(jù)指定的模板創(chuàng)建 CaptureRequest。
-
關(guān)閉相機(jī)設(shè)備。
-
監(jiān)聽(tīng)相機(jī)設(shè)備的狀態(tài),例如斷開(kāi)連接、開(kāi)啟成功和開(kāi)啟失敗等。
????????熟悉 Camera1 的人可能會(huì)說(shuō) CameraDevice 就是 Camera1 的 Camera 類(lèi),實(shí)則不是,Camera 類(lèi)幾乎負(fù)責(zé)了所有相機(jī)的操作,而 CameraDevice 的功能則十分的單一,就是只負(fù)責(zé)建立相機(jī)連接的事務(wù),而更加細(xì)化的相機(jī)操作則交給了稍后會(huì)介紹的 CameraCaptureSession。
1.7 Surface
????????Surface 是一塊用于填充圖像數(shù)據(jù)的內(nèi)存空間,例如你可以使用 SurfaceView 的 Surface 接收每一幀預(yù)覽數(shù)據(jù)用于顯示預(yù)覽畫(huà)面,也可以使用 ImageReader 的 Surface 接收 JPEG 或 YUV 數(shù)據(jù)。每一個(gè) Surface 都可以有自己的尺寸和數(shù)據(jù)格式,你可以從 CameraCharacteristics 獲取某一個(gè)數(shù)據(jù)格式支持的尺寸列表。
1.8 CameraCaptureSession
????????CameraCaptureSession 實(shí)際上就是配置了目標(biāo) Surface 的 Pipeline 實(shí)例,我們?cè)谑褂孟鄼C(jī)功能之前必須先創(chuàng)建 CameraCaptureSession 實(shí)例。一個(gè) CameraDevice 一次只能開(kāi)啟一個(gè) CameraCaptureSession,絕大部分的相機(jī)操作都是通過(guò)向 CameraCaptureSession 提交一個(gè) Capture 請(qǐng)求實(shí)現(xiàn)的,例如拍照、連拍、設(shè)置閃光燈模式、觸摸對(duì)焦、顯示預(yù)覽畫(huà)面等等。
1.9 CaptureRequest
????????CaptureRequest 是向 CameraCaptureSession 提交 Capture 請(qǐng)求時(shí)的信息載體,其內(nèi)部包括了本次 Capture 的參數(shù)配置和接收?qǐng)D像數(shù)據(jù)的 Surface。CaptureRequest 可以配置的信息非常多,包括圖像格式、圖像分辨率、傳感器控制、閃光燈控制、3A 控制等等,可以說(shuō)絕大部分的相機(jī)參數(shù)都是通過(guò) CaptureRequest 配置的。值得注意的是每一個(gè) CaptureRequest 表示一幀畫(huà)面的操作,這意味著你可以精確控制每一幀的 Capture 操作。
1.10 CaptureResult
????????CaptureResult 是每一次 Capture 操作的結(jié)果,里面包括了很多狀態(tài)信息,包括閃光燈狀態(tài)、對(duì)焦?fàn)顟B(tài)、時(shí)間戳等等。例如你可以在拍照完成的時(shí)候,通過(guò) CaptureResult 獲取本次拍照時(shí)的對(duì)焦?fàn)顟B(tài)和時(shí)間戳。需要注意的是,CaptureResult 并不包含任何圖像數(shù)據(jù),前面我們?cè)诮榻B Surface 的時(shí)候說(shuō)了,圖像數(shù)據(jù)都是從 Surface 獲取的。
1.11 一些只有 Camera2 才支持的高級(jí)特性
????????如果要我給出強(qiáng)有力的理由解釋為什么要使用 Camera2,那么通過(guò) Camera2 提供的高級(jí)特性可以構(gòu)建出更加高質(zhì)量的相機(jī)應(yīng)用程序應(yīng)該是最佳理由了。
-
在開(kāi)啟相機(jī)之前檢查相機(jī)信息 出于某些原因,你可能需要先檢查相機(jī)信息再?zèng)Q定是否開(kāi)啟相機(jī),例如檢查閃光燈是否可用。在 Camera1 上,你無(wú)法在開(kāi)機(jī)相機(jī)之前檢查詳細(xì)的相機(jī)信息,因?yàn)檫@些信息都是通過(guò)一個(gè)已經(jīng)開(kāi)啟的相機(jī)實(shí)例提供的。在 Camera2 上,我們有了和相機(jī)實(shí)例完全剝離的 CameraCharacteristics 實(shí)例專(zhuān)門(mén)提供相機(jī)信息,所以我們可以在不開(kāi)啟相機(jī)的前提下檢查幾乎所有的相機(jī)信息。
-
在不開(kāi)啟預(yù)覽的情況下拍照 在 Camera1 上,開(kāi)啟預(yù)覽是一個(gè)很重要的環(huán)節(jié),因?yàn)橹挥性陂_(kāi)啟預(yù)覽之后才能進(jìn)行拍照,因此即使顯示預(yù)覽畫(huà)面與實(shí)際業(yè)務(wù)需求相違背的時(shí)候,你也不得不開(kāi)啟預(yù)覽。而 Camera2 則不強(qiáng)制要求你必須先開(kāi)啟預(yù)覽才能拍照。
-
一次拍攝多張不同格式和尺寸的圖片 在 Camera1 上,一次只能拍攝一張圖片,更不同談多張不同格式和尺寸的圖片了。而 Camera2 則支持一次拍攝多張圖片,甚至是多張格式和尺寸都不同的圖片。例如你可以同時(shí)拍攝一張 1440x1080 的 JPEG 圖片和一張全尺寸的 RAW 圖片。
-
控制曝光時(shí)間 在暗環(huán)境下拍照的時(shí)候,如果能夠適當(dāng)延長(zhǎng)曝光時(shí)間,就可以讓圖像畫(huà)面的亮度得到提高。在 Camera2 上,你可以在規(guī)定的曝光時(shí)長(zhǎng)范圍內(nèi)配置拍照的曝光時(shí)間,從而實(shí)現(xiàn)拍攝長(zhǎng)曝光圖片,你甚至可以延長(zhǎng)每一幀預(yù)覽畫(huà)面的曝光時(shí)間讓整個(gè)預(yù)覽畫(huà)面在暗環(huán)境下也能保證一定的亮度。
-
連拍 連拍 30 張圖片這樣的功能在 Camera2 出現(xiàn)之前恐怕只有系統(tǒng)相機(jī)才能做到了(通過(guò) OpenGL 截取預(yù)覽畫(huà)面的做法除外),也可能是出于這個(gè)原因,市面上的第三方相機(jī)無(wú)一例外都不支持連拍。有了 Camera2,你完全可以讓你的相機(jī)應(yīng)用程序支持連拍功能,甚至是連續(xù)拍 30 張使用不同曝光時(shí)間的圖片。
-
靈活的 3A 控制 3A(AF、AE、AWB)的控制在 Camera2 上得到了最大化的放權(quán),應(yīng)用層可以根據(jù)業(yè)務(wù)需求靈活配置 3A 流程并且實(shí)時(shí)獲取 3A 狀態(tài),而 Camera1 在 3A 的控制和監(jiān)控方面提供的接口則要少了很多。例如你可以在拍照前進(jìn)行 AE 操作,并且監(jiān)聽(tīng)本這次拍照是否點(diǎn)亮閃光燈。
1.12 一些從 Camera1 遷移到 Camera2 的建議
????????如果你熟悉 Camera1,并且打算從 Camera1 遷移到 Camera2 的話,希望以下幾個(gè)建議可以對(duì)你起到幫助:
-
Camera1 嚴(yán)格區(qū)分了預(yù)覽和拍照兩個(gè)流程,而 Camera2 則把這兩個(gè)流程都抽象成了 Capture 行為,只不過(guò)一個(gè)是不斷重復(fù)的 Capture,一個(gè)是一次性的 Capture 而已,所以建議你不要帶著過(guò)多的 Camera1 思維使用 Camera2,避免因?yàn)樗季S上的束縛而無(wú)法充分利用 Camera2 靈活的 API。
-
如同 Camera1 一樣,Camera2 的一些 API 調(diào)用也會(huì)耗時(shí),所以建議你使用獨(dú)立的線程執(zhí)行所有的相機(jī)操作,盡量避免直接在主線程調(diào)用 Camera2 的 API,HandlerThread 是一個(gè)不錯(cuò)的選擇。
-
Camera2 所有的相機(jī)操作都可以注冊(cè)相關(guān)的回調(diào)接口,然后在不同的回調(diào)方法里寫(xiě)業(yè)務(wù)邏輯,這可能會(huì)讓你的代碼因?yàn)椴粔蚓€性而錯(cuò)綜復(fù)雜,建議你可以嘗試使用子線程的阻塞方式來(lái)盡可能地保證代碼的線性執(zhí)行。例如在子線程阻塞等待 CaptureResult,然后繼續(xù)執(zhí)行后續(xù)的操作,而不是將代碼拆分到到
CaptureCallback.onCaptureCompleted()
方法里。 -
你可以認(rèn)為 Camera1 是 Camera2 的一個(gè)子集,也就是說(shuō) Camera1 能做的事情 Camera2 一定能做,反過(guò)來(lái)則不一定行得通。
-
如果你的應(yīng)用程序需要同時(shí)兼容 Camera1 和 Camera2,個(gè)人建議分開(kāi)維護(hù),因?yàn)?Camera1 蹩腳的 API 設(shè)計(jì)很可能讓 Camera2 靈活的 API 無(wú)法得到充分的發(fā)揮,另外將兩個(gè)設(shè)計(jì)上完全不兼容的東西攪和在一起帶來(lái)的痛苦可能遠(yuǎn)大于其帶來(lái)便利性,多寫(xiě)一些冗余的代碼也許還更開(kāi)心。
-
官方說(shuō) Camera2 的性能會(huì)更好,這句話聽(tīng)聽(tīng)就好,起碼在較早期的一些機(jī)器上運(yùn)行 Camera2 的性能并沒(méi)有比 Camera1 好。
-
當(dāng)設(shè)備的 Supported Hardware Level 低于 FULL 的時(shí)候,建議還是使用 Camera1,因?yàn)?FULL 級(jí)別以下的 Camera2 能提供的功能幾乎和 Camera1 一樣,所以倒不如選擇更加穩(wěn)定的 Camera1。
二、 開(kāi)關(guān)相機(jī)
2.1 創(chuàng)建相機(jī)項(xiàng)目
????????第一步要做的就是創(chuàng)建一個(gè)相機(jī)項(xiàng)目,這里我用 AS 創(chuàng)建了一個(gè)叫 Camera2Demo 的項(xiàng)目,并且有一個(gè) Activity 叫 MainActivity。用的開(kāi)發(fā)語(yǔ)言是 Kotlin。為了降低源碼的閱讀難度,不引入任何的第三方庫(kù),不去關(guān)注性能問(wèn)題,也不進(jìn)行任何模式上的設(shè)計(jì),大部分的代碼我都會(huì)寫(xiě)在這個(gè) MainActivity 里面,所有的功能的實(shí)現(xiàn)都盡可能簡(jiǎn)化。
2.2 注冊(cè)相關(guān)權(quán)限
????????在使用相機(jī) API 之前,必須在 AndroidManifest.xml 注冊(cè)相機(jī)權(quán)限 android.permission.CAMERA,聲明我們開(kāi)發(fā)的應(yīng)用程序需要相機(jī)權(quán)限,另外如果你有保存照片的操作,那么讀寫(xiě) SD 卡的權(quán)限也是必須的:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
</manifest>
????????需要注意的是 6.0 以上的系統(tǒng)需要我們?cè)诔绦蜻\(yùn)行的時(shí)候進(jìn)行動(dòng)態(tài)權(quán)限申請(qǐng),所以我們需要在程序啟動(dòng)的時(shí)候去檢查權(quán)限,有任何一個(gè)必要的權(quán)限被用戶拒絕時(shí),我們就彈窗提示用戶程序因?yàn)闄?quán)限被拒絕而無(wú)法正常工作:
class MainActivity : AppCompatActivity() {
companion object {
private const val REQUEST_PERMISSION_CODE: Int = 1
private val REQUIRED_PERMISSIONS: Array<String> = arrayOf(
android.Manifest.permission.CAMERA,
android.Manifest.permission.WRITE_EXTERNAL_STORAGE
)
}
/**
* 判斷我們需要的權(quán)限是否被授予,只要有一個(gè)沒(méi)有授權(quán),我們都會(huì)返回 false,并且進(jìn)行權(quán)限申請(qǐng)操作。
*
* @return true 權(quán)限都被授權(quán)
*/
private fun checkRequiredPermissions(): Boolean {
val deniedPermissions = mutableListOf<String>()
for (permission in REQUIRED_PERMISSIONS) {
if (ContextCompat.checkSelfPermission(this, permission) == PackageManager.PERMISSION_DENIED) {
deniedPermissions.add(permission)
}
}
if (deniedPermissions.isEmpty().not()) {
requestPermissions(deniedPermissions.toTypedArray(), REQUEST_PERMISSION_CODE)
}
return deniedPermissions.isEmpty()
}
}
2.3 配置相機(jī)特性要求
????????你一定不希望用戶在一臺(tái)沒(méi)有任何相機(jī)的手機(jī)上安裝你的相機(jī)應(yīng)用程序吧,因?yàn)槟菢幼鍪菦](méi)有意義的。所以接下來(lái)要做的就是在 AndroidManifest.xml 中配置一些程序運(yùn)行時(shí)必要的相機(jī)特性,如果這些特性不支持,那么用戶在安裝 apk 的時(shí)候就會(huì)因?yàn)闂l件不符合而無(wú)法安裝。
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.darylgo.camera.sample">
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-feature
android:name="android.hardware.camera"
android:required="true" />
</manifest>
????????我們通過(guò) <uses-feature> 標(biāo)簽聲明了我們的應(yīng)用程序必須在具有相機(jī)的手機(jī)上才能運(yùn)行。另外你還可以配置更多的特性要求,例如必須支持自動(dòng)對(duì)焦的相機(jī)才能運(yùn)行你的應(yīng)用程序,更多的特性可以在 官方文檔 上查詢(xún)。
2.4 獲取 CameraManager 實(shí)例
????????CameraManager 是一個(gè)負(fù)責(zé)查詢(xún)和建立相機(jī)連接的系統(tǒng)服務(wù),可以說(shuō) CameraManager 是 Camera2 使用流程的起點(diǎn),所以首先我們要通過(guò) getSystemService() 獲取 CameraManager 實(shí)例:
private val cameraManager: CameraManager by lazy { getSystemService(CameraManager::class.java) }
2.5 獲取相機(jī) ID 列表
????????接下來(lái)我們要獲取所有可用的相機(jī) ID 列表,這個(gè) ID 列表的長(zhǎng)度也代表有多少個(gè)相機(jī)可以使用。使用的 API 是 CameraManager.getCameraIdList(),它會(huì)返回一個(gè)包含所有可用相機(jī) ID 的字符串?dāng)?shù)組:
val cameraIdList = cameraManager.cameraIdList
注意:Kotlin 會(huì)將很多 Java API 的 getter 直接轉(zhuǎn)換成 Kotlin 的 property 語(yǔ)法,所以你會(huì)看到 getCameraIdList() 被轉(zhuǎn)換成了 cameraIdList,后續(xù)會(huì)有很多類(lèi)似的轉(zhuǎn)換,這里提前說(shuō)明下,避免誤解。
2.6 根據(jù)相機(jī) ID 獲取 CameraCharacteristics
????????CameraCharacteristics 是相機(jī)信息的提供者,通過(guò)它我們可以獲取所有相機(jī)信息,這里我們需要根據(jù)攝像頭的方向篩選出前置和后置攝像頭,并且要求相機(jī)的 Hardware Level 必須是 FULL 及以上,所以首先我們要獲取所有相機(jī)的 CameraCharacteristics 實(shí)例,涉及的 API 是 CameraManager.getCameraCharacteristics(),它會(huì)根據(jù)你指定的相機(jī) ID 返回對(duì)應(yīng)的相機(jī)信息:
/**
* 判斷相機(jī)的 Hardware Level 是否大于等于指定的 Level。
*/
fun CameraCharacteristics.isHardwareLevelSupported(requiredLevel: Int): Boolean {
val sortedLevels = intArrayOf(
CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY,
CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED,
CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_FULL,
CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_3
)
val deviceLevel = this[CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL]
if (requiredLevel == deviceLevel) {
return true
}
for (sortedLevel in sortedLevels) {
if (requiredLevel == sortedLevel) {
return true
} else if (deviceLevel == sortedLevel) {
return false
}
}
return false
}
// 遍歷所有可用的攝像頭 ID,只取出其中的前置和后置攝像頭信息。
val cameraIdList = cameraManager.cameraIdList
cameraIdList.forEach { cameraId ->
val cameraCharacteristics = cameraManager.getCameraCharacteristics(cameraId)
if (cameraCharacteristics.isHardwareLevelSupported(REQUIRED_SUPPORTED_HARDWARE_LEVEL)) {
if (cameraCharacteristics[CameraCharacteristics.LENS_FACING] == CameraCharacteristics.LENS_FACING_FRONT) {
frontCameraId = cameraId
frontCameraCharacteristics = cameraCharacteristics
} else if (cameraCharacteristics[CameraCharacteristics.LENS_FACING] == CameraCharacteristics.LENS_FACING_BACK) {
backCameraId = cameraId
backCameraCharacteristics = cameraCharacteristics
}
}
}
2.7 開(kāi)啟相機(jī)
????????調(diào)用 CameraManager.openCamera() 方法開(kāi)啟相機(jī),該方法要求我們傳遞兩個(gè)參數(shù),一個(gè)是相機(jī) ID,一個(gè)是監(jiān)聽(tīng)相機(jī)狀態(tài)的 CameraStateCallback。當(dāng)相機(jī)被成功開(kāi)啟的時(shí)候會(huì)通過(guò) CameraStateCallback.onOpened() 方法回調(diào)一個(gè) CameraDevice 實(shí)例給你,否則的話會(huì)通過(guò) CameraStateCallback.onError() 方法回調(diào)一個(gè) CameraDevice 實(shí)例和一個(gè)錯(cuò)誤碼給你。onOpened() 和 onError() 其實(shí)都意味著相機(jī)已經(jīng)被開(kāi)啟了,唯一的區(qū)別是 onError() 表示開(kāi)啟過(guò)程中出了問(wèn)題,你必須把傳遞給你的 CameraDevice 關(guān)閉,而不是繼續(xù)使用它,具體的 API 介紹可以自行查看文檔。另外,你必須確保在開(kāi)啟相機(jī)之前已經(jīng)被授予了相機(jī)權(quán)限,否則會(huì)拋權(quán)限異常。一個(gè)比較穩(wěn)妥的做法就是每次開(kāi)啟相機(jī)之前檢查相機(jī)權(quán)限。下面是主要代碼片段:
private data class OpenCameraMessage(val cameraId: String, val cameraStateCallback: CameraStateCallback)
@SuppressLint("MissingPermission")
override fun handleMessage(msg: Message): Boolean {
when (msg.what) {
MSG_OPEN_CAMERA -> {
val openCameraMessage = msg.obj as OpenCameraMessage
val cameraId = openCameraMessage.cameraId
val cameraStateCallback = openCameraMessage.cameraStateCallback
cameraManager.openCamera(cameraId, cameraStateCallback, cameraHandler)
Log.d(TAG, "Handle message: MSG_OPEN_CAMERA")
}
}
return false
}
private fun openCamera() {
// 有限選擇后置攝像頭,其次才是前置攝像頭。
val cameraId = backCameraId ?: frontCameraId
if (cameraId != null) {
val openCameraMessage = OpenCameraMessage(cameraId, CameraStateCallback())
cameraHandler?.obtainMessage(MSG_OPEN_CAMERA, openCameraMessage)?.sendToTarget()
} else {
throw RuntimeException("Camera id must not be null.")
}
}
private inner class CameraStateCallback : CameraDevice.StateCallback() {
@WorkerThread
override fun onOpened(camera: CameraDevice) {
cameraDevice = camera
runOnUiThread { Toast.makeText(this@MainActivity, "相機(jī)已開(kāi)啟", Toast.LENGTH_SHORT).show() }
}
@WorkerThread
override fun onError(camera: CameraDevice, error: Int) {
camera.close()
cameraDevice = null
}
}
2.8 關(guān)閉相機(jī)
????????和其他硬件資源的使用一樣,當(dāng)我們不再需要使用相機(jī)時(shí)記得調(diào)用 CameraDevice.close() 方法及時(shí)關(guān)閉相機(jī)回收資源。關(guān)閉相機(jī)的操作至關(guān)重要,因?yàn)槿绻阋恢闭加孟鄼C(jī)資源,其他基于相機(jī)開(kāi)發(fā)的功能都會(huì)無(wú)法正常使用,嚴(yán)重情況下直接導(dǎo)致其他相機(jī)相關(guān)的 APP 無(wú)法正常使用,當(dāng)相機(jī)被完全關(guān)閉的時(shí)候會(huì)通過(guò) CameraStateCallback.onCllosed() 方法通知你相機(jī)已經(jīng)被關(guān)閉。那么在什么時(shí)候關(guān)閉相機(jī)最合適呢?我個(gè)人的建議是在 onPause() 的時(shí)候就一定要關(guān)閉相機(jī),因?yàn)樵谶@個(gè)時(shí)候相機(jī)頁(yè)面已經(jīng)不是用戶關(guān)注的焦點(diǎn),大部分情況下已經(jīng)可以關(guān)閉相機(jī)了。
@SuppressLint("MissingPermission")
override fun handleMessage(msg: Message): Boolean {
when (msg.what) {
MSG_CLOSE_CAMERA -> {
cameraDevice?.close()
Log.d(TAG, "Handle message: MSG_CLOSE_CAMERA")
}
}
return false
}
override fun onPause() {
super.onPause()
closeCamera()
}
private fun closeCamera() {
cameraHandler?.sendEmptyMessage(MSG_CLOSE_CAMERA)
}
private inner class CameraStateCallback : CameraDevice.StateCallback() {
@WorkerThread
override fun onClosed(camera: CameraDevice) {
cameraDevice = null
runOnUiThread { Toast.makeText(this@MainActivity, "相機(jī)已關(guān)閉", Toast.LENGTH_SHORT).show() }
}
}
三、預(yù)覽
3.1 獲取預(yù)覽尺寸
????????在第二節(jié)提到了 CameraCharacteristics 是一個(gè)只讀的相機(jī)信息提供者,其內(nèi)部攜帶大量的相機(jī)信息,包括代表相機(jī)朝向的 LENS_FACING
;判斷閃光燈是否可用的 FLASH_INFO_AVAILABLE
;獲取所有可用 AE 模式的 CONTROL_AE_AVAILABLE_MODES
等等。如果你對(duì) Camera1 比較熟悉,那么 CameraCharacteristics 有點(diǎn)像 Camera1 的 Camera.CameraInfo 或者 Camera.Parameters。CameraCharacteristics 以鍵值對(duì)的方式提供相機(jī)信息,你可以通過(guò) CameraCharacteristics.get()
方法獲取相機(jī)信息,該方法要求你傳遞一個(gè) Key 以確定你要獲取哪方面的相機(jī)信息,例如下面的代碼展示了如何獲取攝像頭方向信息:
val cameraCharacteristics = cameraManager.getCameraCharacteristics(cameraId)
val lensFacing = cameraCharacteristics[CameraCharacteristics.LENS_FACING]
when(lensFacing) {
CameraCharacteristics.LENS_FACING_FRONT -> { // 前置攝像頭 }
CameraCharacteristics.LENS_FACING_BACK -> { // 后置攝像頭 }
CameraCharacteristics.LENS_FACING_EXTERNAL -> { // 外置攝像頭 }
}
????????CameraCharacteristics 有大量的 Key 定義,當(dāng)你在開(kāi)發(fā)過(guò)程中需要獲取某些相機(jī)信息的時(shí)候再去查閱 API文檔即可。
????????由于不同廠商對(duì)相機(jī)的實(shí)現(xiàn)都會(huì)有差異,所以很多參數(shù)在不同的手機(jī)上支持的情況也不一樣,相機(jī)的預(yù)覽尺寸也是,所以接下來(lái)我們就要通過(guò) CameraCharacteristics 獲取相機(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ò) CameraCharacteristics 獲取該設(shè)備支持的所有預(yù)覽尺寸:
val streamConfigurationMap = cameraCharacteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP)
val supportedSizes = streamConfigurationMap?.getOutputSizes(SurfaceTexture::class.java)
????????從上面的代碼可以看出預(yù)覽尺寸列表并不是直接從 CameraCharacteristics 獲取的,而是先通過(guò) SCALER_STREAM_CONFIGURATION_MAP
獲取 StreamConfigurationMap 對(duì)象,然后通過(guò) StreamConfigurationMap.getOutputSizes()
方法獲取尺寸列表,該方法會(huì)要求你傳遞一個(gè) Class 類(lèi)型,然后根據(jù)這個(gè)類(lèi)型返回對(duì)應(yīng)的尺寸列表,如果給定的類(lèi)型不支持,則返回 null,你可以通過(guò) StreamConfigurationMap.isOutputSupportedFor()
方法判斷某一個(gè)類(lèi)型是否被支持,常見(jiàn)的類(lèi)型有:
-
ImageReader:常用來(lái)拍照或接收 YUV 數(shù)據(jù)。
-
MediaRecorder:常用來(lái)錄制視頻。
-
MediaCodec:常用來(lái)錄制視頻。
-
SurfaceHolder:常用來(lái)顯示預(yù)覽畫(huà)面。
-
SurfaceTexture:常用來(lái)顯示預(yù)覽畫(huà)面。
????????由于我們使用的是 SurfaceTexture,所以顯然這里我們就要傳遞 SurfaceTexture.class 獲取支持的尺寸列表。如果我們把所有的預(yù)覽尺寸都打印出來(lái)看時(shí),會(huì)發(fā)現(xiàn)一個(gè)比較特別的情況,就是預(yù)覽尺寸的寬是長(zhǎng)邊,高是短邊,例如 1920x1080,而不是 1080x1920,這是因?yàn)橄鄼C(jī) Sensor 的寬是長(zhǎng)邊,而高是短邊。
????????在獲取到預(yù)覽尺寸列表之后,我們要根據(jù)自己的實(shí)際需求過(guò)濾出其中一個(gè)最符合要求的尺寸,并且把它設(shè)置給相機(jī),在我們的 Demo 里,只有當(dāng)預(yù)覽尺寸的比例和大小都滿足要求時(shí)才能被設(shè)置給相機(jī),如下所示:
@WorkerThread
private fun getOptimalSize(cameraCharacteristics: CameraCharacteristics, clazz: Class<*>, maxWidth: Int, maxHeight: Int): Size? {
val aspectRatio = maxWidth.toFloat() / maxHeight
val streamConfigurationMap = cameraCharacteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP)
val supportedSizes = streamConfigurationMap?.getOutputSizes(clazz)
if (supportedSizes != null) {
for (size in supportedSizes) {
if (size.width.toFloat() / size.height == aspectRatio && size.height <= maxHeight && size.width <= maxWidth) {
return size
}
}
}
return null
}
3.2 配置預(yù)覽尺寸
????????在獲取適合的預(yù)覽尺寸之后,接下來(lái)就是配置預(yù)覽尺寸使其生效了。在配置尺寸方面,Camera2 和 Camera1 有著很大的不同,Camera1 是將所有的尺寸信息都設(shè)置給相機(jī),而 Camera2 則是把尺寸信息設(shè)置給 Surface,例如接收預(yù)覽畫(huà)面的 SurfaceTexture,或者是接收拍照?qǐng)D片的 ImageReader,相機(jī)在輸出圖像數(shù)據(jù)的時(shí)候會(huì)根據(jù) Surface 配置的 Buffer 大小輸出對(duì)應(yīng)尺寸的畫(huà)面。
????????獲取 Surface 的方式有很多種,可以通過(guò) TextureView、SurfaceView、ImageReader 甚至是通過(guò) OpenGL 創(chuàng)建,這里我們要將預(yù)覽畫(huà)面顯示在屏幕上,所以我們選擇了 TextureView,并且通過(guò) TextureView.SurfaceTextureListener
回調(diào)接口監(jiān)聽(tīng) SurfaceTexture 的狀態(tài),在獲取可用的 SurfaceTexture 對(duì)象之后通過(guò) SurfaceTexture.setDefaultBufferSize()
設(shè)置預(yù)覽畫(huà)面的尺寸,最后使用 Surface(SurfaceTexture)
構(gòu)造方法創(chuàng)建出預(yù)覽的 Surface 對(duì)象:
????????首先,我們?cè)诓季治募刑砑右粋€(gè) TextureView,并給它取個(gè) ID 叫 camera_preview:
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<TextureView
android:id="@+id/camera_preview"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
????????然后我們?cè)?Activity 里獲取 TextureView 對(duì)象,并且注冊(cè)一個(gè) TextureView.SurfaceTextureListener
用于監(jiān)聽(tīng) SurfaceTexture 的狀態(tài):
private inner class PreviewSurfaceTextureListener : TextureView.SurfaceTextureListener {
@MainThread
override fun onSurfaceTextureSizeChanged(surfaceTexture: SurfaceTexture, width: Int, height: Int) = Unit
@MainThread
override fun onSurfaceTextureUpdated(surfaceTexture: SurfaceTexture) = Unit
@MainThread
override fun onSurfaceTextureDestroyed(surfaceTexture: SurfaceTexture): Boolean = false
@MainThread
override fun onSurfaceTextureAvailable(surfaceTexture: SurfaceTexture, width: Int, height: Int) {
previewSurfaceTexture = surfaceTexture
}
}
cameraPreview = findViewById<CameraPreview>(R.id.camera_preview)
cameraPreview.surfaceTextureListener = PreviewSurfaceTextureListener()
????????當(dāng) SurfaceTexture 可用的時(shí)候會(huì)回調(diào) onSurfaceTextureAvailable()
方法并且把 SurfaceTexture 對(duì)象和尺寸傳遞給我們,此時(shí)我們要做的就是通過(guò) SurfaceTexture.setDefaultBufferSize()
設(shè)置預(yù)覽畫(huà)面的尺寸并且創(chuàng)建 Surface 對(duì)象:
val previewSize = getOptimalSize(cameraCharacteristics, SurfaceTexture::class.java, width, height)!!
previewSurfaceTexture.setDefaultBufferSize(previewSize.width, previewSize.height)
previewSurface = Surface(previewSurfaceTexture)
????????到這里,用于預(yù)覽的 Surface 就準(zhǔn)備好了,接下來(lái)我們來(lái)看下如何創(chuàng)建 CameraCaptureSession。
3.3 創(chuàng)建 CameraCaptureSession
????????用于接收預(yù)覽畫(huà)面的 Surface 準(zhǔn)備就緒了,接下來(lái)我們要使用這個(gè) Surface 創(chuàng)建一個(gè) CameraCaptureSession 實(shí)例,涉及的方法是 CameraDevice.createCaptureSession()
,該方法要求你傳遞以下三個(gè)參數(shù):
-
outputs:所有用于接收?qǐng)D像數(shù)據(jù)的 Surface,例如本章用于接收預(yù)覽畫(huà)面的 Surface,后續(xù)還會(huì)有用于拍照的 Surface,這些 Surface 必須在創(chuàng)建 Session 之前就準(zhǔn)備好,并且在創(chuàng)建 Session 的時(shí)候傳遞給底層用于配置 Pipeline。
-
callback:用于監(jiān)聽(tīng) Session 狀態(tài)的
CameraCaptureSession.StateCallback
對(duì)象,就如同開(kāi)關(guān)相機(jī)一樣,創(chuàng)建和銷(xiāo)毀 Session 也需要我們注冊(cè)一個(gè)狀態(tài)監(jiān)聽(tīng)器。
-
handler:用于執(zhí)行
CameraCaptureSession.StateCallback
的 Handler 對(duì)象,可以是異步線程的 Handler,也可以是主線程的 Handler,在我們的 Demo 里使用的是主線程 Handler。
private inner class SessionStateCallback : CameraCaptureSession.StateCallback() {
@MainThreadoverride
fun onConfigureFailed(session: CameraCaptureSession) {
}
@MainThreadoverride
fun onConfigured(session: CameraCaptureSession) {
}
@MainThreadoverride
fun onClosed(session: CameraCaptureSession) {
}
}
val sessionStateCallback = SessionStateCallback()
val outputs = listOf(previewSurface)
cameraDevice.createCaptureSession(outputs, sessionStateCallback, mainHandler)
3.4 創(chuàng)建 CaptureRequest
????????在介紹如何開(kāi)啟和關(guān)閉預(yù)覽之前,我們有必要先介紹下 CaptureRequest,因?yàn)樗俏覀儓?zhí)行任何相機(jī)操作都繞不開(kāi)的核心類(lèi),因?yàn)?CaptureRequest 是向 CameraCaptureSession 提交 Capture 請(qǐng)求時(shí)的信息載體,其內(nèi)部包括了本次 Capture 的參數(shù)配置和接收?qǐng)D像數(shù)據(jù)的 Surface。CaptureRequest 可以配置的信息非常多,包括圖像格式、圖像分辨率、傳感器控制、閃光燈控制、3A 控制等等,可以說(shuō)絕大部分的相機(jī)參數(shù)都是通過(guò) CaptureRequest 配置的??梢酝ㄟ^(guò) CameraDevice.createCaptureRequest()
方法創(chuàng)建一個(gè) CaptureRequest.Builder 對(duì)象,該方法只有一個(gè)參數(shù) templateType 用于指定使用何種模板創(chuàng)建 CaptureRequest.Builder 對(duì)象。因?yàn)?CaptureRequest 可以配置的參數(shù)實(shí)在是太多了,如果每一個(gè)參數(shù)都要去配置,那真的是既復(fù)雜又費(fèi)時(shí),所以 Camera2 根據(jù)使用場(chǎng)景的不同,為我們事先配置好了一些常用的參數(shù)模板:
-
TEMPLATE_PREVIEW:適用于配置預(yù)覽的模板。
-
TEMPLATE_RECORD:適用于視頻錄制的模板。
-
TEMPLATE_STILL_CAPTURE:適用于拍照的模板。
-
TEMPLATE_VIDEO_SNAPSHOT:適用于在錄制視頻過(guò)程中支持拍照的模板。
-
TEMPLATE_MANUAL:適用于希望自己手動(dòng)配置大部分參數(shù)的模板。
????????這里我們要?jiǎng)?chuàng)建一個(gè)用于預(yù)覽的 CaptureRequest,所以傳遞了 TEMPLATE_PREVIEW 作為參數(shù):
val requestBuilder = cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW)
????????一個(gè) CaptureRequest 除了需要配置很多參數(shù)之外,還要求至少配置一個(gè) Surface(任何相機(jī)操作的本質(zhì)都是為了捕獲圖像),并且配置的 Surface 必須屬于創(chuàng)建 Session 時(shí)添加的那些 Surface,涉及的方法是 CaptureRequest.Builder.addTarget()
,你可以多次調(diào)用該方法添加多個(gè) Surface。
requestBuilder.addTarget(previewSurface)
????????最后,我們通過(guò) CaptureRequest.Builder.build()
方法創(chuàng)建出一個(gè)只讀的 CaptureRequest 實(shí)例:
val request = requestBuilder.build()
3.5 開(kāi)啟和停止預(yù)覽
????????在 Camera2 里,預(yù)覽本質(zhì)上是不斷重復(fù)執(zhí)行的 Capture 操作,每一次 Capture 都會(huì)把預(yù)覽畫(huà)面輸出到對(duì)應(yīng)的 Surface 上,涉及的方法是 CameraCaptureSession.setRepeatingRequest()
,該方法有三個(gè)參數(shù):
-
request:在不斷重復(fù)執(zhí)行 Capture 時(shí)使用的 CaptureRequest 對(duì)象。
-
callback:監(jiān)聽(tīng)每一次 Capture 狀態(tài)的
CameraCaptureSession.CaptureCallback
對(duì)象,例如onCaptureStarted()
意味著一次 Capture 的開(kāi)始,而onCaptureCompleted()
意味著一次 Capture 的結(jié)束。 -
hander:用于執(zhí)行
CameraCaptureSession.CaptureCallback
的 Handler 對(duì)象,可以是異步線程的 Handler,也可以是主線程的 Handler,在我們的 Demo 里使用的是主線程 Handler。
了解了核心方法之后,開(kāi)啟預(yù)覽的操作就很顯而易見(jiàn)了:
val requestBuilder = cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW)
requestBuilder.addTarget(previewSurface)
val request = requestBuilder.build()
captureSession.setRepeatingRequest(request, RepeatingCaptureStateCallback(), mainHandler)
????????如果要關(guān)閉預(yù)覽的話,可以通過(guò) CameraCaptureSession.stopRepeating()
停止不斷重復(fù)執(zhí)行的 Capture 操作:
captureSession.stopRepeating()
????????到目前為止,如果一切正常的話,預(yù)覽畫(huà)面應(yīng)該就已經(jīng)顯示出來(lái)了。
3.6 適配預(yù)覽比例
????????前面我們使用了一個(gè)占滿屏幕的 TextureView 來(lái)顯示預(yù)覽畫(huà)面,并且預(yù)覽尺寸我們選擇了 4:3 的比例,你很可能會(huì)看到預(yù)覽畫(huà)面變形的情況,這因?yàn)?Surface 的比例和 TextureView 的比例不一致導(dǎo)致的,你可以想象 Surface 就是一張圖片,TextureView 就是 ImageView,將 4:3 的圖片顯示在 16:9 的 ImageView 上必然會(huì)出現(xiàn)畫(huà)面拉伸變形的情況:
預(yù)覽畫(huà)面變形
所以接下來(lái)我們要學(xué)習(xí)的是如何適配不同的預(yù)覽比例。預(yù)覽比例的適配有多種方式:
-
根據(jù)預(yù)覽比例修改 TextureView 的寬高,比如用戶選擇了 4:3 的預(yù)覽比例,這個(gè)時(shí)候我們會(huì)選取 4:3 的預(yù)覽尺寸并且把 TextureView 修改成 4:3 的比例,從而讓畫(huà)面不會(huì)變形。
-
使用固定的預(yù)覽比例,然后根據(jù)比例去選取適合的預(yù)覽尺寸,例如固定 4:3 的比例,選擇 1440x1080 的尺寸,并且把 TextureView 的寬高也設(shè)置成 4:3。
-
固定 TextureView 的寬高,然后根據(jù)預(yù)覽比例使用
TextureView.setTransform()
方法修改預(yù)覽畫(huà)面繪制在 TextureView 上的方式,從而讓預(yù)覽畫(huà)面不變形,這跟ImageView.setImageMatrix()
如出一轍。
簡(jiǎn)單來(lái)說(shuō),解決預(yù)覽畫(huà)面變形的問(wèn)題,本質(zhì)上就是解決畫(huà)面和畫(huà)布比例不一致的問(wèn)題。在我們的 Demo 中,出于簡(jiǎn)化的目的,我們選擇了第二種方式適配比例,因?yàn)檫@種方式實(shí)現(xiàn)起來(lái)比較簡(jiǎn)單,所以我們會(huì)寫(xiě)一個(gè)自定義的 TextureView,讓它的比例固定是 4:3,它的寬度固定填滿父布局,高度根據(jù)比例動(dòng)態(tài)計(jì)算:
class CameraPreview @JvmOverloads constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int = 0) : TextureView(context, attrs, defStyleAttr) {
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
val width = MeasureSpec.getSize(widthMeasureSpec)
setMeasuredDimension(width, width / 3 * 4)
}
}
3.7 認(rèn)識(shí) ImageReader
????????在 Camera2 里,ImageReader 是獲取圖像數(shù)據(jù)的一個(gè)重要途徑,我們可以通過(guò)它獲取各種各樣格式的圖像數(shù)據(jù),例如 JPEG、YUV 和 RAW 等等。我們可以通過(guò) ImageReader.newInstance()
方法創(chuàng)建一個(gè) ImageReader 對(duì)象,該方法要求我們傳遞以下四個(gè)參數(shù):
-
width:圖像數(shù)據(jù)的寬度。
-
height:圖像數(shù)據(jù)的高度。
-
format:圖像數(shù)據(jù)的格式,定義在 ImageFormat 里,例如
ImageFormat.YUV_420_888
。 -
maxImages:最大 Image 個(gè)數(shù),可以理解成 Image 對(duì)象池的大小。
????????當(dāng)有圖像數(shù)據(jù)生成的時(shí)候,ImageReader 會(huì)通過(guò)通過(guò) ImageReader.OnImageAvailableListener.onImageAvailable()
方法通知我們,然后我們可以調(diào)用 ImageReader.acquireNextImage()
方法獲取存有最新數(shù)據(jù)的 Image 對(duì)象,而在 Image 對(duì)象里圖像數(shù)據(jù)又根據(jù)不同格式被劃分多個(gè)部分分別存儲(chǔ)在單獨(dú)的 Plane 對(duì)象里,我們可以通過(guò)調(diào)用 Image.getPlanes()
方法獲取所有的 Plane 對(duì)象的數(shù)組,最后通過(guò) Plane.getBuffer()
獲取每一個(gè) Plane 里存儲(chǔ)的圖像數(shù)據(jù)。以 YUV 數(shù)據(jù)為例,當(dāng)有 YUV 數(shù)據(jù)生成的時(shí)候,數(shù)據(jù)會(huì)被分成 Y、U、V 三部分分別存儲(chǔ)到 Plane 里,如下圖所示:
override fun onImageAvailable(imageReader: ImageReader) {
val image = imageReader.acquireNextImage()
if (image != null) {
val planes = image.planes
val yPlane = planes[0]
val uPlane = planes[1]
val vPlane = planes[2]
val yBuffer = yPlane.buffer // Data from Y channel
val uBuffer = uPlane.buffer // Data from U channel
val vBuffer = vPlane.buffer // Data from V channel
}
image?.close()
}
????????上面的代碼是獲取 YUV 數(shù)據(jù)的流程,特別要注意的是最后一步調(diào)用 Image.close()
方法十分重要,當(dāng)我們不再需要使用某一個(gè) Image 對(duì)象的時(shí)候記得通過(guò)該方法釋放資源,因?yàn)?Image 對(duì)象實(shí)際上來(lái)自于一個(gè)創(chuàng)建 ImageReader 時(shí)就確定大小的對(duì)象池,如果我們不釋放它的話就會(huì)導(dǎo)致對(duì)象池很快就被耗光,并且拋出一個(gè)異常。類(lèi)似的的當(dāng)我們不再需要使用 某一個(gè) ImageReader 對(duì)象的時(shí)候,也要記得調(diào)用 ImageReader.close()
方法釋放資源。
3.8 獲取預(yù)覽數(shù)據(jù)
????????介紹完 ImageReader 之后,接下來(lái)我們就來(lái)創(chuàng)建一個(gè)接收每一幀預(yù)覽數(shù)據(jù)的 ImageReader,并且數(shù)據(jù)格式為 YUV_420_888。首先,我們要先判斷 YUV_420_888 數(shù)據(jù)格式是否支持,所以會(huì)有如下的代碼:
val imageFormat = ImageFormat.YUV_420_888
val streamConfigurationMap = cameraCharacteristics[CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP]
if (streamConfigurationMap?.isOutputSupportedFor(imageFormat) == true) {
// YUV_420_888 is supported
}
????????接著,我們使用前面已經(jīng)確定好的預(yù)覽尺寸創(chuàng)建一個(gè) ImageReader,并且注冊(cè)一個(gè) ImageReader.OnImageAvailableListener
用于監(jiān)聽(tīng)數(shù)據(jù)的更新,最后通過(guò) ImageReader.getSurface()
方法獲取接收預(yù)覽數(shù)據(jù)的 Surface:
val imageFormat = ImageFormat.YUV_420_888
val streamConfigurationMap = cameraCharacteristics[CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP]
if (streamConfigurationMap?.isOutputSupportedFor(imageFormat) == true) {
previewDataImageReader = ImageReader.newInstance(previewSize.width, previewSize.height, imageFormat, 3)
previewDataImageReader?.setOnImageAvailableListener(OnPreviewDataAvailableListener(), cameraHandler)
previewDataSurface = previewDataImageReader?.surface
}
????????創(chuàng)建完 ImageReader,并且獲取它的 Surface 之后,我們就可以在創(chuàng)建 Session 的時(shí)候添加這個(gè) Surface 告訴 Pipeline 我們有一個(gè)專(zhuān)門(mén)接收 YUV_420_888 的 Surface:
val sessionStateCallback = SessionStateCallback()
val outputs = mutableListOf<Surface>()
val previewSurface = previewSurface
val previewDataSurface = previewDataSurface
outputs.add(previewSurface!!)
if (previewDataSurface != null) {
outputs.add(previewDataSurface)
}
cameraDevice.createCaptureSession(outputs, sessionStateCallback, mainHandler)
????????獲取預(yù)覽數(shù)據(jù)和顯示預(yù)覽畫(huà)面一樣都是不斷重復(fù)執(zhí)行的 Capture 操作,所以我們只需要在開(kāi)始預(yù)覽的時(shí)候通過(guò) CaptureRequest.Builder.addTarget()
方法添加接收預(yù)覽數(shù)據(jù)的 Surface 即可,所以一個(gè) CaptureRequest 會(huì)有兩個(gè) Surface,一個(gè)現(xiàn)實(shí)預(yù)覽畫(huà)面的 Surface,一個(gè)接收預(yù)覽數(shù)據(jù)的 Surface:
val requestBuilder = cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW)
val previewSurface = previewSurface
val previewDataSurface = previewDataSurface
requestBuilder.addTarget(previewSurface!!)
if (previewDataSurface != null) {
requestBuilder.addTarget(previewDataSurface)
}
val request = requestBuilder.build()
captureSession.setRepeatingRequest(request, RepeatingCaptureStateCallback(), mainHandler)
????????在開(kāi)始預(yù)覽之后,每一次刷新預(yù)覽畫(huà)面的時(shí)候,都會(huì)通過(guò) ImageReader.OnImageAvailableListener.onImageAvailable()
方法通知我們:
/**
* Called every time the preview frame data is available.
*/
override fun onImageAvailable(imageReader: ImageReader) {
val image = imageReader.acquireNextImage()
if (image != null) {
val planes = image.planes
val yPlane = planes[0]
val uPlane = planes[1]
val vPlane = planes[2]
val yBuffer = yPlane.buffer // Data from Y channel
val uBuffer = uPlane.buffer // Data from U channel
val vBuffer = vPlane.buffer // Data from V channel
}
image?.close()
}
}
3.9 如何矯正圖像數(shù)據(jù)的方向
????????如果你熟悉 Camera1 的話,也許已經(jīng)發(fā)現(xiàn)了一個(gè)問(wèn)題,就是 Camera2 不需要經(jīng)過(guò)任何預(yù)覽畫(huà)面方向的矯正,就可以正確現(xiàn)實(shí)畫(huà)面,而 Camera1 則需要根據(jù)攝像頭傳感器的方向進(jìn)行預(yù)覽畫(huà)面的方向矯正。其實(shí),Camera2 也需要進(jìn)行預(yù)覽畫(huà)面的矯正,只不過(guò)系統(tǒng)幫我們做了而已,當(dāng)我們使用 TextureView 或者 SurfaceView 進(jìn)行畫(huà)面預(yù)覽的時(shí)候,系統(tǒng)會(huì)根據(jù)【設(shè)備自然方向】、【攝像傳感器方向】和【顯示方向】自動(dòng)矯正預(yù)覽畫(huà)面的方向,并且該矯正規(guī)則只適用于顯示方向和和設(shè)備自然方向一致的情況下,舉個(gè)例子,當(dāng)我們把手機(jī)橫放并且允許自動(dòng)旋轉(zhuǎn)屏幕的時(shí)候,看到的預(yù)覽畫(huà)面的方向就是錯(cuò)誤的。此外,當(dāng)我們使用一個(gè) GLSurfaceView 顯示預(yù)覽畫(huà)面或者使用 ImageReader 接收?qǐng)D像數(shù)據(jù)的時(shí)候,系統(tǒng)都不會(huì)進(jìn)行畫(huà)面的自動(dòng)矯正,因?yàn)樗恢牢覀円绾物@示預(yù)覽畫(huà)面,所以我們還是有必要學(xué)習(xí)下如何矯正圖像數(shù)據(jù)的方向,在介紹如何矯正圖像數(shù)據(jù)方向之前,我們需要先了解幾個(gè)概念,它們分別是【設(shè)備自然方向】、【局部坐標(biāo)系】、【顯示方向】和【攝像頭傳感器方向】。
設(shè)備方向
????????當(dāng)我們談?wù)摲较虻臅r(shí)候,實(shí)際上都是相對(duì)于某一個(gè) 0° 方向的角度,這個(gè) 0° 方向被稱(chēng)作自然方向,例如人站立的時(shí)候就是自然方向,你總不會(huì)認(rèn)為一個(gè)人要倒立的時(shí)候才是自然方向吧,而接下來(lái)我們要談?wù)摰脑O(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í)針夾角。為了更清楚的說(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è)備方向是相對(duì)于其現(xiàn)實(shí)空間中自然方向的角度,而顯示方向是相對(duì)局部坐標(biāo)系的角度。
攝像頭傳感器方向
????????攝像頭傳感器方向指的是傳感器采集到的畫(huà)面方向經(jīng)過(guò)順時(shí)針旋轉(zhuǎn)多少度之后才能和局部坐標(biāo)系的 y 軸正方向一致,也就是通過(guò) CameraCharacteristics.SENSOR_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ò) CameraCharacteristics.SENSOR_ORIENTATION 去判斷方向,而不是假設(shè)所有設(shè)備的攝像頭傳感器方向都是 90°。
矯正圖像數(shù)據(jù)的方向
????????介紹完幾個(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í)考慮顯示方向和攝像頭傳感器方向的方法,我把它翻譯成 Kotlin 語(yǔ)法:
private fun getDisplayRotation(cameraCharacteristics: CameraCharacteristics): Int {
val rotation = windowManager.defaultDisplay.rotation
val degrees = when (rotation) {
Surface.ROTATION_0 -> 0
Surface.ROTATION_90 -> 90
Surface.ROTATION_180 -> 180
Surface.ROTATION_270 -> 270
else -> 0
}
val sensorOrientation = cameraCharacteristics[CameraCharacteristics.SENSOR_ORIENTATION]!!
return if (cameraCharacteristics[CameraCharacteristics.LENS_FACING] == CameraCharacteristics.LENS_FACING_FRONT) {
(360 - (sensorOrientation + degrees) % 360) % 360
} else {
(sensorOrientation - degrees + 360) % 360
}
}
????????如果你已經(jīng)完全理解前面介紹的那些角度的概念,那你應(yīng)該很容易就能理解上面這段代碼,實(shí)際上就是通過(guò) WindowManager 獲取當(dāng)前的顯示方向,然后再參照攝像頭傳感器方向以及是否是前后置,最后計(jì)算出我們實(shí)際要旋轉(zhuǎn)的角度。
四、 拍照
4.1 理解 Capture 工作流程
在正式介紹如何拍照之前,我們有必要深入理解幾種不同模式的 Capture 的工作流程,只要理解它們的工作流程就很容易掌握各種拍照模式的實(shí)現(xiàn)原理,在第一章Camera API1 簡(jiǎn)敘述 里我們介紹了 Capture 有以下幾種不同模式:
-
單次模式(One-shot):指的是只執(zhí)行一次的 Capture 操作,例如設(shè)置閃光燈模式、對(duì)焦模式和拍一張照片等。多個(gè)單次模式的 Capture 會(huì)進(jìn)入隊(duì)列按順序執(zhí)行。
-
多次模式(Burst):指的是連續(xù)多次執(zhí)行指定的 Capture 操作,該模式和多次執(zhí)行單次模式的最大區(qū)別是連續(xù)多次 Capture 期間不允許插入其他任何 Capture 操作,例如連續(xù)拍攝 100 張照片,在拍攝這 100 張照片期間任何新的 Capture 請(qǐng)求都會(huì)排隊(duì)等待,直到拍完 100 張照片。多組多次模式的 Capture 會(huì)進(jìn)入隊(duì)列按順序執(zhí)行。
-
重復(fù)模式(Repeating):指的是不斷重復(fù)執(zhí)行指定的 Capture 操作,當(dāng)有其他模式的 Capture 提交時(shí)會(huì)暫停該模式,轉(zhuǎn)而執(zhí)行其他被模式的 Capture,當(dāng)其他模式的 Capture 執(zhí)行完畢后又會(huì)自動(dòng)恢復(fù)繼續(xù)執(zhí)行該模式的 Capture,例如顯示預(yù)覽畫(huà)面就是不斷 Capture 獲取每一幀畫(huà)面。該模式的 Capture 是全局唯一的,也就是新提交的重復(fù)模式 Capture 會(huì)覆蓋舊的重復(fù)模式 Capture。
????????我們舉個(gè)例子來(lái)進(jìn)一步說(shuō)明上面三種模式,假設(shè)我們的相機(jī)應(yīng)用程序開(kāi)啟了預(yù)覽,所以會(huì)提交一個(gè)重復(fù)模式的 Capture 用于不斷獲取預(yù)覽畫(huà)面,然后我們提交一個(gè)單次模式的 Capture,接著我們又提交了一組連續(xù)三次的多次模式的 Capture,這些不同模式的 Capture 會(huì)按照下圖所示被執(zhí)行:
Capture 工作原理
下面是幾個(gè)重要的注意事項(xiàng):
-
無(wú)論 Capture 以何種模式被提交,它們都是按順序串行執(zhí)行的,不存在并行執(zhí)行的情況。
-
重復(fù)模式是一個(gè)比較特殊的模式,因?yàn)樗鼤?huì)保留我們提交的 CaptureRequest 對(duì)象用于不斷重復(fù)執(zhí)行 Capture 操作,所以大多數(shù)情況下重復(fù)模式的 CaptureRequest 和其他模式的 CaptureRequest 是獨(dú)立的,這就會(huì)導(dǎo)致重復(fù)模式的參數(shù)和其他模式的參數(shù)會(huì)有一定的差異,例如重復(fù)模式不會(huì)配置
CaptureRequest.AF_TRIGGER_START
,因?yàn)檫@會(huì)導(dǎo)致相機(jī)不斷觸發(fā)對(duì)焦的操作。 -
如果某一次的 Capture 沒(méi)有配置預(yù)覽的 Surface,例如拍照的時(shí)候,就會(huì)導(dǎo)致本次 Capture 不會(huì)將畫(huà)面輸出到預(yù)覽的 Surface 上,進(jìn)而導(dǎo)致預(yù)覽畫(huà)面卡頓的情況,所以大部分情況下我們都會(huì)將預(yù)覽的 Surface 添加到所有的 CaptureRequest 里。
4.2 如何拍攝單張照片
????????拍攝單張照片是最簡(jiǎn)單的拍照模式,它使用的就是單次模式的 Capture,我們會(huì)使用 ImageReader 創(chuàng)建一個(gè)接收照片的 Surface,并且把它添加到 CaptureRequest 里提交給相機(jī)進(jìn)行拍照,最后通過(guò) ImageReader 的回調(diào)獲取 Image 對(duì)象,進(jìn)而獲取 JPEG 圖像數(shù)據(jù)進(jìn)行保存。
4.2.1 定義回調(diào)接口
????????當(dāng)拍照完成的時(shí)候我們會(huì)得到兩個(gè)數(shù)據(jù)對(duì)象,一個(gè)是通過(guò) onImageAvailable()
回調(diào)給我們的存儲(chǔ)圖像數(shù)據(jù)的 Image,一個(gè)是通過(guò) onCaptureCompleted()
回調(diào)給我們的存儲(chǔ)拍照信息的 CaptureResult,它們是一一對(duì)應(yīng)的,所以我們定義了如下兩個(gè)回調(diào)接口:
private val captureResults: BlockingQueue<CaptureResult> = LinkedBlockingDeque()
private inner class CaptureImageStateCallback : CameraCaptureSession.CaptureCallback() {
@MainThread
override fun onCaptureCompleted(session: CameraCaptureSession, request: CaptureRequest, result: TotalCaptureResult) {
super.onCaptureCompleted(session, request, result)
captureResults.put(result)
}
}
private inner class OnJpegImageAvailableListener : ImageReader.OnImageAvailableListener {
@WorkerThread
override fun onImageAvailable(imageReader: ImageReader) {
val image = imageReader.acquireNextImage()
val captureResult = captureResults.take()
if (image != null && captureResult != null) {
// Save image into sdcard.
}
}
}
4.2.2 創(chuàng)建 ImageReader
????????創(chuàng)建 ImageReader 需要我們指定照片的大小,所以首先我們要獲取支持的照片尺寸列表,并且從中篩選出合適的尺寸,假設(shè)我們要求照片的尺寸最大不能超過(guò) 4032x3024,并且比例必須是 4:3,所以會(huì)有如下篩選尺寸的代碼片段:
@WorkerThread
private fun getOptimalSize(cameraCharacteristics: CameraCharacteristics, clazz: Class<*>, maxWidth: Int, maxHeight: Int): Size? {
val streamConfigurationMap = cameraCharacteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP)
val supportedSizes = streamConfigurationMap?.getOutputSizes(clazz)
return getOptimalSize(supportedSizes, maxWidth, maxHeight)
}
@AnyThread
private fun getOptimalSize(supportedSizes: Array<Size>?, maxWidth: Int, maxHeight: Int): Size? {
val aspectRatio = maxWidth.toFloat() / maxHeight
if (supportedSizes != null) {
for (size in supportedSizes) {
if (size.width.toFloat() / size.height == aspectRatio && size.height <= maxHeight && size.width <= maxWidth) {
return size
}
}
}
return null
}
????????接著我們就可以篩選出合適的尺寸,然后創(chuàng)建一個(gè)圖像格式是 JPEG 的 ImageReader 對(duì)象,并且獲取它的 Surface:
val imageSize = getOptimalSize(cameraCharacteristics, ImageReader::class.java, maxWidth, maxHeight)!!
jpegImageReader = ImageReader.newInstance(imageSize.width, imageSize.height, ImageFormat.JPEG, 5)
jpegImageReader?.setOnImageAvailableListener(OnJpegImageAvailableListener(), cameraHandler)
jpegSurface = jpegImageReader?.surface
4.2.3 創(chuàng)建 CaptureRequest
????????接下來(lái)我們使用 TEMPLATE_STILL_CAPTURE
模板創(chuàng)建一個(gè)用于拍照的 CaptureRequest.Builder 對(duì)象,并且添加拍照的 Surface 和預(yù)覽的 Surface 到其中:
captureImageRequestBuilder = cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_STILL_CAPTURE)
captureImageRequestBuilder.addTarget(previewDataSurface)
captureImageRequestBuilder.addTarget(jpegSurface)
注意:你可能會(huì)疑問(wèn)為什么拍照用的 CaptureRequest 對(duì)象需要添加預(yù)覽的 Surface,這一點(diǎn)我們?cè)谇懊嬗薪忉屵^(guò)了,如果某一次的 Capture 沒(méi)有配置預(yù)覽的 Surface,例如拍照的時(shí)候,就會(huì)導(dǎo)致本次 Capture 不會(huì)將畫(huà)面輸出到預(yù)覽的 Surface 上,進(jìn)而導(dǎo)致預(yù)覽畫(huà)面卡頓的情況,所以大部分情況下我們都會(huì)將預(yù)覽的 Surface 添加到所有的 CaptureRequest 里。
4.2.4 矯正 JPEG 圖片方向
????????在 前面預(yù)覽章節(jié)里我們介紹了一些方向的概念,也提到了攝像頭傳感器的方向很多時(shí)候都不是 0°,這就會(huì)導(dǎo)致我們拍出來(lái)的照片方向是錯(cuò)誤的,例如手機(jī)攝像頭傳感器方向是 90° 的時(shí)候,垂直拿著手機(jī)拍出來(lái)的照片很可能是橫著的:
????????在進(jìn)行圖片方向矯正的時(shí)候,我們的目的是做到所見(jiàn)即所得,也就是用戶在預(yù)覽畫(huà)面里看到的是什么樣,輸出的圖片就是什么樣。為了做到圖片所見(jiàn)即所得,我們要同時(shí)考慮設(shè)備方向和攝像頭傳感器方向,下面是一段來(lái)自官方的圖片矯正代碼:
private fun getJpegOrientation(cameraCharacteristics: CameraCharacteristics, deviceOrientation: Int): Int {
var myDeviceOrientation = deviceOrientation
if (myDeviceOrientation == android.view.OrientationEventListener.ORIENTATION_UNKNOWN) {
return 0
}
val sensorOrientation = cameraCharacteristics.get(CameraCharacteristics.SENSOR_ORIENTATION)!!
// Round device orientation to a multiple of 90
myDeviceOrientation = (myDeviceOrientation + 45) / 90 * 90
// Reverse device orientation for front-facing cameras
val facingFront = cameraCharacteristics.get(CameraCharacteristics.LENS_FACING) == CameraCharacteristics.LENS_FACING_FRONT
if (facingFront) {
myDeviceOrientation = -myDeviceOrientation
}
// Calculate desired JPEG orientation relative to camera orientation to make
// the image upright relative to the device orientation
return (sensorOrientation + myDeviceOrientation + 360) % 360
}
????????這里介紹的一些方向概念,那么上面這段代碼其實(shí)就很容易理解,唯一特別的地方是前置攝像頭輸出的畫(huà)面底層默認(rèn)做了鏡像的翻轉(zhuǎn)才能保證我們?cè)陬A(yù)覽的時(shí)候看到的畫(huà)面就想照鏡子一樣,所以前置攝像頭給的 SENSOR_ORIENTATION 值也是經(jīng)過(guò)鏡像的,但是相機(jī)在輸出 JPEG 的時(shí)候并沒(méi)有進(jìn)行鏡像操作,所以在計(jì)算 JPEG 矯正角度的時(shí)候要對(duì)這個(gè)默認(rèn)鏡像的操作進(jìn)行逆向鏡像。
????????計(jì)算出圖片的矯正角度后,我們要通過(guò) CaptureRequest.JPEG_ORIENTATION
配置這個(gè)角度,相機(jī)在拍照輸出 JPEG 圖像的時(shí)候會(huì)參考這個(gè)角度值從以下兩種方式選一種進(jìn)行圖像方向矯正:
-
直接對(duì)圖像進(jìn)行旋轉(zhuǎn),并且將 Exif 的 ORIENTATION 標(biāo)簽賦值為 0。
-
不對(duì)圖像進(jìn)行旋轉(zhuǎn),而是將旋轉(zhuǎn)信息寫(xiě)入 Exif 的 ORIENTATION 標(biāo)簽里。
? ? ? ?客戶端在顯示圖片的時(shí)候一定要去檢查 Exif 的ORIENTATION 標(biāo)簽的值,并且根據(jù)這個(gè)值對(duì)圖片進(jìn)行對(duì)應(yīng)角度的旋轉(zhuǎn)才能保證圖片顯示方向是正確的。
val deviceOrientation = deviceOrientationListener.orientation
val jpegOrientation = getJpegOrientation(cameraCharacteristics, deviceOrientation)
captureImageRequestBuilder[CaptureRequest.JPEG_ORIENTATION] = jpegOrientation
4.2.5 設(shè)置縮略圖尺寸
????????相機(jī)在輸出 JPEG 圖片的時(shí)候,同時(shí)會(huì)根據(jù)我們通過(guò) CaptureRequest.JPEG_THUMBNAIL_SZIE
配置的縮略圖尺寸生成一張縮略圖寫(xiě)入圖片的 Exif 信息里。在設(shè)置縮略圖尺寸之前,我們首先要獲取相機(jī)支持哪些縮略圖尺寸,與獲取預(yù)覽尺寸或照片尺寸列表方式不一樣的是,縮略圖尺寸列表是直接通過(guò) CameraCharacteristics.JPEG_AVAILABLE_THUMBNAIL_SIZES
獲取的。配置縮略圖尺寸的代碼如下所示:
val availableThumbnailSizes = cameraCharacteristics[CameraCharacteristics.JPEG_AVAILABLE_THUMBNAIL_SIZES]
val thumbnailSize = getOptimalSize(availableThumbnailSizes, maxWidth, maxHeight)
????????在獲取圖片縮略圖的時(shí)候,不能總是假設(shè)圖片一定會(huì)在 Exif 寫(xiě)入縮略圖,當(dāng) Exif 里面沒(méi)有縮略圖數(shù)據(jù)的時(shí)候,要轉(zhuǎn)而直接 Decode 原圖獲取縮略圖,另外無(wú)論是原圖還是縮略圖,都要根據(jù) Exif 的 ORIENTATION 角度進(jìn)行角度矯正才能正確顯示,下面是 Demo 中獲取圖片縮略圖的代碼:
@WorkerThread
private fun getThumbnail(jpegPath: String): Bitmap? {
val exifInterface = ExifInterface(jpegPath)
val orientationFlag = exifInterface.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL)
val orientation = when (orientationFlag) {
ExifInterface.ORIENTATION_NORMAL -> 0.0F
ExifInterface.ORIENTATION_ROTATE_90 -> 90.0F
ExifInterface.ORIENTATION_ROTATE_180 -> 180.0F
ExifInterface.ORIENTATION_ROTATE_270 -> 270.0F
else -> 0.0F
}
var thumbnail = if (exifInterface.hasThumbnail()) {
exifInterface.thumbnailBitmap
} else {
val options = BitmapFactory.Options()
options.inSampleSize = 16
BitmapFactory.decodeFile(jpegPath, options)
}
if (orientation != 0.0F && thumbnail != null) {
val matrix = Matrix()
matrix.setRotate(orientation)
thumbnail = Bitmap.createBitmap(thumbnail, 0, 0, thumbnail.width, thumbnail.height, matrix, true)
}
return thumbnail
}
4.2.6 設(shè)置定位信息
????????拍照的時(shí)候,通常都會(huì)在圖片的 Exif 寫(xiě)入定位信息,我們可以通過(guò) CaptureRequest.JPEG_GPS_LOCATION
配置定位信息,代碼如下:
@WorkerThread
private fun getLocation(): Location? {
val locationManager = getSystemService(LocationManager::class.java)
if (locationManager != null && ContextCompat.checkSelfPermission(this, android.Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED) {
return locationManager.getLastKnownLocation(LocationManager.PASSIVE_PROVIDER)
}
return null
}
val location = getLocation()
captureImageRequestBuilder[CaptureRequest.JPEG_GPS_LOCATION] = location
4.2.7 播放快門(mén)音效
????????在進(jìn)行拍照之前,我們還需要配置拍照時(shí)播放的快門(mén)音效,因?yàn)?Camera2 和 Camera1 不一樣,拍照時(shí)不會(huì)有任何聲音,需要我們?cè)谶m當(dāng)?shù)臅r(shí)候通過(guò) MediaSoundPlayer 播放快門(mén)音效,通常情況我們是在 CaptureStateCallback.onCaptureStarted()
回調(diào)的時(shí)候播放快門(mén)音效:
private val mediaActionSound: MediaActionSound = MediaActionSound()
private inner class CaptureImageStateCallback : CameraCaptureSession.CaptureCallback() {
@MainThread
override fun onCaptureStarted(session: CameraCaptureSession, request: CaptureRequest, timestamp: Long, frameNumber: Long) {
super.onCaptureStarted(session, request, timestamp, frameNumber)
// Play the shutter click sound.
cameraHandler?.post { mediaActionSound.play(MediaActionSound.SHUTTER_CLICK) }
}
@MainThread
override fun onCaptureCompleted(session: CameraCaptureSession, request: CaptureRequest, result: TotalCaptureResult) {
super.onCaptureCompleted(session, request, result)
captureResults.put(result)
}
}
4.2.8 拍照并保存圖片
????????經(jīng)過(guò)一連串的配置之后,我們終于可以開(kāi)拍照了,直接調(diào)用 CameraCaptureSession.capture()
方法把 CaptureRequest 對(duì)象提交給相機(jī)就可以等待相機(jī)輸出圖片了,該方法要求我們?cè)O(shè)置三個(gè)參數(shù):
-
request:本次 Capture 操作使用的 CaptureRequest 對(duì)象。
-
listener:監(jiān)聽(tīng) Capture 狀態(tài)的回調(diào)接口。
-
handler:回調(diào) Capture 狀態(tài)監(jiān)聽(tīng)接口的 Handler 對(duì)象。
captureSession.capture(captureImageRequest, CaptureImageStateCallback(), mainHandler)
????????如果一切順利,相機(jī)在拍照完成的時(shí)候會(huì)通過(guò) CaptureStateCallback.onCaptureCompleted()
回調(diào)一個(gè) CaptureResult 對(duì)象,里面包含了本次拍照的所有信息,另外還會(huì)通過(guò) OnImageAvailableListener.onImageAvailable()
回調(diào)一個(gè)代表圖像數(shù)據(jù)的 Image 對(duì)象。在Demo 中,將獲取到的 CaptureResult 對(duì)象保存到一個(gè)阻塞隊(duì)列中,在 OnImageAvailableListener.onImageAvailable()
回調(diào)的時(shí)候就從這個(gè)阻塞隊(duì)列獲取 CaptureResult 對(duì)象,結(jié)合 Image 對(duì)象對(duì)圖片進(jìn)行保存操作,并且還會(huì)在圖片保存完畢的時(shí)候獲取圖片的縮略圖用于刷新 UI,代碼如下所示:
private inner class OnJpegImageAvailableListener : ImageReader.OnImageAvailableListener {
private val dateFormat: DateFormat = SimpleDateFormat("yyyyMMddHHmmssSSS", Locale.getDefault())
private val cameraDir: String = "${Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM)}/Camera"
@WorkerThread
override fun onImageAvailable(imageReader: ImageReader) {
val image = imageReader.acquireNextImage()
val captureResult = captureResults.take()
if (image != null && captureResult != null) {
image.use {
val jpegByteBuffer = it.planes[0].buffer// Jpeg image data only occupy the planes[0].
val jpegByteArray = ByteArray(jpegByteBuffer.remaining())
jpegByteBuffer.get(jpegByteArray)
val width = it.width
val height = it.height
saveImageExecutor.execute {
val date = System.currentTimeMillis()
val title = "IMG_${dateFormat.format(date)}"http:// e.g. IMG_20190211100833786
val displayName = "$title.jpeg"http:// e.g. IMG_20190211100833786.jpeg
val path = "$cameraDir/$displayName"http:// e.g. /sdcard/DCIM/Camera/IMG_20190211100833786.jpeg
val orientation = captureResult[CaptureResult.JPEG_ORIENTATION]
val location = captureResult[CaptureResult.JPEG_GPS_LOCATION]
val longitude = location?.longitude ?: 0.0
val latitude = location?.latitude ?: 0.0
// Write the jpeg data into the specified file.
File(path).writeBytes(jpegByteArray)
// Insert the image information into the media store.
val values = ContentValues()
values.put(MediaStore.Images.ImageColumns.TITLE, title)
values.put(MediaStore.Images.ImageColumns.DISPLAY_NAME, displayName)
values.put(MediaStore.Images.ImageColumns.DATA, path)
values.put(MediaStore.Images.ImageColumns.DATE_TAKEN, date)
values.put(MediaStore.Images.ImageColumns.WIDTH, width)
values.put(MediaStore.Images.ImageColumns.HEIGHT, height)
values.put(MediaStore.Images.ImageColumns.ORIENTATION, orientation)
values.put(MediaStore.Images.ImageColumns.LONGITUDE, longitude)
values.put(MediaStore.Images.ImageColumns.LATITUDE, latitude)
contentResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values)
// Refresh the thumbnail of image.
val thumbnail = getThumbnail(path)
if (thumbnail != null) {
runOnUiThread {
thumbnailView.setImageBitmap(thumbnail)
thumbnailView.scaleX = 0.8F
thumbnailView.scaleY = 0.8F
thumbnailView.animate().setDuration(50).scaleX(1.0F).scaleY(1.0F).start()
}
}
}
}
}
}
}
4.2.9 前置攝像頭拍照的鏡像問(wèn)題
????????如果你使用前置攝像頭進(jìn)行拍照,雖然照片的方向已經(jīng)被我們矯正了,但是你會(huì)發(fā)現(xiàn)畫(huà)面卻是相反的,例如你在預(yù)覽的時(shí)候人臉在左邊,拍出來(lái)的照片人臉卻是在右邊。出現(xiàn)這個(gè)問(wèn)題的原因是默認(rèn)情況下相機(jī)不會(huì)對(duì) JPEG 圖像進(jìn)行鏡像操作,導(dǎo)致輸出的原始畫(huà)面是非鏡像的。解決這個(gè)問(wèn)題的一個(gè)辦法是拿到 JPEG 數(shù)據(jù)之后再次對(duì)圖像進(jìn)行鏡像操作,然后才保存圖片。
4.3 如何連續(xù)拍攝多張圖片
????????在Demo 中有一個(gè)特殊的拍照功能,就是當(dāng)用戶雙擊快門(mén)按鈕的時(shí)候會(huì)連續(xù)拍攝 10 張照片,其實(shí)現(xiàn)原理就是采用了多次模式的 Capture,所有的配置流程和拍攝單張照片一樣,唯一的區(qū)別是我們使用 CameraCaptureSession.captureBurst()
進(jìn)行拍照,該方法要求我們傳遞一下三個(gè)參數(shù):
-
requests:按順序連續(xù)執(zhí)行的 CaptureRequest 對(duì)象列表,每一個(gè) CaptureRequest 對(duì)象都可以有自己的配置,在我們的 Demo 里出于簡(jiǎn)化的目的,10 個(gè) CaptureRequest 對(duì)象實(shí)際上的都是同一個(gè)。
-
listener:監(jiān)聽(tīng) Capture 狀態(tài)的回調(diào)接口,需要注意的是有多少個(gè) CaptureRequest 對(duì)象就會(huì)回調(diào)該接口多少次。
-
handler:回調(diào) Capture 狀態(tài)監(jiān)聽(tīng)接口的 Handler 對(duì)象。
val captureImageRequest = captureImageRequestBuilder.build()
val captureImageRequests = mutableListOf<CaptureRequest>()
for (i in 1..burstNumber) {
captureImageRequests.add(captureImageRequest)
}
captureSession.captureBurst(captureImageRequests, CaptureImageStateCallback(), mainHandler)
????????接下來(lái)所有的流程就和拍攝單招照片一樣了,每輸出一張圖片我們就將其保存到 SD 卡并且刷新媒體庫(kù)和縮略圖。
4.4 如何連拍
????????連拍這個(gè)功能在 Camera2 出現(xiàn)之前是不可能實(shí)現(xiàn)的,現(xiàn)在我們只需要使用重復(fù)模式的 Capture 就可以輕松實(shí)現(xiàn)連拍功能。在前面里我們使用了重復(fù)模式的 Capture 來(lái)實(shí)現(xiàn)預(yù)覽功能,而這一次我們不僅要用該模式進(jìn)行預(yù)覽,還要在預(yù)覽的同時(shí)也輸出照片,所以我們會(huì)使用 CameraCaptureSession.setRepeatingRequest()
方法開(kāi)始進(jìn)行連拍:
val captureImageRequest = captureImageRequestBuilder.build()
captureSession.setRepeatingRequest(captureImageRequest, CaptureImageStateCallback(), mainHandler)
停止連拍有以下兩種方式:
-
調(diào)用
CameraCaptueSession.stopRepeating()
方法停止重復(fù)模式的 Capture,但是這會(huì)導(dǎo)致預(yù)覽也停止。 -
調(diào)用
CameraCaptueSession.setRepeatingRequest()
方法并且使用預(yù)覽的 CaptureRequest 對(duì)象,停止輸出照片。
在 Demo 里使用了第二種方式:
@MainThread
private fun stopCaptureImageContinuously() {// Restart preview to stop the continuous image capture.startPreview()
}
4.5 如何切換前后置攝像頭
切換前后置攝像頭是一個(gè)很常見(jiàn)的功能,雖然和本章的主要內(nèi)容不相關(guān),但是在 Demo 中已經(jīng)實(shí)現(xiàn),所以這里也順便提一下。我們只要按照以下順序進(jìn)行操作就可以輕松實(shí)現(xiàn)前后置攝像頭的切換:
-
關(guān)閉當(dāng)前攝像頭
-
開(kāi)啟新的攝像頭
-
創(chuàng)建新的 Session
-
開(kāi)啟預(yù)覽
下面是代碼片段,詳細(xì)代碼大家可以自行查看 Demo 源碼:
@MainThread
private fun switchCamera() {
val cameraDevice = cameraDeviceFuture?.get()
val oldCameraId = cameraDevice?.id
val newCameraId = if (oldCameraId == frontCameraId) backCameraId else frontCameraId
if (newCameraId != null) {
closeCamera()
openCamera(newCameraId)
createCaptureRequestBuilders()
setPreviewSize(MAX_PREVIEW_WIDTH, MAX_PREVIEW_HEIGHT)
setImageSize(MAX_IMAGE_WIDTH, MAX_IMAGE_HEIGHT)
createSession()
startPreview()
}
}
4.6 總結(jié)
重復(fù)模式和多次模式都可以實(shí)現(xiàn)連拍功能,其中重復(fù)模式適合沒(méi)有連拍上限的情況,而多次模式適合有連拍上限的情況。
一個(gè) CaptureRequest 可以添加多個(gè) Surface,這就意味著你可以同時(shí)拍攝多張照片。文章來(lái)源:http://www.zghlxwxcb.cn/news/detail-530621.html
拍照獲取 CaptureResult 和 Image 對(duì)象走的是兩個(gè)不同的回調(diào)接口,靈活運(yùn)用子線程的阻塞操作可以簡(jiǎn)化你的代碼邏輯。文章來(lái)源地址http://www.zghlxwxcb.cn/news/detail-530621.html
到了這里,關(guān)于Camera API2 使用說(shuō)明的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!