支持不同屏幕尺寸

对不同屏幕尺寸的支持使各种设备和更多用户能够访问您的应用。

为了尽可能支持更多屏幕尺寸,请设计您的应用布局使其具有响应性和自适应性。响应式/自适应布局无论屏幕尺寸如何都能提供优化的用户体验,使您的应用能够适应手机、平板电脑、折叠屏设备、ChromeOS 设备、纵向和横向方向以及可调整大小的配置,例如 多窗口模式

响应式/自适应布局会根据可用的显示空间进行更改。更改范围从小幅调整布局以填充空间(响应式设计)到完全用另一个布局替换一个布局,以便您的应用能够最佳地适应不同的显示尺寸(自适应设计)。

作为一种声明式 UI 工具包,Jetpack Compose 非常适合设计和实现动态更改布局以在各种显示尺寸上以不同方式呈现内容的布局。

对屏幕级可组合项进行大型布局更改

当使用 Compose 布局整个应用程序时,应用程序级和屏幕级可组合项将占用应用程序用于呈现的所有空间。在此设计级别,更改屏幕的整体布局以利用更大的屏幕可能是有意义的。

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

A diagram showing several different device form factors, including phone, foldable, tablet, and laptop.
图 1. 手机、折叠屏、平板电脑和笔记本电脑的外形尺寸

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

相反,您应该根据实际分配给应用的屏幕部分(例如,Jetpack WindowManager 库提供的当前窗口指标)做出决策。要了解如何在 Compose 应用中使用 WindowManager,请查看 JetNews 示例。

遵循此方法使您的应用更具灵活性,因为它将在上述所有场景中都能很好地运行。使您的布局适应可用的屏幕空间还可以减少支持 ChromeOS 等平台以及平板电脑和折叠屏等外形尺寸所需的特殊处理量。

观察到应用可用的相关空间后,可以将其原始尺寸转换为有意义的尺寸类别,如 使用窗口尺寸类别 中所述。这将尺寸分组到标准尺寸桶中,这些桶是旨在平衡简单性和灵活性的断点,以便为大多数独特情况优化您的应用。这些尺寸类别指的是应用的整体窗口,因此请将这些类别用于影响整体屏幕布局的布局决策。您可以将这些尺寸类别向下传递为状态,也可以执行其他逻辑以创建派生状态以向下传递到嵌套的可组合项。

@Composable
fun MyApp(
    windowSizeClass: WindowSizeClass = currentWindowAdaptiveInfo().windowSizeClass
) {
    // Perform logic on the size class to decide whether to show the top app bar.
    val showTopAppBar = windowSizeClass.windowHeightSizeClass != WindowHeightSizeClass.COMPACT

    // MyScreen knows nothing about window sizes, and performs logic based on a Boolean flag.
    MyScreen(
        showTopAppBar = showTopAppBar,
        /* ... */
    )
}

这种分层方法将屏幕尺寸逻辑限制在一个位置,而不是将其分散到应用中的许多需要保持同步的位置。此单个位置生成状态,该状态可以像传递任何其他应用状态一样显式地向下传递到其他可组合项。显式传递状态简化了单个可组合项,因为它们将只是采用尺寸类别或指定配置以及其他数据作为参数的普通可组合函数。

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

可组合项在可以放置在各种位置时更具可重用性。如果可组合项假设它将始终放置在特定位置并具有特定大小,那么将其重用于其他位置或在可用空间不同的情况下将更加困难。这也意味着单个可重用的可组合项应避免隐式依赖于“全局”大小信息

考虑以下示例:假设一个嵌套的可组合项实现了 列表-详情布局,该布局可能会显示一个窗格或两个并排的窗格。

Screenshot of an app showing two panes side by side.
图 2. 应用屏幕截图,显示典型的列表-详情布局——1 是列表区域;2 是详情区域。

我们希望此决策成为应用整体布局的一部分,因此我们像上面看到的那样从屏幕级可组合项向下传递决策。

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

如果我们希望可组合项根据可用空间独立更改其布局怎么办?例如,如果空间允许,卡片希望显示其他详细信息。我们希望根据某些可用大小执行一些逻辑,但具体是哪个大小?

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

如上所述,我们应该避免尝试使用设备实际屏幕的大小。这对于多个屏幕来说是不准确的,如果应用不是全屏的,它也不准确。

由于可组合项不是屏幕级可组合项,因此为了最大程度地提高可重用性,我们也不应直接使用当前窗口指标。如果组件使用填充(例如内边距)放置,或者如果存在导航栏或应用栏等组件,则可组合项可用的空间量可能与应用可用的整体空间量有很大差异。

因此,我们应该使用可组合项实际用于呈现自身的宽度。我们有两个选项可以获取该宽度。

如果要更改内容的位置显示方式,可以使用修饰符集合或 自定义布局 来使布局具有响应性。这可能很简单,例如让某个子项填充所有可用空间,或者如果空间足够大,则使用多列布局子项。

如果要更改显示的内容,可以使用 BoxWithConstraints 作为更强大的替代方案。此可组合项提供 测量约束,您可以使用这些约束根据可用空间调用不同的可组合项。但是,这样做会付出一些代价,因为 BoxWithConstraints 会将合成推迟到 Layout 阶段,此时这些约束已知,导致在布局期间执行更多工作。

@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 也始终需要它,无论可用宽度如何。

始终传递数据通过使自适应布局的状态更少,从而简化了自适应布局,并避免在尺寸之间切换时触发副作用(这可能是由于窗口调整大小、方向更改或折叠和展开设备导致的)。

此原则还允许在布局更改之间保留状态。通过提升在所有尺寸上可能都不会使用的信息,我们可以在布局尺寸更改时保留用户的状态。例如,我们可以提升 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 是一个使用自适应布局来支持不同屏幕尺寸的应用。

视频