CameraX 使用案例旋转

本主题展示了如何在应用程序中设置 CameraX 使用案例,以获取具有正确旋转信息的图像,无论是来自 ImageAnalysis 还是 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

计算图像的旋转

图像分析

图像分析的分析器从相机接收以图像代理形式的图像。每个图像包含旋转信息,可以通过以下方式访问:

val rotation = imageProxy.imageInfo.rotationDegrees

此值表示图像需要顺时针旋转的度数,以匹配图像分析的目标旋转。在 Android 应用程序的上下文中,图像分析的目标旋转通常会匹配屏幕的方向。

图像捕获

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

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

  • **图像捕获回调:**接收以图像代理形式的具有内存访问权限的图像。
  • **图像保存回调:**在捕获的图像已成功存储在图像捕获输出文件选项指定的位置时调用。该选项可以指定一个文件、一个输出流或在媒体存储中的位置。

捕获图像的旋转,无论其格式(图像代理、文件、输出流、媒体存储 URI)如何,都表示捕获图像需要顺时针旋转的旋转度数,以匹配图像捕获的目标旋转,这在 Android 应用程序的上下文中,通常会匹配屏幕的方向。

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

图像代理

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

媒体存储 URI

val inputStream = contentResolver.openInputStream(outputFileResults.savedUri)
val exif = Exif.createFromInputStream(inputStream)
val rotation = exif.rotation

验证图像的旋转

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

An image's rotation verification flow

图像捕获/图像分析目标旋转指南

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

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

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

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

  2. 确定应用程序的相机活动应该处理所有四种设备方向(纵向、反向纵向、横向和反向横向),还是仅应处理其运行的设备默认支持的方向。

支持所有四种方向

此表格提到了某些指南,适用于设备不会旋转到反向纵向的情况。同样可以应用于不会旋转到反向横向的设备。

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

仅支持设备支持的方向

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

场景 指南 多窗口分屏模式
解锁方向 每次活动创建时设置用例,例如在活动的 onCreate() 回调中。
使用显示侦听器的 onDisplayChanged()。在回调中,更新用例的目标旋转,例如当设备旋转 180 度时。 还处理当设备旋转时(例如 90 度)活动未重新创建的情况。当应用程序占用一半屏幕时,这发生在小尺寸设备上,当应用程序占用三分之二屏幕时,这发生在大尺寸设备上。
锁定方向 仅在活动首次创建时设置用例,例如在活动的 onCreate() 回调中。
使用方向事件侦听器的 onOrientationChanged()。在回调中,更新用例的目标旋转。 还处理当设备旋转时(例如 90 度)活动未重新创建的情况。当应用程序占用一半屏幕时,这发生在小尺寸设备上,当应用程序占用三分之二屏幕时,这发生在大尺寸设备上。
覆盖方向配置更改 仅在活动首次创建时设置用例,例如在活动的 onCreate() 回调中。
使用显示侦听器的 onDisplayChanged()。在回调中,更新用例的目标旋转,例如当设备旋转 180 度时。 还处理当设备旋转时(例如 90 度)活动未重新创建的情况。当应用程序占用一半屏幕时,这发生在小尺寸设备上,当应用程序占用三分之二屏幕时,这发生在大尺寸设备上。

解锁方向

当活动的显示方向(例如纵向或横向)与设备的物理方向匹配时,活动具有解锁方向,反向纵向/横向除外,一些设备默认情况下不支持这些方向。要强制设备旋转到所有四种方向,请将活动的 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" />

锁定方向

当显示保持在相同的显示方向(例如纵向或横向)时,无论设备的物理方向如何,显示都具有锁定方向。这可以通过在活动的声明中指定其 screenOrientation 属性来完成,该声明位于 AndroidManifest.xml 文件中。

当显示具有锁定方向时,系统不会在设备旋转时销毁和重新创建活动。

<!-- The Activity keeps a portrait orientation even as the device rotates. -->
<activity
   android:name=".LockedOrientationActivity"
   android:screenOrientation="portrait" />

覆盖方向配置更改

当活动覆盖方向配置更改时,系统不会在设备的物理方向更改时销毁和重新创建活动。但是,系统会更新 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" />

相机用例设置

在上述场景中,可以在活动首次创建时设置相机用例。

对于具有解锁方向的活动,此设置会在每次设备旋转时完成,因为系统会在方向更改时销毁和重新创建活动。这会导致用例每次默认情况下将它们的目标旋转设置为匹配显示的方向。

对于具有锁定方向或覆盖方向配置更改的活动,此设置只完成一次,即活动首次创建时。

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 可以让您在设备方向发生变化时持续更新摄像机用例的目标旋转。

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)
    }
}