注意:本页面引用了 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 以不同方式报告设备旋转
Display#getRotation() 提供设备的逆时针旋转(从用户角度看)。此值直接代入上述公式。
OrientationEventListener#onOrientationChanged() 返回设备的顺时针旋转(从用户角度看)。在使用上述公式时,请将该值取反。
前置摄像头
图 2. 手机旋转 90 度为横向时的相机预览和传感器。
以下是图 2 中相机传感器生成的图像缓冲区
缓冲区必须逆时针旋转 270 度以调整传感器方向(请参阅上文的相机方向)
然后缓冲区再逆时针旋转 90 度以适应设备旋转,从而在图 2 中正确显示相机预览
这是相机向右旋转至横向的情况
图 3. 手机旋转 270 度(或 -90 度)为横向时的相机预览和传感器。
这是图像缓冲区
缓冲区必须逆时针旋转 270 度以调整传感器方向
然后缓冲区再逆时针旋转 270 度以适应设备旋转
后置摄像头
后置摄像头通常具有 90 度的传感器方向(从设备背面看)。在调整相机预览方向时,传感器图像缓冲区会按传感器旋转量顺时针旋转(而不是像前置摄像头那样逆时针旋转),然后图像缓冲区会按设备旋转量逆时针旋转。
图 4. 带有后置摄像头的手机处于横向(旋转 270 或 -90 度)。
以下是图 4 中相机传感器生成的图像缓冲区
缓冲区必须顺时针旋转 90 度以调整传感器方向
然后缓冲区逆时针旋转 270 度以适应设备旋转
纵横比
显示屏纵横比会随设备方向改变而改变,也会随可折叠设备的折叠和展开、多窗口环境中窗口的调整大小以及应用在辅助显示器上打开而改变。
相机传感器图像缓冲区必须定向和缩放,以匹配取景器 UI 元素的定向和纵横比,因为 UI 会动态改变方向——无论设备是否改变方向。
在新的外形设备上或在多窗口或多显示器环境中,如果您的应用假定相机预览与设备方向(纵向或横向)相同,则您的预览可能方向不正确,缩放不正确,或两者兼而有之。
图 5. 可折叠设备从纵向过渡到横向纵横比,但相机传感器保持纵向。
在图 5 中,应用错误地假定设备逆时针旋转了 90 度;因此,应用以相同的量旋转了预览。
图 6. 可折叠设备从纵向过渡到横向纵横比,但相机传感器保持纵向。
在图 6 中,应用未调整图像缓冲区的纵横比,以使其能够正确缩放以适应相机预览 UI 元素的新尺寸。
固定方向的相机应用通常在可折叠设备和其他大屏幕设备(如笔记本电脑)上遇到问题
图 7. 笔记本电脑上固定方向的纵向应用。
在图 7 中,相机应用的 UI 侧向显示,因为应用的方向仅限于纵向。取景器图像相对于相机传感器的方向是正确的。
内嵌纵向模式
不支持多窗口模式(resizeableActivity="false")并限制其方向(screenOrientation="portrait" 或 screenOrientation="landscape")的相机应用可以在大屏幕设备上以内嵌纵向模式放置,以正确调整相机预览的方向。
内嵌纵向模式会为仅支持纵向的应用程序以纵向留白(insets),即使显示屏的纵横比是横向。仅支持横向的应用程序会以横向留白,即使显示屏的纵横比是纵向。相机图像会旋转以与应用程序 UI 对齐,裁剪以匹配相机预览的纵横比,然后缩放以填充预览。
当相机图像传感器的纵横比与应用程序主活动的纵横比不匹配时,将触发内嵌纵向模式。
图 8. 笔记本电脑上处于内嵌纵向模式的固定方向纵向应用。
在图 8 中,仅支持纵向的相机应用已旋转以在笔记本电脑显示屏上直立显示 UI。由于纵向应用和横向显示屏之间的纵横比差异,该应用以信箱模式显示。相机预览图像已旋转以补偿应用的 UI 旋转(由于内嵌纵向模式),并且图像已裁剪和缩放以适应纵向,从而减小了视野。
旋转、裁剪、缩放
在具有横向纵横比的显示屏上,为仅支持纵向的相机应用调用内嵌纵向模式
图 9. 笔记本电脑上固定方向的纵向应用。
应用程序以纵向留白显示
相机图像旋转 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。
注意: 只有支持 SCALER_ROTATE_AND_CROP API 的相机硬件抽象层(HAL)的设备才能启用内嵌纵向模式。
CameraX
注意: Jetpack CameraX 提供对 Android 5.0 (API 级别 21) 的向后兼容性。
Jetpack CameraX 库使得创建适应传感器方向和设备旋转的相机取景器成为一项简单的任务。
PreviewView 布局元素创建一个相机预览,自动调整传感器方向、设备旋转和缩放。PreviewView 通过应用 FILL_CENTER 缩放类型来保持相机图像的纵横比,该类型使图像居中,但可能会裁剪图像以匹配 PreviewView 的尺寸。要将相机图像以信箱模式显示,请将缩放类型设置为 FIT_CENTER。
要了解使用 PreviewView 创建相机预览的基础知识,请参阅实现预览。
有关完整的示例实现,请参阅 GitHub 上的 CameraXBasic 仓库。
CameraViewfinder
注意: CameraViewfinder 库向后兼容 Android 5.0 (API 级别 21)。
与 预览 用例类似,CameraViewfinder 库提供了一组工具来简化相机预览的创建。它不依赖于 CameraX Core,因此您可以将其无缝集成到现有的 Camera2 代码库中。
您可以不直接使用 Surface,而是使用 CameraViewfinder 小部件来显示 Camera2 的相机馈送。
CameraViewfinder 内部使用 TextureView 或 SurfaceView 来显示相机馈送,并对其应用所需的变换以正确显示取景器。这包括校正它们的纵横比、比例和旋转。
要从 CameraViewfinder 对象请求 surface,您需要创建一个 ViewfinderSurfaceRequest。
此请求包含对 surface 分辨率的要求以及来自 CameraCharacteristics 的相机设备信息。
调用 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
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
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 根据传感器方向旋转传感器图像缓冲区,但不会处理设备旋转或预览缩放。
缩放和旋转可以编码在 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。
注意: Display 方法 getRealSize() 和 getRealMetrics() 在 API 级别 31 中已弃用。这些方法不支持多窗口模式或自由模式。要获取显示密度,请使用 Configuration#densityDpi 而不是 getRealMetrics()。
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() 回调,以了解更高优先级的 activity 抢占相机访问权的情况。
注意: 设置 resizeableActivity="false" 并不能保证独占资源的访问权限,因为另一个 activity 或在辅助显示器上运行的应用程序可能具有更高的优先级。此外,自 Android 12 (API 级别 31) 起,resizeableActivity="false" 并不能阻止 activity 进入多窗口模式,在这种模式下,其他 activity 可能会优先获取独占资源。
有关更多信息,请参阅多重恢复。
额外资源
有关 Camera2 示例,请参阅 GitHub 上的 Camera2Basic 应用。
要了解 CameraX 预览用例,请参阅 CameraX 实现预览。
有关 CameraX 相机预览示例实现,请参阅 GitHub 上的 CameraXBasic 仓库。
有关 ChromeOS 上相机预览的信息,请参阅 相机方向。
有关为可折叠设备开发的信息,请参阅了解可折叠设备。