支持不同屏幕尺寸

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

为了尽可能支持更多屏幕尺寸,请设计您的应用布局使其具有响应性和自适应性。响应式/自适应布局提供优化的用户体验,无论屏幕尺寸如何,使您的应用能够适应手机、平板电脑、折叠屏设备、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 等平台以及平板电脑和折叠屏等外形尺寸所需的特殊处理。

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

@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,
        /* ... */
    )
}

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

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

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

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

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 会将组合推迟到布局阶段,此时这些约束已知,从而导致在布局期间执行更多工作。

@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 中的自定义布局,请参阅以下其他资源。

示例应用

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

视频