提升相机体验

1. 准备工作

可折叠设备有什么特别之处?

可折叠设备是一项卓越的创新。它们提供独特的体验,并带来独特的机会,通过免触摸使用的桌面界面等差异化功能满足用户的需求。

前提条件

  • 具备开发 Android 应用的基础知识
  • 了解有关 Hilt 依赖项注入框架的基础知识

构建内容

在此 Codelab 中,您将针对可折叠设备构建具有优化布局的相机应用。

运行的应用的屏幕截图

您会从一个基本相机应用入手,该应用不会响应任何设备折叠状态,也没有使用更好的后置摄像头来提高自拍效果。您需要更新源代码,以便在设备展开时将预览移至较小的显示屏,并对设置为桌面模式的手机做出反应。

虽然相机应用是此 API 最便捷的用例,但您在此 Codelab 中学到的这两项功能都可以应用于任何应用。

学习内容

  • 如何使用 Jetpack Window Manager 对折叠状态变化做出反应
  • 如何将应用移至可折叠设备较小的显示屏

所需条件

  • 最新版本的 Android Studio
  • 可折叠设备或可折叠模拟器

2. 进行设置

获取起始代码

  1. 如果您已安装 Git,只需运行以下命令即可。如需检查是否已安装 Git,请在终端或命令行中输入 git --version,并验证其是否正确执行。
git clone https://github.com/android/large-screen-codelabs.git
  1. 可选:如果您未安装 Git,可以点击下方按钮下载此 Codelab 的全部代码:

打开第一个模块

  • 在 Android Studio 中,打开 /step1 下的第一个模块。

Android Studio 的屏幕截图,显示了此 Codelab 的相关代码

如果系统要求您使用最新版 Gradle,请进行更新。

3. 运行并观察

  1. 在模块 step1 上运行代码。

如您所见,这是一个简单的相机应用。您可以在前置摄像头和后置摄像头之间切换,还可以调整宽高比。但是,左侧第一个按钮当前不会执行任何操作,但它将成为后自拍模式的入口点。

应用屏幕截图,其中突出显示了后置自拍模式图标

  1. 现在,尝试将设备置于半开位置,合页并不完全平展或合上,而是形成 90 度角。

如您所见,应用不会响应不同的设备折叠状态,因此布局不会改变,导致合页位于取景器的中间。

4. 了解 Jetpack WindowManager

Jetpack WindowManager 库可帮助应用开发者为可折叠设备打造经过优化的体验。它包含 FoldingFeature 类,该类描述灵活显示屏中的折叠边或两个物理显示面板之间的合页。您可通过其 API 访问与设备相关的重要信息:

FoldingFeature 类包含更多信息(例如 occlusionType()isSeparating()),但此 Codelab 不会深入探讨这些信息。

从 1.1.0-beta01 版本开始,该库使用 WindowAreaController,该 API 使后置显示模式能够将当前窗口移至与后置摄像头对齐的显示屏,非常适合使用后置摄像头自拍,以及许多其他使用场景!

添加依赖项

  • 如需在应用中使用 Jetpack WindowManager,您需要将以下依赖项添加到模块级 build.gradle 文件中:

step1/build.gradle

def work_version = '1.1.0-beta01'
implementation "androidx.window:window:$work_version"
implementation "androidx.window:window-java:$work_version"
implementation "androidx.window:window-core:$work_version"

现在,您可以访问应用中的 FoldingFeatureWindowAreaController 类。您可以使用它们打造卓越的可折叠相机体验!

5. 实现后置自拍模式

先从后置显示屏模式开始。允许此模式的 API 是 WindowAreaControllerJavaAdapter,它需要 Executor 并返回存储当前状态的 WindowAreaSession。在 Activity 被销毁并重新创建后,应保留此 WindowAreaSession,以便您将其存储在 ViewModel 中,从而在配置发生变化时安全地进行存储。

  1. MainActivity 中声明以下变量:

step1/MainActivity.kt

private lateinit var windowAreaController: WindowAreaControllerJavaAdapter
private lateinit var displayExecutor: Executor
  1. 然后,在 onCreate() 方法中初始化这些变量:

step1/MainActivity.kt

windowInfoTracker = WindowInfoTracker.getOrCreate(this)
displayExecutor = ContextCompat.getMainExecutor(this)
windowAreaController = WindowAreaControllerJavaAdapter(WindowAreaController.getOrCreate())

现在,您的 Activity 已准备好将内容移动到较小的显示屏上,但您需要存储会话。

  1. 如需存储会话,请打开 CameraViewModel 并在其中声明此变量:

step1/CameraViewModel.kt

var rearDisplaySession: WindowAreaSession? = null
        private set

您需要将 rearDisplaySession 作为变量,因为它会在您每次创建时发生变化,但您希望确保它不能从外部更新,因为您现在创建了一个在需要时更新它的方法。

  1. 将以下代码粘贴到 CameraViewModel 中:

step1/CameraViewModel.kt

fun updateSession(newSession: WindowAreaSession? = null) {
        rearDisplaySession = newSession
}

每当代码需要更新会话时,系统都会调用此方法,将此方法封装到单个接入点会很有帮助。

Rear Display API 可与监听器方法搭配使用:当您请求将内容移至较小的屏幕时,系统会启动通过监听器的 onSessionStarted() 方法返回的会话。如果您想返回内部(较大)显示屏,需要关闭会话并在 onSessionEnded() 方法中收到确认信息。您会利用这些方法以更新 CameraViewModel 内的 rearDisplaySession。如需创建此类监听器,您需要实现 WindowAreaSessionCallback 接口。

  1. 修改 MainActivity 声明,使其实现 WindowAreaSessionCallback 接口:

step1/MainActivity.kt

class MainActivity : AppCompatActivity(), WindowAreaSessionCallback

现在,在 MainActivity 中实现 onSessionStartedonSessionEnded 方法。对于第一个方法,您需要保存 WindowAreaSession,并在第二个方法中将其重置为 null。这尤为有用,因为您可以使用 WindowAreaSession 来决定是启动会话还是关闭现有会话。

step1/MainActivity.kt

override fun onSessionEnded() {
    viewModel.updateSession(null)
}

override fun onSessionStarted(session: WindowAreaSession) {
    viewModel.updateSession(session)
}
  1. MainActivity.kt 文件中,编写此 API 正常运行所需的最后一段代码:

step1/MainActivity.kt

private fun startRearDisplayMode() {
   if (viewModel.rearDisplaySession != null) {
      viewModel.rearDisplaySession?.close()
   } else {
      windowAreaController.startRearDisplayModeSession(
         this,
         displayExecutor,
         this
      )
   }
}

如前所述,为了解需要执行的操作,您需要检查 CameraViewModel 中是否存在 rearDisplaySession:如果不是 null,则表示会话已经发生,因此会关闭。另一方面,如果它是 null,您可以使用 windowAreaController 启动一个新会话,并传递两次 Activity。第一次被用作 Context,第二次被用作 WindowAreaSessionCallback 监听器。

  1. 现在,构建并运行应用。如果您之后将设备展开并点按后方的显示按钮,系统会提示您如下消息:

启动后显示屏模式时显示的用户提示的屏幕截图。

  1. 点击立即切换屏幕,您会看到内容移到外部显示屏了!

6. 实现桌面模式

现在,您将让应用能够感知折叠:您可以根据折叠的方向将内容移到设备的一侧或设备的合页上。为此,您需要在 FoldingStateActor 内执行操作,以便将您的代码与 Activity 分离,以提高可读性。

此 API 的核心部分包含在 WindowInfoTracker 接口中,该接口是使用需要 Activity 的静态方法创建的:

step1/CameraCodelabDependencies.kt

@Provides
fun provideWindowInfoTracker(activity: Activity) =
        WindowInfoTracker.getOrCreate(activity)

您不需要编写此代码,因为它已经存在,但了解 WindowInfoTracker 的构建方式很有帮助。

  1. 如需监听任何窗口更改,请在 ActivityonResume() 方法中监听以下更改:

step1/MainActivity.kt

lifecycleScope.launch {
    foldingStateActor.checkFoldingState(
         this@MainActivity,
         binding.viewFinder
    )
}
  1. 现在,打开 FoldingStateActor 文件,以填写 checkFoldingState() 方法。

如您所见,它在 ActivityRESUMED 阶段运行,并且利用 WindowInfoTracker 来监听任何布局更改。

step1/FoldingStateActor.kt

windowInfoTracker.windowLayoutInfo(activity)
      .collect { newLayoutInfo ->
         activeWindowLayoutInfo = newLayoutInfo
         updateLayoutByFoldingState(cameraViewfinder)
      }

使用 WindowInfoTracker 接口,您可以调用 windowLayoutInfo() 以收集 WindowLayoutInfoFlow(其中包含 DisplayFeature 中的所有可用信息)。

最后一步是对这些变化做出反应并相应地移动内容。您需要在 updateLayoutByFoldingState() 方法中一步一步地执行此操作。

  1. 确保 activityLayoutInfo 包含一些 DisplayFeature 属性,并且其中至少有一个属性为 FoldingFeature,否则您不需要执行任何操作:

step1/FoldingStateActor.kt

val foldingFeature = activeWindowLayoutInfo?.displayFeatures
            ?.firstOrNull { it is FoldingFeature } as FoldingFeature?
            ?: return
  1. 计算折叠边的位置,以确保设备位置会影响您的布局,并且没有超出层次结构的边界:

step1/FoldingStateActor.kt

val foldPosition = FoldableUtils.getFeaturePositionInViewRect(
            foldingFeature,
            cameraViewfinder.parent as View
        ) ?: return

现在,您确定 FoldingFeature 会影响布局,因此您需要移动内容。

  1. 检查 FoldingFeature 是否为 HALF_OPEN,或者您刚刚恢复内容的位置。如果它为 HALF_OPEN,则您需要执行另一项检查,并根据折叠边的方向执行不同操作:

step1/FoldingStateActor.kt

if (foldingFeature.state == FoldingFeature.State.HALF_OPENED) {
    when (foldingFeature.orientation) {
        FoldingFeature.Orientation.VERTICAL -> {
            cameraViewfinder.moveToRightOf(foldPosition)
        }
        FoldingFeature.Orientation.HORIZONTAL -> {
            cameraViewfinder.moveToTopOf(foldPosition)
        }
    }
} else {
    cameraViewfinder.restore()
}

如果折叠边为 VERTICAL,则您需要向右移动内容,否则需要将其移到折叠边顶部。

  1. 构建并运行您的应用,然后展开设备并将其置于桌面模式,看看内容相应地移动!

7. 恭喜

在此 Codelab 中,您了解了可折叠设备、折叠状态更改和 Rear Display API 的特别之处。

深入阅读

参考文档