相機預覽

注意:本頁面是指 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 的前置鏡頭,相機感應器產生的圖片緩衝區如下所示:

相機感應器旋轉為橫向,圖片方向為橫向,位於左上方。

圖片必須逆時針旋轉 270 度,讓預覽畫面的方向符合裝置方向:

直向的相機感應器,圖片方向朝上。

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

裝置旋轉

裝置旋轉是指裝置從自然方向旋轉的角度值。舉例來說,視旋轉方向而定,橫向的手機旋轉為 90 或 270 度。

相機感應器影像緩衝區旋轉的角度必須與裝置旋轉 (感應器方向範圍外) 相同,相機預覽畫面才能上方顯示。

螢幕方向計算

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

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

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

其中 sign1 代表前置鏡頭,-1 代表後置鏡頭。

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

運算式 deviceOrientationDegrees * sign + 360 會將裝置旋轉由逆時針旋轉,用於後置鏡頭 (例如,將逆時針旋轉 270 度到 90 度)。模數運算會將結果擴充為小於 360 度 (例如,將 540 度旋轉角度調整為 180 度)。

不同的 API 回報裝置旋轉情況的方式不同:

前置鏡頭

無論螢幕方向為橫向,感應器的預覽畫面和感應器都朝上。
圖 2 相機預覽畫面和感應器,將手機從 90 度轉成橫向。

以下是相機感應器產生的影像緩衝區,如圖 2 所示:

相機感應器為橫向,圖片朝上。

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

相機感應器已旋轉為直向,圖片方向為橫向,位於右上方。

接著,緩衝區會逆時針旋轉 90 度,藉此瞭解裝置旋轉情況,進而產生圖 2 中相機預覽畫面的正確方向:

相機感應器旋轉為橫向,圖片正上方。

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

螢幕方向為橫向,但感應器上下顛倒。
圖 3. 相機預覽畫面和感應器,將手機從 270 度 (或 -90 度) 轉為橫向。

以下是圖片緩衝區:

相機感應器旋轉為橫向,圖片上下顛倒。

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

相機感應器分級為直向,圖片方向為左上方。

接著,緩衝區會逆時針旋轉 270 度,以便考量裝置旋轉情況:

相機感應器旋轉為橫向,圖片正上方。

後置鏡頭

後置鏡頭的感應器方向通常為 90 度 (如從裝置背面查看)。指定相機預覽畫面的方向時,感應器圖片緩衝區會順時針旋轉感應器旋轉程度 (而不是像前置鏡頭一樣順時針旋轉),接著圖像緩衝區就會依照裝置旋轉量逆時針旋轉。

螢幕方向為橫向,但感應器上下顛倒。
圖 4. 手機配備橫向鏡頭的橫向模式 (旋轉 270 度或 -90 度)。

以下是相機感應器的影像緩衝區,如圖 4 所示:

相機感應器旋轉為橫向,圖片上下顛倒。

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

相機感應器分級為直向,圖片方向為左上方。

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

相機感應器旋轉為橫向,圖片正上方。

顯示比例

當裝置螢幕方向改變,同時也會在折疊式裝置折疊及展開、在多視窗環境中調整視窗大小,以及當次要螢幕上開啟應用程式時,顯示長寬比也會改變。

相機感應器圖片緩衝區必須方向和縮放,以符合觀景窗 UI 元素的方向和顯示比例,因為 UI 會以動態方式變更螢幕方向 (無論裝置是否改變方向)。

在新的板型規格或多視窗或多螢幕環境中,如果應用程式假設相機預覽畫面的方向與裝置 (直向或橫向) 相同,則預覽方向可能有誤、縮放不正確,或同時處於兩種狀態。

未折疊的折疊式裝置,直向相機預覽畫面旋轉為方向。
圖 5. 折疊式裝置會從直向轉為橫向,但相機感應器仍會保持直向。

在圖 5 中,應用程式誤假設裝置已逆時針旋轉 90 度,因此應用程式會旋轉相同的預覽畫面。

折疊式裝置螢幕正上方顯示相機預覽畫面,但由於縮放不正確,因此處於擠壓狀態。
圖 6.折疊式裝置會從直向轉為橫向,但相機感應器仍會保持直向。

在圖 6 中,應用程式未調整圖片緩衝區的顯示比例,因而無法配合相機預覽 UI 元素的新尺寸縮放畫面。

一般來說,固定方向的相機應用程式在折疊式裝置和其他大螢幕裝置上 (例如筆記型電腦) 會發生問題:

筆電上的相機預覽畫面為直立,但應用程式 UI 顯示並排顯示。
圖 7. 筆記型電腦的固定方向直向應用程式。

在圖 7 中,由於應用程式的方向限制為直向,因此相機應用程式的 UI 會傾斜。觀景窗圖片的方向正確 (相對於相機感應器)。

插邊直向模式

如果相機應用程式不支援多視窗模式 (resizeableActivity="false"),並限制螢幕方向 (screenOrientation="portrait"screenOrientation="landscape"),則可將這類相機應用程式置於大螢幕裝置的直向模式 (screenOrientation="portrait"screenOrientation="landscape") 上,以便正確調整相機預覽畫面的方向。

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

當相機圖片感應器的顯示比例與應用程式主要活動的顯示比例不符時,就會觸發插邊直向模式。

在筆電上以直向直向顯示相機預覽畫面和應用程式 UI。寬幅預覽圖片會縮放並裁剪,以符合直向的方向。
圖 8. 筆電上處於插邊模式的固定螢幕方向應用程式。

在圖 8 中,僅限直向的相機應用程式經過旋轉,在筆記型電腦螢幕上直接顯示 UI。由於直向應用程式和橫向顯示畫面的顯示比例差異,應用程式會加上黑邊。為了補償應用程式的 UI 旋轉 (因插邊直向模式),相機預覽圖片已旋轉;為配合直向模式,圖片則已經過裁剪及縮放,進而減少視野。

旋轉、裁剪、縮放

在具有橫向長寬比的螢幕上,系統會針對僅限直向相機應用程式叫用插邊直向模式:

筆電上的相機預覽畫面為直立,但應用程式 UI 顯示並排顯示。
圖 9.筆記型電腦的固定方向直向應用程式。

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

應用程式已旋轉為直向,並加上黑邊。圖片為傾斜,並位於右上方。

相機圖片會旋轉 90 度,以調整應用程式的方向:

感應器圖片已旋轉 90 度,讓圖片保持直立。

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

裁剪相機圖片已調整大小,以填滿相機預覽畫面。

在折疊式裝置上,相機感應器的方向可以是直向,而螢幕的顯示比例為橫向:

相機預覽和應用程式 UI 會轉向寬幅、展開的螢幕。
圖 10. 裝置未折疊,使用僅限直向的相機應用程式,以及相機感應器和螢幕的不同顯示比例。

由於相機預覽畫面會隨感應器方向旋轉,因此圖片在觀景窗中會正確調整方向,但僅限直向的應用程式會傾斜。

採用直向模式時,只需要在應用程式直向顯示上下黑邊,應用程式和相機預覽畫面即可正確調整方向:

加上黑邊的應用程式處於直向模式,且在折疊式裝置上顯示相機預覽畫面。

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 程式庫提供一組工具,可簡化相機預覽畫面的建立流程。這個程式庫依附於 CameraX Core,因此您可以順暢地將其整合至現有的 Camera2 程式碼集。

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

CameraViewfinder 會在內部使用 TextureViewSurfaceView 顯示相機畫面,並對這些畫面套用必要轉換,以便正確顯示觀景窗。包括修正其顯示比例、縮放和旋轉。

如要透過 CameraViewfinder 物件要求途徑,您必須建立 ViewfinderSurfaceRequest

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

呼叫 requestSurfaceAsync() 會將要求傳送至介面提供者,亦即 TextureViewSurfaceView,並取得 SurfaceListenableFuture

呼叫 markSurfaceSafeToRelease() 會通知途徑供應器不需要該介面,並可釋出相關資源。

Kotlin

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

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

Java

    void startCamera(){
        Size previewResolution = new Size(width, height);
        ViewfinderSurfaceRequest viewfinderSurfaceRequest =
                new ViewfinderSurfaceRequest(previewResolution, characteristics);
        ListenableFuture surfaceListenableFuture =
                cameraViewfinder.requestSurfaceAsync(viewfinderSurfaceRequest);

        Futures.addCallback(surfaceListenableFuture, new FutureCallback() {
            @Override
            public void onSuccess(Surface result) {
                /* create a CaptureSession using this surface as usual */
            }
            @Override public void onFailure(Throwable t) { /* something went wrong */}
        },  ContextCompat.getMainExecutor(context));
    }

SurfaceView

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

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

您必須確保圖片緩衝區的顯示比例與 SurfaceView 的顯示比例相符,這樣就能在元件的 onMeasure() 方法中,縮放 SurfaceView 的內容達成此目的:

(computeRelativeRotation() 原始碼位於下方的相對旋轉部分)。

Kotlin

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)
}

Java

@Override
void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    int width = MeasureSpec.getSize(widthMeasureSpec);
    int height = MeasureSpec.getSize(heightMeasureSpec);

    int relativeRotation = computeRelativeRotation(characteristics, surfaceRotationDegrees);

    if (previewWidth > 0f && previewHeight > 0f) {

        /* Scale factor required to scale the preview to its original size on the x-axis. */
        float scaleX = (relativeRotation % 180 == 0)
                       ? (float) width / previewWidth
                       : (float) width / previewHeight;

        /* Scale factor required to scale the preview to its original size on the y-axis. */
        float scaleY = (relativeRotation % 180 == 0)
                       ? (float) height / previewHeight
                       : (float) height / previewWidth;

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

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

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

TextureView

TextureView 的成效比 SurfaceView 低,而且工作量更大,但 TextureView 可讓您最大限度地控制相機預覽畫面。

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

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

相對輪播

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

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

CameraCharacteristicsSurface 類別可以計算相機感應器的相對旋轉角度:

Kotlin

/**
 * 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)!!

    // 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
}

Java

/**
 * 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 int computeRelativeRotation(
    CameraCharacteristics characteristics,
    int surfaceRotationDegrees
){
    Integer sensorOrientationDegrees =
        characteristics.get(CameraCharacteristics.SENSOR_ORIENTATION);

    // Reverse device orientation for back-facing cameras.
    int sign = characteristics.get(CameraCharacteristics.LENS_FACING) ==
        CameraCharacteristics.LENS_FACING_FRONT ? 1 : -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() 回呼,即可透過優先順序較高的活動掌握相機的先佔存取權。

詳情請參閱「多視窗運作」。

其他資源

  • 如需 Camera2 範例,請參閱 GitHub 上的 Camera2Basic 應用程式
  • 如要瞭解 CameraX 預覽用途,請參閱 CameraX 的「導入預覽」相關說明。
  • 如需 CameraX 相機預覽的實作範例,請參閱 GitHub 上的 CameraXBasic 存放區。
  • 如要瞭解 ChromeOS 中的相機預覽,請參閱「相機方向」。
  • 如要進一步瞭解如何為折疊式裝置開發應用程式,請參閱「瞭解折疊式裝置」。