相机预览

注意:本页介绍的是 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 度后的图片缓冲区。

宽高比

显示宽高比不仅会在设备屏幕方向发生变化时发生变化,还会在可折叠设备折叠和展开时、在多窗口模式环境中调整窗口大小时以及在副屏幕上打开应用时发生变化。

摄像头传感器图像缓冲区必须经过定向和缩放,以匹配取景器界面元素的屏幕方向和宽高比,因为界面会动态更改屏幕方向,无论设备是否更改屏幕方向。

在新的设备规格或多窗口模式/多屏幕环境中,如果您的应用假定相机预览与设备具有相同的屏幕方向(纵向或横向),则预览的方向和/或缩放比例可能会不正确。

展开的可折叠设备,其纵向相机预览画面侧向显示。
图 16. 可折叠设备从纵向宽高比转换为横向宽高比,但摄像头传感器仍保持纵向。

在图 16 中,应用错误地假设设备逆时针旋转了 90 度,因此将预览旋转了相同的角度。

展开的可折叠设备,其相机预览画面竖直,但由于缩放不正确而遭到挤压。
图 17. 可折叠设备从纵向宽高比转换为横向宽高比,相机预览缩放不当。

在图 17 中,应用未调整图片缓冲区的宽高比,使其无法正确缩放以适应相机预览界面元素的新尺寸。

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

笔记本电脑上的相机预览画面是竖直的,但应用界面是横向的。
图 18. 笔记本电脑上采用固定纵向屏幕方向的应用。

在图 18 中,相机应用的界面是侧向的,因为该应用的方向仅限于纵向。取景器图像相对于摄像头传感器正确定向。

竖屏插页式广告模式

不支持多窗口模式 (resizeableActivity="false") 且限制其屏幕方向(screenOrientation="portrait" 或 screenOrientation="landscape")的相机应用可以在大屏设备上置于边衬区人像模式,以正确调整相机预览的方向。

即使显示屏宽高比为横向,边衬区纵向模式也会以纵向显示仅限纵向模式的应用。 仅限横向的应用在横屏状态下会进入信箱模式,即使显示宽高比为纵向也是如此。相机图像会旋转以与应用界面对齐,经过剪裁以匹配相机预览的宽高比,然后缩放以填充预览。

当摄像头图像传感器的宽高比与应用的主要 activity 的宽高比不匹配时,系统会触发内嵌纵向模式。

笔记本电脑上以正确的纵向模式显示的相机预览和应用界面。
            宽预览图片会经过缩放和剪裁,以适应纵向屏幕。
图 19. 笔记本电脑上处于边衬区纵向模式的固定屏幕方向纵向应用。

在图 19 中,仅支持竖屏模式的相机应用已旋转,以便在笔记本电脑显示屏上竖直显示界面。由于纵向应用与横向显示屏之间的宽高比存在差异,应用会显示黑边。相机预览图片已旋转,以补偿应用的界面旋转(由于插边纵向模式),并且图片已剪裁和缩放,以适应纵向屏幕方向,从而缩小了视野范围。

旋转、剪裁、缩放

在宽高比为横向的显示屏上,针对仅限纵向模式的相机应用调用了内嵌人像模式:

笔记本电脑上的相机预览画面是竖直的,但应用界面是横向的。
图 20. 笔记本电脑上固定纵向屏幕方向的应用。

应用在纵向屏幕方向下进入黑边模式:

应用旋转为纵向模式并进入信箱模式。图片是横向的,顶部朝右。
图 21. 在传感器图像旋转之前,仅限纵向的应用在横向显示屏上旋转并进入信箱模式。

相机图像旋转 90 度,以适应应用的方向调整:

传感器图像旋转 90 度,使其直立。
图 22. 相机图片旋转 90 度,以便与重新调整方向的应用界面保持一致。

图像会被剪裁为相机预览的宽高比,然后缩放到填满预览(视野范围会缩小):

经过剪裁的相机图像会缩放到填满相机预览画面。
图 23. 相机图像经过剪裁和缩放,以填充纵向相机预览。

在可折叠设备上,摄像头传感器的方向可以是纵向,而显示屏的宽高比可以是横向:

相机预览和应用界面在展开后的宽屏上横向显示。
图 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

与 Preview 用例类似,CameraViewfinder 库提供了一组工具,可简化相机预览的创建。它不依赖于 CameraX Core,因此您可以将其无缝集成到现有的 Camera2 代码库中。

您可以直接使用 CameraViewfinder widget 来显示 Camera2 的摄像头画面,而不是直接使用 Surface。

CameraViewfinder 在内部使用 TextureView 或 SurfaceView 来显示摄像头画面,并对它们应用所需的转换,以正确显示取景器。这包括校正其宽高比、缩放比例和旋转角度。

如需从 CameraViewfinder 对象请求 Surface,您需要创建 ViewfinderSurfaceRequest。

此请求包含 CameraCharacteristics 提供的 Surface 分辨率和相机设备信息要求。

调用 requestSurfaceAsync() 会将请求发送到 Surface 提供程序(TextureView 或 SurfaceView),并获取 Surface 的 ListenableFuture。

调用 markSurfaceSafeToRelease() 会通知 Surface 提供程序,告知其不需要 Surface,可以释放相关资源。

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 会根据传感器方向旋转传感器图像缓冲区,但不会处理设备旋转或预览缩放。

缩放和旋转可以在 Matrix 转换中进行编码。如需了解如何正确缩放和旋转 TextureView,请参阅在相机应用中支持可调整大小的 Surface。

相对旋转

摄像头传感器的相对旋转角度是指使摄像头传感器输出与设备屏幕方向保持一致所需的旋转角度。

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 之前,多窗口模式环境中只有最顶层的可见 activity 处于 RESUMED 状态。这会让用户感到困惑,因为系统没有提供任何指示来表明哪个 activity 已恢复。

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

在 API 级别 29 或更高级别上使用摄像头、麦克风或任何专属或单例资源的应用必须支持多项恢复模式。例如,如果三个已恢复的 activity 想要使用相机,则只有一个能够访问此独占资源。每个 activity 都必须实现 onDisconnected() 回调,以便随时了解优先级较高的 activity 对摄像头的抢占式访问。

如需了解详情,请参阅多项恢复。

其他资源