支持不同显示尺寸

支持不同的显示尺寸可让最广泛的设备和最多的用户访问您的应用。

为了支持尽可能多的显示尺寸(无论是不同的设备屏幕还是多窗口模式下不同的应用窗口),请将您的应用布局设计为响应式和自适应式。响应式/自适应布局无论显示尺寸如何,都能提供优化的用户体验,使您的应用能够适应手机、平板电脑、折叠屏设备、ChromeOS 设备、纵向和横向方向,以及可调整大小的显示配置(如分屏模式和桌面窗口化)。

响应式/自适应布局会根据可用的显示空间而变化。变化范围从填充空间的细微布局调整(响应式设计)到完全用另一个布局替换,以便您的应用能最好地适应不同的显示尺寸(自适应设计)。

作为声明式 UI 工具包,Jetpack Compose 是设计和实现动态变化的布局的理想选择,以便在不同显示尺寸上以不同方式呈现内容。

明确内容级可组合项的大型布局更改

应用级和内容级可组合项占用应用可用的所有显示空间。对于这些类型的可组合项,在大屏幕上更改应用的整体布局可能是有意义的。

避免使用物理硬件值来做出布局决策。 试图根据固定的有形值(设备是平板电脑吗?物理屏幕是否具有特定的宽高比?)进行决策可能会很诱人,但这些问题的答案对于确定 UI 可用空间可能没有用。

图 1. 手机、折叠屏、平板电脑和笔记本电脑外形尺寸

在平板电脑上,应用可能在多窗口模式下运行,这意味着应用可能与其他应用共享屏幕。在桌面窗口模式或 ChromeOS 上,应用可能在可调整大小的窗口中。甚至可能有不止一个物理屏幕,例如折叠屏设备。在所有这些情况下,物理屏幕尺寸与决定如何显示内容无关。

相反,您应该根据 Jetpack WindowManager 库提供的当前窗口指标描述的分配给您应用的屏幕实际部分做出决策。有关如何在 Compose 应用中使用 WindowManager 的示例,请参阅 JetNews 示例。

使您的布局适应可用显示空间还可以减少支持 ChromeOS 等平台以及平板电脑和折叠屏等外形尺寸所需的特殊处理量。

确定应用可用空间的指标后,将原始尺寸转换为窗口尺寸类别,如使用窗口尺寸类别中所述。窗口尺寸类别是旨在平衡应用逻辑简单性和优化应用以适应大多数显示尺寸的灵活性的断点。窗口尺寸类别指代应用的整个窗口,因此使用这些类别进行影响整体应用布局的布局决策。您可以将窗口尺寸类别作为状态向下传递,或者执行额外的逻辑来创建派生状态以向下传递给嵌套的可组合项。

@Composable
fun MyApp(
    windowSizeClass: WindowSizeClass = currentWindowAdaptiveInfo().windowSizeClass
) {
    // Decide whether to show the top app bar based on window size class.
    val showTopAppBar = windowSizeClass.isHeightAtLeastBreakpoint(WindowSizeClass.HEIGHT_DP_MEDIUM_LOWER_BOUND)

    // MyScreen logic is based on the showTopAppBar boolean flag.
    MyScreen(
        showTopAppBar = showTopAppBar,
        /* ... */
    )
}

分层方法将显示尺寸逻辑限制在单个位置,而不是将其分散到应用中许多需要保持同步的位置。单个位置会生成状态,可以像任何其他应用状态一样显式传递给其他可组合项。显式传递状态简化了单个可组合项,因为这些可组合项会接受窗口尺寸类别或指定的配置以及其他数据。

灵活的嵌套可组合项可重用

当可组合项可以放置在各种位置时,它们更具可重用性。如果可组合项必须放置在特定位置并具有特定大小,则它在其他上下文中不太可能可重用。这也意味着单个可重用可组合项应避免隐式依赖于全局显示尺寸信息。

设想一个嵌套的可组合项,它实现了一个列表-详情布局,该布局可以显示单个窗格或并排的两个窗格

An app showing two panes side by side.
图 2. 显示典型列表-详情布局的应用——1 是列表区域;2 是详情区域。

列表-详情决策应该是应用整体布局的一部分,因此该决策是从内容级可组合项向下传递的

@Composable
fun AdaptivePane(
    showOnePane: Boolean,
    /* ... */
) {
    if (showOnePane) {
        OnePane(/* ... */)
    } else {
        TwoPane(/* ... */)
    }
}

如果您希望可组合项根据可用显示空间独立更改其布局,例如,一张卡片在空间允许的情况下显示更多详情,该怎么办?您希望根据某些可用显示尺寸执行一些逻辑,但具体是哪个尺寸?

图 3. 窄卡片仅显示图标和标题,宽卡片显示图标、标题和简短描述。

避免尝试使用设备的实际屏幕尺寸。这对于不同类型的屏幕不准确,如果应用不是全屏,也不准确。

由于可组合项不是内容级可组合项,请勿直接使用当前窗口指标。如果组件带有内边距(例如使用 insets),或者如果应用包含导航栏或应用栏等组件,则可组合项可用的显示空间量可能与应用可用的总空间量显著不同。

使用可组合项实际用于渲染自身的宽度。您有两个选项可以获取该宽度

  • 如果您想改变内容显示的位置方式,请使用修饰符集合或自定义布局使布局具有响应性。这可能像让子项填充所有可用空间一样简单,或者在空间足够的情况下使用多列布局子项。

  • 如果您想改变显示什么,请使用 BoxWithConstraints 作为更强大的替代方案。BoxWithConstraints 提供测量约束,您可以使用这些约束根据可用显示空间调用不同的可组合项。但是,这会带来一些开销,因为 BoxWithConstraints 会将组合推迟到布局阶段,此时这些约束才为人所知,从而导致在布局期间执行更多工作。

@Composable
fun Card(/* ... */) {
    BoxWithConstraints {
        if (maxWidth < 400.dp) {
            Column {
                Image(/* ... */)
                Title(/* ... */)
            }
        } else {
            Row {
                Column {
                    Title(/* ... */)
                    Description(/* ... */)
                }
                Image(/* ... */)
            }
        }
    }
}

确保所有数据都可用于不同的显示尺寸

在实现利用额外显示空间的可组合项时,您可能会想偷懒,将数据作为当前显示尺寸的副作用加载。

然而,这样做与单向数据流的原则相悖,在单向数据流中,数据可以提升并提供给可组合项以适当地渲染。应该向可组合项提供足够的数据,以便可组合项始终具有足够的内容以适应任何显示尺寸,即使内容的某些部分可能并非总是使用。

@Composable
fun Card(
    imageUrl: String,
    title: String,
    description: String
) {
    BoxWithConstraints {
        if (maxWidth < 400.dp) {
            Column {
                Image(imageUrl)
                Title(title)
            }
        } else {
            Row {
                Column {
                    Title(title)
                    Description(description)
                }
                Image(imageUrl)
            }
        }
    }
}

基于 Card 示例,请注意 description 始终传递给 Card。即使 description 仅在宽度允许显示时使用,Card 始终需要 description,无论可用宽度如何。

始终传递足够的内容通过使其无状态并避免在显示尺寸之间切换时触发副作用(这可能由于窗口大小调整、方向更改或设备折叠和展开而发生)来简化自适应布局。

此原则还允许在布局更改时保留状态。通过提升可能并非在所有显示尺寸都使用的信息,您可以在布局尺寸更改时保留应用状态。例如,您可以提升一个 showMore 布尔标志,以便在显示调整大小导致布局在隐藏和显示内容之间切换时保留应用状态。

@Composable
fun Card(
    imageUrl: String,
    title: String,
    description: String
) {
    var showMore by remember { mutableStateOf(false) }

    BoxWithConstraints {
        if (maxWidth < 400.dp) {
            Column {
                Image(imageUrl)
                Title(title)
            }
        } else {
            Row {
                Column {
                    Title(title)
                    Description(
                        description = description,
                        showMore = showMore,
                        onShowMoreToggled = { newValue ->
                            showMore = newValue
                        }
                    )
                }
                Image(imageUrl)
            }
        }
    }
}

了解详情

要了解有关 Compose 中自适应布局的更多信息,请参阅以下资源

示例应用

  • CanonicalLayouts 是一个经过验证的设计模式存储库,可在大型显示设备上提供最佳用户体验
  • JetNews 展示了如何设计一个应用,使其 UI 适应并利用可用显示空间
  • Reply 是一个支持移动设备、平板电脑和折叠屏设备的自适应示例
  • Now in Android 是一个使用自适应布局来支持不同显示尺寸的应用

视频