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

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

val rotation = imageProxy.imageInfo.rotationDegrees

此值表示图像需要顺时针旋转多少度才能与 ImageAnalysis 的目标旋转匹配。在 Android 应用的上下文中,ImageAnalysis 的目标旋转通常会与屏幕的方向相匹配。

ImageCapture

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

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

  • OnImageCapturedCallbackImageProxy 的形式接收具有内存访问权限的图像。
  • OnImageSavedCallback当捕获的图像已成功存储在 ImageCapture.OutputFileOptions 指定的位置时调用。这些选项可以指定 FileOutputStreamMediaStore 中的位置。

捕获图像的旋转,无论其格式如何(ImageProxyFileOutputStreamMediaStore Uri)都表示捕获的图像需要顺时针旋转多少度才能与 ImageCapture 的目标旋转匹配,同样,在 Android 应用的上下文中,它通常会与屏幕的方向相匹配。

可以通过以下方式之一检索捕获图像的旋转

ImageProxy

val rotation = imageProxy.imageInfo.rotationDegrees

文件

val exif = Exif.createFromFile(file)
val rotation = exif.rotation

输出流

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 用例在成功捕获请求后从相机接收 ImageProxyImageProxy 包装图像及其相关信息,包括其旋转。此旋转信息表示图像必须旋转多少度才能与用例的目标旋转匹配。

An image's rotation verification flow

ImageCapture/ImageAnalysis 目标旋转指南

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

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

要在您的应用中选择遵循哪些指南

  1. 验证您的应用的相机 Activity 是否具有锁定方向、解锁方向,或者是否覆盖方向配置更改。

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

支持所有四种方向

此表提到了在设备未旋转到反向纵向的情况下应遵循的某些指南。同样可以应用于未旋转到反向横向的设备。

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

仅支持设备支持的方向

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

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

解锁方向

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

当显示屏锁定方向时,系统不会在设备旋转时销毁和重新创建 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,此设置只执行一次,即在首次创建 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)
    }
}