相机预览

注意:本页介绍的是 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

其中,sign 为前置摄像头的 1,后置摄像头的 -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 度,以考虑设备旋转:

摄像头传感器已旋转至横向且包含图片

宽高比

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

当界面动态更改屏幕方向(无论设备是否更改屏幕方向)时,相机传感器图像缓冲区必须调整方向和缩放比例,以匹配取景器界面元素的方向和宽高比。

在新型设备、多窗口或多显示屏环境中,如果您的设备是 应用假定相机预览的方向与设备相同 (纵向或横向)您的预览方向可能不正确,或为横向 不正确,或者两者兼有。

展开的可折叠设备,开启人像相机预览
            。
图 5. 可折叠设备从纵向宽高比切换为横向宽高比,但摄像头传感器保持纵向方向。

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

展开的可折叠设备,相机预览画面为竖直,但出现挤压
            导致出现这种问题
图 6. 可折叠设备从纵向宽高比切换为横向宽高比,但摄像头传感器保持纵向方向。

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

屏幕方向固定的相机应用在可折叠设备上通常会出现问题, 笔记本电脑等其他大屏设备:

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

在图 7 中,相机应用的界面是横向的,因为该应用的屏幕方向仅限于纵向。取景器图像已正确朝向 相对于相机传感器的位置。

嵌入式纵向模式

不支持多窗口模式的相机应用 (resizeableActivity="false") 并限制它们的屏幕方向 (screenOrientation="portrait" 或 screenOrientation="landscape") 在大屏设备上可以置于边衬区纵向模式,以便正确定位 相机预览

以竖屏方式插入纵向模式信箱模式(边衬区)且仅支持纵向模式的应用 即使显示屏宽高比为横向也是如此。 即使应用仅以横向模式显示,在横屏状态下仍会显示信箱模式 显示宽高比为纵向。相机图片会旋转以与应用界面对齐,剪裁以匹配相机预览的宽高比,然后缩放以填充预览。

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

笔记本电脑上处于正确纵向模式的相机预览和应用界面。
            宽屏预览图片会缩放和剪裁,以适应纵向模式。
图 8. 已开启边衬区人像模式下固定屏幕方向的纵向应用 笔记本电脑

在图 8 中,仅限纵向的相机应用已旋转,以便在笔记本电脑显示屏上竖屏显示界面。由于纵向应用和横向显示屏之间的宽高比不同,应用会进入信箱模式。相机预览图片已旋转,以补偿应用界面的旋转(由于采用内嵌纵向模式),并且图片已剪裁和缩放以适应纵向屏幕方向,从而缩小了视野范围。

旋转、剪裁、缩放

为显示屏上的仅支持人像的相机应用调用边衬区人像模式 横向宽高比的图片:

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

应用在纵向模式下进入信箱模式:

应用旋转为纵向模式并进入信箱模式。图片横向,顶部在右侧。

相机图片会旋转 90 度,以适应 app:

传感器图片已旋转 90 度,以使其竖立。

系统会将图片剪裁为相机预览的宽高比,然后缩放以填充预览(视野范围会缩小):

剪裁后的相机图片已缩放以填满相机预览。

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

在展开的宽屏显示屏上,相机预览和应用界面侧向显示。
图 10. 展开的设备,其中包含仅限纵向的相机应用,以及 摄像头传感器和显示屏宽高比的不同。

由于相机预览会旋转以调整传感器方向,因此图片在取景器中会正确显示,但仅限纵向的应用会横向显示。

插入竖屏模式只需要在纵向模式下让应用进入信箱模式 正确定位应用和相机预览:

在可折叠设备上,竖屏模式下采用信箱模式的应用,相机预览竖屏显示。

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 库提供了一组工具来简化相机预览的创建。它不依赖于 CameraX Core,因此您可以将其无缝集成到 现有的 Camera2 代码库。

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

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

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

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

正在呼叫requestSurfaceAsync() 将请求发送到 Surface 提供程序,该提供程序可以是 TextureView 或 SurfaceView 并获取 ListenableFuture 为 Surface。

正在呼叫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的宽高比,这可以通过缩放内容 位于组件的 SurfaceView 中, onMeasure() 方法:

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

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

相对旋转

相机传感器的相对旋转角度是 使摄像头传感器输出与设备方向对齐。

SurfaceView 和 TextureView 等组件使用相对旋转来确定预览图片的 x 和 y 缩放比例。它还用于指定传感器图像缓冲区的旋转。

通过 CameraCharacteristics 和 Surface 类可用于计算 摄像头传感器的相对旋转:

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() 回调,以知晓更高的优先级对相机的抢先访问 活动。

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

其他资源