本主题演示如何在应用中设置 CameraX 用例,以获取包含正确旋转信息的图像,无论是来自 ImageAnalysis
还是 ImageCapture
用例。因此,
ImageAnalysis
用例的Analyzer
应接收具有正确旋转的帧。ImageCapture
用例应拍摄具有正确旋转的照片。
术语
本主题使用以下术语,因此理解每个术语的含义至关重要:
- 显示方向
- 这指的是设备的哪一侧朝上,可以是四个值之一:纵向、横向、反向纵向或反向横向。
- 显示旋转
- 这是由
Display.getRotation()
返回的值,表示设备从其自然方向逆时针旋转的度数。 - 目标旋转
- 这表示设备顺时针旋转以达到其自然方向的度数。
如何确定目标旋转
以下示例展示了如何根据设备的自然方向确定其目标旋转。
示例 1:纵向自然方向
设备示例:Pixel 3 XL | |
---|---|
自然方向 = 纵向 显示旋转 = 0 |
![]() |
自然方向 = 纵向 显示旋转 = 90 |
![]() |
示例 2:横向自然方向
设备示例:Pixel C | |
---|---|
自然方向 = 横向 显示旋转 = 0 |
![]() |
自然方向 = 横向 显示旋转 = 270 |
![]() |
图像旋转
哪端朝上?传感器方向在 Android 中定义为一个常量值,表示设备处于自然位置时,传感器从设备顶部旋转的度数(0、90、180、270)。对于图表中的所有情况,图像旋转描述了数据应如何顺时针旋转才能显示为正立。
以下示例展示了根据相机传感器方向,图像旋转应如何。它们还假设目标旋转设置为显示旋转。
示例 1:传感器旋转 90 度
设备示例:Pixel 3 XL | |
---|---|
显示旋转 = 0 |
|
显示旋转 = 90 |
|
示例 2:传感器旋转 270 度
设备示例:Nexus 5X | |
---|---|
显示旋转 = 0 |
|
显示旋转 = 90 |
|
示例 3:传感器旋转 0 度
设备示例:Pixel C(平板电脑) | |
---|---|
显示旋转 = 0 |
|
显示旋转 = 270 |
|
计算图像的旋转
ImageAnalysis
ImageAnalysis
的 Analyzer
从相机接收以 ImageProxy
形式的图像。每张图像都包含旋转信息,可通过
val rotation = imageProxy.imageInfo.rotationDegrees
此值表示图像需要顺时针旋转的度数,以匹配 ImageAnalysis
的目标旋转。在 Android 应用中,ImageAnalysis
的目标旋转通常与屏幕方向匹配。
ImageCapture
回调附加到 ImageCapture
实例,以在捕获结果准备就绪时发出信号。结果可以是捕获的图像或错误。
拍照时,提供的回调可以是以下类型之一:
OnImageCapturedCallback
: 接收以ImageProxy
形式的内存访问图像。OnImageSavedCallback
: 当捕获的图像成功存储在ImageCapture.OutputFileOptions
指定的位置时调用。该选项可以指定一个File
、一个OutputStream
,或者MediaStore
中的位置。
捕获图像的旋转,无论其格式为何(ImageProxy
、File
、OutputStream
、MediaStore Uri
),都表示捕获图像需要顺时针旋转的度数,以匹配 ImageCapture
的目标旋转,这在 Android 应用中通常与屏幕方向匹配。
检索捕获图像的旋转可以通过以下方法之一完成:
ImageProxy
val rotation = imageProxy.imageInfo.rotationDegrees
文件
val exif = Exif.createFromFile(file) val rotation = exif.rotation
OutputStream
val byteArray = outputStream.toByteArray() val exif = Exif.createFromInputStream(ByteArrayInputStream(byteArray)) val rotation = exif.rotation
MediaStore uri
val inputStream = contentResolver.openInputStream(outputFileResults.savedUri) val exif = Exif.createFromInputStream(inputStream) val rotation = exif.rotation
验证图像的旋转
ImageAnalysis
和 ImageCapture
用例在成功捕获请求后从相机接收 ImageProxy
。一个 ImageProxy
封装了图像及其信息,包括其旋转。此旋转信息表示图像必须旋转的度数,以匹配用例的目标旋转。
ImageCapture/ImageAnalysis 目标旋转指南
由于许多设备默认情况下不会旋转到反向纵向或反向横向,因此某些 Android 应用不支持这些方向。应用是否支持它会改变用例目标旋转的更新方式。
下面是两个表格,定义了如何使 用例的目标旋转与显示旋转保持同步。第一个表格展示了在支持所有四个方向的情况下如何操作;第二个表格仅处理设备默认旋转到的方向。
要选择您的应用中遵循的指南:
验证您的应用相机
Activity
是锁定方向、解锁方向,还是覆盖了方向配置更改。决定您的应用相机
Activity
应该处理所有四种设备方向(纵向、反向纵向、横向和反向横向),还是只处理其运行设备默认支持的方向。
支持所有四种方向
此表格列出了设备不旋转到反向纵向时应遵循的某些指南。同样适用于不旋转到反向横向的设备。
场景 | 指南 | 单窗口模式 | 多窗口分屏模式 |
---|---|---|---|
解锁方向 | 每次 Activity 创建时设置用例,例如在 Activity 的 onCreate() 回调中。 |
||
使用 OrientationEventListener 的 onOrientationChanged() 。在回调中,更新用例的目标旋转。这处理了系统不重新创建 Activity 即使在方向更改后(例如设备旋转 180 度时)的情况。 |
也处理显示处于反向纵向且设备默认不旋转到反向纵向的情况。 | 也处理 Activity 在设备旋转(例如 90 度)时未重新创建的情况。这发生在小型设备上,当应用占用屏幕一半时,以及在大型设备上,当应用占用屏幕三分之二时。 |
|
可选:在 AndroidManifest 文件中将 Activity 的 screenOrientation 属性设置为 fullSensor 。 |
这允许 UI 在设备处于反向纵向时保持正立,并允许系统在设备旋转 90 度时重新创建 Activity 。 |
对默认不旋转到反向纵向的设备没有影响。在显示处于反向纵向时不支持多窗口模式。 | |
锁定方向 | 仅在 Activity 首次创建时设置用例,例如在 Activity 的 onCreate() 回调中。 |
||
使用 OrientationEventListener 的 onOrientationChanged() 。在回调中,更新除预览之外的用例的目标旋转。 |
也处理 Activity 在设备旋转(例如 90 度)时未重新创建的情况。这发生在小型设备上,当应用占用屏幕一半时,以及在大型设备上,当应用占用屏幕三分之二时。 |
||
方向配置更改已覆盖 | 仅在 Activity 首次创建时设置用例,例如在 Activity 的 onCreate() 回调中。 |
||
使用 OrientationEventListener 的 onOrientationChanged() 。在回调中,更新用例的目标旋转。 |
也处理 Activity 在设备旋转(例如 90 度)时未重新创建的情况。这发生在小型设备上,当应用占用屏幕一半时,以及在大型设备上,当应用占用屏幕三分之二时。 |
||
可选:在 AndroidManifest 文件中将 Activity 的 screenOrientation 属性设置为 fullSensor。 | 允许 UI 在设备处于反向纵向时保持正立。 | 对默认不旋转到反向纵向的设备没有影响。在显示处于反向纵向时不支持多窗口模式。 |
仅支持设备支持的方向
仅支持设备默认支持的方向(可能包括也可能不包括反向纵向/反向横向)。
场景 | 指南 | 多窗口分屏模式 |
---|---|---|
解锁方向 | 每次 Activity 创建时设置用例,例如在 Activity 的 onCreate() 回调中。 |
|
使用 DisplayListener 的 onDisplayChanged() 。在回调中,更新用例的目标旋转,例如设备旋转 180 度时。 |
也处理 Activity 在设备旋转(例如 90 度)时未重新创建的情况。这发生在小型设备上,当应用占用屏幕一半时,以及在大型设备上,当应用占用屏幕三分之二时。 |
|
锁定方向 | 仅在 Activity 首次创建时设置用例,例如在 Activity 的 onCreate() 回调中。 |
|
使用 OrientationEventListener 的 onOrientationChanged() 。在回调中,更新用例的目标旋转。 |
也处理 Activity 在设备旋转(例如 90 度)时未重新创建的情况。这发生在小型设备上,当应用占用屏幕一半时,以及在大型设备上,当应用占用屏幕三分之二时。 |
|
方向配置更改已覆盖 | 仅在 Activity 首次创建时设置用例,例如在 Activity 的 onCreate() 回调中。 |
|
使用 DisplayListener 的 onDisplayChanged() 。在回调中,更新用例的目标旋转,例如设备旋转 180 度时。 |
也处理 Activity 在设备旋转(例如 90 度)时未重新创建的情况。这发生在小型设备上,当应用占用屏幕一半时,以及在大型设备上,当应用占用屏幕三分之二时。 |
解锁方向
当 Activity
的显示方向(例如纵向或横向)与设备的物理方向匹配时,它具有解锁方向,但反向纵向/横向除外,某些设备默认不支持。要强制设备旋转到所有四个方向,请在 AndroidManifest
文件中将 Activity
的 screenOrientation
属性设置为 fullSensor
。
在多窗口模式下,默认不支持反向纵向/横向的设备不会旋转到反向纵向/横向,即使其 screenOrientation
属性设置为 fullSensor
。
<!-- The Activity has an unlocked orientation, but might not rotate to reverse portrait/landscape in single-window mode if the device doesn't support it by default. --> <activity android:name=".UnlockedOrientationActivity" /> <!-- The Activity has an unlocked orientation, and will rotate to all four orientations in single-window mode. --> <activity android:name=".UnlockedOrientationActivity" android:screenOrientation="fullSensor" />
锁定方向
当显示保持相同的显示方向(例如纵向或横向)时,无论设备的物理方向如何,它都具有锁定方向。这可以通过在 AndroidManifest.xml
文件中 Activity
的声明中指定其 screenOrientation
属性来完成。
当显示具有锁定方向时,系统不会在设备旋转时销毁并重新创建 Activity
。
<!-- The Activity keeps a portrait orientation even as the device rotates. --> <activity android:name=".LockedOrientationActivity" android:screenOrientation="portrait" />
方向配置更改已覆盖
当一个 Activity
覆盖方向配置更改时,系统不会在设备物理方向更改时销毁并重新创建它。但系统会更新 UI 以匹配设备的物理方向。
<!-- The Activity's UI might not rotate in reverse portrait/landscape if the device doesn't support it by default. --> <activity android:name=".OrientationConfigChangesOverriddenActivity" android:configChanges="orientation|screenSize" /> <!-- The Activity's UI will rotate to all 4 orientations in single-window mode. --> <activity android:name=".OrientationConfigChangesOverriddenActivity" android:configChanges="orientation|screenSize" android:screenOrientation="fullSensor" />
相机用例设置
在上述场景中,可以在 Activity
首次创建时设置相机用例。
对于具有解锁方向的 Activity
,每次设备旋转时都会执行此设置,因为系统会在方向更改时销毁并重新创建 Activity
。这导致用例每次默认将其目标旋转设置为匹配显示方向。
对于具有锁定方向或覆盖方向配置更改的 Activity
,此设置在 Activity
首次创建时执行一次。
class CameraActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) val cameraProcessFuture = ProcessCameraProvider.getInstance(this) cameraProcessFuture.addListener(Runnable { val cameraProvider = cameraProcessFuture.get() // By default, the use cases set their target rotation to match the // display’s rotation. val preview = buildPreview() val imageAnalysis = buildImageAnalysis() val imageCapture = buildImageCapture() cameraProvider.bindToLifecycle( this, cameraSelector, preview, imageAnalysis, imageCapture) }, mainExecutor) } }
OrientationEventListener 设置
使用 OrientationEventListener
允许您在设备方向更改时持续更新相机用例的目标旋转。
class CameraActivity : AppCompatActivity() { private val orientationEventListener by lazy { object : OrientationEventListener(this) { override fun onOrientationChanged(orientation: Int) { if (orientation == ORIENTATION_UNKNOWN) { return } val rotation = when (orientation) { in 45 until 135 -> Surface.ROTATION_270 in 135 until 225 -> Surface.ROTATION_180 in 225 until 315 -> Surface.ROTATION_90 else -> Surface.ROTATION_0 } imageAnalysis.targetRotation = rotation imageCapture.targetRotation = rotation } } } override fun onStart() { super.onStart() orientationEventListener.enable() } override fun onStop() { super.onStop() orientationEventListener.disable() } }
DisplayListener 设置
使用 DisplayListener
允许您在某些情况下更新相机用例的目标旋转,例如当系统在设备旋转 180 度后不销毁并重新创建 Activity
时。
class CameraActivity : AppCompatActivity() { private val displayListener = object : DisplayManager.DisplayListener { override fun onDisplayChanged(displayId: Int) { if (rootView.display.displayId == displayId) { val rotation = rootView.display.rotation imageAnalysis.targetRotation = rotation imageCapture.targetRotation = rotation } } override fun onDisplayAdded(displayId: Int) { } override fun onDisplayRemoved(displayId: Int) { } } override fun onStart() { super.onStart() val displayManager = getSystemService(Context.DISPLAY_SERVICE) as DisplayManager displayManager.registerDisplayListener(displayListener, null) } override fun onStop() { super.onStop() val displayManager = getSystemService(Context.DISPLAY_SERVICE) as DisplayManager displayManager.unregisterDisplayListener(displayListener) } }