详解 | 为可折叠设备构建响应式 UI

为可折叠设备和大屏设备优化您的应用

Android 设备的屏幕尺寸日新月异,随着平板和可折叠设备的普及度越来越高,在开发响应式用户界面时,了解您应用的窗口尺寸和状态显得尤为重要。Jetpack WindowManager 现已进入 beta 测试阶段,这个库提供了与 Android 框架中 WindowManager 比较相似的功能,包括了对支持响应式 UI、检测屏幕改变的回调适配器和测试窗口 API 的支持。但 Jetpack WindowManager 还新增了对可折叠设备和 ChromeOS 这类窗口环境的支持。

新的 WindowManager API 包含了以下内容:

  • WindowLayoutInfo: 包含了窗口的显示特性,例如该窗口是否可折叠或包含铰链
  • FoldingFeature: 让您能够监听可折叠设备的折叠状态得以判断设备的姿态
  • WindowMetrics: 提供当前窗口或全部窗口的显示指标

Jetpack WindowManager 不与 Android 绑定,这让 API 能够迅速地迭代以支持快速发展的市场,还让开发者们能够通过更新库而不必等待 Android 版本更新来获得支持。

现在,Jetpack WindowManager 库已进入 beta 测试阶段,我们鼓励所有开发者来使用 Jetpack WindowManager,其与设备无关 API、测试 API 以及它引入的 WindowMetrics,使您的应用能够轻松响应窗口尺寸的变化。已经进入 beta 测试阶段,意味着您可以安心地专注于在这些设备上打造激动人心的体验,Jetpack WindowManager 最低支持到 API 14。

关于 Jetpack WindowManager

Jetpack WindowManager 是一个以 Kotlin 优先的现代化库,它支持不同形态的新设备,并提供 “类 AppCompat” 的功能以构建具有响应式 UI 的应用。

折叠状态

支持可折叠设备是 Jetpack WindowManager 库最直观的功能。当设备的折叠状态变化时,应用将收到相应的事件,进而更新 UI 界面以支持新的用户交互。

△ 在 Samsung Galaxy Z Fold2 上运行的 Google Duo

△ 在 Samsung Galaxy Z Fold2 上运行的 Google Duo

您可以通过 Google Duo 学习案例 来了解如何支持可折叠设备。

折叠状态有两种,分别是 FLAT (展平) 和 HALF_OPENED (半开)。对于 FLAT,您可以认为表面是完全平整打开的,尽管有些情况下它有可能被铰链分割。对于 HALF_OPENED,窗口中有至少两个逻辑区域。我们在下方用图片说明了每种状态各自可能的情况。

△ 折叠状态: FLAT 和 HALF-OPENED

△ 折叠状态: FLATHALF-OPENED

在应用活跃的状态下,可以通过 Kotlin 数据流收集事件来获得折叠状态改变的信息。

我们通过 lifecycleScope 来控制事件收集的开始和结束,正如文章《设计 repeatOnLifeCycle API 背后的故事》和示例代码所述:

lifecycleScope.launch(Dispatchers.Main) {
    // 传递给 repeatOnLifecycle 的代码块将在生命周期进入 STARTED 时执行
    // 并在生命周期为 STOPPED 时取消
    // repeatOnLifecycle 将会在生命周期再次进入 STARTED 时自动重启代码块
    lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
        // 当生命周期处于 STARTED 时安全地从 windowInfoRepository 中收集数据
        // 当生命周期进入 STOPPED 时停止收集数据
        windowInfoRepository.windowLayoutInfo
            .collect { newLayoutInfo ->
                updateStateLog(newLayoutInfo)
                updateCurrentState(newLayoutInfo)
            }
    }
}

当用户可以看到应用时,应用可以使用其接收到的 WindowLayoutInfo 对象中包含的信息更新布局。

FoldingFeature 包括了诸如铰链 方向,及折叠功能是否创建了两个逻辑屏幕区域 (isSeparating 属性) 这类信息。我们能使用这些值来检查设备是否处于桌面模式 (屏幕半开并且铰链处于水平方向):

△ 设备处于 TableTop 模式

△ 设备处于 TableTop 模式

private fun isTableTopMode(foldFeature: FoldingFeature) =
    foldFeature.isSeparating && 
            foldFeature.orientation == FoldingFeature.Orientation.HORIZONTAL

或者书本模式 (屏幕半开并且铰链处于垂直方向):

△ 设备处于 Book 模式

△ 设备处于 Book 模式

private fun isBookMode(foldFeature: FoldingFeature) =
    foldFeature.isSeparating &&
            foldFeature.orientation == FoldingFeature.Orientation.VERTICAL

请参阅: 可折叠设备中的桌面模式,文内示例介绍了如何在媒体播放器应用中实现这样的功能。

注意: 在主线程/UI 线程中收集事件这点十分重要,这能避免在 UI 和事件处理之间的同步问题。

支持响应式 UI

Android 设备的屏幕尺寸变化十分频繁,因此着手设计能够完全自适应和响应式的 UI 非常重要。Jetpack WindowManager 库中包含的另一个功能——能够检索当前窗口和最大窗口的指标信息。这和 API 30 当中的 WindowMetrics API 类似,但它向后兼容到 API 14。

Jetpack WindowManager 提供了两种途径来检索 WindowMetrics 信息,通过数据流事件中的流或者通过 WindowMetricsCalculator 类进行同步处理。

当在编写视图代码时,使用异步 API 可能比较困难 (比如 onMeasure),此时可以使用 WindowMetricsCalculator。

val windowMetrics = 
    WindowMetricsCalculator.getOrCreate().computeCurrentWindowMetrics(activity)

另一个使用场景是用于测试中 (详见下面的测试一节)。

在处理应用 UI 的高阶用法中,通过该库提供的 WindowInfoRepository#currentWindowMetrics 能够在窗口尺寸变更时收到通知,这与是否触发配置变更无关。

这个例子是关于如何根据可用区域来切换您的布局:

// 因为 repeatOnLifecycle 是挂起函数,所以创建一个新的协程
lifecycleScope.launch(Dispatchers.Main) {
   // 传递给 repeatOnLifecycle 的代码块将在生命周期进入 STARTED 时执行
    // 并在生命周期为 STOPPED 时取消
    // 它将会在生命周期再次进入 STARTED 时自动重启
   lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
       // 当生命周期处于 STARTED 时安全地从 windowInfoRepository 中收集数据
       // 当生命周期进入 STOPPED 时停止收集数据
       windowInfoRepository.currentWindowMetrics
           .collect { windowMetrics ->
               val currentBounds = windowMetrics.bounds
               Log.i(TAG, "New bounds: {$currentBounds}")
               // 我们可以根据需要在这里更新布局
           }
   }
}

回调适配器

要在 Java 编程语言中使用这个库或者使用回调接口,请在您的应用中添加 androidx.window:window-java 依赖。该组件提供了 WindowInfoRepositoryCallbackAdapter,您可以通过它注册 (取消注册) 一个用以接收设备姿态及窗口指标信息更新的回调。

public class SplitLayoutActivity extends AppCompatActivity {

   private WindowInfoRepositoryCallbackAdapter windowInfoRepository;
   private ActivitySplitLayoutBinding binding;
   private final LayoutStateChangeCallback layoutStateChangeCallback =
           new LayoutStateChangeCallback();

   @Override
   protected void onCreate(@Nullable Bundle savedInstanceState) {
       super.onCreate(savedInstanceState);

       binding = ActivitySplitLayoutBinding.inflate(getLayoutInflater());
       setContentView(binding.getRoot());

       windowInfoRepository =
               new WindowInfoRepositoryCallbackAdapter(WindowInfoRepository.getOrCreate(this));
   }

   @Override
   protected void onStart() {
       super.onStart();
       windowInfoRepository.addWindowLayoutInfoListener(Runnable::run, layoutStateChangeCallback);
   }

   @Override
   protected void onStop() {
       super.onStop();
       windowInfoRepository.removeWindowLayoutInfoListener(layoutStateChangeCallback);
   }

   class LayoutStateChangeCallback implements Consumer<WindowLayoutInfo> {
       @Override
       public void accept(WindowLayoutInfo windowLayoutInfo) {
           binding.splitLayout.updateWindowLayout(windowLayoutInfo);
       }
   }
}

测试

开发者们讲到,更健壮的测试 API 对于维护 LTS (长期支持) 是十分关键的。让我们来聊聊如何在普通设备上测试可折叠设备姿态。

现在,我们已经知道 Jetpack WindowManager 库可以在设备姿态改变时,向您的应用发送通知,以便您修改应用的布局。

该库在 androidx.window:window-testing 中提供了 WindowLayoutInfoPublisherRule 让您能够发布一个 WindowInfoLayout 以支持测试 FoldingFeature:

import androidx.window.testing.layout.FoldingFeature
import androidx.window.testing.layout.WindowLayoutInfoPublisherRule

我们可以在测试中虚拟一个 FoldingFeature:

val feature = FoldingFeature(
   activity = activity,
   center = center,
   size = 0,
   orientation = VERTICAL,
   state = HALF_OPENED
)
val expected =
   WindowLayoutInfo.Builder().setDisplayFeatures(listOf(feature)).build()

publisherRule.overrideWindowLayoutInfo(expected)

然后使用 WindowLayoutInfoPublisherRule 来发布它:

val publisherRule = WindowLayoutInfoPublisherRule()

publisherRule.overrideWindowLayoutInfo(expected)

最后,使用可用的 Espresso 匹配器 来检查我们正在测试的 Activity 的布局是否符合预期。

下面这个测试中发布了一个处于 HALF_OPENED 状态并且铰链垂直于屏幕中心的 FoldingFeature:

@Test
fun testDeviceOpen_Vertical(): Unit = testScope.runBlockingTest {
   activityRule.scenario.onActivity { activity ->
       val feature = FoldingFeature(
           activity = activity,
           orientation = VERTICAL,
           state = HALF_OPENED
       )
       val expected =
           WindowLayoutInfo.Builder().setDisplayFeatures(listOf(feature)).build()

       val value = testScope.async {
           activity.windowInfoRepository().windowLayoutInfo.first()
       }
       publisherRule.overrideWindowLayoutInfo(expected)
       runBlockingTest {
           Assert.assertEquals(
               expected,
               value.await()
           )
       }
   }

    // 检查在有垂直折叠特性时 start_layout 在 end_layout 的左侧
    // 这需要在足够大的屏幕上运行测试以适应屏幕上的两个视图
   onView(withId(R.id.start_layout))
       .check(isCompletelyLeftOf(withId(R.id.end_layout)))
}

查看示例代码

Github 上的 最新示例 展示了如何使用 Jetpack WindowManager 库从 WindowLayoutInfo 流收集信息,或者通过向 WindowInfoRepositoryCallbackAdapter 注册回调来获取显示姿态信息。

该实例还包含一些测试,它们可以在任何设备或模拟器中运行。

在您的应用中使用 WindowManager

可折叠设备及双屏设备不再仅仅是实验性的或前瞻的——大屏幕空间和额外的设备姿态已经被证实是具有用户价值的,而且现在有更多的设备可供您的用户选择。可折叠设备和双屏设备代表了智能手机的自然进化。对于 Android 开发者来说,这提供了一个进入正在增长的高端市场的机会,感谢设备制造商们重新开始关注大屏设备。

我们去年推出了 Jetpack WindowManager alpha01 版本。该库自那时起开始稳步地发展,早期的反馈让其有了很大的改进。现在,它已经拥抱了 Android 的 Kotlin 优先理念,从回调驱动模型逐渐过渡到协程和数据流。随着 WindowManager 进入测试阶段,API 已经稳定,我们强烈建议使用它。

更新并不仅限于此。我们计划为该库添加更多功能,并使其发展成为与 AppCompat 解绑的系统 UI 库,使开发者能够在所有的 Android 设备上轻松实现现代化的、响应式的 UI。

欢迎您 点击这里 向我们提交反馈,或分享您喜欢的内容、发现的问题。您的反馈对我们非常重要,感谢您的支持!