[Android翻译]解除对WindowManager的束缚
原文作者:medium.com/@pmaggi
发布时间:2021年8月20日 - 6分钟阅读
为可折叠设备和大屏幕设备优化应用程序
Android的屏幕尺寸正在迅速变化,随着平板电脑和可折叠设备的不断普及,了解你的应用程序的窗口尺寸和状态对于开发一个响应式的UI至关重要。Jetpack WindowManager现在处于测试阶段,它是一个库和API,提供类似于Android框架WindowManager的功能,包括对响应式UI的支持、检测屏幕变化的回调适配器以及窗口测试API。但Jetpack WindowManager还提供了对新型设备的支持,如可折叠设备和Chrome OS等窗口环境。
新的WindowManager APIs包括以下内容。
- WindowLayoutInfo:包含了一个窗口的显示特征,例如窗口是否包含了折叠或铰链
- FoldingFeature:使你能够监测可折叠设备的折叠状态,以确定设备的姿势
- WindowMetrics:提供当前窗口的指标或整体显示的指标
Jetpack WindowManager与安卓系统没有捆绑,允许更快地迭代API,以快速支持快速发展的设备市场,并使应用程序开发人员能够采用库的更新,而不必等待最新的安卓版本。
现在该库已经进入测试阶段,我们鼓励所有的开发者采用Jetpack WindowManager,它具有设备无关的API,测试API,以及带来WindowMetrics,使你能够轻松应对窗口尺寸的变化。逐步过渡到测试版意味着你可以对你所采用的API有信心,使你可以完全专注于在这些设备上建立令人兴奋的体验。Jetpack WindowManager支持低至API 14的功能检测。
该库
Jetpack WindowManager是一个现代的、以Kotlin为首的库,它支持新的设备形态因素,并提供 "类似AppCompat "的功能,以构建具有响应式用户界面的应用程序。
折叠状态
这个库所提供的最明显的功能是对可折叠设备的支持。当设备的折叠状态发生变化时,应用程序可以接收事件,允许更新用户界面以支持新的用户互动。
三星Galaxy Z Fold2上的Google Duo
请看这个Google Duo案例研究,它介绍了如何为可折叠设备添加支持。
有两种可能的折叠状态:平面和半开放。对于FLAT,你可以认为表面是完全平坦地打开的,尽管在某些情况下它可能被铰链分割。对于HALF_OPENED,窗口至少有两个逻辑区域。下面,我们有图片说明每种状态的可能性。
折叠状态。平坦和半开放
当应用程序处于活动状态时,应用程序可以通过收集Kotlin流的事件来接收关于折叠状态变化的信息。 为了开始和停止事件收集,我们可以使用一个生命周期范围,正如 repeatOnLifeCycle API设计故事博文和下面的代码示例中所解释的。
lifecycleScope.launch(Dispatchers.Main) {
// The block passed to repeatOnLifecycle is executed when the lifecycle
// is at least STARTED and is cancelled when the lifecycle is STOPPED.
// It automatically restarts the block when the lifecycle is STARTED again.
lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
// Safely collects from windowInfoRepository when the lifecycle is STARTED
// and stops collection when the lifecycle is STOPPED.
windowInfoRepository.windowLayoutInfo
.collect { newLayoutInfo ->
updateStateLog(newLayoutInfo)
updateCurrentState(newLayoutInfo)
}
}
}
然后,应用程序可以使用收到的WindowLayoutInfo对象中的可用信息,在应用程序对用户可见时更新其布局。
FoldingFeature包括铰链方向和折叠功能是否创建两个逻辑屏幕区域(isSeparating属性)等信息。我们可以使用这些值来检查设备是否处于桌面模式(半开,铰链水平)。
设备处于TableTop模式
private fun isTableTopMode(foldFeature: FoldingFeature) =
foldFeature.isSeparating &&
foldFeature.orientation == FoldingFeature.Orientation.HORIZONTAL
或处于书本模式(半开,铰链垂直)。
设备在书本模式下
private fun isBookMode(foldFeature: FoldingFeature) =
foldFeature.isSeparating &&
foldFeature.orientation == FoldingFeature.Orientation.VERTICAL
你可以在《可折叠设备上的桌面模式》一文中看到一个例子,说明如何为一个媒体播放器应用程序做到这一点。
注意:在主/UI线程上收集这些事件很重要,以避免UI和处理这些事件之间的同步问题。
对响应式UI的支持
由于安卓系统中的屏幕尺寸变化非常频繁,因此开始设计完全自适应和响应式的UI非常重要。WindowManager库中包含的另一个功能是能够检索当前和最大的窗口度量信息。这与API 30中包含的框架WindowMetrics API提供的信息类似,但它向后兼容到API 14。
Jetpack WindowManager提供了两种检索WindowMetrics信息的方式,作为流事件流或通过WindowMetricsCalculator类同步进行。
当在视图中写代码时,异步的API可能太难处理(比如onMeasure),就使用WindowMetricsCalculator。
val windowMetrics =
WindowMetricsCalculator.getOrCreate().computeCurrentWindowMetrics(activity)
另一个用例是在测试中(见下面的测试)。
对于应用程序UI的更高层次的处理,使用WindowInfoRepository#currentWindowMetrics来获得库的通知,当有一个窗口大小的变化时,独立于这个变化是否触发了配置的变化。
下面是一个如何根据你的可用区域的大小来切换你的布局的例子。
// Create a new coroutine since repeatOnLifecycle is a suspend function
lifecycleScope.launch(Dispatchers.Main) {
// The block passed to repeatOnLifecycle is executed when the lifecycle
// is at least STARTED and is cancelled when the lifecycle is STOPPED.
// It automatically restarts the block when the lifecycle is STARTED again.
lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
// Safely collect from currentWindowMetrics when the lifecycle is STARTED
// and stops collection when the lifecycle is STOPPED
windowInfoRepository.currentWindowMetrics
.collect { windowMetrics ->
val currentBounds = windowMetrics.bounds
Log.i(TAG, "New bounds: {$currentBounds}")
// We can update the layout if needed from here
}
}
}
回调适配器 要在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对于维持长期支持至关重要。让我们来谈谈如何在正常设备上测试可折叠的姿势。
到目前为止,我们已经看到Jetpack WindowManager库在设备姿态发生变化时通知你的应用程序,这样你就可以修改应用程序的布局。
该库在androidx.window:window-testing中提供了WindowLayoutInfoPublisherRule,它使你可以在测试FoldingFeature的支持下发布WindowInfoLayout。
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匹配器检查我们正在测试的活动的布局是否符合预期。
下面是一个测试发布FoldingFeature的例子,它在屏幕中心有一个HALF_OPENED的垂直铰链。
@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()
)
}
}
// Checks that start_layout is on the left of end_layout with a vertical folding feature.
// This requires to run the test on a big enough screen to fit both views on screen
onView(withId(R.id.start_layout))
.check(isCompletelyLeftOf(withId(R.id.end_layout)))
}
请看它的运行情况。代码样本
GitHub上的一个最新样本显示了如何使用Jetpack WindowManager库来检索显示姿势信息,从WindowLayoutInfo流中收集信息或通过WindowInfoRepositoryCallbackAdapter注册一个回调。
该样本还包括一些测试,可以在任何设备或模拟器上运行。
在你的应用程序中采用WindowManager
可折叠和双屏设备不再是实验性的或未来主义的--大的显示区域和额外的姿势具有被证实的用户价值,而且现在有更多的设备可以供你的用户使用。可折叠设备和双屏设备代表了智能手机的自然进化。对于安卓开发者来说,他们提供了进入一个正在增长的高端市场的机会,这也得益于设备制造商的重新关注。
我们去年推出了Jetpack WindowManager alpha01。从那时起,该库有了稳定的发展,针对早期的反馈有了一些很大的改进。该库现在已经接受了Android的Kotlin优先理念,从回调驱动的模型过渡到coroutines和flow。随着WindowManager现在处于测试阶段,该API已经稳定,我们强烈建议采用。 而更新并不限于此。我们计划为该库添加更多的功能,并将其发展成一个用于系统UI的非捆绑式AppCompat,使开发者能够在所有的Android设备上轻松实现现代的、响应式的UI。