注意:此页面指的是 Camera2 包。除非您的应用需要 Camera2 中特定的低级功能,否则我们建议您使用 CameraX。CameraX 和 Camera2 都支持 Android 5.0(API 级别 21)及更高版本。
在 Android 设备上,摄像头和摄像头预览并不总是具有相同的方向。
无论设备是手机、平板电脑还是电脑,摄像头都固定在设备上的某个位置。当设备方向改变时,摄像头方向也会改变。
因此,摄像头应用通常假定设备方向和摄像头预览的长宽比之间存在固定的关系。当手机处于纵向模式时,摄像头预览假定高度大于宽度。当手机(和摄像头)旋转到横向模式时,摄像头预览预计宽度大于高度。
但是,这些假设受到了新型态设备(例如 折叠式设备)和显示模式(例如 多窗口 和 多显示器)的挑战。折叠式设备会改变显示尺寸和长宽比,而不会改变方向。多窗口模式会将摄像头应用限制在屏幕的一部分,无论设备方向如何都会缩放摄像头预览。多显示器模式允许使用方向可能与主显示器不同的辅助显示器。
摄像头方向
Android 兼容性定义 指定摄像头图像传感器“必须定向,使摄像头的长边与屏幕的长边对齐。也就是说,当设备处于横向模式时,摄像头必须以横向模式捕获图像。这适用于设备的自然方向,也适用于横向为主的设备和纵向为主的设备。”
摄像头与屏幕的排列方式最大限度地增加了摄像头应用中摄像头取景器的显示区域。此外,图像传感器通常以横向长宽比输出其数据,其中 4:3 最常见。
摄像头传感器的自然方向是横向。在图 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 以不同的方式报告设备旋转
Display#getRotation()
提供设备的逆时针旋转(从用户的角度)。此值按原样插入上述公式。OrientationEventListener#onOrientationChanged()
返回设备的顺时针旋转(从用户的角度)。取反值用于上述公式。
前置摄像头
以下是图 2 中摄像头传感器生成的图像缓冲区
缓冲区必须逆时针旋转 270 度以调整传感器方向(参见上面的 摄像头方向)
然后,缓冲区逆时针再旋转 90 度以考虑设备旋转,从而在图 2 中获得摄像头预览的正确方向
以下是摄像头向右旋转到横向模式的情况
以下是图像缓冲区
缓冲区必须逆时针旋转 270 度以调整传感器方向
然后,缓冲区逆时针再旋转 270 度以考虑设备旋转
后置摄像头
后置摄像头通常具有 90 度的传感器方向(从设备背面查看)。定向摄像头预览时,传感器图像缓冲区会顺时针旋转传感器旋转量(而不是像前置摄像头那样逆时针旋转),然后图像缓冲区会逆时针旋转设备旋转量。
以下是图 4 中摄像头传感器生成的图像缓冲区
缓冲区必须顺时针旋转 90 度以调整传感器方向
然后,缓冲区逆时针旋转 270 度以考虑设备旋转
长宽比
当设备方向改变时,显示器长宽比也会改变,当折叠式设备折叠和展开时,当在多窗口环境中调整窗口大小时,以及当应用在辅助显示器上打开时,显示器长宽比也会改变。
摄像头传感器图像缓冲区必须定向并缩放以匹配取景器 UI 元素的方向和长宽比,因为 UI 会动态更改方向——无论设备是否更改方向。
在新形态设备或多窗口或多显示器环境中,如果您的应用假定摄像头预览与设备具有相同的方向(纵向或横向),则预览的方向可能不正确,缩放可能不正确,或者两者都不正确。
在图 5 中,应用程序错误地认为设备逆时针旋转了 90 度;因此,应用程序将预览旋转了相同的量。
在图 6 中,应用程序没有调整图像缓冲区的长宽比,以使其能够正确缩放以适应摄像头预览 UI 元素的新尺寸。
固定方向的摄像头应用通常在折叠式设备和其他大屏幕设备(如笔记本电脑)上会出现问题
在图 7 中,摄像头应用的 UI 是侧向的,因为应用的方向仅限于纵向。取景器图像相对于摄像头传感器方向正确。
嵌入式纵向模式
不支持多窗口模式的摄像头应用(resizeableActivity="false"
)并限制其方向(screenOrientation="portrait"
或 screenOrientation="landscape"
)可以放置在大屏幕设备上的嵌入式纵向模式中,以正确定向摄像头预览。
嵌入式纵向模式即使显示器长宽比为横向,也会在纵向模式下放置(嵌入)仅纵向应用。即使显示器长宽比为纵向,仅横向应用也会在横向模式下放置。摄像头图像会旋转以与应用 UI 对齐,裁剪以匹配摄像头预览的长宽比,然后缩放以填充预览。
当摄像头图像传感器长宽比与应用程序主活动的长宽比不匹配时,将触发嵌入式纵向模式。
在图 8 中,仅纵向摄像头应用已旋转以在笔记本电脑显示屏上直立显示 UI。由于纵向应用和横向显示器之间的长宽比差异,该应用已添加了黑边。摄像头预览图像已旋转以补偿应用的 UI 旋转(由于嵌入式纵向模式),并且图像已裁剪并缩放以适应纵向方向,从而减少了视野。
旋转、裁剪、缩放
对于长宽比为横向的显示器上的仅纵向摄像头应用,将调用嵌入式纵向模式
该应用在纵向模式下添加了黑边
摄像头图像旋转 90 度以调整应用的重新定向
图像被裁剪到摄像头预览的长宽比,然后缩放以填充预览(视野减小)
在可折叠设备上,相机传感器的方向可以是纵向的,而显示屏的纵横比可以是横向的。
由于相机预览会旋转以调整传感器方向,因此图像在取景器中方向正确,但仅纵向模式的应用程序是侧向的。
嵌入式纵向模式只需要在纵向方向上为应用程序添加黑边,即可正确地调整应用程序和相机预览的方向。
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
类似于预览用例,CameraViewfinder库提供了一组工具来简化相机预览的创建。它不依赖于 CameraX Core,因此您可以将其无缝集成到现有的 Camera2 代码库中。
您可以使用CameraViewfinder
小部件来显示 Camera2 的相机画面,而无需直接使用Surface
。
CameraViewfinder
在内部使用TextureView
或SurfaceView
来显示相机画面,并在其上应用所需的变换以正确显示取景器。这包括校正其纵横比、比例和旋转。
要从CameraViewfinder
对象请求surface,您需要创建一个ViewfinderSurfaceRequest
。
此请求包含来自CameraCharacteristics
的surface分辨率和相机设备信息的要求。
调用requestSurfaceAsync()
会将请求发送到 surface 提供程序(它是TextureView
或SurfaceView
),并获取Surface
的ListenableFuture
。
调用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
的纵横比匹配,您可以在组件的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
根据传感器方向旋转传感器图像缓冲区,但不处理设备旋转或预览缩放。
缩放和旋转可以编码在Matrix变换中。要了解如何正确缩放和旋转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 之前,只有多窗口环境中位于顶层的可见活动处于RESUMED
状态。这对于用户来说令人困惑,因为系统没有提供任何指示哪个活动已恢复。
Android 10(API 级别 29)引入了多重恢复,其中所有可见活动都处于RESUMED
状态。如果例如,透明活动位于活动之上,或者活动不可聚焦(例如,在画中画模式下,请参阅画中画支持),则可见活动仍然可以进入PAUSED
状态。
在 API 级别 29 或更高版本上使用相机、麦克风或任何独占或单例资源的应用程序必须支持多重恢复。例如,如果三个恢复的活动想要使用相机,则只有一个活动能够访问此独占资源。每个活动都必须实现onDisconnected()
回调,以便了解更高优先级的活动抢占对相机的访问。
有关更多信息,请参阅多重恢复。
其他资源
- 有关 Camera2 示例,请参阅 GitHub 上的Camera2Basic 应用程序。
- 要了解 CameraX 预览用例,请参阅 CameraX 实现预览。
- 有关 CameraX 相机预览示例实现,请参阅 GitHub 上的CameraXBasic代码库。
- 有关 ChromeOS 上的相机预览信息,请参阅相机方向。
- 有关为可折叠设备开发的信息,请参阅了解可折叠设备。