CameraX 用例旋转

本主题介绍了如何在您的应用中设置 CameraX 用例才能使 ImageAnalysisImageCapture 用例中的图片具有正确的旋转信息。其目的是:

  • 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 实例附加有一个回调,用于在拍摄结果就绪时发出信号。拍摄结果要么是拍摄的图片,要么是错误。

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

  • OnImageCapturedCallback:以 ImageProxy 的形式接收具有内存访问权限的图片。
  • OnImageSavedCallback:在拍摄的图片已成功存储到 ImageCapture.OutputFileOptions 指定的位置后调用。通过这些选项可以指定 FileOutputStream 位置或 MediaStore 中的位置。

无论拍摄的图片是什么格式(ImageProxyFileOutputStreamMediaStore Uri),图片的旋转角度都表示需要将拍摄的图片顺时针旋转多少度才能与 ImageCapture 的目标旋转角度保持一致。而在 Android 应用中,该目标旋转角度通常与屏幕的方向一致。

您可以通过以下其中一种方式检索所拍图片的旋转信息:

ImageProxy

val rotation = imageProxy.imageInfo.rotationDegrees

File

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 用例在拍摄请求成功后从相机接收 ImageProxyImageProxy 中封装了图片及其相关信息,包括其旋转信息。这些旋转信息表示必须将图片旋转多少度才能与相应用例的目标旋转角度保持一致。

图片的旋转信息验证流程

ImageCapture/ImageAnalysis 目标旋转角度准则

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

以下两个表定义了如何保持用例的目标旋转角度与屏幕旋转角度同步。第一个表展示如何在支持全部四个屏幕方向时保持同步;第二个表仅处理设备可默认旋转到的屏幕方向。

为了选择要在您的应用中遵循的准则,请执行以下操作:

  1. 验证应用的相机 Activity 是已锁定屏幕方向、未锁定屏幕方向,还是会覆盖屏幕方向配置更改。

  2. 确定应用的相机 Activity 是应该处理全部四个设备屏幕方向(纵向、反向纵向、横向和反向横向),还是只应该处理运行该应用的设备默认支持的屏幕方向。

支持全部四个屏幕方向

下表列出了设备不会旋转到反向纵向的情况下应遵循的一些准则。同样的准则也适用于不会旋转到反向横向的设备。

场景 准则 单窗口模式 多窗口分屏模式
屏幕方向未锁定 每次创建 Activity 时设置用例,例如在 ActivityonCreate() 回调中进行设置。
使用 OrientationEventListeneronOrientationChanged()。在回调内,更新用例的目标旋转角度。这可以处理系统即使在屏幕方向发生变化(例如,设备旋转了 180 度)后也不重新创建 Activity 的情况。 还要处理屏幕方向为反向纵向而设备默认不会旋转到反向纵向的情况。 还要处理设备旋转(例如,旋转 90 度)时不重新创建 Activity 的情况。如果应用在小巧的设备上占据一半屏幕或在尺寸较大的设备上占据三分之二屏幕,就会出现这种情况。
可选:在 AndroidManifest 文件中将 ActivityscreenOrientation 属性设置为 fullSensor 这样,当设备处于反向纵向时,界面就可以纵向显示,而每当设备旋转 90 度时,系统也会重新创建 Activity 对于默认不会旋转到反向纵向的设备没有任何影响。当屏幕方向为反向纵向时,不支持多窗口模式。
屏幕方向已锁定 仅在首次创建 Activity 时设置一次用例,例如在 ActivityonCreate() 回调中进行设置。
使用 OrientationEventListeneronOrientationChanged()。在回调内,更新用例的目标旋转角度。 还要处理设备旋转(例如,旋转 90 度)时不重新创建 Activity 的情况。如果应用在小巧的设备上占据一半屏幕或在尺寸较大的设备上占据三分之二屏幕,就会出现这种情况。
覆盖屏幕方向 configChanges 仅在首次创建 Activity 时设置一次用例,例如在 ActivityonCreate() 回调中进行设置。
使用 OrientationEventListeneronOrientationChanged()。在回调内,更新用例的目标旋转角度。 还要处理设备旋转(例如,旋转 90 度)时不重新创建 Activity 的情况。如果应用在小巧的设备上占据一半屏幕或在尺寸较大的设备上占据三分之二屏幕,就会出现这种情况。
可选:在 AndroidManifest 文件中将 Activity 的 screenOrientation 属性设置为 fullSensor。 当设备处于反向纵向时,界面就可以纵向显示。 对于默认不会旋转到反向纵向的设备没有任何影响。当屏幕方向为反向纵向时,不支持多窗口模式。

仅支持设备支持的屏幕方向

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

场景 准则 多窗口分屏模式
屏幕方向未锁定 每次创建 Activity 时设置用例,例如在 ActivityonCreate() 回调中进行设置。
使用 DisplayListeneronDisplayChanged()。在回调内,更新用例的目标旋转角度,例如当设备旋转了 180 度时。 还要处理设备旋转(例如,旋转 90 度)时不重新创建 Activity 的情况。如果应用在小巧的设备上占据一半屏幕或在尺寸较大的设备上占据三分之二屏幕,就会出现这种情况。
屏幕方向已锁定 仅在首次创建 Activity 时设置一次用例,例如在 ActivityonCreate() 回调中进行设置。
使用 OrientationEventListeneronOrientationChanged()。在回调内,更新用例的目标旋转角度。 还要处理设备旋转(例如,旋转 90 度)时不重新创建 Activity 的情况。如果应用在小巧的设备上占据一半屏幕或在尺寸较大的设备上占据三分之二屏幕,就会出现这种情况。
覆盖屏幕方向 configChanges 仅在首次创建 Activity 时设置一次用例,例如在 ActivityonCreate() 回调中进行设置。
使用 DisplayListeneronDisplayChanged()。在回调内,更新用例的目标旋转角度,例如当设备旋转了 180 度时。 还要处理设备旋转(例如,旋转 90 度)时不重新创建 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 会覆盖屏幕方向配置更改时,系统不会在设备的物理屏幕方向发生变化时销毁并重新创建此 Activity。但是,系统会更新界面,使其与设备的物理屏幕方向保持一致。

<!-- 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 == UNKNOWN_ORIENTATION) {
                    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)
    }
}