相机预览

注意:本页介绍的是 Camera2 软件包。除非您的应用需要 Camera2 中的特定低级功能,否则我们建议您使用 CameraX。CameraX 和 Camera2 都支持 Android 5.0(API 级别 21)及更高版本。

Android 上的相机和相机预览的方向不一定始终相同 设备。

相机位于设备上的固定位置,而无论设备是否 是手机、平板电脑或计算机。当设备屏幕方向改变时, 改变摄像头方向。

因此,相机应用通常会假定 设备屏幕方向和相机预览的宽高比。当 手机纵向显示,则假定相机预览画面较高 大于宽度。当手机(和相机)旋转为横向时, 相机预览画面的宽度预计会大于高度。

但是,这些假设受到了可折叠设备等新外形规格设备的挑战。 设备和显示模式 例如 多窗口模式多显示屏。 可折叠设备会不改变显示大小和宽高比 屏幕方向。多窗口模式会将相机应用限制在 调整相机预览,而不考虑设备的屏幕方向。 多显示屏模式支持使用辅助显示屏,但辅助显示屏可能 与主显示屏的方向相同。

摄像头方向

通过 Android 兼容性定义 规定摄像头图像传感器“必须朝向正确方向,以便长 摄像头的尺寸与屏幕的长度方向一致也就是说, 设备处于横屏模式时,摄像头必须以 横向模式。无论设备的自然 屏幕方向;也就是说,它适用于以横屏为主的设备 纵向主要设备。”

摄像头到屏幕的排列可将摄像头的显示区域最大化 取景器。此外,图像传感器通常会以 横向宽高比,4:3 最常见。

手机和相机传感器均处于竖屏模式。
图 1. 手机和相机传感器的典型关系 屏幕方向。

相机传感器的自然屏幕方向为横向。在图 1 中,传感器 前置摄像头的位置(摄像头指向的方向与 会相对于手机旋转 270 度,以便符合 Android 兼容性定义。

为了向应用提供传感器旋转信息, camera2 API 包含一个 SENSOR_ORIENTATION 常量。对于大多数手机和平板电脑,设备会报告传感器方向 前置摄像头的视角为 270 度, 使用设备背面) 传感器与设备的长边对齐。笔记本电脑摄像头通常会报告 传感器方向为 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 创建相机预览的基础知识,请参阅 实现预览

有关完整的示例实现,请参阅 CameraXBasic 代码库。

相机取景器

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

您不必使用 Surface 您可以直接使用 CameraViewfinder 微件,用于显示 Camera2 的相机画面。

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 并调用以下代码来检查设备旋转角度: Display#getRotation()onDisplayChanged() 回调。

专属资源

在 Android 10 之前,只有多窗口模式下最顶层的可见 activity 处于 RESUMED 状态。这会让用户感到困惑 系统不会提供关于哪个 activity 已恢复的指示。

Android 10(API 级别 29)引入了多项恢复功能,其中所有可见 activity 处于 RESUMED 状态。可见 activity 仍可进入 PAUSED 状态,例如,透明 activity 位于 activity 之上 该 activity 不可聚焦,例如在画中画模式下(请参阅 画中画支持)。

使用摄像头、麦克风或任何专有或专有技术的应用 API 级别 29 或更高级别上的单例资源必须支持多项恢复。对于 例如,如果三个已恢复的 Activity 想要使用相机,则只有一个 Activity 能够 访问此专属资源。每个 activity 都必须实现 onDisconnected() 回调,以知晓更高的优先级对相机的抢先访问 活动。

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

其他资源