1. 程式人生 > >組長問我幾天能開發一個人見人愛的百萬量級Android相機,我是這樣回答的...

組長問我幾天能開發一個人見人愛的百萬量級Android相機,我是這樣回答的...

紅色 可能 圖像變形 編寫 註意 點擊事件 修復 事件處理 修改

歡迎大家前往騰訊雲+社區,獲取更多騰訊海量技術實踐幹貨哦~

本文由QQ空間開發團隊發表於雲+社區專欄

最近我負責開發了一個跟Android相機有關的需求,新功能允許用戶使用手機攝像頭,快速拍攝特定尺寸(1:1或3:4)的照片,並支持在拍攝出的照片上做貼紙相關的操作。由於之前沒有接觸過Android相機開發,所以在整個開發過程中踩了不少坑,費了不少時間和精力。這篇文章總結了Android相機開發的相關知識、流程,以及容易遇到的坑,希望能幫助今後可能會接觸Android相機開發的朋友快速上手,節省時間,少走彎路。

一.Android中開發相機應用的兩種方式

Android系統提供了兩種使用手機相機資源實現拍攝功能的方法,一種是直接通過Intent調用系統相機組件,這種方法快速方便,適用於直接獲得照片的場景,如上傳相冊,微博、朋友圈發照片等。另一種是使用相機API來定制自定義相機,這種方法適用於需要定制相機界面或者開發特殊相機功能的場景,如需要對照片做裁剪、濾鏡處理,添加貼紙,表情,地點標簽等。這篇文章主要是從如何使用相機API來定制自定義相機這個方向展開的。

二.相機API中關鍵類解析

通過相機API實現拍攝功能涉及以下幾個關鍵類和接口:

Camera:最主要的類,用於管理和操作camera資源。它提供了完整的相機底層接口,支持相機資源切換,設置預覽/拍攝尺寸,設定光圈、曝光、聚焦等相關參數,獲取預覽/拍攝幀數據等功能,主要方法有以下這些:

  • open():獲取camera實例。
  • setPreviewDisplay(SurfaceHolder):綁定繪制預覽圖像的surface。surface是指向屏幕窗口原始圖像緩沖區(raw buffer)的一個句柄,通過它可以獲得這塊屏幕上對應的canvas,進而完成在屏幕上繪制View的工作。通過surfaceHolder可以將Camera和surface連接起來,當camera和surface連接後,camera獲得的預覽幀數據就可以通過surface顯示在屏幕上了。
  • setPrameters設置相機參數,包括前後攝像頭,閃光燈模式、聚焦模式、預覽和拍照尺寸等。
  • startPreview():開始預覽,將camera底層硬件傳來的預覽幀數據顯示在綁定的surface上。
  • stopPreview():停止預覽,關閉camra底層的幀數據傳遞以及surface上的繪制。
  • release():釋放Camera實例
  • takePicture(Camera.ShutterCallback shutter, Camera.PictureCallback raw, Camera.PictureCallback jpeg):這個是實現相機拍照的主要方法,包含了三個回調參數。shutter是快門按下時的回調,raw是獲取拍照原始數據的回調,jpeg是獲取經過壓縮成jpg格式的圖像數據的回調。

SurfaceView:用於繪制相機預覽圖像的類,提供給用戶實時的預覽圖像。普通的view以及派生類都是共享同一個surface的,所有的繪制都必須在UI線程中進行。而surfaceview是一種比較特殊的view,它並不與其他普通view共享surface,而是在內部持有了一個獨立的surface,surfaceview負責管理這個surface的格式、尺寸以及顯示位置。由於UI線程還要同時處理其他交互邏輯,因此對view的更新速度和幀率無法保證,而surfaceview由於持有一個獨立的surface,因而可以在獨立的線程中進行繪制,因此可以提供更高的幀率。自定義相機的預覽圖像由於對更新速度和幀率要求比較高,所以比較適合用surfaceview來顯示。

SurfaceHolder:surfaceholder是控制surface的一個抽象接口,它能夠控制surface的尺寸和格式,修改surface的像素,監視surface的變化等等,surfaceholder的典型應用就是用於surfaceview中。surfaceview通過getHolder()方法獲得surfaceholder 實例,通過後者管理監聽surface 的狀態。

SurfaceHolder.Callback接口:負責監聽surface狀態變化的接口,有三個方法:

  • surfaceCreated(SurfaceHolder holder):在surface創建後立即被調用。在開發自定義相機時,可以通過重載這個函數調用camera.open()、camera.setPreviewDisplay(),來實現獲取相機資源、連接camera和surface等操作。
  • surfaceChanged(SurfaceHolder holder, int format, int width, int height):在surface發生format或size變化時調用。在開發自定義相機時,可以通過重載這個函數調用camera.startPreview來開啟相機預覽,使得camera預覽幀數據可以傳遞給surface,從而實時顯示相機預覽圖像。
  • surfaceDestroyed(SurfaceHolder holder):在surface銷毀之前被調用。在開發自定義相機時,可以通過重載這個函數調用camera.stopPreview(),camera.release()來實現停止相機預覽及釋放相機資源等操作。

三.自定義相機的開發過程

定制一個自定義相機應用,通常需要完成以下步驟,其流程圖如圖1所示:

  • 檢測並訪問相機資源 檢查手機是否存在相機資源,如果存在,請求訪問相機資源。
  • 創建預覽類 創建繼承自SurfaceView並實現SurfaceHolder接口的拍攝預覽類。此類能夠顯示相機的實時預覽圖像。
  • 建立預覽布局 有了拍攝預覽類,即可創建一個布局文件,將預覽畫面與設計好的用戶界面控件融合在一起。
  • 設置拍照監聽器 給用戶界面控件綁定監聽器,使其能響應用戶操作(如按下按鈕), 開始拍照過程。
  • 拍照並保存文件 將拍攝獲得的圖像轉換成位圖文件,最終輸出保存成各種常用格式的圖片。
  • 釋放相機資源 相機是一個共享資源,必須對其生命周期進行細心的管理。當相機使用完畢後,應用程序必須正確地將其釋放,以免其它程序訪問使用時,發生沖突。

技術分享圖片

圖1 定制自定義相機的過程

對應到代碼編寫上可以分成三個步驟:

第一步:在AndroidManifest.xml中添加Camera相關功能使用的權限,具體聲明有以下這些:

技術分享圖片

第二步:編寫相機操作功能類CameraOperationHelper。采用單例模式來統一管理相機資源,封裝相機API的直接調用,並提供用於跟自定義相機Activity做UI交互的回調接口,其功能函數如下,主要有創建\釋放相機,連接\開始\關閉預覽界面,拍照,自動對焦,切換前後攝像頭,切換閃光燈模式等,具體實現可以參考官方API文檔。

技術分享圖片

技術分享圖片

第三步:編寫自定義相機Activity,主要是定制相機界面,實現UI交互邏輯,如按鈕點擊事件處理,icon資源切換,鏡頭尺寸切換動畫等。這裏需要聲明一個SurfaceView對象來實時顯示相機預覽畫面。通過SurfaceHolder及其Callback接口來一同管理屏幕surface和相機資源的連接,相機預覽圖像的顯示/關閉。

技術分享圖片

四. 開發過程遇到的一些坑

下面再講講我在開發自定義相機時踩過的一些坑:

1. Activity設為豎屏時,SurfaceView預覽圖像顛倒90度。

說明這個問題之前,先介紹下Android手機上幾個方向的概念:

屏幕方向:在Android系統中,屏幕的左上角是坐標系統的原點(0,0)坐標。原點向右延伸是X軸正方向,原點向下延伸是Y軸正方向。

相機傳感器方向:手機相機的圖像數據都是來自於攝像頭硬件的圖像傳感器,這個傳感器在被固定到手機上後有一個默認的取景方向,如下圖2所示,坐標原點位於手機橫放時的左上角,即與橫屏應用的屏幕X方向一致。換句話說,與豎屏應用的屏幕X方向呈90度角。

技術分享圖片

圖2 相機傳感器方向示意圖

相機的預覽方向:由於手機屏幕可以360度旋轉,為了保證用戶無論怎麽旋轉手機都能看到“正確”的預覽畫面(這個“正確”是指顯示在UI預覽界面的畫面與人眼看到的眼前的畫面是一致的),Android系統底層根據當前手機屏幕的方向對圖像傳感器采集到的數據進行了旋轉處理,然後才送給顯示系統,因此可以保證預覽畫面始終“正確”。在相機API中可以通過setDisplayOrientation()設置相機預覽方向。在默認情況下,這個值為0,與圖像傳感器一致。因此對於橫屏應用來說,由於屏幕方向和預覽方向一致,預覽圖像不會顛倒90度。但是對於豎屏應用,屏幕方向和預覽方向垂直,所以會出現顛倒90度現象。為了得到正確的預覽畫面,必須通過API將相機的預覽方向旋轉90,保持與屏幕方向一致,如圖3所示。

技術分享圖片

圖3 相機預覽方向示意圖

(紅色箭頭為預覽方向,藍色方向為屏幕方向)

相機的拍照方向:當點擊拍照按鈕,拍攝的照片是由圖像傳感器采集到的數據直接存儲到SDCard上產生的,因此,相機的拍照方向與傳感器方向是一致的。

2. SurfaceView預覽圖像、拍攝照片拉伸變形

說明這個問題之前,同樣先說一下幾個跟相機有關的尺寸。

SurfaceView尺寸:即自定義相機應用中用於顯示相機預覽圖像的View的尺寸,當它鋪滿全屏時就是屏幕的大小。這裏surfaceview顯示的預覽圖像暫且稱作手機預覽圖像。

Previewsize:相機硬件提供的預覽幀數據尺寸。預覽幀數據傳遞給SurfaceView,實現預覽圖像的顯示。這裏預覽幀數據對應的預覽圖像暫且稱作相機預覽圖像。

Picturesize:相機硬件提供的拍攝幀數據尺寸。拍攝幀數據可以生成位圖文件,最終保存成.jpg或者.png等格式的圖片。這裏拍攝幀數據對應的圖像稱作相機拍攝圖像。圖4說明了以上幾種圖像及照片之間的關系。手機預覽圖像是直接提供給用戶看的圖像,它由相機預覽圖像生成,拍攝照片的數據則來自於相機拍攝圖像。

技術分享圖片

圖4 幾種圖像之間的關系

下面說下我在開發過程中遇到的三種拉伸變形現象:

1、手機預覽畫面中物體被拉伸變形。

2、拍攝照片中物體被拉伸變形。

3、點擊拍照瞬間,手機預覽畫面會停頓下,此時的圖像是拉伸變形的,然後預覽畫面恢復後圖像又正常了。

現象1的原因是SurfaceView和Previewsize的長寬比率不一致。因為手機預覽視圖的圖像是由相機預覽圖像根據SurfaceView大小縮放得來的,當長寬比不一致時必然會導致圖像變形。後兩個現象的原因則是Previewsize和Picturesize的長寬比率不一致所致,查了相關的資料,發現其具體原因跟某些手機相機硬件的底層實現有關。總之為了避免以上幾種變形現象的發生,在開發時最好將SurfaceView、PreviewSize、PictureSize三個尺寸保證長寬比例一致。具體實現可以先通過camera.getSupportedPreviewSizes()和camera.getSupportedPictureSizes()獲得相機硬件支持的所有預覽和拍攝尺寸,然後在裏面篩選出和SurfaceView的長寬比一致並且大小合適的尺寸,通過camera.setPrameters來更新設置。註意:市場上手機相機硬件支持的尺寸一般都是主流的4:3或者16:9,所以SurfaceView尺寸不能太奇葩,最好也設置成這樣的長寬比。

3. 各種crash

技術分享圖片

技術分享圖片

技術分享圖片

前兩個Crash的原因是:相機硬件在聚焦和拍照前必須要保證已經連接到surface,並且開啟相機預覽,surface有收到預覽數據。如果在還沒有執行camera. setPreviewDisplay或者未調用camera. startPreview之前,就調用camera.autofocus或camera.takepicture,就會出現這個運行時異常。對應到自定義相機的代碼中,要註意在拍照按鈕事件響應中執行camera.autofocus或camera.takepicture前,一定要檢驗camera有沒有設置預覽Surfaceview並開啟了相機預覽。這裏有個方法可以判斷預覽狀態:Camera.setPreviewCallback是預覽幀數據的回調函數,它會在SurfaceView收到相機的預覽幀數據時被調用,因此在裏面可以設置是否允許對焦和拍照的標誌位。

技術分享圖片

還有一點要註意,camera.takePicture()在執行過程中會執行camera.stopPreview來獲取拍攝幀數據,表現為預覽畫面卡住,而如果此時用戶點擊了按鈕的話,也就是調用camera.takepicture,也會出現上面的crash,因此在開發時,可能還需要屏蔽拍照按鈕的連續點擊。

第三個crash則涉及圖像的裁剪,由於要支持1:1或者4:3尺寸鏡頭,所以會需要對預覽視圖進行裁剪,由於是豎屏應用,所以裁剪區域的坐標系跟相機傳感器方向是成90度角的,表現在裁剪裏就是,屏幕上的x方向,對應在拍攝圖像上是高度方向,而屏幕上的y方向,對應到拍攝圖像上則是寬度方向。因此在計算時要一定註意坐標系的轉換以及越界保護。

技術分享圖片

4. 前置攝像頭的鏡像效果

Android相機硬件有個特殊設定,就是對於前置攝像頭,在展示預覽視圖時采用類似鏡面的效果,顯示的是攝像頭成像的鏡像。而拍攝出的照片則仍采用攝像頭成像。看到這裏,大家可能會有些懷疑,不妨現在就試試自己Android手機上的前置攝像頭,對比下預覽圖像和拍攝出照片的區別。這是由於底層相機在傳遞前置攝像頭預覽數據時做了水平翻轉變換,即將x方向鏡像翻轉180度。這個變化對之前豎屏預覽的方向也會造成影響,本來對於後置攝像頭旋轉90度即可使預覽視圖正確,而對前置攝像頭,如果也旋轉90度的話,看到的預覽圖像則是上下顛倒的(因為x方向翻轉了180度),因此必須再旋轉180度,才能顯示正確,如圖5所示,大家可以結合之前相機預覽方向的示意圖一起理解。

技術分享圖片

技術分享圖片

圖5 前置攝像頭的預覽方向示意圖

此外,由於拍攝圖像並沒有做水平翻轉,所以對於前置攝像頭拍出來的照片,用戶會發現跟預覽時所見的是左右翻轉的。這個在一定程度上會影響用戶體驗。為了解決這個問題,可以對前置攝像頭拍攝的圖像在生成位圖文件時增加一個水平翻轉矩陣變換。

5. 鎖屏下相機資源的釋放問題

為了節省手機電量,不浪費相機資源,在開發的自定義相機裏,如果預覽圖像已不需要顯示,如按Home鍵盤切換後臺或者鎖屏後,此時就應該關閉預覽並把相機資源釋放掉。參考官方API文檔,當surfaceView變成可見時,會創建surface並觸發surfaceHolder.callback接口中surfaceCreated回調函數。而surfaceview變成不可見時,則會銷毀surface,並觸發surfacedestroyed回調函數。我們可以在對應的回調函數裏,處理相機的相關操作,如連接surface、開啟/關閉預覽。 至於相機資源釋放,則可以放在Acticity的onpause裏執行。相應的,要重新恢復預覽圖像時,可以把相機資源申請和初始化放在Acticity的onResume裏執行,然後通過創建surfaceview,將camera和surface相連並開啟預覽。

技術分享圖片

但是在開發過程中發現,對於按HOME鍵切後臺場景,程序可以正常運行。對於鎖屏場景,則在重新申請相機資源時會發生crash,說相機資源訪問失敗。那麽原因是什麽呢?我在代碼裏增加了調試log, 檢查了代碼的執行順序,結果如下:

在自定義相機頁面按HOME鍵時的執行流程:

  • 程序運行->按HOME鍵
  • Activity調用的順序是onPause->onStop
  • SurfaceView調用了surfaceDestroyed方法
  • 然後再切回程序
  • Activity調用的順序是onRestart->onStart->onResume
  • SurfaceView調用了surfaceCreated->surfaceChanged方法
  • 而對於鎖屏,其執行流程則是:
  • Activity只調用onPause方法
  • 解鎖後Activity調用onResume方法
  • SurfaceView中surfaceholder.callback的所有方法都沒有執行

問題找到了,由於鎖屏時,callback的回調方法沒有執行,導致相機和預覽的連接還沒有斷開,相機資源就被釋放了,所以導致在重新申請相機資源時,系統報crash。根據上面的文檔,推測是鎖屏下系統並沒有改變surfaceview的可見性,於是我嘗試在onPause和onResume時通過手動設置surfaceview的visibile屬性,結果發現可以正常觸發回調函數了。由於在切後臺或者鎖屏時,用戶本來就應該看不到surfaceview,因此這種手動更改surfaceview的可見性的方法,並不會對用戶的體驗造成影響。

技術分享圖片

問答
Android - 如何修復權限異常?
相關閱讀
深入理解Autorelease Pool
ComponentKit框架解析之一—初識CK
Android 內存泄漏分析心得
【每日課程推薦】機器學習實戰!快速入門在線廣告業務及CTR相應知識

此文已由作者授權騰訊雲+社區發布,更多原文請點擊

搜索關註公眾號「雲加社區」,第一時間獲取技術幹貨,關註後回復1024 送你一份技術課程大禮包!

海量技術實踐經驗,盡在雲加社區!

組長問我幾天能開發一個人見人愛的百萬量級Android相機,我是這樣回答的...