相機預覽

注意:本頁面所述是指 Camera2 套件。除非應用程式需要 Camera2 的特定低階功能,否則建議使用 CameraX。CameraX 和 Camera2 均支援 Android 5.0 (API 級別 21) 以上版本。

在 Android 裝置上,相機和相機預覽畫面的方向不一定相同。

無論裝置是手機、平板電腦或電腦,相機在裝置上的位置都是固定的。當裝置方向變更時,相機方向也會變更。

因此,相機應用程式通常會假設裝置方向和相機預覽畫面的顯示比例之間存在固定關係。當手機處於直向模式時,相機預覽畫面的高度會大於寬度。當手機 (和相機) 旋轉至橫向時,相機預覽畫面的寬度應大於高度。

不過,隨著摺疊式裝置等新板型規格,以及多視窗模式和多螢幕等顯示模式問世,這些假設已不再適用。摺疊式裝置會變更螢幕大小和顯示比例,但不會變更螢幕方向。多視窗模式會將相機應用程式限制在螢幕的一部分,無論裝置方向為何,都會縮放相機預覽畫面。多螢幕模式可讓您使用次要螢幕,其方向可能與主要螢幕不同。

切換前後鏡頭

Android 相容性定義規定,相機影像感應器的方向「必須經過調整,讓相機的長邊對齊螢幕的長邊。也就是說,當裝置處於橫向時,相機必須以橫向拍攝影像。無論裝置的自然方向為何,這項設定都適用,也就是說,這項設定適用於橫向和直向裝置。

相機與螢幕的排列方式可最大化相機應用程式中相機觀景窗的顯示區域。此外,影像感應器通常會以橫向顯示比例輸出資料,其中 4:3 最為常見。

手機和相機感應器皆為直向。
圖 1:手機和相機感應器方向的一般關係。

鏡頭感應器的自然螢幕方向為橫向。在圖 1 中,前置鏡頭 (與螢幕朝向相同方向的鏡頭) 的感應器相對於手機旋轉 270 度,以符合 Android 相容性定義。

如要向應用程式公開感應器旋轉角度,camera2 API 包含 SENSOR_ORIENTATION 常數。在大多數手機和平板電腦上,裝置會回報前置鏡頭的感應器方向為 270 度,後置鏡頭則為 90 度 (從裝置背面觀看的視角),讓感應器的長邊與裝置的長邊對齊。筆電相機通常會回報感應器方向為 0 或 180 度。

由於相機影像感應器會以感應器的自然方向 (橫向) 輸出資料 (影像緩衝區),因此影像緩衝區必須旋轉 SENSOR_ORIENTATION 指定的角度,相機預覽畫面才能以裝置的自然方向直立顯示。前置鏡頭會逆時針旋轉,後置鏡頭則會順時針旋轉。

舉例來說,對於圖 1 中的前置鏡頭,鏡頭感應器產生的圖片緩衝區如下所示:

相機感應器旋轉為橫向,圖片側向,左上。
圖 2 前置鏡頭感應器在自然橫向方向的原始圖片緩衝區。

圖片必須逆時針旋轉 270 度,預覽畫面方向才會與裝置方向一致:

相機感應器為直向,影像為直立。
圖 3. 前置鏡頭影像緩衝區逆時針旋轉 270 度,以符合直向裝置方向。

後置鏡頭會產生與前一個緩衝區方向相同的圖片緩衝區,但 SENSOR_ORIENTATION 為 90 度。因此,緩衝區會順時針旋轉 90 度。

裝置旋轉

裝置旋轉是指裝置從自然方向旋轉的角度。舉例來說,橫向擺放的手機裝置旋轉角度為 90 或 270 度,具體取決於旋轉方向。

相機感應器圖像緩衝區必須旋轉與裝置旋轉相同的度數 (加上感應器方向的度數),相機預覽畫面才會直立顯示。

方向計算

相機預覽畫面的正確方向會考量感應器方向和裝置旋轉。

感應器圖片緩衝區的整體旋轉角度可使用下列公式計算:

rotation = (sensorOrientationDegrees - deviceOrientationDegrees * sign + 360) % 360

其中 sign 是前置鏡頭的 1,後置鏡頭的 -1。

如果是前置鏡頭,影像緩衝區會逆時針旋轉 (從感應器的自然方向)。如果是後置鏡頭,感應器圖片緩衝區會順時針旋轉。

運算式 deviceOrientationDegrees * sign + 360 會將後置鏡頭的裝置旋轉角度從逆時針轉換為順時針 (例如將逆時針 270 度轉換為順時針 90 度)。模數運算會將結果縮放至小於 360 度 (例如將 540 度旋轉縮放至 180 度)。

不同的 API 會以不同方式回報裝置旋轉:

前置鏡頭

相機預覽畫面和感應器皆為橫向,感應器正面朝上。
圖 4. 手機轉為橫向 90 度時的相機預覽畫面和感應器。

以下是圖 4 中相機感應器產生的圖片緩衝區:

相機感應器為橫向,影像為直向。
圖 5. 手機逆時針旋轉 90 度時,前置鏡頭感應器產生的影像緩衝區。

緩衝區必須逆時針旋轉 270 度,才能調整感應器方向 (請參閱上方的「相機方向」):

相機感應器旋轉至直向,圖片橫向,位於右上方。
圖 6. 圖片緩衝區逆時針旋轉 270 度,以配合感應器方向。

接著,緩衝區會逆時針旋轉 90 度,以因應裝置旋轉,進而產生圖 4 中正確的相機預覽方向:

鏡頭感應器旋轉為橫向,圖片直向。
圖 7. 根據裝置方向逆時針旋轉 90 度後的圖片緩衝區。

以下是相機向右轉為橫向:

相機預覽畫面和感應器皆為橫向,但感應器上下顛倒。
圖 8. 手機轉 270 度 (或 -90 度) 至橫向時的相機預覽畫面和感應器。

以下是圖片緩衝區:

鏡頭感應器旋轉為橫向,影像上下顛倒。
圖 9。 手機逆時針旋轉 270 度時,前置鏡頭感應器產生的影像緩衝區。

緩衝區必須逆時針旋轉 270 度,才能調整感應器方向:

相機感應器旋轉為直向,圖片側向,位於左上方。
圖 10. 圖片緩衝區逆時針旋轉 270 度,以配合感應器方向。

接著,緩衝區會再逆時針旋轉 270 度,以配合裝置旋轉:

鏡頭感應器旋轉為橫向,圖片直向。
圖 11. 根據裝置方向逆時針旋轉 270 度後的圖片緩衝區。

後置鏡頭

後置鏡頭的感應器方向通常為 90 度 (從裝置背面觀看)。調整相機預覽畫面方向時,感應器影像緩衝區會依感應器旋轉角度順時針旋轉 (而非像前置鏡頭一樣逆時針旋轉),然後影像緩衝區會依裝置旋轉角度逆時針旋轉。

相機預覽畫面和感應器皆為橫向,但感應器上下顛倒。
圖 12。 手機後置鏡頭為橫向 (旋轉 270 度或 -90 度)。

以下是圖 12 中相機感應器的圖像緩衝區:

鏡頭感應器旋轉為橫向,影像上下顛倒。
圖 13。 手機處於橫向模式時,後置鏡頭感應器產生的影像緩衝區。

緩衝區必須順時針旋轉 90 度,才能調整感應器方向:

相機感應器旋轉為直向,圖片側向,位於左上方。
圖 14。 圖片緩衝區順時針旋轉 90 度,以配合後置感應器方向。

接著,緩衝區會逆時針旋轉 270 度,以因應裝置旋轉:

鏡頭感應器旋轉為橫向,圖片直向。
圖 15。 裝置方向逆時針旋轉 270 度後的圖片緩衝區。

顯示比例

螢幕顯示比例會隨著裝置螢幕方向變更而改變,但也會在摺疊式裝置摺疊和展開、在多視窗模式中調整視窗大小,以及在次要螢幕上開啟應用程式時變更。

相機感應器影像緩衝區必須經過調整方向和縮放,以符合觀景窗 UI 元素的方向和顯示比例,因為 UI 會動態變更方向,無論裝置是否變更方向。

在新的板型規格或多視窗模式/多螢幕環境中,如果應用程式假設相機預覽畫面的方向與裝置相同 (直向或橫向),預覽畫面的方向和/或縮放比例可能會不正確。

展開的摺疊式裝置,直向相機預覽畫面轉為橫向。
圖 16。 摺疊式裝置從直向轉換為橫向顯示比例,但鏡頭感應器仍維持直向。

在圖 16 中,應用程式誤以為裝置逆時針旋轉 90 度,因此將預覽畫面旋轉相同角度。

展開的摺疊式裝置,相機預覽畫面直立,但因縮放比例不正確而遭到擠壓。
圖 17。 摺疊式裝置從直向轉換為橫向顯示比例,相機預覽畫面縮放比例不正確。

在圖 17 中,應用程式未調整圖片緩衝區的顯示比例,因此無法正確縮放,以符合相機預覽 UI 元素的新尺寸。

固定方向的相機應用程式在摺疊式裝置和其他大螢幕裝置 (例如筆電) 上,通常會遇到以下問題:

筆電上的相機預覽畫面為直向,但應用程式 UI 為橫向。
圖 18。 筆電上固定直向的應用程式。

在圖 18 中,相機應用程式的使用者介面是橫向,因為應用程式的方向限制為僅限直向。觀景窗影像的方向與相機感應器一致。

內嵌肖像模式

如果相機應用程式不支援多視窗模式 (resizeableActivity="false"),且限制螢幕方向 (screenOrientation="portrait" 或 screenOrientation="landscape"),則可在大螢幕裝置上進入插邊直向模式,以正確方向顯示相機預覽畫面。

即使螢幕顯示比例為橫向,插邊直向模式也會在直向模式下,為僅限直向的應用程式加上上下黑邊 (插邊)。即使螢幕顯示比例為直向,僅限橫向的應用程式也會在橫向模式下加上黑邊。相機影像會旋轉,與應用程式 UI 對齊,並裁剪為符合相機預覽畫面的顯示比例,然後縮放至填滿預覽畫面。

當相機影像感應器的顯示比例與應用程式主要活動的顯示比例不符時,就會觸發內嵌肖像模式。

筆電上的相機預覽畫面和應用程式 UI 採用正確的直向模式。
            系統會縮放及裁剪寬預覽圖片,以符合直向。
圖 19。 筆電上採用插邊直向模式的固定方向直向應用程式。

在圖 19 中,僅限直向的相機應用程式已旋轉,在筆電螢幕上直向顯示 UI。由於直向應用程式和橫向螢幕的顯示比例不同,因此應用程式會加上上下黑邊。相機預覽圖片已旋轉,以補償應用程式 UI 旋轉 (由於插邊直向模式),且圖片已裁剪並縮放,以符合直向方向,減少視野。

旋轉、裁剪、縮放

在顯示比例為橫向的螢幕上,僅限直向的相機應用程式會叫用插邊直向模式:

筆電上的相機預覽畫面為直向,但應用程式 UI 為橫向。
圖 20. 筆電上固定直向的應用程式。

應用程式在直向模式下會加上黑邊:

應用程式旋轉至直向,並加上黑邊。圖片橫向,頂端朝右。
圖 21. 在感應器圖片旋轉前,僅限直向的應用程式在橫向螢幕上旋轉並加上黑邊。

相機圖片會旋轉 90 度,配合應用程式重新調整方向:

感應器圖片旋轉 90 度,使其直立。
圖 22.相機圖片旋轉 90 度,與重新調整方向的應用程式 UI 對齊。

圖片會裁剪為相機預覽畫面的顯示比例,然後縮放至填滿預覽畫面 (視野會縮小):

裁剪後的相機影像會縮放,填滿相機預覽畫面。
圖 23.裁剪及縮放相機影像,填滿直向相機預覽畫面。

在摺疊式裝置上,鏡頭感應器的方向可以是直向,而螢幕的顯示比例則為橫向:

相機預覽畫面和應用程式 UI 轉向寬螢幕、展開的螢幕。
圖 24.展開的裝置,搭載僅限直向的相機應用程式,以及不同顯示比例的相機感應器和螢幕。

由於相機預覽畫面會根據感應器方向旋轉,因此觀景窗中的圖片方向正確,但僅限直向的應用程式會橫向顯示。

插邊肖像模式只需要在直向模式下為應用程式加上黑邊,即可正確調整應用程式和相機預覽畫面的方向:

在摺疊式裝置上,直向模式的上下黑邊應用程式,相機預覽畫面為直向。
圖 25.在展開的折疊式裝置上,僅限直向的應用程式會以內嵌直向模式加上黑邊。

API

自 Android 12 (API 級別 31) 起,應用程式也可以透過 CaptureRequest 類別的 SCALER_ROTATE_AND_CROP 屬性,明確控制插邊直向模式。

預設值為 SCALER_ROTATE_AND_CROP_AUTO,可讓系統叫用內嵌直向模式。SCALER_ROTATE_AND_CROP_90 是內嵌肖像模式的行為,如前文所述。

並非所有裝置都支援所有 SCALER_ROTATE_AND_CROP 值。如要取得支援的值清單,請參閱 CameraCharacteristics#SCALER_AVAILABLE_ROTATE_AND_CROP_MODES。

CameraX

Jetpack CameraX 程式庫可讓您輕鬆建立能配合感應器方向和裝置旋轉角度的相機觀景窗。

PreviewView 版面配置元素會建立相機預覽畫面,並根據感應器方向、裝置旋轉和縮放比例自動調整。PreviewView 會套用 FILL_CENTER 縮放類型,將圖片置中,但可能會裁剪圖片以符合 PreviewView 的尺寸,藉此維持相機圖片的顯示比例。如要為相機圖片加上上下黑邊,請將縮放類型設為 FIT_CENTER。

如要瞭解如何使用 PreviewView 建立相機預覽的基本概念,請參閱「實作預覽」。

如需完整的實作範例,請參閱 GitHub 上的 CameraXBasic 存放區。

CameraViewfinder

與預覽用途類似,CameraViewfinder 程式庫提供一組工具,可簡化相機預覽畫面的建立程序。這項功能不依附於 CameraX Core,因此可完美整合至現有的 Camera2 程式碼集。

您可以使用 CameraViewfinder 小工具顯示 Camera2 的攝影機畫面,而不直接使用 Surface。

CameraViewfinder 會在內部使用 TextureView 或 SurfaceView 顯示攝影機畫面,並對其套用必要的轉換,以正確顯示觀景窗。包括修正顯示比例、縮放比例和旋轉角度。

如要向 CameraViewfinder 物件要求介面,您必須建立 ViewfinderSurfaceRequest。

這項要求包含 CameraCharacteristics 的介面解析度和相機裝置資訊。

呼叫 requestSurfaceAsync() 會將要求傳送至介面供應器 (TextureView 或 SurfaceView),並取得 Surface 的 ListenableFuture。

呼叫 markSurfaceSafeToRelease() 會通知途徑供應商不需要途徑,且可釋出相關資源。

fun startCamera() {
    val previewResolution = Size(width, height)
    val viewfinderSurfaceRequest =
        ViewfinderSurfaceRequest(previewResolution, characteristics)
    val surfaceListenableFuture =
        cameraViewfinder.requestSurfaceAsync(viewfinderSurfaceRequest)

    Futures.addCallback(surfaceListenableFuture, object : FutureCallback<Surface> {
        override fun onSuccess(surface: Surface) {
            /* create a CaptureSession using this surface as usual */
        }
        override fun onFailure(t: Throwable) { /* something went wrong */ }
    }, ContextCompat.getMainExecutor(context))
}

SurfaceView

如果預覽畫面不需要處理,且不是動畫,SurfaceView 是建立相機預覽畫面的簡單方法。

SurfaceView 會自動旋轉相機感應器圖片緩衝區,以符合螢幕方向,同時考量感應器方向和裝置旋轉角度。不過,系統會縮放圖片緩衝區,以符合 SurfaceView 尺寸,而不考慮顯示比例。

您必須確保圖片緩衝區的顯示比例與 SurfaceView 的顯示比例相符,方法是在元件的 onMeasure() 方法中縮放 SurfaceView 的內容:

(computeRelativeRotation() 原始碼位於下方的「相對旋轉」一節)。

override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
    val width = MeasureSpec.getSize(widthMeasureSpec)
    val height = MeasureSpec.getSize(heightMeasureSpec)

    val relativeRotation = computeRelativeRotation(characteristics, surfaceRotationDegrees)

    if (previewWidth > 0f && previewHeight > 0f) {
        /* Scale factor required to scale the preview to its original size on the x-axis. */
        val scaleX =
            if (relativeRotation % 180 == 0) {
                width.toFloat() / previewWidth
            } else {
                width.toFloat() / previewHeight
            }
        /* Scale factor required to scale the preview to its original size on the y-axis. */
        val scaleY =
            if (relativeRotation % 180 == 0) {
                height.toFloat() / previewHeight
            } else {
                height.toFloat() / previewWidth
            }

        /* Scale factor required to fit the preview to the SurfaceView size. */
        val finalScale = min(scaleX, scaleY)

        setScaleX(1 / scaleX * finalScale)
        setScaleY(1 / scaleY * finalScale)
    }
    setMeasuredDimension(width, height)
}

如要進一步瞭解如何將 SurfaceView 做為相機預覽畫面,請參閱「相機方向」。

TextureView

TextureView 的效能不如 SurfaceView,且需要更多工作,但 TextureView 可讓您最大程度地控制相機預覽。

TextureView 會根據感應器方向旋轉感應器圖片緩衝區,但不會處理裝置旋轉或預覽縮放。

縮放和旋轉可以編碼為矩陣轉換。如要瞭解如何正確縮放及旋轉 TextureView,請參閱「在相機應用程式中支援可調整大小的介面」。

相對旋轉

相機感應器的相對旋轉角度,是指將相機感應器輸出內容與裝置方向對齊所需的旋轉角度。

SurfaceView 和 TextureView 等元件會使用相對旋轉角度,決定預覽圖片的 x 和 y 縮放比例係數。也可用於指定感應器圖片緩衝區的旋轉角度。

CameraCharacteristics 和 Surface 類別可計算相機感應器的相對旋轉角度:

/**
 * Computes rotation required to transform the camera sensor output orientation to the
 * device's current orientation in degrees.
 *
 * @param characteristics The CameraCharacteristics to query for the sensor orientation.
 * @param surfaceRotationDegrees The current device orientation as a Surface constant.
 * @return Relative rotation of the camera sensor output.
 */
public fun computeRelativeRotation(
    characteristics: CameraCharacteristics,
    surfaceRotationDegrees: Int
): Int {
    val sensorOrientationDegrees =
        characteristics.get(CameraCharacteristics.SENSOR_ORIENTATION) ?: 0

    // Reverse device orientation for back-facing cameras.
    val sign = if (characteristics.get(CameraCharacteristics.LENS_FACING) ==
        CameraCharacteristics.LENS_FACING_FRONT
    ) 1 else -1

    // Calculate desired orientation relative to camera orientation to make
    // the image upright relative to the device orientation.
    return (sensorOrientationDegrees - surfaceRotationDegrees * sign + 360) % 360
}

視窗指標

不應使用螢幕大小來判斷相機觀景窗的尺寸;相機應用程式可能會在螢幕的一部分執行,無論是在行動裝置的多視窗模式下,還是在 ChromeOS 的自由模式下。

WindowManager#getCurrentWindowMetrics() (在 API 級別 30 中新增) 會傳回應用程式視窗的大小,而非螢幕大小。Jetpack WindowManager 程式庫方法 WindowMetricsCalculator#computeCurrentWindowMetrics() 和 WindowInfoTracker#currentWindowMetrics() 提供類似的支援,可回溯相容於 API 級別 14。

旋轉 180 度

裝置旋轉 180 度 (例如從自然方向到自然方向上下顛倒) 不會觸發 onConfigurationChanged() 回呼。因此,相機預覽畫面可能會上下顛倒。

如要偵測 180 度旋轉,請實作 DisplayListener,並在 onDisplayChanged() 回呼中呼叫 Display#getRotation(),檢查裝置旋轉。

專屬資源

在 Android 10 之前,多視窗模式環境中只有最上層可見的活動會處於 RESUMED 狀態。這會讓使用者感到困惑,因為系統並未指出哪個活動已恢復。

Android 10 (API 級別 29) 導入了多重繼續功能,所有可見活動都會處於 RESUMED 狀態。如果活動上有公開活動,或是活動無法聚焦 (例如在子母畫面模式下時),可見活動仍可能進入 PAUSED 狀態 (請參閱「子母畫面支援」)。

在 API 級別 29 以上,使用相機、麥克風或任何專屬或單例模式資源的應用程式,必須支援多重繼續。舉例來說,如果三個重新啟用的活動都要使用相機,只有一個活動能存取這項專屬資源。每個活動都必須實作 onDisconnected() 回呼,才能掌握優先順序較高的活動是否搶先存取相機。

詳情請參閱「多重繼續」。

其他資源