CameraX 用例旋转

本主题演示如何在应用中设置 CameraX 用例,以获取包含正确旋转信息的图像,无论是来自 ImageAnalysis 还是 ImageCapture 用例。因此,

  • ImageAnalysis 用例的 Analyzer 应接收具有正确旋转的帧。
  • ImageCapture 用例应拍摄具有正确旋转的照片。

术语

本主题使用以下术语,因此理解每个术语的含义至关重要:

显示方向
这指的是设备的哪一侧朝上,可以是四个值之一:纵向、横向、反向纵向或反向横向。
显示旋转
这是由 Display.getRotation() 返回的值,表示设备从其自然方向逆时针旋转的度数。
目标旋转
这表示设备顺时针旋转以达到其自然方向的度数。

如何确定目标旋转

以下示例展示了如何根据设备的自然方向确定其目标旋转。

示例 1:纵向自然方向

设备示例:Pixel 3 XL

自然方向 = 纵向
当前方向 = 纵向

显示旋转 = 0
目标旋转 = 0

自然方向 = 纵向
当前方向 = 横向

显示旋转 = 90
目标旋转 = 90

示例 2:横向自然方向

设备示例:Pixel C

自然方向 = 横向
当前方向 = 横向

显示旋转 = 0
目标旋转 = 0

自然方向 = 横向
当前方向 = 纵向

显示旋转 = 270
目标旋转 = 270

图像旋转

哪端朝上?传感器方向在 Android 中定义为一个常量值,表示设备处于自然位置时,传感器从设备顶部旋转的度数(0、90、180、270)。对于图表中的所有情况,图像旋转描述了数据应如何顺时针旋转才能显示为正立。

以下示例展示了根据相机传感器方向,图像旋转应如何。它们还假设目标旋转设置为显示旋转。

示例 1:传感器旋转 90 度

设备示例:Pixel 3 XL

显示旋转 = 0
显示方向 = 纵向
图像旋转 = 90

显示旋转 = 90
显示方向 = 横向
图像旋转 = 0

示例 2:传感器旋转 270 度

设备示例:Nexus 5X

显示旋转 = 0
显示方向 = 纵向
图像旋转 = 270

显示旋转 = 90
显示方向 = 横向
图像旋转 = 180

示例 3:传感器旋转 0 度

设备示例:Pixel C(平板电脑)

显示旋转 = 0
显示方向 = 横向
图像旋转 = 0

显示旋转 = 270
显示方向 = 纵向
图像旋转 = 90

计算图像的旋转

ImageAnalysis

ImageAnalysisAnalyzer 从相机接收以 ImageProxy 形式的图像。每张图像都包含旋转信息,可通过

val rotation = imageProxy.imageInfo.rotationDegrees

此值表示图像需要顺时针旋转的度数,以匹配 ImageAnalysis 的目标旋转。在 Android 应用中,ImageAnalysis 的目标旋转通常与屏幕方向匹配。

ImageCapture

回调附加到 ImageCapture 实例,以在捕获结果准备就绪时发出信号。结果可以是捕获的图像或错误。

拍照时,提供的回调可以是以下类型之一:

  • OnImageCapturedCallback: 接收以 ImageProxy 形式的内存访问图像。
  • OnImageSavedCallback: 当捕获的图像成功存储在 ImageCapture.OutputFileOptions 指定的位置时调用。该选项可以指定一个 File、一个 OutputStream,或者 MediaStore 中的位置。

捕获图像的旋转,无论其格式为何(ImageProxyFileOutputStreamMediaStore 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

验证图像的旋转

ImageAnalysisImageCapture 用例在成功捕获请求后从相机接收 ImageProxy。一个 ImageProxy 封装了图像及其信息,包括其旋转。此旋转信息表示图像必须旋转的度数,以匹配用例的目标旋转。

An image's rotation verification flow

ImageCapture/ImageAnalysis 目标旋转指南

由于许多设备默认情况下不会旋转到反向纵向或反向横向,因此某些 Android 应用不支持这些方向。应用是否支持它会改变用例目标旋转的更新方式。

下面是两个表格,定义了如何使 用例的目标旋转与显示旋转保持同步。第一个表格展示了在支持所有四个方向的情况下如何操作;第二个表格仅处理设备默认旋转到的方向。

要选择您的应用中遵循的指南:

  1. 验证您的应用相机 Activity 是锁定方向、解锁方向,还是覆盖了方向配置更改。

  2. 决定您的应用相机 Activity 应该处理所有四种设备方向(纵向、反向纵向、横向和反向横向),还是只处理其运行设备默认支持的方向。

支持所有四种方向

此表格列出了设备不旋转到反向纵向时应遵循的某些指南。同样适用于不旋转到反向横向的设备。

场景 指南 单窗口模式 多窗口分屏模式
解锁方向 每次 Activity 创建时设置用例,例如在 ActivityonCreate() 回调中。
使用 OrientationEventListeneronOrientationChanged()。在回调中,更新用例的目标旋转。这处理了系统不重新创建 Activity 即使在方向更改后(例如设备旋转 180 度时)的情况。 也处理显示处于反向纵向且设备默认不旋转到反向纵向的情况。 也处理 Activity 在设备旋转(例如 90 度)时未重新创建的情况。这发生在小型设备上,当应用占用屏幕一半时,以及在大型设备上,当应用占用屏幕三分之二时。
可选:在 AndroidManifest 文件中将 ActivityscreenOrientation 属性设置为 fullSensor 这允许 UI 在设备处于反向纵向时保持正立,并允许系统在设备旋转 90 度时重新创建 Activity 对默认不旋转到反向纵向的设备没有影响。在显示处于反向纵向时不支持多窗口模式。
锁定方向 仅在 Activity 首次创建时设置用例,例如在 ActivityonCreate() 回调中。
使用 OrientationEventListeneronOrientationChanged()。在回调中,更新除预览之外的用例的目标旋转。 也处理 Activity 在设备旋转(例如 90 度)时未重新创建的情况。这发生在小型设备上,当应用占用屏幕一半时,以及在大型设备上,当应用占用屏幕三分之二时。
方向配置更改已覆盖 仅在 Activity 首次创建时设置用例,例如在 ActivityonCreate() 回调中。
使用 OrientationEventListeneronOrientationChanged()。在回调中,更新用例的目标旋转。 也处理 Activity 在设备旋转(例如 90 度)时未重新创建的情况。这发生在小型设备上,当应用占用屏幕一半时,以及在大型设备上,当应用占用屏幕三分之二时。
可选:在 AndroidManifest 文件中将 Activity 的 screenOrientation 属性设置为 fullSensor。 允许 UI 在设备处于反向纵向时保持正立。 对默认不旋转到反向纵向的设备没有影响。在显示处于反向纵向时不支持多窗口模式。

仅支持设备支持的方向

仅支持设备默认支持的方向(可能包括也可能不包括反向纵向/反向横向)。

场景 指南 多窗口分屏模式
解锁方向 每次 Activity 创建时设置用例,例如在 ActivityonCreate() 回调中。
使用 DisplayListeneronDisplayChanged()。在回调中,更新用例的目标旋转,例如设备旋转 180 度时。 也处理 Activity 在设备旋转(例如 90 度)时未重新创建的情况。这发生在小型设备上,当应用占用屏幕一半时,以及在大型设备上,当应用占用屏幕三分之二时。
锁定方向 仅在 Activity 首次创建时设置用例,例如在 ActivityonCreate() 回调中。
使用 OrientationEventListeneronOrientationChanged()。在回调中,更新用例的目标旋转。 也处理 Activity 在设备旋转(例如 90 度)时未重新创建的情况。这发生在小型设备上,当应用占用屏幕一半时,以及在大型设备上,当应用占用屏幕三分之二时。
方向配置更改已覆盖 仅在 Activity 首次创建时设置用例,例如在 ActivityonCreate() 回调中。
使用 DisplayListeneronDisplayChanged()。在回调中,更新用例的目标旋转,例如设备旋转 180 度时。 也处理 Activity 在设备旋转(例如 90 度)时未重新创建的情况。这发生在小型设备上,当应用占用屏幕一半时,以及在大型设备上,当应用占用屏幕三分之二时。

解锁方向

Activity 的显示方向(例如纵向或横向)与设备的物理方向匹配时,它具有解锁方向,但反向纵向/横向除外,某些设备默认不支持。要强制设备旋转到所有四个方向,请在 AndroidManifest 文件中将 ActivityscreenOrientation 属性设置为 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)
    }
}