让应用具备折叠感知能力

借助展开的大显示屏和独特的折叠状态,我们能够在可折叠设备上打造全新用户体验。如需让应用具备折叠感知能力,请使用 Jetpack WindowManager 库,它为可折叠设备的窗口功能(如折叠边或合页)提供了一个 API surface。如果应用具备折叠感知能力,就能调整其布局,避免将重要内容放在折叠边或合页区域,并将折叠边或合页用作自然分隔符。

了解设备是否支持桌面折叠状态或图书折叠状态等配置,有助于您做出关于支持不同布局或提供特定功能的决策。

窗口信息

Jetpack WindowManager 中的 WindowInfoTracker 接口会公开窗口布局信息。该接口的 windowLayoutInfo() 方法会返回一个 WindowLayoutInfo 数据流,该数据流会将可折叠设备的折叠状态告知您的应用。WindowInfoTracker#getOrCreate() 方法会创建一个 WindowInfoTracker 实例。

WindowManager 支持使用 Kotlin Flow 和 Java 回调收集 WindowLayoutInfo 数据。

Kotlin 数据流

如需开始和停止 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(版本 23),则可以通过工件来使用 ObservableFlowable 收集 WindowLayoutInfo 更新,而无需使用 Kotlin Flow。

androidx.window:window-rxjava2androidx.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 of the WindowLayoutInfo observable.
        disposable
?.dispose()
   
}
}

可折叠设备显示屏的功能

Jetpack WindowManager 的 WindowLayoutInfo 类会以 DisplayFeature 元素列表的形式提供显示窗口的功能。

FoldingFeature 是一种 DisplayFeature,它提供了有关可折叠设备显示屏的信息,其中包括:

  • state:设备的折叠状态,即 FLATHALF_OPENED

  • orientation:折叠边或铰链的方向,即 HORIZONTALVERTICAL

  • occlusionType:折叠边或铰链是否遮住了显示屏的一部分,即 NONEFULL

  • isSeparating:折叠边或铰链是否创建了两个逻辑显示区域,即 true 或 false

HALF_OPENED 的可折叠设备始终将 isSeparating 报告为 true,因为屏幕被分为两个显示区域。此外,当应用跨两块屏幕时,isSeparating 在双屏设备上始终为 true。

FoldingFeature bounds 属性(继承自 DisplayFeature)表示折叠功能(如折叠边或合页)的边界矩形。边界可用于相对于地图项在屏幕上定位元素:

KotlinJava
override fun onCreate(savedInstanceState: Bundle?) {
   
...
    lifecycleScope
.launch(Dispatchers.Main) {
        lifecycle
.repeatOnLifecycle(Lifecycle.State.STARTED) {
           
// Safely collects from WindowInfoTracker 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<FoldingFeature>()
                       
.firstOrNull()
                   
// Use information from the foldingFeature object.
               
}

       
}
   
}
}
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) {
               
// Use information from the feature object.
           
}
       
}
   
}
}

桌上模式

利用 FoldingFeature 对象中包含的信息,您的应用可以支持桌上模式等折叠状态。在这种模式下,手机置于平面上、铰链处于水平位置,可折叠屏幕处于半开状态。

桌面模式让用户无需拿着手机就能轻松操作手机。桌面模式非常适合在观看媒体内容、拍照和进行视频通话时使用。

图 1. 桌面模式下的视频播放器应用。

使用 FoldingFeature.StateFoldingFeature.Orientation 确定设备是否处于桌面折叠状态:

KotlinJava

fun isTableTopPosture(foldFeature : FoldingFeature?) : Boolean {
    contract
{ returns(true) implies (foldFeature != null) }
   
return foldFeature?.state == FoldingFeature.State.HALF_OPENED &&
            foldFeature
.orientation == FoldingFeature.Orientation.HORIZONTAL
}


boolean isTableTopPosture(FoldingFeature foldFeature) {
   
return (foldFeature != null) &&
           
(foldFeature.getState() == FoldingFeature.State.HALF_OPENED) &&
           
(foldFeature.getOrientation() == FoldingFeature.Orientation.HORIZONTAL);
}

确定设备处于桌面模式后,应相应地更新应用布局。对于媒体应用,这通常意味着将播放媒体置于折线上方,控件和补充内容置于下方,从而提供免触摸观看或收听体验。

在 Android 15(API 级别 35)及更高版本中,无论设备的当前状态如何,您都可以调用同步 API 来检测设备是否支持桌上模式。

该 API 提供了设备支持的折叠状态列表。如果列表包含桌上模式,您可以拆分应用布局以支持该模式,并针对桌上模式和全屏布局对应用界面运行 A/B 测试。

KotlinJava
if (WindowSdkExtensions.getInstance().extensionsVersion >= 6) {
   
val postures = WindowInfoTracker.getOrCreate(context).supportedPostures
   
if (postures.contains(TABLE_TOP)) {
       
// Device supports tabletop posture.
   
}
}
if (WindowSdkExtensions.getInstance().getExtensionVersion() >= 6) {
   
List<SupportedPosture> postures = WindowInfoTracker.getOrCreate(context).getSupportedPostures();
   
if (postures.contains(SupportedPosture.TABLETOP)) {
       
// Device supports tabletop posture.
   
}
}

示例

图书模式

可折叠设备的另一种独特的折叠状态是图书折叠状态。在这种状态下,设备处于半开状态,并且铰链是垂直的。图书模式适合阅读电子书。在大屏可折叠设备上,像装订的书籍一样打开,采用双页布局,图书折叠状态能带来如同阅读真实图书的体验。

如果您想在免触摸拍照时以不同的宽高比拍摄照片,也可以使用该模式进行摄影。

您可以采用与桌面模式相同的技术实现图书模式。唯一的不同之处是代码应检查折叠功能应用的屏幕方向是否为垂直,而非水平:

KotlinJava
fun isBookPosture(foldFeature : FoldingFeature?) : Boolean {
    contract
{ returns(true) implies (foldFeature != null) }
   
return foldFeature?.state == FoldingFeature.State.HALF_OPENED &&
            foldFeature
.orientation == FoldingFeature.Orientation.VERTICAL
}
boolean isBookPosture(FoldingFeature foldFeature) {
   
return (foldFeature != null) &&
           
(foldFeature.getState() == FoldingFeature.State.HALF_OPENED) &&
           
(foldFeature.getOrientation() == FoldingFeature.Orientation.VERTICAL);
}

窗口大小变化

应用的显示区域可能会因设备配置变更(例如设备折叠或展开、旋转,或窗口在多窗口模式下调整大小)而发生变化。

借助 Jetpack WindowManager 的 WindowMetricsCalculator 类,您可以检索当前和最大窗口指标。与 API 级别 30 中引入的平台 WindowMetrics 一样,WindowManager WindowMetrics 会提供窗口边界,不过 API 可向后兼容到 API 级别 14。

请参阅使用窗口大小类别

其他资源

示例

  • Jetpack WindowManager:如何使用 Jetpack WindowManager 库的示例
  • Jetcaster:使用 Compose 实现桌面模式

Codelab