相机预览

注意:本页介绍的是 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 在内部使用 TextureViewSurfaceView 显示相机画面,并对其应用所需的转换 正确显示取景器。 这包括校正其宽高比、缩放比例和旋转角度。

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

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

正在呼叫requestSurfaceAsync() 将请求发送到 Surface 提供程序,该提供程序可以是 TextureViewSurfaceView 并获取 ListenableFutureSurface

正在呼叫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

相对旋转

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

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

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

其他资源