借助展开的大型显示屏和独特的折叠状态,我们能够在可折叠设备上打造全新用户体验。如需让应用具备折叠感知能力,请使用 Jetpack WindowManager 库,它为可折叠设备的窗口功能(如折叠边或合页)提供了一个 API Surface。当应用具备折叠感知能力时,应用可以调整其布局,避免将重要内容放在折叠边或合页区域,并将折叠边或合页用作自然分隔符。
窗口信息
Jetpack WindowManager 中的 WindowInfoTracker
接口会公开窗口布局信息。该接口的 windowLayoutInfo()
方法会返回一个 WindowLayoutInfo
数据流,该数据流会将可折叠设备的折叠状态告知您的应用。WindowInfoTracker
getOrCreate()
方法会创建 WindowInfoTracker
的实例。
WindowManager 支持通过 Kotlin Flow 和 Java 回调收集 WindowLayoutInfo
数据。
Kotlin Flow
如需开始和停止 WindowLayoutInfo
数据收集,您可以使用可重启的生命周期感知型协程,在生命周期至少处于 STARTED
时,系统会执行 repeatOnLifecycle
代码块,当生命周期为 STOPPED
时停止执行。当生命周期再次处于 STARTED
时,系统会自动重新开始执行代码块。在以下示例中,代码块会收集并使用 WindowLayoutInfo
数据:
class DisplayFeaturesActivity : AppCompatActivity() {
private lateinit var binding: ActivityDisplayFeaturesBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityDisplayFeaturesBinding.inflate(layoutInflater)
setContentView(binding.root)
lifecycleScope.launch(Dispatchers.Main) {
lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
WindowInfoTracker.getOrCreate(this@DisplayFeaturesActivity)
.windowLayoutInfo(this@DisplayFeaturesActivity)
.collect { newLayoutInfo ->
// Use newLayoutInfo to update the layout.
}
}
}
}
}
Java 回调
借助 androidx.window:window-java
依赖项中包含的回调兼容性层,您无需使用 Kotlin Flow 即可收集 WindowLayoutInfo
更新。该工件包含 WindowInfoTrackerCallbackAdapter
类,该类会调整 WindowInfoTracker
来支持注册(和取消注册)用于接收 WindowLayoutInfo
更新的回调,例如:
public class SplitLayoutActivity extends AppCompatActivity {
private WindowInfoTrackerCallbackAdapter windowInfoTracker;
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());
windowInfoTracker =
new WindowInfoTrackerCallbackAdapter(WindowInfoTracker.getOrCreate(this));
}
@Override
protected void onStart() {
super.onStart();
windowInfoTracker.addWindowLayoutInfoListener(
this, Runnable::run, layoutStateChangeCallback);
}
@Override
protected void onStop() {
super.onStop();
windowInfoTracker
.removeWindowLayoutInfoListener(layoutStateChangeCallback);
}
class LayoutStateChangeCallback implements Consumer<WindowLayoutInfo> {
@Override
public void accept(WindowLayoutInfo newLayoutInfo) {
SplitLayoutActivity.this.runOnUiThread( () -> {
// Use newLayoutInfo to update the layout.
});
}
}
}
RxJava 支持
如果您已在使用 RxJava
(版本 2
或 3
),您可以通过工件来使用 Observable
或 Flowable
收集 WindowLayoutInfo
更新,而不使用 Kotlin Flow。
androidx.window:window-rxjava2
和 androidx.window:window-rxjava3
依赖项提供的兼容性层包含 WindowInfoTracker#windowLayoutInfoFlowable()
和 WindowInfoTracker#windowLayoutInfoObservable()
方法,使您的应用能够接收 WindowLayoutInfo
更新,例如:
class RxActivity: AppCompatActivity {
private lateinit var binding: ActivityRxBinding
private var disposable: Disposable? = null
private lateinit var observable: Observable<WindowLayoutInfo>
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
binding = ActivitySplitLayoutBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
// Create a new observable
observable = WindowInfoTracker.getOrCreate(this@RxActivity)
.windowLayoutInfoObservable(this@RxActivity)
}
@Override
protected void onStart() {
super.onStart();
// Subscribe to receive WindowLayoutInfo updates
disposable?.dispose()
disposable = observable
.observeOn(AndroidSchedulers.mainThread())
.subscribe { newLayoutInfo ->
// Use newLayoutInfo to update the layout
}
}
@Override
protected void onStop() {
super.onStop();
// Dispose the WindowLayoutInfo observable
disposable?.dispose()
}
}
可折叠设备显示屏的功能
Jetpack WindowManager 的 WindowLayoutInfo
类可将显示窗口的功能作为 DisplayFeature
元素列表提供。
FoldingFeature
是一种类型的 DisplayFeature
,它提供了有关可折叠设备显示屏的信息,其中包括:
state
:设备的折叠状态,即FLAT
或HALF_OPENED
orientation
:折叠边或合页的方向,即HORIZONTAL
或VERTICAL
occlusionType
:折叠边或合页是否遮住了显示屏的一部分,即NONE
或FULL
isSeparating
:折叠边或合页是否创建了两个逻辑显示区域,即 true 或 false
HALF_OPENED
的可折叠设备始终将 isSeparating
报告为 true,因为屏幕被分为两个显示区域。此外,当应用跨两块屏幕时,isSeparating
在双屏设备上始终为 true。
FoldingFeature
bounds
属性(继承自 DisplayFeature
)表示折叠功能的边界矩形(如折叠边或合页)。可以用边界将元素放到与功能相关的屏幕上。
请使用 FoldingFeature
state
来确定设备是处于桌面模式还是图书模式,以便相应地自定义应用布局,例如:
Kotlin
override fun onCreate(savedInstanceState: Bundle?) { ... 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 windowInfoRepo when the lifecycle is STARTED // and stops collection when the lifecycle is STOPPED WindowInfoTracker.getOrCreate(this@MainActivity) .windowLayoutInfo(this@MainActivity) .collect { layoutInfo -> // New posture information val foldingFeature = layoutInfo.displayFeatures .filterIsInstance() .firstOrNull() when { isTableTopPosture(foldingFeature) -> enterTabletopMode(foldingFeature) isBookPosture(foldingFeature) -> enterBookMode(foldingFeature) isSeparating(foldingFeature) -> // Dual-screen device if (foldingFeature.orientation == HORIZONTAL) { enterTabletopMode(foldingFeature) } else { enterBookMode(foldingFeature) } else -> enterNormalMode() } } } } } @OptIn(ExperimentalContracts::class) fun isTableTopPosture(foldFeature : FoldingFeature?) : Boolean { contract { returns(true) implies (foldFeature != null) } return foldFeature?.state == FoldingFeature.State.HALF_OPENED && foldFeature.orientation == FoldingFeature.Orientation.HORIZONTAL } @OptIn(ExperimentalContracts::class) fun isBookPosture(foldFeature : FoldingFeature?) : Boolean { contract { returns(true) implies (foldFeature != null) } return foldFeature?.state == FoldingFeature.State.HALF_OPENED && foldFeature.orientation == FoldingFeature.Orientation.VERTICAL } @OptIn(ExperimentalContracts::class) fun isSeparating(foldFeature : FoldingFeature?) : Boolean { contract { returns(true) implies (foldFeature != null) } return foldFeature?.state == FoldingFeature.State.FLAT && foldFeature.isSeparating }
Java
private WindowInfoTrackerCallbackAdapter windowInfoTracker; private final LayoutStateChangeCallback layoutStateChangeCallback = new LayoutStateChangeCallback(); @Override protected void onCreate(@Nullable Bundle savedInstanceState) { ... windowInfoTracker = new WindowInfoTrackerCallbackAdapter(WindowInfoTracker.getOrCreate(this)); } @Override protected void onStart() { super.onStart(); windowInfoTracker.addWindowLayoutInfoListener( this, Runnable::run, layoutStateChangeCallback); } @Override protected void onStop() { super.onStop(); windowInfoTracker.removeWindowLayoutInfoListener(layoutStateChangeCallback); } class LayoutStateChangeCallback implements Consumer<WindowLayoutInfo> { @Override public void accept(WindowLayoutInfo newLayoutInfo) { // Use newLayoutInfo to update the Layout List<DisplayFeature> displayFeatures = newLayoutInfo.getDisplayFeatures(); for (DisplayFeature feature : displayFeatures) { if (feature instanceof FoldingFeature) { if (isTableTopPosture((FoldingFeature) feature)) { enterTabletopMode(feature); } else if (isBookPosture((FoldingFeature) feature)) { enterBookMode(feature); } else if (isSeparating((FoldingFeature) feature)) { // Dual-screen device if (((FoldingFeature) feature).getOrientation() == FoldingFeature.Orientation.HORIZONTAL) { enterTabletopMode(feature); } else { enterBookMode(feature); } } else { enterNormalMode(); } } } } } private boolean isTableTopPosture(FoldingFeature foldFeature) { return (foldFeature != null) && (foldFeature.getState() == FoldingFeature.State.HALF_OPENED) && (foldFeature.getOrientation() == FoldingFeature.Orientation.HORIZONTAL); } private boolean isBookPosture(FoldingFeature foldFeature) { return (foldFeature != null) && (foldFeature.getState() == FoldingFeature.State.HALF_OPENED) && (foldFeature.getOrientation() == FoldingFeature.Orientation.VERTICAL); } private boolean isSeparating(FoldingFeature foldFeature) { return (foldFeature != null) && (foldFeature.getState() == FoldingFeature.State.FLAT) && (foldFeature.isSeparating() == true); }
在双屏设备上,即使 FoldingFeature
状态为 FLAT
,也请使用为桌面模式和图书模式设计的布局。
当 isSeparating
为 true 时,请避免使界面控件与折叠边或合页靠得太近,否则会难以触及这些控件。请使用 occlusionType
决定是否将内容放置在折叠功能 bounds
内。
窗口大小变化
应用的显示区域可能会因设备配置变更(例如设备折叠或展开、旋转,或窗口在多窗口模式下调整大小)而发生变化。
借助 Jetpack WindowManager 的 WindowMetricsCalculator
类,您可以检索当前和最大窗口指标。如 API 级别 30 中引入的平台 WindowMetrics
,WindowManager WindowMetrics
提供窗口边界,但 API 向后兼容 API 级别 14。
请在 activity 的 onCreate()
或 onConfigurationChanged()
方法中使用 WindowMetrics
来针对当前窗口大小配置应用的布局,例如:
Kotlin
override fun onConfigurationChanged(newConfig: Configuration) { super.onConfigurationChanged(newConfig) val windowMetrics = WindowMetricsCalculator.getOrCreate() .computeCurrentWindowMetrics(this@MainActivity) val bounds = windowMetrics.getBounds() ... }
Java
@Override public void onConfigurationChanged(@NonNull Configuration newConfig) { super.onConfigurationChanged(newConfig); final WindowMetrics windowMetrics = WindowMetricsCalculator.getOrCreate() .computeCurrentWindowMetrics(this); final Rect bounds = windowMetrics.getBounds(); ... }
另请参阅支持不同的屏幕尺寸。
其他资源
示例
- Jetpack WindowManager:如何使用 Jetpack WindowManager 库的示例
- Jetcaster:使用 Compose 实现桌面模式