相機方向

如果 Android 應用程式使用相機,處理螢幕方向時需要注意一些特殊事項。本文假設您瞭解 Android camera2 API 的基本概念。如要瞭解 camera2 的概況,請參閱我們的網誌文章摘要。此外,建議您先嘗試編寫相機應用程式,再閱讀本文。

背景

在 Android 相機應用程式中處理螢幕方向很棘手,需要考量下列因素:

  • 自然方向:裝置處於「正常」位置時的螢幕方向,通常是手機的直向,以及筆電的橫向。
  • 感應器方向:感應器實際安裝在裝置上的方向。
  • 螢幕旋轉:裝置從自然方向實際旋轉的角度。
  • 觀景窗大小:用於顯示相機預覽畫面的觀景窗大小。
  • 相機輸出的圖片大小。

這些因素加總起來,會為相機應用程式帶來大量可能的 UI 和預覽設定。本文旨在說明開發人員如何瀏覽這些資訊,並在 Android 應用程式中正確處理相機方向。

為簡化說明,除非另有說明,否則請假設所有範例都使用後置鏡頭。此外,以下所有相片皆為模擬,以便清楚說明。

瞭解螢幕方向

自然方向

自然方向是指裝置處於正常預期位置時的螢幕方向。手機的自然方向通常是直向,換句話說,手機的寬度較短,高度較長。筆記型電腦的自然方向為橫向,也就是寬度較長,高度較短。平板電腦的情況稍微複雜一點,因為平板電腦可以直向或橫向顯示。

自然方向插圖,包含手機、筆電和觀察者側的物體

感應器方向

正式來說,感應器方向的測量方式是:感應器輸出圖像需要順時針旋轉幾度,才能符合裝置的自然方向。換句話說,感應器方向是指感應器安裝在裝置上之前,逆時針旋轉的角度。從螢幕上來看,旋轉方向似乎是順時針,這是因為後置鏡頭感應器安裝在裝置的「背面」。

根據 Android 10 相容性定義 7.5.5 相機方向,前置和後置鏡頭「必須經過調整,讓相機的長邊對齊螢幕的長邊」。

攝影機的輸出緩衝區為橫向大小。由於手機的自然方向通常為直向,感應器方向通常會與自然方向相差 90 或 270 度,以便輸出緩衝區的長邊與螢幕的長邊相符。如果裝置的自然方向為橫向 (例如 Chromebook),感應器方向會有所不同。在這些裝置上,影像感應器同樣會放置在輸出緩衝區的長邊與螢幕長邊對齊的位置。由於兩者都是橫向大小,因此方向一致,感應器方向為 0 或 180 度。

自然方向插圖,包含手機、筆電和觀察者側的物體

以下插圖顯示觀察者從裝置螢幕上看到的內容:

感應器方向插圖,顯示手機、筆電和觀察者側的物體

請參考下列情境:

場景:可愛的 Android 公仔 (蟲蟲機器人)

手機 筆記型電腦
圖片插圖:透過手機的後置鏡頭感應器查看 圖片插圖:透過筆電的後置鏡頭感應器查看

由於手機上的感應器方向通常為 90 或 270 度,如果不考慮感應器方向,您取得的圖片會如下所示:

手機 筆記型電腦
圖片插圖:透過手機的後置鏡頭感應器查看 圖片插圖:透過筆電的後置鏡頭感應器查看

假設逆時針感應器方向儲存在 sensorOrientation 變數中。如要補償感應器方向,您需要順時針旋轉輸出緩衝區 `sensorOrientation`,將方向重新與裝置的自然方向對齊。

在 Android 中,應用程式可以使用 TextureView 或 SurfaceView 顯示相機預覽畫面。如果應用程式正確使用這兩者,都能處理感應器方向。我們會在後續章節中詳細說明如何考量感應器方向。

螢幕旋轉

螢幕旋轉的正式定義為螢幕上繪製的圖像旋轉,與裝置從自然方向實際旋轉的方向相反。以下各節假設螢幕旋轉角度都是 90 的倍數。如果以絕對角度擷取螢幕旋轉角度,請將其四捨五入至最接近的 {0、90、180、270}。

以下各節中的「螢幕方向」是指裝置的實際方向為橫向或直向,與「螢幕旋轉」不同。

假設您將裝置從先前的位置逆時針旋轉 90 度,如下圖所示:

90 度螢幕旋轉插圖,顯示手機、筆電和觀察者側的物體

假設輸出緩衝區已根據感應器方向旋轉,您會得到下列輸出緩衝區:

手機 筆記型電腦
圖片插圖:透過手機的後置鏡頭感應器查看 圖片插圖:透過筆電的後置鏡頭感應器查看

如果顯示器旋轉角度儲存在 displayRotation 變數中,您應以 displayRotation 逆時針旋轉輸出緩衝區,才能取得正確的圖像。

如果是前置鏡頭,螢幕旋轉會以與螢幕相反的方向作用於圖像緩衝區。如果是前置鏡頭,請依順時針方向旋轉緩衝區,旋轉角度為 displayRotatation。

注意事項

螢幕旋轉會測量裝置的逆時針旋轉角度。並非所有螢幕方向/旋轉 API 都適用這項規則。

舉例來說,

請注意,螢幕旋轉是相對於自然方向。舉例來說,如果將手機旋轉 90 或 270 度,螢幕就會變成橫向。相較之下,如果將筆電旋轉相同角度,螢幕會呈現直向。應用程式應隨時注意這點,絕不應假設裝置的自然方向。

範例

讓我們使用先前的圖示,說明方向和旋轉角度。

綜合方向插圖,其中手機和筆電未旋轉,且有一個物件

手機 筆記型電腦
自然螢幕方向 = 直向 自然螢幕方向 = 橫向
感應器方向 = 90 感應器方向 = 0
螢幕旋轉角度 = 0 螢幕旋轉角度 = 0
螢幕顯示方向 = 直向 螢幕顯示方向 = 橫向

綜合方向插圖,其中手機和筆電未旋轉,且有一個物件

手機 筆記型電腦
自然螢幕方向 = 直向 自然螢幕方向 = 橫向
感應器方向 = 90 感應器方向 = 0
螢幕旋轉角度 = 90 螢幕旋轉角度 = 90
螢幕顯示方向 = 橫向 螢幕顯示方向 = 直向

觀景窗尺寸

應用程式應一律根據螢幕方向、旋轉和解析度調整觀景窗大小。一般來說,應用程式應讓觀景窗的方向與目前的螢幕方向相同。換句話說,應用程式應將觀景窗的長邊對齊螢幕的長邊。

相機的圖片輸出大小

選擇預覽的圖片輸出大小時,請盡可能選擇等於或略大於觀景窗的大小。一般來說,您不希望輸出緩衝區放大,因為這會導致像素化。此外,也不要選擇過大的尺寸,以免降低效能和耗用更多電量。

JPEG 方向

我們先從常見情況開始,也就是拍攝 JPEG 相片。在 camera2 API 中,您可以在擷取要求中傳遞 JPEG_ORIENTATION,指定輸出 JPEG 要順時針旋轉多少角度。

以下是我們提及內容的快速回顧:

  • 如要處理感應器方向,您必須將圖像緩衝區順時針旋轉 sensorOrientation 度。
  • 如要處理螢幕旋轉,您需要將緩衝區逆時針旋轉 displayRotation (後置鏡頭) 或順時針旋轉 (前置鏡頭)。

將這 2 個因素加總,即可得出要順時針旋轉的量:

  • sensorOrientation - displayRotation 後置鏡頭。
  • sensorOrientation + displayRotation 前置鏡頭。

如要查看這項邏輯的程式碼範例,請參閱 JPEG_ORIENTATION 說明文件。請注意,文件範例程式碼中的 deviceOrientation 是指裝置順時針旋轉。因此顯示旋轉的符號會反轉。

預覽

相機預覽畫面呢?應用程式顯示相機預覽畫面主要有 2 種方式:SurfaceView 和 TextureView。因此需要不同的方法才能正確處理方向。

SurfaceView

一般來說,如果不需要處理或製作預覽緩衝區的動畫,建議使用 SurfaceView 顯示相機預覽畫面。效能比 TextureView 更高,且耗用的資源較少。

SurfaceView 的版面配置也相對簡單。您只需要擔心顯示相機預覽畫面的 SurfaceView 顯示比例。

來源

在 SurfaceView 底下,Android 平台會旋轉輸出緩衝區,以配合裝置的螢幕方向。換句話說,這項屬性會同時考量感應器方向螢幕旋轉。簡單來說,如果螢幕是橫向,預覽畫面也會是橫向,直向也是同理。

如下表所示。請注意,螢幕旋轉本身不會決定來源的方向。

螢幕旋轉 手機 (自然螢幕方向 = 直向) 筆電 (自然螢幕方向 = 橫向)
0 直向圖片,蟲蟲機器人的頭部朝上 橫向圖片,蟲蟲機器人的頭部朝上
90 橫向圖片,蟲蟲機器人的頭部朝上 直向圖片,蟲蟲機器人的頭部朝上
180 直向圖片,蟲蟲機器人的頭部朝上 橫向圖片,蟲蟲機器人的頭部朝上
270 橫向圖片,蟲蟲機器人的頭部朝上 直向圖片,蟲蟲機器人的頭部朝上

版面配置

如您所見,SurfaceView 已經為我們處理了一些棘手的事情。但現在需要考慮觀景窗的大小,或螢幕上預覽畫面要多大。SurfaceView 會自動縮放來源緩衝區,以符合其尺寸。請務必確保觀景窗的長寬比與 sourcebuffer 相同。舉例來說,如果您嘗試將直向預覽畫面放入橫向的 SurfaceView,就會出現類似下方的扭曲畫面:

插圖:將直向預覽畫面放入橫向觀景窗時,Android 機器人會遭到拉伸

一般來說,觀景窗的長寬比 (即寬度/高度) 應與來源的長寬比相同。如果不想在觀景窗中裁剪圖片 (也就是為了修正顯示畫面而剪掉部分像素),請考慮以下兩種情況:aspectRatioActivity 大於 aspectRatioSource,以及小於或等於 aspectRatioSource

aspectRatioActivity > aspectRatioSource

您可以將案件視為「更廣泛」的活動。以下範例假設您有一個 16:9 的活動和 4:3 的來源。

aspectRatioActivity = 16/9 ≈ 1.78
aspectRatioSource = 4/3 ≈ 1.33

首先,觀景窗也必須是 4:3。接著,您要將來源和觀景窗放入活動中,如下所示:

插圖:活動的顯示比例大於內部觀景窗的顯示比例

在這種情況下,您應讓觀景窗的高度與活動高度相符,同時讓觀景窗的顯示比例與來源的顯示比例相同。虛擬程式碼如下:

viewfinderHeight = activityHeight;
viewfinderWidth = activityHeight * aspectRatioSource;
aspectRatioActivity ≤ aspectRatioSource

另一種情況是活動「較窄」或「較高」。我們可以沿用上一個範例,但下例中您將裝置旋轉 90 度,使活動為 9:16,來源為 3:4。

aspectRatioActivity = 9/16 = 0.5625
aspectRatioSource = 3/4 = 0.75

在本例中,您希望將來源和觀景窗放入活動中,如下所示:

插圖:活動的顯示比例小於內部觀景窗的顯示比例

您應讓觀景窗的寬度與活動的寬度相符 (而非先前案例中的高度),同時讓觀景窗的長寬比與來源的長寬比相同。虛擬程式碼:

viewfinderWidth = activityWidth;
viewfinderHeight = activityWidth / aspectRatioSource;
音訊切割

Camera2 範例中的 AutoFitSurfaceView.kt (github) 會覆寫 SurfaceView,並使用在兩個維度中等於或「略大於」活動的圖片,然後裁剪溢出的內容,藉此處理長寬比不符的問題。如果應用程式希望預覽畫面涵蓋整個活動,或完全填滿固定尺寸的檢視區塊,且不希望圖片失真,這項功能就非常實用。

警告

上述範例會將預覽畫面放大至略大於活動,盡量填滿螢幕空間。這是因為根據預設,溢出的部分會遭到父項版面配置 (或 ViewGroup) 裁剪。這項行為與 RelativeLayout 和 LinearLayout 一致,但與 ConstraintLayout 不一致。ConstraintLayout 可能會調整子項 View 的大小,使其符合版面配置,這會破壞預期的「置中裁剪」效果,導致預覽畫面遭到延展。您可以將這個提交做為參考。

TextureView

TextureView 可提供相機預覽內容的最大控制權,但會造成效能成本。此外,要讓攝影機預覽畫面顯示正確,也需要更多工作。

來源

在 TextureView 下方,Android 平台會根據感應器方向旋轉輸出緩衝區,以符合裝置的自然方向。雖然 TextureView 會處理感應器方向,但不會處理螢幕旋轉。這會將輸出緩衝區與裝置的自然方向對齊,因此您必須自行處理螢幕旋轉。

如下表所示。嘗試依據對應的螢幕旋轉角度旋轉圖案,您會發現 SurfaceView 中的圖案其實相同。

螢幕旋轉 手機 (自然螢幕方向 = 直向) 筆電 (自然螢幕方向 = 橫向)
0 直向圖片,蟲蟲機器人的頭部朝上 橫向圖片,蟲蟲機器人的頭部朝上
90 直向圖片,Bugdroid 的頭部朝向右側 橫向圖片,蟲蟲機器人的頭部朝向右側
180 橫向圖片,蟲蟲機器人的頭部朝上 直向圖片,蟲蟲機器人的頭部朝上
270 橫向圖片,蟲蟲機器人的頭部朝上 直向圖片,蟲蟲機器人的頭部朝上

版面配置

就 TextureView 而言,版面配置有點棘手。先前建議使用 TextureView 的轉換矩陣,但該方法不適用於所有裝置。建議您改為按照本文所述步驟操作。

在 TextureView 上正確配置預覽畫面的 3 步驟程序:

  1. 將 TextureView 的大小設為與所選預覽大小相同。
  2. 將可能遭到延展的 TextureView 縮放回預覽畫面的原始尺寸。
  3. 將 TextureView 逆時針旋轉 displayRotation 度。

假設手機的螢幕旋轉角度為 90 度。

插圖:手機螢幕旋轉 90 度,顯示物件

1. 將 TextureView 的大小設為與所選預覽大小相同

假設您選擇的預覽大小為 previewWidth × previewHeight,其中 previewWidth > previewHeight (感應器輸出內容本來就是橫向)。設定擷取工作階段時,應呼叫 SurfaceTexture#setDefaultBufferSize(int width, height) 指定預覽大小 (previewWidth × previewHeight)。

呼叫 setDefaultBufferSize 前,請務必將 TextureView 的大小設為 `previewWidth × previewHeight`,並使用 View#setLayoutParams(android.view.ViewGroup.LayoutParams)。這是因為 TextureView 會使用測量寬度和高度呼叫 SurfaceTexture#setDefaultBufferSize(int width, height)。如果未事先明確設定 TextureView 的大小,可能會導致競爭條件。如要解決這個問題,請先明確設定 TextureView 的大小。

現在 TextureView 可能與來源的尺寸不符。以手機為例,來源是直向形狀,但由於您剛才設定的 layoutParams,TextureView 是橫向形狀。這樣一來,預覽畫面就會遭到拉伸,如下圖所示:

圖片插圖:直向預覽畫面經過延展,以符合所選預覽大小的 TextureView

2. 將可能遭到延展的 TextureView 縮放回預覽畫面的原始尺寸

如要將延展的預覽畫面縮放回來源的尺寸,請考慮下列事項:

來源的維度 (sourceWidth × sourceHeight) 為:

  • previewHeight × previewWidth,如果自然方向為直向或反向直向 (感應器方向為 90 或 270 度)
  • previewWidth × previewHeight,如果自然螢幕方向為橫向或反向橫向 (感應器方向為 0 或 180 度)

利用 View#setScaleX(float)View#setScaleY(float) 修正延展問題

  • setScaleX(sourceWidth / previewWidth)
  • setScaleY(sourceHeight / previewHeight)

圖片插圖:顯示延展的預覽畫面縮放回原始尺寸的程序

3. 逆時針旋轉預覽畫面 `displayRotation`

如先前所述,您應逆時針旋轉預覽畫面 displayRotation,以補償螢幕旋轉。

方法是使用 View#setRotation(float)

  • setRotation(-displayRotation),因為這會順時針旋轉。

圖片:預覽畫面旋轉以配合裝置螢幕方向的程序

範例

注意:如果您先前在程式碼中將轉換矩陣用於 TextureView,預覽畫面在 Chromebook 等自然橫向裝置上可能無法正常顯示。可能是轉換矩陣錯誤地假設感應器方向為 90 或 270 度。如需解決方法,請參閱 GitHub 上的這個修訂版本,但我們強烈建議您遷移應用程式,改用本文所述方法。