支持不同的屏幕尺寸

如果应用支持不同的屏幕尺寸,可以让尽可能多的设备和更多的用户使用您的应用。

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

响应式/自适应布局会根据可用的显示空间而发生变化。更改范围很广,从填满空间的小型布局调整,到将一种布局完全替换为另一种布局,让应用能够最好地适应不同的显示尺寸(自适应设计)。

作为声明式界面工具包,Jetpack Compose 非常适合用于设计和实现可动态更改以适应各种屏幕尺寸以不同方式呈现内容的布局。

以显式方式对屏幕级可组合项布局进行大幅调整

使用 Compose 布置整个应用时,应用级和屏幕级可组合项会占用分配给应用进行渲染的所有空间。在应用设计的这个层面上,可能有必要更改屏幕的整体布局以充分利用屏幕空间。

避免根据物理硬件值来确定布局。您可能很想根据固定的有形价值(设备是平板电脑吗?实体屏幕是否有特定的宽高比?),但这些问题的答案可能对于确定界面可使用的空间可能没有用处。

展示多种不同设备外形规格(包括手机、可折叠设备、平板电脑和笔记本电脑)的示意图。
图 1. 手机、可折叠设备、平板电脑和笔记本电脑外形规格

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

相反,您应该根据分配给应用的实际屏幕区域来决定如何显示,例如 Jetpack WindowManager 库提供的当前窗口指标。如需了解如何在 Compose 应用中使用 WindowManager,请查看 JetNews 示例。

遵循此方法可提高应用的灵活性,因为它将在以上所有场景中都能正常运行。让布局适应可用的屏幕空间,还可以减少为支持 ChromeOS 等平台以及平板电脑和可折叠设备等外形规格而执行的特殊处理的工作量。

观察到应用可用的相关空间后,将原始大小转换为有意义的大小类别会很有帮助,如窗口大小类别中所述。这会将尺寸分组到标准尺寸存储分区中,这些是一些断点,目的是平衡简单性和灵活性,以便针对大多数独特情形优化您的应用。这些大小类是指应用的整个窗口,因此请使用这些类来确定影响整体屏幕布局的布局。您可以将这些尺寸类作为状态向下传递,也可以执行其他逻辑来创建派生状态,以传递给嵌套可组合项。

@OptIn(ExperimentalMaterial3AdaptiveApi::class)
@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,
        /* ... */
    )
}

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

灵活的嵌套可组合项可以重复使用

将可组合项放置在各种不同的位置,可以提高它们的可重用性。如果可组合项假定始终将其放置在具有特定尺寸的某个位置,那么将更难在其他位置的其他位置或具有不同可用空间量重复使用该可组合项。这也意味着各个可重复使用的可组合项应避免隐式依赖于“全局”大小信息

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

并排显示两个窗格的应用屏幕截图。
图 2. 显示典型列表-详情布局的应用屏幕截图 - 1 是列表区域;2 是详情区域。

我们希望将此决策纳入应用的整体布局中,因此我们从屏幕级可组合项传递此决策,如上方所示:

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

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

两张不同卡片的示例。
图 3. 一个较窄的卡片,其中仅显示一个图标和标题;另一张较宽的卡片,其中显示了图标、标题和简短说明。

如上所示,我们应该避免尝试使用设备的实际屏幕尺寸。对于多屏幕,此结果将不准确,如果应用并非全屏显示,则此结果将不准确。

由于该可组合项不是屏幕级可组合项,为了最大限度地提高可重用性,我们也不应该直接使用当前的窗口指标。如果组件在放置时有内边距(例如边衬区),或者有导航栏或应用栏等组件,那么系统为可组合项分配的空间量与应用可使用的总空间量可能会有很大的差距。

因此,我们应使用可组合项实际获得的宽度来渲染自身。我们可以通过以下两种方法获取该宽度:

如果您要更改内容的显示位置方式,可以使用一系列修饰符或自定义布局使布局具有自适应能力。这很简单,只需让某个子项填充所有可用空间,或者为子级布置多个列(如果有足够的空间)。

如果您想更改显示的内容,可以使用 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 中的自定义布局,请参阅下面列出的其他资源。

示例应用

  • 大屏幕规范化布局包含经过验证的设计模式,可在大屏设备上提供最佳用户体验
  • JetNews 介绍了如何设计可调整其界面以充分利用可用空间的应用
  • Reply 是一个用于支持移动设备、平板电脑和可折叠设备的自适应示例
  • Now in Android 应用使用自适应布局来支持不同屏幕尺寸

视频