支持不同的显示屏尺寸

如果应用支持不同的显示屏尺寸,便可尽可能多地覆盖各种各样的设备和用户。

为了尽量支持各种显示尺寸(无论是不同的设备屏幕,还是多窗口模式下的不同应用窗口),应用应该采用自适应布局。自适应布局提供经过优化的用户体验(无论显示屏尺寸如何),使应用可以适应手机、平板电脑、可折叠设备、ChromeOS 设备、竖屏和横屏方向以及可调整大小的显示屏配置(例如分屏模式和窗口化模式)。

响应式/自适应布局会根据可用的显示空间自动调整。这些变化包括:从填满空间的小幅布局调整(自适应设计),到用一种布局完全替换另一种布局,以便应用能够最好地适应不同的显示屏尺寸(自适应设计)。

作为声明式界面工具包,Jetpack Compose 非常适合用来设计和实现可动态变化的布局,以针对不同的屏幕尺寸以不同方式呈现内容。

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

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

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

图 1. 手机、可折叠设备、平板电脑和笔记本电脑外形规格

在平板电脑上,应用可能会在多窗口模式下运行,这意味着,该应用可能会与另一个应用分屏显示。在桌面窗口化模式下或在 ChromeOS 中,应用可能会位于可调整大小的窗口中。甚至可能会有多个实体屏幕,例如可折叠设备。在所有这些情况下,实体屏幕的尺寸与决定如何显示内容并不相关。

相反,您应根据分配给应用的实际屏幕区域(由 Jetpack WindowManager 库提供的当前窗口指标描述)做出决策。如需查看如何在 Compose 应用中使用 WindowManager 的示例,请参阅 JetNews 示例。

让布局自适应可用显示空间,还可以减少支持 ChromeOS 等平台以及平板电脑和可折叠设备等外形规格所需的特殊处理。

确定应用的可用空间指标后,请按照使用窗口大小类别中的说明,将原始大小转换为窗口大小类别。窗口大小类别是一些断点,旨在平衡应用逻辑的简单性与灵活性,以便针对大多数显示屏尺寸优化应用。

窗口大小类别是指应用的整体窗口,因此请使用这些类别来做出影响应用整体布局的布局决策。您可以将窗口大小类别作为状态进行传递,也可以执行其他逻辑来创建派生状态以传递给嵌套可组合项。

@Composable
fun MyApp(
    windowSizeClass: WindowSizeClass = currentWindowAdaptiveInfo(supportLargeAndXLargeWidth = true).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,
        /* ... */
    )
}

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

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

将可组合项放置在各种不同的位置,可以提高它们的可重用性。如果某个可组合项必须放置在特定位置并具有特定尺寸,则该可组合项不太可能在其他上下文中重复使用。这也意味着可重用的个别可组合项应避免隐式依赖于全局显示尺寸信息。

假设用一个嵌套可组合项来实现列表详情布局,该布局可能会显示一个窗格或并排显示两个窗格:

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

列表详情决策应纳入应用的整体布局中,因此该决策会从内容级可组合项向下传递:

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

如果您想让一个可组合项根据可用显示空间单独调整其布局,该怎么办?例如,在空间允许的情况下,让卡片显示更多详细信息。您希望根据一些可用的显示大小执行一些逻辑,但具体是哪种大小?

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

避免尝试使用设备实际屏幕的大小。对于不同类型的屏幕或者不是全屏显示的应用,这会不准确。

由于该可组合项不是内容级可组合项,因此请勿直接使用当前的窗口指标。

如果组件在放置时有内边距(例如边衬区),或者应用包含导航栏或应用栏等组件,那么系统为可组合项分配的显示空间量与应用可使用的总空间量可能会有很大的差距。

使用实际提供给可组合项以进行渲染的宽度。您可以通过以下两种方式获取该宽度:

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

  • 如果您想更改显示的内容,请使用 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 展示了如何设计一款应用,使其界面能够自适应地利用可用显示空间。
  • Reply 是一个自适应示例,支持移动设备、平板电脑和可折叠设备。
  • Now in Android 是一款使用自适应布局来支持不同屏幕尺寸的应用。

视频