相机预览

注意:此页面指的是 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;
}

窗口指标

不应使用屏幕尺寸来确定相机取景器的尺寸;相机应用程序可能在屏幕的一部分中运行,无论是移动设备上的多窗口模式还是 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()回调,以便了解更高优先级的活动抢占对相机的访问。

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

其他资源