Android相機開發那些坑

版權聲明:本文由王梓原創文章,轉載請註明出處: 
文章原文連接:https://www.qcloud.com/community/article/168canvas

來源:騰雲閣 https://www.qcloud.com/community函數

 

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

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

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

二.相機API中關鍵類解析

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

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狀態變化的接口,有三個方法:orm

  • 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度角。
xml

圖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的緣由是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的可見性的方法,並不會對用戶的體驗形成影響。

相關文章
相關標籤/搜索