使用 Jetpack WindowManager 支持折叠屏和双屏设备

1. 开始之前

本实操代码实验室将教你开发双屏和折叠屏设备的基础知识。完成后,该应用可以支持 Pixel Fold、Microsoft Surface Duo、Samsung Galaxy Z Fold 5 等折叠屏设备。

先决条件

要完成本代码实验室,您需要

您将学习什么

创建一个简单的应用,该应用可以

  • 显示设备功能
  • 检测应用是否在折叠屏或双屏设备上运行
  • 确定设备状态
  • 使用 Jetpack WindowManager 处理新型态设备。

您需要什么

Android 模拟器 v30.0.6+ 包含带有虚拟铰链传感器和 3D 视图的折叠屏支持。您可以使用一些折叠屏模拟器,如下面的图片所示

ca76200cc00b6ce6.png

2. 单屏设备与折叠屏设备

与之前移动设备相比,折叠屏设备为用户提供了更大的屏幕和更灵活的用户界面。折叠时,这些设备通常比普通尺寸的平板电脑更小,使其更便携和实用。

在撰写本文时,有两种类型的折叠屏设备

  • 单屏折叠屏设备,只有一个可以折叠的屏幕。用户可以使用 多窗口 模式在同一屏幕上同时运行多个应用。
  • 双屏折叠屏设备,两个屏幕由铰链连接。这些设备也可以折叠,但它们具有两个不同的逻辑显示区域。

9ff347a7c8483fed.png

与平板电脑和其他单屏移动设备一样,折叠屏设备可以

  • 在一个显示区域中运行一个应用。
  • 并排运行两个应用,每个应用位于不同的显示区域(使用 多窗口 模式)。

与单屏设备不同,折叠屏设备还支持不同的姿势。姿势可用于以不同的方式显示内容。

bac1d8089687c0c2.png

当应用跨越整个显示区域(在双屏折叠屏设备上使用所有显示区域)时,折叠屏设备可以提供不同的跨屏姿势。

折叠屏设备还可以提供折叠姿势,例如桌面模式,这样您就可以在平放的屏幕部分和向您倾斜的屏幕部分之间进行逻辑划分,以及帐篷模式,这样您就可以像设备使用支架一样可视化内容。

3. Jetpack WindowManager

Jetpack WindowManager 库帮助应用开发者支持新的设备形态,并为旧版和新版平台上的各种 WindowManager 功能提供通用的 API 接口。

主要功能

Jetpack WindowManager 版本 1.1.0 包含 FoldingFeature 类,该类描述了柔性显示屏中的折叠或两个物理显示面板之间的铰链。其 API 提供了访问与设备相关的重要信息的功能

使用 WindowInfoTracker 接口,您可以访问 windowLayoutInfo() 以收集包含所有可用 DisplayFeatureFlowWindowLayoutInfo

4. 设置

创建一个新项目,并选择“空活动”模板

a5ce5c7fb033ec4c.png

将所有参数保留为默认值。

声明依赖项

为了使用 Jetpack WindowManager,请在应用或模块的 build.gradle 文件中添加依赖项

app/build.gradle

dependencies {
    ext.windowmanager_version = "1.1.0"

    implementation "androidx.window:window:$windowmanager_version"
    androidTestImplementation "androidx.window:window-testing:$windowmanager_version"

    // Needed to use lifecycleScope to collect the WindowLayoutInfo flow
    implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.6.2"
}

使用 WindowManager

可以通过 WindowManager 的 WindowInfoTracker 接口访问窗口功能。

打开 MainActivity.kt 源文件,并调用 WindowInfoTracker.getOrCreate(this@MainActivity) 以初始化与当前活动关联的 WindowInfoTracker 实例

MainActivity.kt

import androidx.window.layout.WindowInfoTracker

private lateinit var windowInfoTracker: WindowInfoTracker

override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
     
        windowInfoTracker = WindowInfoTracker.getOrCreate(this@MainActivity)
}

使用 WindowInfoTracker 实例,获取有关设备当前窗口状态的信息。

5. 设置应用 UI

从 Jetpack WindowManager 获取有关窗口度量、布局和显示配置的信息。在主活动布局中显示这些信息,每个信息使用一个 TextView

创建一个 ConstraintLayout,其中包含三个 TextView,居中于屏幕上。

打开 activity_main.xml 文件,并粘贴以下内容

activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
   xmlns:app="http://schemas.android.com/apk/res-auto"
   xmlns:tools="http://schemas.android.com/tools"
   android:id="@+id/constraint_layout"
   android:layout_width="match_parent"
   android:layout_height="match_parent"
   tools:context=".MainActivity">

    <TextView
        android:id="@+id/window_metrics"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:padding="20dp"
        tools:text="Window metrics"
        android:textSize="20sp"
        app:layout_constraintBottom_toTopOf="@+id/layout_change"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintVertical_chainStyle="packed" />

    <TextView
        android:id="@+id/layout_change"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:padding="20dp"
        tools:text="Layout change"
        android:textSize="20sp"
        app:layout_constrainedWidth="true"
        app:layout_constraintBottom_toTopOf="@+id/configuration_changed"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/window_metrics" />

    <TextView
        android:id="@+id/configuration_changed"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:padding="20dp"
        tools:text="Using one logic/physical display - unspanned"
        android:textSize="20sp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/layout_change" />

</androidx.constraintlayout.widget.ConstraintLayout>

现在,我们将使用 视图绑定 在代码中连接这些 UI 元素。为此,我们首先在应用的 build.gradle 文件中启用它

app/build.gradle

android {
   // Other configurations

   buildFeatures {
      viewBinding true
   }
}

根据 Android Studio 的建议同步 Gradle 项目,并在 MainActivity.kt 中使用以下代码使用视图绑定

MainActivity.kt

class MainActivity : AppCompatActivity() {
    private lateinit var windowInfoTracker: WindowInfoTracker
    private lateinit var binding: ActivityMainBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)

        windowInfoTracker = WindowInfoTracker.getOrCreate(this@MainActivity)
    }
}

6. 可视化 WindowMetrics 信息

MainActivityonCreate 方法中,调用一个函数来获取和显示 WindowMetrics 信息。在 onCreate 方法中添加 obtainWindowMetrics() 调用

MainActivity.kt

override fun onCreate(savedInstanceState: Bundle?) {
   super.onCreate(savedInstanceState)
   binding = ActivityMainBinding.inflate(layoutInflater)
   setContentView(binding.root)

   windowInfoTracker = WindowInfoTracker.getOrCreate(this@MainActivity)

   obtainWindowMetrics()
}

实现 obtainWindowMetrics 方法

MainActivity.kt

import androidx.window.layout.WindowMetricsCalculator

private fun obtainWindowMetrics() {
   val wmc = WindowMetricsCalculator.getOrCreate()
   val currentWM = wmc.computeCurrentWindowMetrics(this).bounds.flattenToString()
   val maximumWM = wmc.computeMaximumWindowMetrics(this).bounds.flattenToString()
   binding.windowMetrics.text =
       "CurrentWindowMetrics: ${currentWM}\nMaximumWindowMetrics: ${maximumWM}"
}

通过其伴随函数 getOrCreate() 获取 WindowMetricsCalculator 的实例。

使用该 WindowMetricsCalculator 实例,将信息设置到 windowMetrics TextView 中。使用函数 computeCurrentWindowMetrics.boundscomputeMaximumWindowMetrics.bounds 返回的值。

这些值提供了有关窗口占据区域的度量的有用信息。

运行应用。在双屏模拟器(如下图所示)中,您将获得与模拟器镜像的 设备尺寸 相符的 CurrentWindowMetrics。您还可以在应用以单屏模式运行时查看度量

f6f0deff678fd722.png

当应用跨越显示屏时,窗口度量会发生变化,如下面的图片所示,因此它们现在反映了应用使用的更大的窗口区域

f1ce73d7198b4990.png

当前和最大窗口度量具有相同的值,因为应用始终处于运行状态并占用整个可用显示区域,无论是在单屏还是双屏上。

在具有水平折叠的折叠屏模拟器中,当应用跨越整个物理显示屏和应用以多窗口模式运行时,值会有所不同

d00e53154f32d7df.png

如左侧图片所示,这两个度量具有相同的值,因为正在运行的应用使用了整个可用显示区域,即当前和最大可用区域。

但在右侧图片中,应用以多窗口模式运行,您可以看到当前度量如何显示应用在该特定区域(顶部)的拆分屏幕模式中运行的区域的尺寸,以及最大度量如何显示设备具有的最大显示区域。

WindowMetricsCalculator 提供的度量对于确定应用正在使用或可以使用窗口的区域非常有用。

7. 可视化 FoldingFeature 信息

现在注册以接收窗口布局更改以及模拟器或设备的 DisplayFeatures 的特征和边界。

要从WindowInfoTracker#windowLayoutInfo()中收集信息,请使用为每个Lifecycle对象定义的lifecycleScope。在此作用域中启动的任何协程在 Lifecycle 被销毁时都会被取消。您可以通过lifecycle.coroutineScopelifecycleOwner.lifecycleScope属性访问生命周期协程作用域。

MainActivityonCreate方法中,调用一个函数来获取并显示WindowInfoTracker信息。首先在onCreate方法中添加一个onWindowLayoutInfoChange()调用。

MainActivity.kt

override fun onCreate(savedInstanceState: Bundle?) {
   super.onCreate(savedInstanceState)
   binding = ActivityMainBinding.inflate(layoutInflater)
   setContentView(binding.root)

   windowInfoTracker = WindowInfoTracker.getOrCreate(this@MainActivity)

   obtainWindowMetrics()
   onWindowLayoutInfoChange()
}

使用该函数的实现,以便在每次新的布局配置更改时获取信息。

定义函数签名和框架。

MainActivity.kt

private fun onWindowLayoutInfoChange() {
}

使用函数接收的参数(一个WindowInfoTracker),获取其WindowLayoutInfo数据。WindowLayoutInfo包含位于窗口内的DisplayFeature列表。例如,铰链或显示器折叠可能会横跨窗口,在这种情况下,将视觉内容和交互元素分成两组(例如,列表详细信息或视图控件)可能很有意义。

仅报告当前窗口边界内存在的特征。如果窗口在屏幕上移动或调整大小,其位置和大小可能会发生变化。

通过在lifecycle-runtime-ktx依赖项中定义的lifecycleScope,获取一个包含所有显示特征列表的WindowLayoutInfoflow。添加onWindowLayoutInfoChange的主体。

MainActivity.kt

import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch

private fun onWindowLayoutInfoChange() {
    lifecycleScope.launch(Dispatchers.Main) {
        lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
            windowInfoTracker.windowLayoutInfo(this@MainActivity)
                .collect { value ->
                    updateUI(value)
                }
        }
    }
}

updateUI函数正在从collect中调用。实现此函数以显示和打印从WindowLayoutInfoflow接收到的信息。检查WindowLayoutInfo数据是否具有显示特征。如果是,则显示特征以某种方式与应用的 UI 交互。如果WindowLayoutInfo数据没有任何显示特征,则应用在单屏设备或模式或多窗口模式下运行。

MainActivity.kt

import androidx.window.layout.WindowLayoutInfo

private fun updateUI(newLayoutInfo: WindowLayoutInfo) {
    binding.layoutChange.text = newLayoutInfo.toString()
    if (newLayoutInfo.displayFeatures.isNotEmpty()) {
        binding.configurationChanged.text = "Spanned across displays"
    } else {
        binding.configurationChanged.text = "One logic/physical display - unspanned"
    }
}

运行应用。在双屏模拟器中,您有

a6f6452155742925.png

WindowLayoutInfo为空。它有一个空的List<DisplayFeature>。但是,如果您有一个中间带有铰链的模拟器,为什么您无法从 WindowManager 获取信息呢?

WindowManager(通过WindowInfoTracker)仅当应用跨显示器(物理或非物理)扩展时,才会提供WindowLayoutInfo数据(设备功能类型、设备功能边界和设备姿势)。因此,在上图中,应用在单屏幕模式下运行时,WindowLayoutInfo为空。

有了这些信息,您就知道应用在哪个模式下运行(单屏幕模式或跨显示器扩展),然后您就可以对 UI/UX 进行更改,提供更适合这些特定配置的用户体验。

在没有两个物理显示器的设备上(它们通常没有物理铰链),应用可以使用多窗口模式并排运行。在这些设备上,当应用在多窗口模式下运行时,应用的行为就像在前面的示例中那样在单屏上运行。当应用占用所有逻辑显示器时,它的行为与应用扩展时相同。请参见下图

eacdd758eefb6c3c.png

当应用在多窗口模式下运行时,WindowManager 提供一个空的List<LayoutInfo>

总而言之,仅当应用占用所有逻辑显示器并与设备特征(折叠或铰链)相交时,您才会获得WindowLayoutInfo数据。在所有其他情况下,您都不会获得任何信息。32e4190913b452e4.png

当您跨显示器扩展应用时会发生什么?在双屏模拟器中,WindowLayoutInfo将具有一个FoldingFeature对象,该对象提供有关设备功能的数据:一个HINGE、该功能的边界(Rect(0,0 - 1434,1800))以及设备的姿势(状态)(FLAT)。

586f15def7d23ffd.png

让我们看看每个字段的含义

  • type = TYPE_HINGE:此双屏模拟器镜像具有物理铰链的真实 Surface Duo 设备,这就是 WindowManager 报告的内容。
  • Bounds [0, 0 - 1434, 1800]:表示应用程序窗口中特征的边界矩形,以窗口坐标空间为单位。如果您阅读Surface Duo 设备的尺寸规格,您会发现铰链位于这些边界(左、上、右、下)报告的确切位置。
  • State:有两个不同的值表示设备的姿势(状态)。
  • HALF_OPENED:可折叠设备的铰链处于打开和关闭状态之间的中间位置,并且柔性屏幕的各个部分或物理屏幕面板之间存在非平坦的角度。
  • FLAT:可折叠设备完全打开,并且呈现给用户屏幕空间是平坦的。

模拟器默认以 180 度打开,因此 WindowManager 返回的姿势为FLAT

如果您使用“虚拟传感器”选项将模拟器的姿势更改为“半开”姿势,WindowManager 将通知您新的位置:HALF_OPENED

cba02ab39d6d346b.png

使用 WindowManager 调整 UI/UX

如显示窗口布局信息的图形所示,显示的信息被显示特征截断,这里也是一样

ff2caf93916f1682.png

这不是最佳的用户体验。您可以使用 WindowManager 提供的信息来调整您的 UI/UX。

如您之前所见,当您的应用跨所有不同的显示区域扩展时,也是您的应用与设备特征相交时,因此 WindowManager 提供窗口布局信息作为显示状态和显示边界。因此,在这里,当应用扩展时,您需要使用这些信息并调整您的 UI/UX。

然后您要做的就是在应用扩展时调整当前的 UI/UX,这样就不会有重要信息被显示特征截断或隐藏。您将创建一个镜像设备显示特征的视图,并将其用作约束被截断或隐藏的TextView的参考,这样您就不会再有丢失的信息了。

出于学习目的,将此新视图着色,以便可以轻松地看到它位于真实设备显示特征所在的相同位置,并且具有相同的尺寸。

activity_main.xml中添加您将用作设备特征参考的新视图。

activity_main.xml

<!-- It's not important where this view is placed by default, it will be positioned dynamically at runtime -->
<View
    android:id="@+id/folding_feature"
    android:layout_width="0dp"
    android:layout_height="0dp"
    android:background="@android:color/holo_red_dark"
    android:visibility="gone"
    tools:ignore="MissingConstraints" />

MainActivity.kt中,转到您用于显示给定WindowLayoutInfo信息的updateUI()函数,并在您具有显示特征的 if-else 案例中添加一个新的函数调用。

MainActivity.kt

private fun updateUI(newLayoutInfo: WindowLayoutInfo) {
   binding.layoutChange.text = newLayoutInfo.toString()
   if (newLayoutInfo.displayFeatures.isNotEmpty()) {
       binding.configurationChanged.text = "Spanned across displays"
       alignViewToFoldingFeatureBounds(newLayoutInfo)
   } else {
       binding.configurationChanged.text = "One logic/physical display - unspanned"
   }
}

您已添加了函数alignViewToFoldingFeatureBounds,该函数接收WindowLayoutInfo作为参数。

创建该函数。在函数内部,创建您的ConstraintSet以便对您的视图应用新的约束。然后,使用WindowLayoutInfo获取显示特征边界。由于WindowLayoutInfo返回一个DisplayFeature列表,而DisplayFeature只是一个接口,因此将其转换为FoldingFeature以访问所有信息。

MainActivity.kt

import androidx.constraintlayout.widget.ConstraintSet
import androidx.window.layout.FoldingFeature

private fun alignViewToFoldingFeatureBounds(newLayoutInfo: WindowLayoutInfo) {
   val constraintLayout = binding.constraintLayout
   val set = ConstraintSet()
   set.clone(constraintLayout)

   // Get and translate the feature bounds to the View's coordinate space and current
   // position in the window.
   val foldingFeature = newLayoutInfo.displayFeatures[0] as FoldingFeature
   val bounds = getFeatureBoundsInWindow(foldingFeature, binding.root)


   // Rest of the code to be added in the following steps
}

定义一个getFeatureBoundsInWindow()函数以将特征边界转换为视图的坐标空间和窗口中的当前位置。

MainActivity.kt

import android.graphics.Rect
import android.view.View
import androidx.window.layout.DisplayFeature

/**
 * Get the bounds of the display feature translated to the View's coordinate space and current
 * position in the window. This will also include view padding in the calculations.
 */
private fun getFeatureBoundsInWindow(
    displayFeature: DisplayFeature,
    view: View,
    includePadding: Boolean = true
): Rect? {
    // Adjust the location of the view in the window to be in the same coordinate space as the feature.
    val viewLocationInWindow = IntArray(2)
    view.getLocationInWindow(viewLocationInWindow)

    // Intersect the feature rectangle in window with view rectangle to clip the bounds.
    val viewRect = Rect(
        viewLocationInWindow[0], viewLocationInWindow[1],
        viewLocationInWindow[0] + view.width, viewLocationInWindow[1] + view.height
    )

    // Include padding if needed
    if (includePadding) {
        viewRect.left += view.paddingLeft
        viewRect.top += view.paddingTop
        viewRect.right -= view.paddingRight
        viewRect.bottom -= view.paddingBottom
    }

    val featureRectInView = Rect(displayFeature.bounds)
    val intersects = featureRectInView.intersect(viewRect)

    // Checks to see if the display feature overlaps with our view at all
    if ((featureRectInView.width() == 0 && featureRectInView.height() == 0) ||
        !intersects
    ) {
        return null
    }

    // Offset the feature coordinates to view coordinate space start point
    featureRectInView.offset(-viewLocationInWindow[0], -viewLocationInWindow[1])

    return featureRectInView
}

有了有关显示特征边界的信息,您可以使用它来设置参考视图的正确高度大小并相应地移动它。

alignViewToFoldingFeatureBounds的完整代码将是

MainActivity.kt - alignViewToFoldingFeatureBounds

private fun alignViewToFoldingFeatureBounds(newLayoutInfo: WindowLayoutInfo) {
    val constraintLayout = binding.constraintLayout
    val set = ConstraintSet()
    set.clone(constraintLayout)

    // Get and Translate the feature bounds to the View's coordinate space and current
    // position in the window.
    val foldingFeature = newLayoutInfo.displayFeatures[0] as FoldingFeature
    val bounds = getFeatureBoundsInWindow(foldingFeature, binding.root)

    bounds?.let { rect ->
        // Some devices have a 0px width folding feature. We set a minimum of 1px so we
        // can show the view that mirrors the folding feature in the UI and use it as reference.
        val horizontalFoldingFeatureHeight = (rect.bottom - rect.top).coerceAtLeast(1)
        val verticalFoldingFeatureWidth = (rect.right - rect.left).coerceAtLeast(1)

        // Sets the view to match the height and width of the folding feature
        set.constrainHeight(
            R.id.folding_feature,
            horizontalFoldingFeatureHeight
        )
        set.constrainWidth(
            R.id.folding_feature,
            verticalFoldingFeatureWidth
        )

        set.connect(
            R.id.folding_feature, ConstraintSet.START,
            ConstraintSet.PARENT_ID, ConstraintSet.START, 0
        )
        set.connect(
            R.id.folding_feature, ConstraintSet.TOP,
            ConstraintSet.PARENT_ID, ConstraintSet.TOP, 0
        )

        if (foldingFeature.orientation == FoldingFeature.Orientation.VERTICAL) {
            set.setMargin(R.id.folding_feature, ConstraintSet.START, rect.left)
            set.connect(
                R.id.layout_change, ConstraintSet.END,
                R.id.folding_feature, ConstraintSet.START, 0
            )
        } else {
            // FoldingFeature is Horizontal
            set.setMargin(
                R.id.folding_feature, ConstraintSet.TOP,
                rect.top
            )
            set.connect(
                R.id.layout_change, ConstraintSet.TOP,
                R.id.folding_feature, ConstraintSet.BOTTOM, 0
            )
        }

        // Set the view to visible and apply constraints
        set.setVisibility(R.id.folding_feature, View.VISIBLE)
        set.applyTo(constraintLayout)
    }
}

现在与设备显示特征冲突的TextView会考虑特征所在的位置,因此其内容永远不会被截断或隐藏。

67b41810704d0011.png

在双屏模拟器(上图,左)中,您可以看到如何显示跨显示器内容且被铰链截断的TextView不再被截断,因此没有丢失信息。

在可折叠模拟器(上图,右)中,您将看到一条浅红色的线,表示折叠显示特征所在的位置,并且TextView现在已放置在特征下方。因此,当设备折叠时(例如,笔记本电脑姿势下 90 度),没有任何信息受特征影响。

如果您想知道双屏模拟器上的显示特征在哪里(因为这是一个铰链式设备),则表示特征的视图被铰链隐藏。但是,如果应用从跨越到未跨越,您将在与特征相同的位置看到它,并具有正确的宽度和高度。

1a309ab775c49a6a.png

8. 其他 Jetpack WindowManager 工件

除了帮助您以不同方式与组件交互的主要工件外,WindowManager 还附带其他有用的工件,同时考虑您在构建应用时使用的当前环境。

Java 工件

如果您使用的是 Java 编程语言而不是 Kotlin,或者通过回调监听事件更适合您的架构,那么 WindowManager 的 Java 工件非常有用,因为它提供了一个 Java 友好的 API 来注册和注销通过回调的事件监听器。

RxJava 工件

如果您已经在使用 RxJava(版本 23),您可以使用特定的工件来帮助您保持代码的一致性,无论您使用的是 Observables 还是 Flowables

9. 使用 Jetpack WindowManager 进行测试

在任何模拟器或设备上测试折叠姿势对于测试 UI 元素如何在 FoldingFeature 周围放置非常有用。

为了实现这一点,WindowManager 带有一个非常有用的工件用于 Instrumentation 测试。

让我们看看如何使用它。

除了主要的 WindowManager 依赖项之外,我们还在 app 的 build.gradle 文件中添加了测试工件:androidx.window:window-testing

window-testing 工件带有一个有用的新 TestRule,称为 WindowLayoutInfoPublisherRule,它将有助于测试使用 WindowLayoutInfo 值的流。 WindowLayoutInfoPublisherRule 允许您根据需要推送不同的 WindowLayoutInfo 值。

为了使用它,并由此创建一个示例来帮助您使用此新工件测试您的 UI,请更新 Android Studio 模板创建的测试类。将 ExampleInstrumentedTest 类中的所有代码替换为以下内容

ExampleInstrumentedTest.kt

import androidx.test.ext.junit.rules.ActivityScenarioRule
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.window.testing.layout.WindowLayoutInfoPublisherRule
import org.junit.Rule
import org.junit.rules.RuleChain
import org.junit.rules.TestRule
import org.junit.runner.RunWith

@RunWith(AndroidJUnit4::class)
class MainActivityTest {
    private val activityRule = ActivityScenarioRule(MainActivity::class.java)
    private val publisherRule = WindowLayoutInfoPublisherRule()

    @get:Rule
    val testRule: TestRule

    init {
        testRule = RuleChain.outerRule(publisherRule).around(activityRule)
    }
}

提到的规则已与 ActvityScenarioRule 链接。

为了模拟 FoldingFeature,新的工件带有一些 非常有用的函数来执行此操作。以下是最简单的函数,它提供了一些默认值。

MainActivity 中,TextView 位于折叠特征的左侧。创建一个测试来检查此操作是否正确实现。

创建一个名为 testText_is_left_of_Vertical_FoldingFeature 的测试

ExampleInstrumentedTest.kt

import androidx.window.layout.FoldingFeature.Orientation.Companion.VERTICAL
import androidx.window.layout.FoldingFeature.State.Companion.FLAT
import androidx.window.testing.layout.FoldingFeature
import androidx.window.testing.layout.TestWindowLayoutInfo
import org.junit.Test

@Test
fun testText_is_left_of_Vertical_FoldingFeature() {
   activityRule.scenario.onActivity { activity ->
       val hinge = FoldingFeature(
           activity = activity,
           state = FLAT,
           orientation = VERTICAL,
           size = 2
       )

       val expected = TestWindowLayoutInfo(listOf(hinge))
       publisherRule.overrideWindowLayoutInfo(expected)
   }

   // Add Assertion with EspressoMatcher here

}

测试 FoldingFeature 的状态为 FLAT,方向为 VERTICAL。我们已定义了一个特定的大小,因为我们希望在测试中的 UI 中显示模拟的 FoldingFeature,以便我们可以看到它在设备上的位置。

我们使用之前实例化的 WindowLayoutInfoPublishRule 发布模拟的 FoldingFeaure,以便我们可以像使用真实的 WindowLayoutInfo 数据一样获取它。

最后一步就是测试我们的 UI 元素是否位于应有的位置,避免 FoldingFeature。为此,我们只需使用 EspressoMatchers,在刚刚创建的测试末尾添加断言。

ExampleInstrumentedTest.kt

import androidx.test.espresso.assertion.PositionAssertions
import androidx.test.espresso.matcher.ViewMatchers.withId

onView(withId(R.id.layout_change)).check(
    PositionAssertions.isCompletelyLeftOf(withId(R.id.folding_feature))
)

完整的测试将是

ExampleInstrumentedTest.kt

@Test
fun testText_is_left_of_Vertical_FoldingFeature() {
    activityRule.scenario.onActivity { activity ->
        val hinge = FoldingFeature(
            activity = activity,
            state = FoldingFeature.State.FLAT,
            orientation = FoldingFeature.Orientation.VERTICAL,
            size = 2
        )
        val expected = TestWindowLayoutInfo(listOf(hinge))
        publisherRule.overrideWindowLayoutInfo(expected)
    }
    onView(withId(R.id.layout_change)).check(
        PositionAssertions.isCompletelyLeftOf(withId(R.id.folding_feature))
    )
}
val horizontal_hinge = FoldingFeature(
   activity = activity,
   state = FLAT,
   orientation = HORIZONTAL,
   size = 2
)

您现在可以在设备或模拟器上运行测试以检查应用程序的行为是否符合预期。请注意,您不需要折叠式设备或模拟器来运行此测试。

10. 恭喜!

Jetpack WindowManager 帮助开发人员处理可折叠等新型态设备。

WindowManager 提供的信息对于调整 Android 应用程序以适应可折叠设备以提供更好的用户体验非常有帮助。

概括来说,在本 Codelab 中,您学习了

  • 什么是可折叠设备
  • 不同可折叠设备之间的差异
  • 可折叠设备、单屏设备和平板电脑之间的差异
  • Jetpack WindowManager API
  • 使用 Jetpack WindowManager 并使我们的应用程序适应新的设备外形尺寸
  • 使用 Jetpack WindowManager 进行测试

了解更多