注意:本页介绍的是 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 度,以考虑设备旋转:
宽高比
显示宽高比会在设备屏幕方向发生变化时发生变化,但也会在可折叠设备折叠和展开时、在多窗口环境中调整窗口大小时,以及在应用在辅助显示屏上打开时发生变化。
当界面动态更改屏幕方向(无论设备是否更改屏幕方向)时,相机传感器图像缓冲区必须调整方向和缩放比例,以匹配取景器界面元素的方向和宽高比。
在新型设备、多窗口或多显示屏环境中,如果您的设备是
应用假定相机预览的方向与设备相同
(纵向或横向)您的预览方向可能不正确,或为横向
不正确,或者两者兼有。
图 5. 可折叠设备从纵向宽高比切换为横向宽高比,但摄像头传感器保持纵向方向。
在图 5 中,应用错误地假定设备旋转了 90
逆时针角度;因此,应用将预览画面旋转相同的幅度。
图 6. 可折叠设备从纵向宽高比切换为横向宽高比,但摄像头传感器保持纵向方向。
在图 6 中,应用未调整图片缓冲区的宽高比,以便其能够正确缩放以适应相机预览界面元素的新尺寸。
屏幕方向固定的相机应用在可折叠设备上通常会出现问题,
笔记本电脑等其他大屏设备:
图 7. 笔记本电脑上固定屏幕方向的纵向应用。
在图 7 中,相机应用的界面是横向的,因为该应用的屏幕方向仅限于纵向。取景器图像已正确朝向
相对于相机传感器的位置。
嵌入式纵向模式
不支持多窗口模式的相机应用
(resizeableActivity="false")
并限制它们的屏幕方向
(screenOrientation="portrait"
或 screenOrientation="landscape")
在大屏设备上可以置于边衬区纵向模式,以便正确定位
相机预览
以竖屏方式插入纵向模式信箱模式(边衬区)且仅支持纵向模式的应用
即使显示屏宽高比为横向也是如此。
即使应用仅以横向模式显示,在横屏状态下仍会显示信箱模式
显示宽高比为纵向。相机图片会旋转以与应用界面对齐,剪裁以匹配相机预览的宽高比,然后缩放以填充预览。
当相机图像传感器的宽高比与应用的主要 activity 的宽高比不匹配时,系统会触发嵌入式纵向模式。
图 8. 已开启边衬区人像模式下固定屏幕方向的纵向应用
笔记本电脑
在图 8 中,仅限纵向的相机应用已旋转,以便在笔记本电脑显示屏上竖屏显示界面。由于纵向应用和横向显示屏之间的宽高比不同,应用会进入信箱模式。相机预览图片已旋转,以补偿应用界面的旋转(由于采用内嵌纵向模式),并且图片已剪裁和缩放以适应纵向屏幕方向,从而缩小了视野范围。
旋转、剪裁、缩放
为显示屏上的仅支持人像的相机应用调用边衬区人像模式
横向宽高比的图片:
图 9. 笔记本电脑上的固定纵向应用。
应用在纵向模式下进入信箱模式:
相机图片会旋转 90 度,以适应
app:
系统会将图片剪裁为相机预览的宽高比,然后缩放以填充预览(视野范围会缩小):
在可折叠设备上,摄像头传感器的方向可以是纵向
而显示屏的宽高比为横向:
图 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。
注意 :仅限具有相机硬件抽象层的设备
(HAL),它支持
SCALER_ROTATE_AND_CROP API 可以启用边衬区人像模式。
CameraX
注意:Jetpack CameraX 向后兼容 Android 5.0(API 级别 21)。
Jetpack CameraX 库
创建可适应传感器方向和
完成一项简单的任务。
PreviewView 布局元素会创建相机预览,并自动根据传感器方向、设备旋转和缩放进行调整。PreviewView 会保持
将
FILL_CENTER
缩放类型,将图片居中,但可能会根据尺寸对其进行剪裁
(位于 PreviewView 中)。如需将相机图片设置为信箱模式,请将缩放类型设置为 FIT_CENTER。
如需了解使用 PreviewView 创建相机预览的基础知识,请参阅实现预览。
如需查看完整的示例实现,请参阅 GitHub 上的 CameraXBasic 代码库。
相机取景器
注意:CameraViewfinder 库向后兼容 Android 5.0(API 级别 21)。
与预览用例类似,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
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的宽高比,这可以通过缩放内容
位于组件的 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。
注意: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()
回调,以知晓更高的优先级对相机的抢先访问
活动。
注意:设置 resizeableActivity="false" 并不能保证对专属资源的访问权限,因为在辅助显示屏上运行的其他 activity 或应用可能具有更高的优先级。此外,
从 Android 12(API 级别 31)开始,resizeableActivity="false" 不会阻止
activity 进入多窗口模式,而在该模式下,其他 activity 可能会进入多窗口模式
资源的优先级。
如需了解详情,请参阅多恢复。
其他资源
如需查看 Camera2 示例,请参阅 Camera2Basic 应用
。
如需了解 CameraX 预览用例,请参阅 CameraX 实现预览。
有关 CameraX 相机预览示例实现,请参阅
CameraXBasic
代码库。
如需了解 ChromeOS 上的相机预览,请参阅相机方向。
如需了解如何针对可折叠设备进行开发,请参阅了解可折叠设备。