如果 Android 應用程式使用相機,處理螢幕方向時需要注意一些特殊事項。本文假設您瞭解 Android camera2 API 的基本概念。如要瞭解 camera2 的概況,請參閱我們的網誌文章或摘要。此外,建議您先嘗試編寫相機應用程式,再閱讀本文。
背景
在 Android 相機應用程式中處理螢幕方向很棘手,需要考量下列因素:
- 自然方向:裝置處於「正常」位置時的螢幕方向,通常是手機的直向,以及筆電的橫向。
- 感應器方向:感應器實際安裝在裝置上的方向。
- 螢幕旋轉:裝置從自然方向實際旋轉的角度。
- 觀景窗大小:用於顯示相機預覽畫面的觀景窗大小。
- 相機輸出的圖片大小。
這些因素加總起來,會為相機應用程式帶來大量可能的 UI 和預覽設定。本文旨在說明開發人員如何瀏覽這些資訊,並在 Android 應用程式中正確處理相機方向。
為簡化說明,除非另有說明,否則請假設所有範例都使用後置鏡頭。此外,以下所有相片皆為模擬,以便清楚說明。
瞭解螢幕方向
自然方向
自然方向是指裝置處於正常預期位置時的螢幕方向。手機的自然方向通常是直向,換句話說,手機的寬度較短,高度較長。筆記型電腦的自然方向為橫向,也就是寬度較長,高度較短。平板電腦的情況稍微複雜一點,因為平板電腦可以直向或橫向顯示。
感應器方向
正式來說,感應器方向的測量方式是:感應器輸出圖像需要順時針旋轉幾度,才能符合裝置的自然方向。換句話說,感應器方向是指感應器安裝在裝置上之前,逆時針旋轉的角度。從螢幕上來看,旋轉方向似乎是順時針,這是因為後置鏡頭感應器安裝在裝置的「背面」。
根據 Android 10 相容性定義 7.5.5 相機方向,前置和後置鏡頭「必須經過調整,讓相機的長邊對齊螢幕的長邊」。
攝影機的輸出緩衝區為橫向大小。由於手機的自然方向通常為直向,感應器方向通常會與自然方向相差 90 或 270 度,以便輸出緩衝區的長邊與螢幕的長邊相符。如果裝置的自然方向為橫向 (例如 Chromebook),感應器方向會有所不同。在這些裝置上,影像感應器同樣會放置在輸出緩衝區的長邊與螢幕長邊對齊的位置。由於兩者都是橫向大小,因此方向一致,感應器方向為 0 或 180 度。
以下插圖顯示觀察者從裝置螢幕上看到的內容:
請參考下列情境:
| 手機 | 筆記型電腦 |
|---|---|
![]() |
![]() |
由於手機上的感應器方向通常為 90 或 270 度,如果不考慮感應器方向,您取得的圖片會如下所示:
| 手機 | 筆記型電腦 |
|---|---|
![]() |
![]() |
假設逆時針感應器方向儲存在 sensorOrientation 變數中。如要補償感應器方向,您需要順時針旋轉輸出緩衝區 `sensorOrientation`,將方向重新與裝置的自然方向對齊。
在 Android 中,應用程式可以使用 TextureView 或 SurfaceView 顯示相機預覽畫面。如果應用程式正確使用這兩者,都能處理感應器方向。我們會在後續章節中詳細說明如何考量感應器方向。
螢幕旋轉
螢幕旋轉的正式定義為螢幕上繪製的圖像旋轉,與裝置從自然方向實際旋轉的方向相反。以下各節假設螢幕旋轉角度都是 90 的倍數。如果以絕對角度擷取螢幕旋轉角度,請將其四捨五入至最接近的 {0、90、180、270}。
以下各節中的「螢幕方向」是指裝置的實際方向為橫向或直向,與「螢幕旋轉」不同。
假設您將裝置從先前的位置逆時針旋轉 90 度,如下圖所示:
假設輸出緩衝區已根據感應器方向旋轉,您會得到下列輸出緩衝區:
| 手機 | 筆記型電腦 |
|---|---|
![]() |
![]() |
如果顯示器旋轉角度儲存在 displayRotation 變數中,您應以 displayRotation 逆時針旋轉輸出緩衝區,才能取得正確的圖像。
如果是前置鏡頭,螢幕旋轉會以與螢幕相反的方向作用於圖像緩衝區。如果是前置鏡頭,請依順時針方向旋轉緩衝區,旋轉角度為 displayRotatation。
注意事項
螢幕旋轉會測量裝置的逆時針旋轉角度。並非所有螢幕方向/旋轉 API 都適用這項規則。
舉例來說,
-
如果您使用
Display#getRotation(),會如本文所述取得逆時針旋轉角度。 - 如果您使用 OrientationEventListener#onOrientationChanged(int),則會改為取得順時針旋轉角度。
請注意,螢幕旋轉是相對於自然方向。舉例來說,如果將手機旋轉 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,就會出現類似下方的扭曲畫面:
一般來說,觀景窗的長寬比 (即寬度/高度) 應與來源的長寬比相同。如果不想在觀景窗中裁剪圖片 (也就是為了修正顯示畫面而剪掉部分像素),請考慮以下兩種情況: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 | ![]() |
![]() |
| 180 | ![]() |
![]() |
| 270 | ![]() |
![]() |
版面配置
就 TextureView 而言,版面配置有點棘手。先前建議使用 TextureView 的轉換矩陣,但該方法不適用於所有裝置。建議您改為按照本文所述步驟操作。
在 TextureView 上正確配置預覽畫面的 3 步驟程序:
- 將 TextureView 的大小設為與所選預覽大小相同。
- 將可能遭到延展的 TextureView 縮放回預覽畫面的原始尺寸。
-
將 TextureView 逆時針旋轉
displayRotation度。
假設手機的螢幕旋轉角度為 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 是橫向形狀。這樣一來,預覽畫面就會遭到拉伸,如下圖所示:
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),因為這會順時針旋轉。
範例
-
PreviewViewJetpack 中的 camerax 會處理 TextureView 版面配置,如先前所述。這會使用 PreviewCorrector 設定轉換。
注意:如果您先前在程式碼中將轉換矩陣用於 TextureView,預覽畫面在 Chromebook 等自然橫向裝置上可能無法正常顯示。可能是轉換矩陣錯誤地假設感應器方向為 90 或 270 度。如需解決方法,請參閱 GitHub 上的這個修訂版本,但我們強烈建議您遷移應用程式,改用本文所述方法。





















