相机预览

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

其中,sign1 前置摄像头,-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 度,以根据应用的屏幕方向进行调整:

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

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

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

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

相机预览和应用界面从展开的宽屏显示屏侧面翻转过来。
图 10. 设备处于展开状态时,相机仅支持纵向模式,相机传感器和显示屏宽高比不同。

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

边衬区纵向模式只需在纵向模式下为应用添加信箱模式,即可正确定向应用和相机预览:

采用竖屏模式且进入信箱模式的应用,在可折叠设备上竖直相机预览。

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

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

CameraViewfinder 在内部使用 TextureViewSurfaceView 显示相机画面,并对其应用必要的转换以正确显示取景器。这涉及到更正其宽高比、缩放比例和旋转。

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

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

调用 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 {
        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 surfaceListenableFuture =
                cameraViewfinder.requestSurfaceAsync(viewfinderSurfaceRequest);

        Futures.addCallback(surfaceListenableFuture, new FutureCallback() {
            @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 根据传感器方向旋转传感器图像缓冲区,但不处理设备旋转或预览缩放。

缩放和旋转可以在矩阵转换中编码。如需了解如何正确缩放和旋转 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 位于 activity 之上或该 activity 不可聚焦(例如处于画中画模式),可见的 activity 仍可进入 PAUSED 状态(请参阅画中画支持)。

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

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

其他资源

  • 如需查看 Camera2 示例,请参阅 GitHub 上的 Camera2Basic 应用
  • 如需了解 CameraX 预览用例,请参阅 CameraX 实现预览
  • 如需查看 CameraX 相机预览实现示例,请参阅 GitHub 上的 CameraXBasic 代码库。
  • 如需了解 ChromeOS 上的相机预览,请参阅相机方向
  • 如需了解如何针对可折叠设备进行开发,请参阅了解可折叠设备