相机预览

注意:此页面指的是 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")可以在大屏幕设备上置于嵌入式纵向模式以正确调整相机预览的方向。

嵌入式纵向模式即使显示器纵横比为横向,也会在纵向方向上为仅纵向应用添加黑边(嵌入)。仅横向应用即使显示器纵横比为纵向,也会在横向方向上添加黑边。相机图像将旋转以与应用 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)开始,应用还可以通过 SCALER_ROTATE_AND_CROP 属性显式控制嵌入式纵向模式CaptureRequest 类。

默认值为 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 在内部使用 TextureViewSurfaceView 来显示摄像头馈送,并在其上应用所需的转换以正确显示取景器。 这包括校正它们的纵横比、比例和旋转。

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

此请求包含来自 CameraCharacteristics 的 Surface 分辨率和摄像头设备信息的要求。

调用 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;
}

窗口指标

不应使用屏幕尺寸来确定摄像头取景器的尺寸;摄像头应用可能在屏幕的一部分中运行,无论是移动设备上的多窗口模式还是 Chrome OS 上的自由窗格模式。

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 存储库。
  • 有关 Chrome OS 上的摄像头预览信息,请参阅 摄像头方向
  • 有关为折叠式设备开发的信息,请参阅 了解折叠式设备