重點整理
- 實作 CameraCore class
- 完成 video size , preview size , sensor orientaion 的讀取
- 成功打開相機
第一步
New一個新專案出來,在activity_main.xml上先拉個畫面出來吧!很簡單,我們就一個 TextureView、一個 Button就行,完成以下:
1 | <?xml version="1.0" encoding="utf-8"?> |
接著到 styles.xml 裡的AppTheme增加以下兩行程式碼:
1 | <!-- 取消系統預設的 tool bar --> |
因為我們要用全螢幕來顯示相機預覽,所以任何toolbar通通要消失不見,連v7 toolbar我們也沒有建立。完成後應該在preview的版面會看到像這樣:
然後打開你的app層的build.gradle,加入RxJava library,我接下來的範例中就會開始有Rx的操作。
1 | dependencies { |
然後請產生一個 Class RootApplication,我們要建立 Application 進入點:
1 | class RootApplication : Application() { |
照著範例程式,一樣的去繼承Application實作onCreate(),這是Android開啟App後的第一個進入點,我們要在上面先把CameraManager建立好。
CameraManager用 static 的方式去保存,因為 Application 只有在onCreate這一次的機會進行其他設定或把相關App要用的資源準備、初始化,等等之類的工作都會在這裡執行。
而在之後,若想要取得某些App全體共用的資源時,開發者是拿不到 Application的實例,這時只好將建立好的資源存在這個Class的static欄位上,以便日後存取。(Application對於App的生命週期,在此不多加詳述)
之所以可以這麼做,其實是一個很簡單的道理,Application在App啟動之後被建立而且是一次,在此之後除非App被系統殺掉,那麼這個Application都會存在一份。
這就類似單例模式,所以我通常會被全體共享用的資源放在上面,但注意不要太依賴,資料也盡量簡單就好,把握這個原則,最小化你想要存取的東西就好。
完成後,記得最後在AndroidManifest.xml裡application tag中新增android:name=”” 指定這個class,讓App加載:
1 | <?xml version="1.0" encoding="utf-8"?> |
大致上到這裡 build 應該都沒什麼問題,進入下一步驟。
第二步
這一階段,我們要開始封裝 Camera2的一切東西,為了讓其他物件使用順暢,根據需求建構一些簡單易用的funtion直接在我們的app取用,說明白一點就是把Camera2再封裝成一層變成更高一級的API,讓我們自己使用。
新建一個 kt class,命名為: CameraCore,然後先加入以下兩個method:
1 | //純預覽,什麼動作都不做 |
preivew的主要方法回傳了 Observable
回顧一下架構圖,我們若想要讓Camera的資料流入到UI上,必須提供 surface ,而這裡預覽的主要function就是
- startPreview(surface: List
)
對於CameraCaptureSession來說,他並不用知道Surface到底來自哪裡,但是我們的App是需要知道Surface的用途,因此針對需求,我們要自行取找到哪些 Surface 並設定給 Session。
close(),既然有開就有關閉,所以也要先寫出來。
接下來,補上兩個重點方法,一個是 kt的init 和 openCamera:
1 | init { |
openCamera 一樣回傳 return Observable.just(false),先通過編譯器檢查。
kt的init函式,這段程式放在這個物件的哪個位置,當物件產生之後會依循著編譯順序,等到該是init要進行的時候才會進行,這個方法跟建構子是有區別性的哦!
所以我的習慣我會讓init放在所有宣告的建構子和屬性成員之下,然後開始做初始化。那麼就開始先宣告一些這個CameraCore會用到的全域變數,請把這些變數全部放於init上方:
1 | //open 的時候拿到 |
這裏要先講解,videoSize, previewSize , sensorOrientation的用途,
- videoSize:該手機對於相機錄影可以提供的size,從CameraCharacteristics取得。這裡我會去選擇適合於螢幕尺寸比例的video size,官方範例中是直接找到 4:3 比例的最小 video size,應該是為了縮減影片大小,不過我在錄影的設定上,會降低一些品質,所以也先不要煩惱影片會太大
- previewSize:相機可以輸出預覽的size,從CameraCharacteristics取得。這個size每一台手機都不太一樣,甚至他並不會跟可支援的video size列表一模一樣,完全是手機硬體底層控制,我們也無法去更改。
- sensorOrientation:相機物理的旋轉角度,基本上是固定的,不是90就是270
Preview Size的問題
特別說明一下,為什麼一定要規定previewSize? 因為我是用TextureView,而其實我也是從錯誤訊息得知,如果你設定的BufferSize寬高不符合這個比例,就會噴錯,是個從經驗中學習,原理為何,我還沒深究過。
總而言之,遵循著API的方式做的話,假如我在TexturSurface設定BufferSize => 1920 x 1080,結果我的手機相機卻不支援 1920 x 1080 的 preview,這時候就一定會噴錯。
但對於 SurfaceView設定FixedSize,其實沒錯誤發生,但影響的是畫面會被拉伸而已,並不是很好的解決方式。
也因此 video size 跟 preview size 還有 screen size 是有相關聯性的,假如我今天的需求是全螢幕預覽加錄影,我得到以下資訊:
- Screen : 1080x1920
- Video : 1280x720 , 960x720 , 640x480
- Preview : 1440x1088 , 1280x720 , 960x720 , 640x480
我使用了 1080x1920 對 Texture BufferSize設定,然後開始輸出,就得到錯誤說無法支援的preivew size,可是這時候若選擇了 1440x1088 又會發生什麼事情? 你的畫面會被嚴重拉伸,比例很不正常。
要取得最佳大小,是先用 Screen 的 短/長 = Ratio 得到的是正常寬高比,所以我們要用 Screen.width / Screen.height。
以這個假設例子來看,得到的是 9:16 , 顛倒則是 16:9 之比例,所以我們要找的正常比值應是 16:9 的輸出品質,Video就會是第一個:1280x720;Preview則是第二個:1280x720。
正常手機的preview應該會有 16:9 的 1080p 顯示,不過我這裡有一支手機前鏡頭可能不太好,測試起來確實只有到1440x1088,研判Screen之所以會有 1080x1920 的輸出,應該是其解析度(dpi)較高的關係。
那麼渲染1280x720的圖像到1920x1080像素上時,是一定會有模糊感的差距,但如果為了解決掉full screen不拉伸,必須要捨棄掉一些東西,所以這個做法是比較恰當的。
當然你也可以去制定設定值,比如我要App可以支援 1080p、720p、480p的錄製,那麼在預覽到手機上的時候就要照著你拿到的Preview比例去對TextureView做畫面寬高設定。
假如我要以1440x1088輸出1080p高解析度的時候,為了符合比例值,通常會將螢幕的寬縮短、高縮短,兩邊對稱1440x1088的比例,那麼就會正常。
只是可能你的UI下方 Layout版面會空出一塊出來。
第三步
PreviewSize說的那麼多,其實是為了接下來的選擇方式,請先在RootApplication的 companion object block中加入以下兩個static 變數:
1 | companion object { |
然後在 onCreate裡實作取得 display size的方法:
1 | override fun onCreate() { |
以上使用了 private set,讓這個欄位對外界而言只能是唯讀,對內部則可以設定,保護這個欄位不受外界干擾。
回到CameraCore,在 init block 中寫下:
1 | init { |
然後我們要實作一下 chooseSizeFitWithScreen private 方法:
1 | private fun chooseSizeFitWithScreen(choices: Array<Size>): Size { |
程式碼說明:
先用短邊/長邊取得螢幕比例值,再用這個比例找尋相同的最大比例,請注意,是相同的最大比例。for loop在這裡迴圈所有的size可以找到最大比例是因為從CameraCharacteristics取得到可支援size,是遞減排序,由大到小。
那如果想要針對video size找尋比較適合的尺寸來減少影片大小,就要多寫一個方法,針對video的部分去判斷,但這裡為了快速帶範例,就不多做此步驟。
第四步
開啟相機
第二章的最後是打開相機,也是整個Camera2的第一步,如果根本打不開,就什麼都別談了。
現在把注意力放到之前我們寫的 openCamera 方法,開始實作吧!
首先,我們要先建立一個Obserable,在這裡我使用了 Obserable.create 方法建立,最後回傳帶Boolean值的輸出,ture就是成功開啟,false就是失敗,只要在open接到失敗的結果,我們就不用再繼續往下做。
請先把 return Observable.just(false),這行刪除,改回傳以下:
1 | return Observable.create { |
因為是泛型的關係,所以Observable.create會自動帶入泛型值,我們不用特別宣告
1 | return Observable.create { |
openCamera帶三個參數,第一個是 cameraId string value、第二個是 callback 介面直接匿名類別實作、第三個是 handelr,這裡我使用 null,使用當前 Thread的Handler。
特別說明 handler 在這裡的意義,如果你不特別啟用另一條HandlerThread的話,這裡就會使用 main thread的Handler,如果你用了 thread (跟 handler thread不同)去open,然後這個參數傳進去null值,那麼就會出現錯誤警告你沒有設定 Handler,如一定要用 其他執行緒來跑,就要使用 HandlerThread 加上 Handler,讓他有loop,才能正常執行,想必應該是內部做了執行緒安全和UI Render 不衝突的機制。
開啟成功之後,在onOpened call back裡將device保存起來,並且 fire true,然後完成這個Observable:
1 | override fun onOpened(device: CameraDevice?) { |
在其他 onDisconnected , onError call back,分別寫下:
1 | override fun onDisconnected(device: CameraDevice?) { |
最後在我們開啟相機之前,要先在 AndroidManifest.xml 加上權限,並且在 Activity 中動態請求使用者授權:
1 | <!-- 請求權限 --> |
在Activity中請求:
- 先宣告一下請求碼和 CameraCore class,方便之後用:
使用lateinit,代表之後一定要初始化,這樣就不用加上問號做optional的判斷
1 | class MainActivity : AppCompatActivity() { |
- 實作 onCreate,初始化 cameraCore
1 | override fun onCreate(savedInstanceState: Bundle?) { |
- 實作 onResume,沒授權不開啟
1 | override fun onResume() { |
- 在 openCamera 訂閱後的 onNext 結果中,實作以下:
1 | cameraCore.openCamera().subscribe { |
如果你有看到這行輸出,代表相機開啟成功!