相机预览

注意:本页面引用了 Camera2 软件包。除非您的应用需要 Camera2 的特定低级功能,否则我们建议使用 CameraX。CameraX 和 Camera2 都支持 Android 5.0 (API 级别 21) 及更高版本。

在 Android 设备上,相机和相机预览的方向并非总是一致。

相机在设备上的位置是固定的,无论设备是手机、平板电脑还是电脑。当设备方向改变时,相机方向也会改变。

因此,相机应用通常会假设设备方向与相机预览的纵横比之间存在固定的关系。当手机处于纵向时,相机预览通常假设其高度大于宽度。当手机(和相机)旋转到横向时,相机预览预计会比其高度更宽。

但是,新的外形设备(如可折叠设备)和显示模式(如多窗口多显示屏)对这些假设提出了挑战。可折叠设备在不改变方向的情况下改变显示尺寸和纵横比。多窗口模式将相机应用限制在屏幕的一部分,无论设备方向如何,都会缩放相机预览。多显示屏模式支持使用辅助显示器,这些显示器可能与主显示器方向不同。

相机方向

Android 兼容性定义规定,相机图像传感器“必须定向,使相机的长边与屏幕的长边对齐。也就是说,当设备处于横向时,相机必须以横向捕获图像。这适用于设备的自然方向;也就是说,它适用于横向优先设备和纵向优先设备。”

相机与屏幕的排列最大化了相机应用中相机取景器的显示区域。此外,图像传感器通常以横向纵横比输出数据,其中 4:3 最为常见。

Phone and camera sensor both in portrait orientation.
图 1. 手机和相机传感器方向的典型关系。

相机传感器的自然方向是横向。在图 1 中,前置摄像头(与显示屏指向同一方向的摄像头)的传感器相对于手机旋转了 270 度,以符合 Android 兼容性定义。

为了向应用公开传感器旋转信息,camera2 API 包含一个 SENSOR_ORIENTATION 常量。对于大多数手机和平板电脑,设备报告前置摄像头的传感器方向为 270 度,后置摄像头(从设备背面看)的传感器方向为 90 度,这使得传感器的长边与设备的长边对齐。笔记本电脑摄像头通常报告 0 或 180 度的传感器方向。

由于相机图像传感器以传感器的自然方向(横向)输出其数据(图像缓冲区),因此图像缓冲区必须旋转 SENSOR_ORIENTATION 指定的角度,以便相机预览在设备的自然方向上显示为直立。对于前置摄像头,旋转是逆时针的;对于后置摄像头,是顺时针的。

例如,对于图 1 中的前置摄像头,相机传感器生成的图像缓冲区如下所示

Camera sensor rotated to landscape orientation with image
            sideways, top left.

图像必须逆时针旋转 270 度,以便预览的方向与设备方向匹配

Camera sensor in portrait orientation with image upright.

后置摄像头将生成与上述缓冲区相同方向的图像缓冲区,但 SENSOR_ORIENTATION 为 90 度。因此,缓冲区顺时针旋转 90 度。

设备旋转

设备旋转是设备相对于其自然方向旋转的角度。例如,处于横向的手机具有 90 或 270 度的设备旋转,具体取决于旋转方向。

相机传感器图像缓冲区必须旋转与设备旋转相同的角度(除了传感器方向的角度),以便相机预览显示为直立。

方向计算

相机预览的正确方向需要考虑传感器方向和设备旋转。

传感器图像缓冲区的整体旋转可以通过以下公式计算

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

其中 sign 对于前置摄像头为 1,对于后置摄像头为 -1

对于前置摄像头,图像缓冲区逆时针旋转(从传感器的自然方向)。对于后置摄像头,传感器图像缓冲区顺时针旋转。

表达式 deviceOrientationDegrees * sign + 360 将设备旋转从逆时针转换为顺时针,适用于后置摄像头(例如,将逆时针 270 度转换为顺时针 90 度)。模数运算将结果缩放至小于 360 度(例如,将 540 度旋转缩放至 180 度)。

不同的 API 以不同方式报告设备旋转

前置摄像头

Camera preview and sensor both in landscape orientation, sensor
            is right side up.
图 2. 手机旋转 90 度为横向时的相机预览和传感器。

以下是图 2 中相机传感器生成的图像缓冲区

Camera sensor in landscape orientation with image upright.

缓冲区必须逆时针旋转 270 度以调整传感器方向(请参阅上文的相机方向

Camera sensor rotated to portait orientation with image sideways,
            top right.

然后缓冲区再逆时针旋转 90 度以适应设备旋转,从而在图 2 中正确显示相机预览

Camera sensor rotated to landscape orientation with image
            upright.

这是相机向右旋转至横向的情况

Camera preview and sensor both in landscape orientation, but
            sensor is upside down.
图 3. 手机旋转 270 度(或 -90 度)为横向时的相机预览和传感器。

这是图像缓冲区

Camera sensor rotated to landscape orientation with image upside
            down.

缓冲区必须逆时针旋转 270 度以调整传感器方向

Camera sensor rated to portrait orientation with image sideways,
            top left.

然后缓冲区再逆时针旋转 270 度以适应设备旋转

Camera sensor rotated to landscape orientation with image
            upright.

后置摄像头

后置摄像头通常具有 90 度的传感器方向(从设备背面看)。在调整相机预览方向时,传感器图像缓冲区会按传感器旋转量顺时针旋转(而不是像前置摄像头那样逆时针旋转),然后图像缓冲区会按设备旋转量逆时针旋转。

Camera preview and sensor both in landscape orientation, but
            sensor is upside down.
图 4. 带有后置摄像头的手机处于横向(旋转 270 或 -90 度)。

以下是图 4 中相机传感器生成的图像缓冲区

Camera sensor rotated to landscape orientation with image upside
            down.

缓冲区必须顺时针旋转 90 度以调整传感器方向

Camera sensor rated to portrait orientation with image sideways,
            top left.

然后缓冲区逆时针旋转 270 度以适应设备旋转

Camera sensor rotated to landscape orientation with image
            upright.

纵横比

显示屏纵横比会随设备方向改变而改变,也会随可折叠设备的折叠和展开、多窗口环境中窗口的调整大小以及应用在辅助显示器上打开而改变。

相机传感器图像缓冲区必须定向和缩放,以匹配取景器 UI 元素的定向和纵横比,因为 UI 会动态改变方向——无论设备是否改变方向。

在新的外形设备上或在多窗口或多显示器环境中,如果您的应用假定相机预览与设备方向(纵向或横向)相同,则您的预览可能方向不正确,缩放不正确,或两者兼而有之。

Unfolded foldable device with portrait camera preview turned
            sideways.
图 5. 可折叠设备从纵向过渡到横向纵横比,但相机传感器保持纵向。

在图 5 中,应用错误地假定设备逆时针旋转了 90 度;因此,应用以相同的量旋转了预览。

Unfolded foldable device with camera preview upright but squashed
            because of incorrect scaling.
图 6. 可折叠设备从纵向过渡到横向纵横比,但相机传感器保持纵向。

在图 6 中,应用未调整图像缓冲区的纵横比,以使其能够正确缩放以适应相机预览 UI 元素的新尺寸。

固定方向的相机应用通常在可折叠设备和其他大屏幕设备(如笔记本电脑)上遇到问题

Camera preview on laptop is upright but app UI is sideways.
图 7. 笔记本电脑上固定方向的纵向应用。

在图 7 中,相机应用的 UI 侧向显示,因为应用的方向仅限于纵向。取景器图像相对于相机传感器的方向是正确的。

内嵌纵向模式

不支持多窗口模式(resizeableActivity="false")并限制其方向(screenOrientation="portrait"screenOrientation="landscape")的相机应用可以在大屏幕设备上以内嵌纵向模式放置,以正确调整相机预览的方向。

内嵌纵向模式会为仅支持纵向的应用程序以纵向留白(insets),即使显示屏的纵横比是横向。仅支持横向的应用程序会以横向留白,即使显示屏的纵横比是纵向。相机图像会旋转以与应用程序 UI 对齐,裁剪以匹配相机预览的纵横比,然后缩放以填充预览。

当相机图像传感器的纵横比与应用程序主活动的纵横比不匹配时,将触发内嵌纵向模式。

Camera preview and app UI in proper portrait orientation on laptop.
            Wide preview image is scaled and cropped to fit portrait
            orientation.
图 8. 笔记本电脑上处于内嵌纵向模式的固定方向纵向应用。

在图 8 中,仅支持纵向的相机应用已旋转以在笔记本电脑显示屏上直立显示 UI。由于纵向应用和横向显示屏之间的纵横比差异,该应用以信箱模式显示。相机预览图像已旋转以补偿应用的 UI 旋转(由于内嵌纵向模式),并且图像已裁剪和缩放以适应纵向,从而减小了视野。

旋转、裁剪、缩放

在具有横向纵横比的显示屏上,为仅支持纵向的相机应用调用内嵌纵向模式

Camera preview on laptop is upright but app UI is sideways.
图 9. 笔记本电脑上固定方向的纵向应用。

应用程序以纵向留白显示

App rotated to portrait orientation and letterboxed. Image is
            sideways, top to the right.

相机图像旋转 90 度以适应应用程序的重新定向

Sensor image rotated 90 degrees to make it upright.

图像被裁剪以适应相机预览的纵横比,然后缩放以填充预览(视野减小)

Cropped camera image scaled to fill camera preview.

在可折叠设备上,相机传感器的方向可以是纵向,而显示屏的纵横比可以是横向

Camera preview and app UI turned sideways of wide, unfolded display.
图 10. 未折叠设备,带有仅限纵向的相机应用,以及相机传感器和显示屏的不同纵横比。

由于相机预览已旋转以适应传感器方向,图像在取景器中方向正确,但仅限纵向的应用则侧向显示。

内嵌纵向模式只需将应用以纵向留白,即可正确调整应用和相机预览的方向

Letterboxed app in portait orientation with camera preview
            upright on foldable device.

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 代码库中。

您可以不直接使用 Surface,而是使用 CameraViewfinder 小部件来显示 Camera2 的相机馈送。

CameraViewfinder 内部使用 TextureViewSurfaceView 来显示相机馈送,并对其应用所需的变换以正确显示取景器。这包括校正它们的纵横比、比例和旋转。

要从 CameraViewfinder 对象请求 surface,您需要创建一个 ViewfinderSurfaceRequest

此请求包含对 surface 分辨率的要求以及来自 CameraCharacteristics 的相机设备信息。

调用 requestSurfaceAsync() 将请求发送到 surface 提供程序(TextureViewSurfaceView),并获取 SurfaceListenableFuture

调用 markSurfaceSafeToRelease() 会通知 surface 提供程序不再需要 surface,并且可以释放相关资源。

Kotlin

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

Java

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

        Futures.addCallback(surfaceListenableFuture, new FutureCallback<Surface>() {
            @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,请参阅在相机应用中支持可调整大小的 surface

相对旋转

相机传感器的相对旋转是指将相机传感器输出与设备方向对齐所需的旋转量。

相对旋转由 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 之前,多窗口环境中只有最顶层可见的 activity 处于 RESUMED 状态。这让用户感到困惑,因为系统没有指示哪个 activity 已恢复。

Android 10 (API 级别 29) 引入了多重恢复,所有可见的 activity 都处于 RESUMED 状态。可见的 activity 仍然可以进入 PAUSED 状态,例如,如果一个透明 activity 位于该 activity 之上,或者该 activity 不可聚焦,例如在画中画模式下(请参阅画中画支持)。

在 API 级别 29 或更高版本上使用相机、麦克风或任何独占或单例资源的应用程序必须支持多重恢复。例如,如果三个已恢复的 activity 都想使用相机,则只有一个能够访问此独占资源。每个 activity 都必须实现 onDisconnected() 回调,以了解更高优先级的 activity 抢占相机访问权的情况。

有关更多信息,请参阅多重恢复

额外资源