让应用具备折叠感知能力

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

了解设备是否支持桌面模式或图书配置等配置 状态可以指导有关支持不同布局或提供 特定功能

窗口信息

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

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

Kotlin 数据流

若要开始和停止 WindowLayoutInfo 数据收集,您可以使用可重启的 生命周期感知型协程,其中 repeatOnLifecycle 代码块是 当生命周期至少为 STARTED 时执行,在 生命周期为 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 数据流的情况下更新 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 支持

如果您已在使用 RxJava23 版), 您可以利用一些工件来使用 ObservableFlowable 在不使用 Kotlin 数据流的情况下收集 WindowLayoutInfo 更新。

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 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)表示折叠功能的边界矩形(如折叠边或铰链)。可以用边界将元素放到与功能相关的屏幕上:

Kotlin

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.
                }

        }
    }
}

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

桌面折叠状态

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

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

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

您可以使用 FoldingFeature.StateFoldingFeature.Orientation 确定设备是否处于桌面模式:

Kotlin

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

Java

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 测试。

Kotlin

if (WindowSdkExtensions.getInstance().extensionsVersion >= 6) {
    val postures = WindowInfoTracker.getOrCreate(context).supportedPostures
    if (postures.contains(TABLE_TOP)) {
        // Device supports tabletop posture.
   }
}

Java

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

示例

图书模式

另一项独特的可折叠功能是图书折叠状态,即设备处于半开状态 合页是垂直的图书模式非常适合阅读电子书。在大屏可折叠设备上以双页版式显示图书,就像装订的书籍一样可以打开,畅享纸质书般的阅读体验。

如果您想捕捉不同的方面,也可用于摄影 无需动手即可拍照。

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

Kotlin

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

Java

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

窗口大小变化

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

借助 Jetpack WindowManager WindowMetricsCalculator 类,您可以 检索当前和最大窗口指标。喜欢这个平台 WindowMetrics 在 API 级别 30 中引入,即 WindowManager WindowMetrics:提供窗口边界,但该 API 具有向后兼容性 最低为 API 级别 14。

请参阅使用窗口大小类别

其他资源

示例

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

Codelab