1. 开始之前
本实践代码实验室将教你双屏和折叠屏设备开发的基础知识。完成后,应用程序可以支持 Pixel Fold、Microsoft Surface Duo、Samsung Galaxy Z Fold 5 等折叠屏设备。
先决条件
要完成此代码实验室,您需要
您将学习
创建一个简单的应用程序,该应用程序可以
- 显示设备功能
- 检测应用程序是否在折叠屏或双屏设备上运行
- 确定设备状态
- 使用 Jetpack WindowManager 处理新型态设备。
您需要准备
- Android Studio Arctic Fox 或更高版本
- 折叠屏设备或模拟器
Android 模拟器 v30.0.6+ 包含带有虚拟铰链传感器和 3D 视图的折叠屏支持。您可以使用一些折叠屏模拟器,如下面的图片所示。
- 如果您想使用双屏模拟器,可以下载适用于您的平台(Windows、MacOS 或 GNU/Linux)的 Microsoft Surface Duo 模拟器。
2. 单屏设备与折叠屏设备
与之前的移动设备相比,折叠屏设备为用户提供了更大的屏幕和更通用的用户界面。折叠时,这些设备通常比普通尺寸的平板电脑更小,使其更便携且功能更强大。
在撰写本文时,有两种类型的折叠屏设备
- 单屏折叠屏设备,只有一个可以折叠的屏幕。用户可以使用
多窗口
模式在同一屏幕上同时运行多个应用程序。 - 双屏折叠屏设备,两个屏幕由铰链连接。这些设备也可以折叠,但它们有两个不同的逻辑显示区域。
与平板电脑和其他单屏移动设备一样,折叠屏设备可以
- 在一个显示区域运行一个应用程序。
- 并排运行两个应用程序,每个应用程序在一个不同的显示区域(使用
多窗口
模式)。
与单屏设备不同,折叠屏设备还支持不同的姿态。姿态可以用来以不同的方式显示内容。
当应用程序跨越整个显示区域(在双屏折叠屏设备上使用所有显示区域)时,折叠屏设备可以提供不同的跨度姿态。
折叠屏设备还可以提供折叠姿态,例如桌面模式,这样您可以在平坦的屏幕部分和朝向您的倾斜部分之间进行逻辑分割,以及帐篷模式,这样您可以将内容可视化,就像设备使用了支架装置一样。
3. Jetpack WindowManager
Jetpack WindowManager 库帮助应用程序开发者支持新型态设备,并为旧版和新版平台上的各种 WindowManager 功能提供通用的 API 接口。
主要功能
Jetpack WindowManager 版本 1.1.0 包含 FoldingFeature
类,该类描述了柔性显示屏中的折叠或两个物理显示面板之间的铰链。其 API 提供了访问与设备相关的关键信息的功能
state()
:从定义的姿态列表中提供设备的当前姿态(FLAT
和HALF_OPENED
)isSeparating()
:计算是否应将FoldingFeature
视为将窗口分割成多个物理区域,用户可以将其视为逻辑上独立的区域occlusionType()
:计算遮挡模式以确定FoldingFeature
是否遮挡窗口的一部分。orientation()
:如果FoldingFeature
的宽度大于高度,则返回FoldingFeature.Orientation.HORIZONTAL
;否则,返回FoldingFeature.Orientation.VERTICAL
。bounds()
:提供一个Rect
实例,其中包含设备功能的边界,例如物理铰链的边界。
使用 WindowInfoTracker
接口,您可以访问 windowLayoutInfo()
以收集 Flow
的 WindowLayoutInfo
,其中包含所有可用的 DisplayFeature
。
4. 设置
创建一个新项目并选择“空活动”模板
将所有参数保留为默认值。
声明依赖项
为了使用 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 信息
在 MainActivity
的 onCreate
方法中,调用一个函数来获取和显示 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.bounds
和 computeMaximumWindowMetrics.bounds
返回的值。
这些值提供了有关窗口占据区域的度量信息的实用信息。
运行应用程序。在双屏模拟器(如下图所示)中,您将获得与模拟器镜像的 设备尺寸相符的 CurrentWindowMetrics
。您还可以在应用程序以单屏模式运行时查看度量。
当应用跨越多个显示屏时,窗口度量值会像下图一样发生变化,从而反映应用使用的更大窗口区域。
当前和最大窗口度量值相同,因为应用始终运行并占据整个可用显示区域,无论是在单屏还是双屏模式下。
在具有水平折叠的可折叠模拟器中,当应用跨越整个物理显示屏和应用在多窗口模式下运行时,值会有所不同。
正如左侧图像所示,这两个度量值相同,因为正在运行的应用使用了当前和最大可用显示区域。
但在右侧图像中,应用在多窗口模式下运行,您可以看到当前度量值显示了应用在分屏模式的特定区域(顶部)中运行的区域的尺寸,并且您可以看到最大度量值显示了设备拥有的最大显示区域。
由WindowMetricsCalculator
提供的度量值对于确定应用正在使用或可以使用窗口区域非常有用。
7. 可视化FoldingFeature信息
现在注册以接收窗口布局变化以及模拟器或设备DisplayFeatures
的特性和边界。
要从WindowInfoTracker#windowLayoutInfo()
收集信息,请使用为每个Lifecycle
对象定义的lifecycleScope
。在此作用域中启动的任何协程都将在Lifecycle销毁时取消。您可以通过lifecycle.coroutineScope
或lifecycleOwner.lifecycleScope
属性访问生命周期的协程作用域。
在MainActivity
的onCreate
方法中,调用一个函数来获取和显示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
,获取flow
的WindowLayoutInfo
,其中包含所有显示功能的列表。添加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
调用。实现此函数以显示和打印从flow
的WindowLayoutInfo
接收到的信息。检查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"
}
}
运行应用程序。在双屏模拟器中,您有
WindowLayoutInfo
为空。它有一个空的List<DisplayFeature>
。但是,如果您有一个中间带有铰链的模拟器,为什么您没有从WindowManager获取信息呢?
WindowManager(通过WindowInfoTracker
)只会在应用跨越显示屏(物理的或非物理的)时提供WindowLayoutInfo
数据(设备功能类型、设备功能边界和设备姿态)。因此,在上图中,当应用在单屏模式下运行时,WindowLayoutInfo
为空。
拥有这些信息后,您可以知道应用在何种模式下运行(单屏模式或跨越多个显示屏),然后您可以在UI/UX中进行更改,提供更适合这些特定配置的更好的用户体验。
在没有两个物理显示屏的设备上(它们通常没有物理铰链),应用程序可以使用多窗口模式并排运行。在这些设备上,当应用在多窗口模式下运行时,应用的行为就像在单屏设备上的上一示例一样。当应用占据所有逻辑显示屏时,它的行为与应用跨越多个显示屏时相同。参见下图
当应用在多窗口模式下运行时,WindowManager提供一个空的List<LayoutInfo>
。
总而言之,只有当应用占据所有逻辑显示屏并与设备功能(折叠或铰链)相交时,才会获取WindowLayoutInfo
数据。在所有其他情况下,您都不会获得任何信息。
当您跨越多个显示屏扩展应用时会发生什么?在双屏模拟器中,WindowLayoutInfo
将具有一个FoldingFeature
对象,该对象提供有关设备功能的数据:一个HINGE
、该功能的边界(Rect
(0, 0 - 1434, 1800))以及设备的姿态(状态)(FLAT
)。
让我们看看每个字段的含义
type = TYPE_HINGE
:此双屏模拟器镜像具有物理铰链的真实Surface Duo设备,这就是WindowManager报告的内容。Bounds
[0, 0 - 1434, 1800]:表示应用程序窗口中窗口坐标空间内功能的边界矩形。如果您阅读Surface Duo设备的尺寸规格,您会看到铰链位于这些边界(左、上、右、下)报告的确切位置。State
:有两个不同的值代表设备的设备姿态(状态)。HALF_OPENED
:可折叠设备的铰链处于打开和关闭状态之间的中间位置,并且灵活屏幕的各个部分或物理屏幕面板之间存在非平面角度。FLAT
:可折叠设备完全打开,呈现给用户的屏幕空间是平坦的。
模拟器默认以180度打开,因此WindowManager返回的姿态为FLAT
。
如果您使用“虚拟传感器”选项将模拟器的姿态更改为“半开”姿态,WindowManager将通知您新的位置:HALF_OPENED
。
使用WindowManager调整您的UI/UX
如显示窗口布局信息的图所示,显示的信息被显示功能裁剪了,这里也是一样。
这不是最佳的用户体验。您可以使用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
接口列表,你需要将其转换为 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
会考虑功能的位置,因此其内容不会被截断或隐藏。
在双屏模拟器(上图,左)中,你可以看到跨屏幕显示内容并被铰链截断的 TextView
现在不再被截断,因此没有丢失信息。
在折叠式模拟器(上图,右)中,你会看到一条浅红色的线,表示折叠式显示功能的位置,并且 TextView
现在已放置在该功能下方。因此,当设备折叠时(例如,以笔记本电脑姿势折叠 90 度),没有任何信息受到该功能的影响。
如果你想知道双屏模拟器上的显示功能在哪里(因为它是一种铰链式设备),表示该功能的视图被铰链隐藏了。但是,如果应用从跨度状态更改为非跨度状态,你将看到它位于与功能相同的位置,并具有正确的宽度和高度。
8. 其他 Jetpack WindowManager 构件
除了主要的 WindowManager 构件之外,WindowManager 还提供其他有用的构件,这些构件可以帮助你以不同的方式与组件交互,同时考虑到构建应用时使用的当前环境。
Java 构件
如果你使用的是 Java 编程语言而不是 Kotlin,或者如果通过回调监听事件更适合你的架构,那么 WindowManager 的 Java 构件非常有用,因为它提供了一个 Java 友好的 API 来注册和注销通过回调监听事件。
RxJava 构件
如果你已经在使用 RxJava
(版本 2
或 3
),你可以使用特定的构件来帮助你保持代码的一致性,无论你使用的是 Observables
还是 Flowables
。
9. 使用 Jetpack WindowManager 进行测试
在任何模拟器或设备上测试折叠姿态对于测试如何在 FoldingFeature
周围放置 UI 元素非常有用。
为此,WindowManager 提供了一个非常有用的构件用于 instrumentation 测试。
让我们看看如何使用它。
除了主要的 WindowManager 依赖项之外,我们还在应用的 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 进行测试