注意:本页介绍的是 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 度,以考虑设备旋转:

宽高比
显示宽高比会在设备屏幕方向发生变化时发生变化,但也会在可折叠设备折叠和展开时、在多窗口环境中调整窗口大小时,以及在应用在辅助显示屏上打开时发生变化。
当界面动态更改屏幕方向(无论设备是否更改屏幕方向)时,相机传感器图像缓冲区必须调整方向和缩放比例,以匹配取景器界面元素的方向和宽高比。
在新型设备、多窗口或多显示屏环境中,如果您的设备是 应用假定相机预览的方向与设备相同 (纵向或横向)您的预览方向可能不正确,或为横向 不正确,或者两者兼有。

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

在图 6 中,应用未调整图片缓冲区的宽高比,以便其能够正确缩放以适应相机预览界面元素的新尺寸。
屏幕方向固定的相机应用在可折叠设备上通常会出现问题, 笔记本电脑等其他大屏设备:

在图 7 中,相机应用的界面是横向的,因为该应用的屏幕方向仅限于纵向。取景器图像已正确朝向 相对于相机传感器的位置。
嵌入式纵向模式
不支持多窗口模式的相机应用
(resizeableActivity="false"
)
并限制它们的屏幕方向
(screenOrientation="portrait"
或 screenOrientation="landscape"
)
在大屏设备上可以置于边衬区纵向模式,以便正确定位
相机预览
以竖屏方式插入纵向模式信箱模式(边衬区)且仅支持纵向模式的应用 即使显示屏宽高比为横向也是如此。 即使应用仅以横向模式显示,在横屏状态下仍会显示信箱模式 显示宽高比为纵向。相机图片会旋转以与应用界面对齐,剪裁以匹配相机预览的宽高比,然后缩放以填充预览。
当相机图像传感器的宽高比与应用的主要 activity 的宽高比不匹配时,系统会触发嵌入式纵向模式。

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

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

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

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

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

由于相机预览会旋转以调整传感器方向,因此图片在取景器中会正确显示,但仅限纵向的应用会横向显示。
插入竖屏模式只需要在纵向模式下让应用进入信箱模式 正确定位应用和相机预览:

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()
回调,以知晓更高的优先级对相机的抢先访问
活动。
如需了解详情,请参阅多恢复。
其他资源
- 如需查看 Camera2 示例,请参阅 Camera2Basic 应用 。
- 如需了解 CameraX 预览用例,请参阅 CameraX 实现预览。
- 有关 CameraX 相机预览示例实现,请参阅 CameraXBasic 代码库。
- 如需了解 ChromeOS 上的相机预览,请参阅相机方向。
- 如需了解如何针对可折叠设备进行开发,请参阅了解可折叠设备。