这个系列文章我们来介绍一位海外工程师如何探索 CameraX 音视频相机技术,对于想要开始学习音视频技术的朋友,这些文章是份不错的入门资料,这是第 9 篇:CameraX 用例旋转。
—— 来自公众号关键帧Keyframe的分享
1、如何确定目标旋转
以下示例展示了如何根据设备的自然方向确定目标旋转。
1.1、示例 1:肖像自然方向
| 设备示例:Pixel 3 XL |
|---|
| 自然方向 = 肖像 当前方向 = 肖像 显示旋转 = 0 目标旋转 = 0 |
| 自然方向 = 肖像 当前方向 = 横向 显示旋转 = 90 目标旋转 = 90 |
1.2、示例 2:横向自然方向
| 设备示例:Pixel C |
|---|
| 自然方向 = 横向 当前方向 = 横向 显示旋转 = 0 目标旋转 = 0 |
| 自然方向 = 横向 当前方向 = 肖像 显示旋转 = 270 目标旋转 = 270 |
2、图像旋转
传感器方向在 Android 中定义为一个常数值,表示当设备处于自然位置时,传感器相对于设备顶部顺时针旋转的度数(0、90、180、270)。在下图的所有示例中,图像旋转描述了数据应如何顺时针旋转以保持直立。
以下示例展示了根据相机传感器方向图像旋转应如何变化。它们还假设目标旋转设置为显示旋转。
2.1、示例 1:传感器旋转 90 度
| 设备示例:Pixel 3 XL |
|---|
| 显示旋转 = 0 显示方向 = 肖像 图像旋转 = 90 |
| 显示旋转 = 90 显示方向 = 横向 图像旋转 = 0 |
2.2、示例 2:传感器旋转 270 度
| 设备示例:Nexus 5X |
|---|
| 显示旋转 = 0 显示方向 = 肖像 图像旋转 = 270 |
| 显示旋转 = 90 显示方向 = 横向 图像旋转 = 180 |
2.3、示例 3:传感器旋转 0 度
| 设备示例:Pixel C(平板电脑) |
|---|
| 显示旋转 = 0 显示方向 = 横向 图像旋转 = 0 |
| 显示旋转 = 270 显示方向 = 肖像 图像旋转 = 90 |
3、计算图像旋转
3.1、ImageAnalysis
ImageAnalysis 的 Analyzer 以 ImageProxy 的形式接收来自相机的图像。每张图像都包含可通过以下方式访问的旋转信息:
val rotation = imageProxy.imageInfo.rotationDegrees
此值表示图像需要顺时针旋转的度数,以匹配 ImageAnalysis 的目标旋转。在 Android 应用的上下文中,ImageAnalysis 的目标旋转通常与屏幕方向一致。
3.2、ImageCapture
向 ImageCapture 实例附加一个回调,以指示何时准备好捕获结果。结果可以是捕获的图像或错误。
拍照时,提供的回调可以是以下类型之一:
OnImageCapturedCallback:以ImageProxy的形式接收具有内存访问权限的图像。OnImageSavedCallback:在捕获的图像已成功存储在ImageCapture.OutputFileOptions指定的位置时调用。选项可以指定File、OutputStream或MediaStore中的位置。
无论捕获图像的格式(ImageProxy、File、OutputStream、MediaStore 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
3.3、验证图像旋转
在成功捕获请求后,ImageAnalysis 和 ImageCapture 用例从相机接收 ImageProxy。ImageProxy 包裹图像及其信息,包括其旋转。此旋转信息表示图像需要旋转的度数以匹配用例的目标旋转。
4、ImageCapture / ImageAnalysis 目标旋转指南
由于许多设备默认不旋转到反向肖像或反向横向,一些 Android 应用不支持这些方向。应用是否支持这些方向会改变更新用例目标旋转的方式。
以下是两个表格,定义如何使目标旋转与显示旋转同步。第一个表格展示了如何支持所有四种方向;第二个表格仅处理设备默认旋转的方向。
选择在应用中遵循哪些指南:
- 确认应用的相机
Activity是否有锁定的方向、未锁定的方向,或者是否覆盖方向配置更改。 - 决定应用的相机
Activity是否应处理所有四种设备方向(肖像、反向肖像、横向和反向横向),或者是否仅处理设备默认支持的方向。
4.1、支持所有四种方向
| 场景 | 指南 | 单窗口模式 | 多窗口分屏模式 |
|---|---|---|---|
| 未锁定方向 | 每次创建 Activity 时设置用例,例如在 Activity 的 onCreate() 回调中。 | ||
使用 OrientationEventListener 的 onOrientationChanged()。在回调中,更新用例的目标旋转。这处理了即使在方向更改后系统也不会重新创建 Activity 的情况,例如当设备旋转 180 度时。 | 还处理显示处于反向肖像方向且设备默认不旋转到反向肖像的情况。 | 还处理设备旋转时(例如 90 度)Activity 不会被重新创建的情况。这在小尺寸设备上应用占据屏幕一半,以及在大尺寸设备上占据三分之二屏幕时发生。 | |
可选:在 AndroidManifest 文件中将 Activity 的 screenOrientation 属性设置为 fullSensor 。 | 这允许在设备处于反向肖像方向时 UI 保持直立,并允许系统在设备旋转 90 度时重新创建 Activity 。 | 对默认不旋转到反向肖像的设备无效。多窗口模式不支持在显示处于反向肖像方向时使用。 | |
| 锁定方向 | 仅在 Activity 首次创建时设置用例,例如在 Activity 的 onCreate() 回调中。 | ||
使用 OrientationEventListener 的 onOrientationChanged()。在回调中,更新用例的目标旋转。 | 还处理设备旋转时(例如 90 度)Activity 不会被重新创建的情况。这在小尺寸设备上应用占据屏幕一半,以及在大尺寸设备上占据三分之二屏幕时发生。 | ||
| 覆盖方向配置更改 | 仅在 Activity 首次创建时设置用例,例如在 Activity 的 onCreate() 回调中。 | ||
使用 OrientationEventListener 的 onOrientationChanged()。在回调中,更新用例的目标旋转。 | 还处理设备旋转时(例如 90 度)Activity 不会被重新创建的情况。这在小尺寸设备上应用占据屏幕一半,以及在大尺寸设备上占据三分之二屏幕时发生。 | ||
可选:在 AndroidManifest 文件中将 Activity 的 screenOrientation 属性设置为 fullSensor 。 | 允许在设备处于反向肖像方向时 UI 保持直立。 | 对默认不旋转到反向肖像的设备无效。多窗口模式不支持在显示处于反向肖像方向时使用。 |
4.2、仅支持设备默认方向
仅支持设备默认支持的方向(可能包括或不包括反向肖像 / 反向横向)。
| 场景 | 指南 | 多窗口分屏模式 |
|---|---|---|
| 未锁定方向 | 每次创建 Activity 时设置用例,例如在 Activity 的 onCreate() 回调中。 | |
使用 DisplayListener 的 onDisplayChanged()。在回调中,更新用例的目标旋转,例如当设备旋转 180 度时。 | 还处理设备旋转时(例如 90 度)Activity 不会被重新创建的情况。这在小尺寸设备上应用占据屏幕一半,以及在大尺寸设备上占据三分之二屏幕时发生。 | |
| 锁定方向 | 仅在 Activity 首次创建时设置用例,例如在 Activity 的 onCreate() 回调中。 | |
使用 OrientationEventListener的 onOrientationChanged()。在回调中,更新用例的目标旋转。 | 还处理设备旋转时(例如 90 度)Activity 不会被重新创建的情况。这在小尺寸设备上应用占据屏幕一半,以及在大尺寸设备上占据三分之二屏幕时发生。 | |
| 覆盖方向配置更改 | 仅在 Activity 首次创建时设置用例,例如在 Activity 的 onCreate() 回调中。 | |
使用 DisplayListener 的 onDisplayChanged()。在回调中,更新用例的目标旋转,例如当设备旋转 180 度时。 | 还处理设备旋转时(例如 90 度)Activity 不会被重新创建的情况。这在小尺寸设备上应用占据屏幕一半,以及在大尺寸设备上占据三分之二屏幕时发生。 |
4.3、未锁定方向
当 Activity 的显示方向(例如肖像或横向)与设备的物理方向匹配时,它具有未锁定的方向,反向肖像 / 横向除外,一些设备默认不支持这些方向。要强制设备旋转到所有四种方向,请将 Activity 的 screenOrientation 属性设置为 fullSensor。
在多窗口模式下,即使 screenOrientation 属性设置为 fullSensor,默认不支持反向肖像 / 横向的设备也不会旋转到反向肖像 / 横向。
当 Activity 的显示方向(例如肖像或横向)与设备的物理方向匹配时,它具有未锁定的方向,反向肖像 / 横向除外,一些设备默认不支持这些方向。要强制设备旋转到所有四种方向,请将 Activity 的 screenOrientation 属性设置为 fullSensor。
在多窗口模式下,即使 screenOrientation 属性设置为 fullSensor,默认不支持反向肖像 / 横向的设备也不会旋转到反向肖像 / 横向。
<!-- 该 Activity 具有未锁定的方向,但如果设备默认不支持反向肖像 / 横向,在单窗口模式下可能不会旋转到这些方向。 -->
<activity android:name=".UnlockedOrientationActivity" />
<!-- 该 Activity 具有未锁定的方向,在单窗口模式下将旋转到所有四种方向。 -->
<activity
android:name=".UnlockedOrientationActivity"
android:screenOrientation="fullSensor" />
4.4、锁定方向
当显示保持在同一方向(例如肖像或横向)而不考虑设备的物理方向时,它具有锁定的方向。这可以通过在 AndroidManifest.xml 文件中的 Activity 声明中指定 Activity 的 screenOrientation 属性来实现。
当显示具有锁定的方向时,系统不会在设备旋转时销毁并重新创建 Activity。
<!-- 该 Activity 即使在设备旋转时也保持肖像方向。 -->
<activity
android:name=".LockedOrientationActivity"
android:screenOrientation="portrait" />
4.5、覆盖方向配置更改
当 Activity 覆盖方向配置更改时,系统不会在设备物理方向更改时销毁并重新创建它。不过,系统会更新 UI 以匹配设备的物理方向。
<!-- 如果设备默认不支持反向肖像 / 横向,该 Activity 的 UI 可能不会旋转到这些方向。 -->
<activity
android:name=".OrientationConfigChangesOverriddenActivity"
android:configChanges="orientation|screenSize" />
<!-- 在单窗口模式下,该 Activity 的 UI 将旋转到所有四种方向。 -->
<activity
android:name=".OrientationConfigChangesOverriddenActivity"
android:configChanges="orientation|screenSize"
android:screenOrientation="fullSensor" />
4.6、相机用例设置
在上述场景中,相机用例可以在 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()
// 默认情况下,用例将其目标旋转设置为与显示的方向匹配。
val preview = buildPreview()
val imageAnalysis = buildImageAnalysis()
val imageCapture = buildImageCapture()
cameraProvider.bindToLifecycle(
this, cameraSelector, preview, imageAnalysis, imageCapture)
}, mainExecutor)
}
}
4.7、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()
}
}
4.8、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)
}
}
音视频方向学习、求职,欢迎加入我们的星球
丰富的音视频知识、面试题、技术方案干货分享,还可以进行面试辅导

版权声明:本文内容转自互联网,本文观点仅代表作者本人。本站仅提供信息存储空间服务,所有权归原作者所有。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至1393616908@qq.com 举报,一经查实,本站将立刻删除。