让您的应用支持折叠

大型展开式显示屏和独特的折叠状态为可折叠设备带来了全新的用户体验。要使您的应用支持折叠,请使用 Jetpack WindowManager 库,该库为可折叠设备窗口功能(例如折叠和铰链)提供了一个 API 表面。当您的应用支持折叠时,它可以调整其布局以避免将重要内容放置在折叠或铰链区域,并使用折叠和铰链作为自然分隔线。

窗口信息

Jetpack WindowManager 中的 WindowInfoTracker 接口公开了窗口布局信息。该接口的 windowLayoutInfo() 方法返回 WindowLayoutInfo 数据流,通知您的应用有关可折叠设备折叠状态的信息。WindowInfoTrackergetOrCreate() 方法创建 WindowInfoTracker 的实例。

WindowManager 提供了对使用 Kotlin Flows 和 Java 回调收集 WindowLayoutInfo 数据的支持。

Kotlin Flows

要启动和停止 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 依赖项中包含的回调兼容层使您能够收集 WindowLayoutInfo 更新,而无需使用 Kotlin Flow。该工件包含 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 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。

FoldingFeaturebounds 属性(继承自 DisplayFeature)表示折叠特征(如折叠或铰链)的边界矩形。边界可用于相对于特征在屏幕上定位元素。

Kotlin

override fun onCreate(savedInstanceState: Bundle?) {
    ...
    lifecycleScope.launch(Dispatchers.Main) {
        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()
                    // 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 对象中包含的信息,您的应用程序可以支持诸如桌面模式之类的姿势,其中手机放在表面上,铰链处于水平位置,可折叠屏幕半开。

桌面模式为用户提供了无需手持手机即可操作手机的便利。桌面模式非常适合观看媒体、拍照和进行视频通话。

A video player app in tabletop mode

使用 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);
}

一旦您知道设备处于桌面模式,请相应地更新您的应用程序布局。对于媒体应用程序,这通常意味着将播放放在折叠上方,并将控件和辅助内容放置在下方,以实现免提观看或聆听体验。

示例

书本模式

另一种独特的可折叠姿势是书本模式,其中设备半开,铰链处于垂直位置。书本模式非常适合阅读电子书。在像装订的书本一样打开的大屏幕可折叠设备上采用两页布局,书本模式可以捕捉到阅读真本书的体验。

如果您想在免提拍照时捕捉不同的纵横比,也可以使用它。

使用与桌面模式相同的技术实施书本模式。唯一的区别是代码应该检查折叠特征方向是垂直的还是水平的。

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 类允许您检索当前和最大窗口指标。与 API 级别 30 中引入的平台 WindowMetrics 一样,WindowManager WindowMetrics 提供窗口边界,但 API 向后兼容到 API 级别 14。

参见 窗口大小类

其他资源

示例

代码实验室