使用 mediaQuery 查询自适应布局的信息

您需要各种类型的信息(例如设备功能和应用状态)来更新应用布局。 窗口宽度和高度是最常用的信息。 除此之外,您还可以参考以下信息:

  • 窗口状态
  • 指控设备精度
  • 键盘类型
  • 设备是否支持摄像头和麦克风
  • 用户与设备显示屏之间的距离

由于信息是动态更新的,因此您需要监控信息,并在发生任何更新时触发重组。 mediaQuery 函数会抽象化信息检索的细节,让您专注于定义触发布局更新的条件。

以下示例会在可折叠设备处于桌面模式时将布局切换为 TabletopLayout:

@Composable
fun VideoPlayer(
    // ...
) {
    // ...
            if (mediaQuery { windowPosture == UiMediaScope.Posture.Tabletop }) {
                TabletopLayout()
            } else {
                FlatLayout()
            }
    // ...
}

启用 mediaQuery 函数

如需启用 mediaQuery 函数,请将 ComposeUiFlags 对象的 isMediaQueryIntegrationEnabled 属性设置为 true:

class MyApplication : Application() {
    override fun onCreate() {
        ComposeUiFlags.isMediaQueryIntegrationEnabled = true
        super.onCreate()
    }
}

定义带参数的条件

您可以将条件定义为在 UiMediaScope 中求值的 lambda。mediaQuery 函数会根据当前状态和设备功能评估条件。 该函数会返回一个布尔值,因此您可以使用条件分支(例如 if 表达式)来确定布局。表 1 介绍了 UiMediaScope 中可用的参数。

参数 值类型 说明
windowWidth Dp 当前窗口宽度(以 dp 为单位)。
windowHeight Dp 当前窗口高度(以 dp 为单位)。
windowPosture UiMediaScope.Posture 应用窗口的当前折叠状态。
pointerPrecision UiMediaScope.PointerPrecision 可用指控设备的最高精度。
keyboardKind UiMediaScope.KeyboardKind 可用或已连接的键盘类型。
hasCamera Boolean 设备是否支持摄像头。
hasMicrophone Boolean 设备是否支持麦克风。
viewingDistance UiMediaScope.ViewingDistance 用户与设备屏幕之间的典型距离。

UiMediaScope 对象会解析参数的值。 mediaQuery 函数使用 LocalUiMediaScope.current 访问 UiMediaScope 对象,该对象表示当前设备功能和上下文。当发生任何更改(例如用户更改设备状态)时,此对象会动态更新。 然后,mediaQuery 函数使用更新后的 UiMediaScope 对象对 query lambda 求值,并返回一个布尔值。 例如,以下代码段会根据 windowPosture 参数值在 TabletopLayout 和 FlatLayout 之间进行选择。

@Composable
fun VideoPlayer(
    // ...
) {
    // ...
            if (mediaQuery { windowPosture == UiMediaScope.Posture.Tabletop }) {
                TabletopLayout()
            } else {
                FlatLayout()
            }
    // ...
}

根据窗口大小做出决策

窗口大小类别是一组主观的视口断点,有助于您设计、开发和测试自适应布局。您可以将表示当前窗口大小的两个参数与窗口大小类别中定义的阈值进行比较。 以下示例会根据窗口宽度更改窗格数量。 WindowSizeClass 类包含窗口大小类别的阈值常量(图 1)。

derivedMediaQuery 函数会评估 query lambda 并将结果封装在 derivedStateOf 中。由于 windowWidth 和 windowHeight 可能会频繁更新,因此在 query lambda 中引用这些形参时,请调用 derivedMediaQuery 函数,而不是 mediaQuery 函数。

val narrowerThanMedium by derivedMediaQuery {
    windowWidth < WindowSizeClass.WIDTH_DP_MEDIUM_LOWER_BOUND.dp
}
val narrowerThanExpanded by derivedMediaQuery {
    windowWidth < WindowSizeClass.WIDTH_DP_EXPANDED_LOWER_BOUND.dp
}
when {
    narrowerThanMedium -> SinglePaneLayout()
    narrowerThanExpanded -> TwoPaneLayout()
    else -> ThreePaneLayout()
}

图 1. 布局会根据窗口宽度进行更新。

根据窗口姿态更新布局

windowPosture 参数将当前窗口状态描述为 UiMediaScope.Posture 对象。您可以通过将参数与 UiMediaScope.Posture 类中定义的值进行比较,来检查当前姿态。以下示例会根据窗口状态切换布局:

when {
    mediaQuery { windowPosture == UiMediaScope.Posture.Tabletop } -> TabletopLayout()
    mediaQuery { windowPosture == UiMediaScope.Posture.Book } -> BookLayout()
    mediaQuery { windowPosture == UiMediaScope.Posture.Flat } -> FlatLayout()
}

检查可用指控设备的精确度

高精度指控设备可帮助用户精确地指向界面元素。 指控设备的精确度取决于设备类型。

pointerPrecision 参数描述了可用指控设备(例如鼠标和触摸屏)的精确度。UiMediaScope.PointerPrecision 类中定义了四个值:Fine、Coarse、Blunt 和 None。None 表示没有可用的指控设备。 精度从高到低依次为:Fine、Coarse 和 Blunt。

如果有多个指控设备可用,且它们的精度不同,则系统会使用最高精度来解析该参数。 例如,如果有两个指控设备(一个 Fine 精度设备和一个 Blunt 精度设备),则 Fine 是 pointerPrecision 参数的值。

以下示例展示了当用户使用低精度指控设备时,按钮会变大:

if (mediaQuery { pointerPrecision == UiMediaScope.PointerPrecision.Blunt }) {
    LargeSizeButton()
} else {
    NormalSizeButton()
}

查看可用的键盘类型

keyboardKind 参数表示可用键盘的类型:Physical、Virtual 和 None。 如果屏幕键盘已显示,并且硬件键盘同时可用,则该参数会解析为 Physical。 如果两者均未检测到,则 None 为参数的值。以下示例显示了一条消息,建议用户在未检测到键盘时连接键盘:

if (mediaQuery { keyboardKind == UiMediaScope.KeyboardKind.None }) {
    SuggestKeyboardConnect()
}

检查设备是否支持摄像头和麦克风

部分设备不支持摄像头或麦克风。 您可以使用 hasCamera 参数和 hasMicrophone 参数检查设备是否支持摄像头和麦克风。以下示例展示了在设备支持摄像头和麦克风时,可与它们搭配使用的按钮:

Row {
    OutlinedTextField(state = rememberTextFieldState())
    // Show the MicButton when the device supports a microphone.
    if (mediaQuery { hasMicrophone }) {
        MicButton()
    }
    // Show the CameraButton when the device supports a camera.
    if (mediaQuery { hasCamera }) {
        CameraButton()
    }
}

根据估算的观看距离调整界面

观看距离是决定布局的一个因素。 如果用户在一定距离外使用应用,他们会希望文字和界面元素更大。 viewingDistance 参数会根据设备类型及其典型使用场景,提供观看距离的估算值。

UiMediaScope.ViewingDistance 类中定义了三个值:Near、Medium 和 Far。Near 表示屏幕处于近距离,Far 表示从远处观看设备。 以下示例在观看距离为 Far 或 Medium 时增大了字号:

val fontSize = when {
    mediaQuery { viewingDistance == UiMediaScope.ViewingDistance.Far } -> 20.sp
    mediaQuery { viewingDistance == UiMediaScope.ViewingDistance.Medium } -> 18.sp
    else -> 16.sp
}

预览界面组件

您可以在可组合函数中调用 mediaQuery 和 derivedMediaQuery 函数,以预览界面组件。 以下代码段会根据 windowPosture 参数值在 TabletopLayout 和 FlatLayout 之间进行选择。 如需预览 TabletopLayout,windowPosture 参数应为 UiMediaScope.Posture.Tabletop。

when {
    mediaQuery { windowPosture == UiMediaScope.Posture.Tabletop } -> TabletopLayout()
    mediaQuery { windowPosture == UiMediaScope.Posture.Book } -> BookLayout()
    mediaQuery { windowPosture == UiMediaScope.Posture.Flat } -> FlatLayout()
}

mediaQuery 和 derivedMediaQuery 函数会在 UiMediaScope 对象(作为 LocalUiMediaScope.current 提供)中评估给定的 query lambda。您可以通过以下步骤替换该设置:

  1. 启用 mediaQuery 函数。
  2. 定义一个实现 UiMediaScope 接口的自定义对象。
  3. 使用 CompositionLocalProvider 函数将自定义对象设置为 LocalUiMediaScope。
  4. 在 CompositionLocalProvider 函数的内容 lambda 中调用要预览的可组合项。

您可以通过以下示例预览 TabletopLayout:

@Preview
@Composable
fun PreviewLayoutForTabletop() {
    // Step 1: Enable the mediaQuery function
    ComposeUiFlags.isMediaQueryIntegrationEnabled = true

    val currentUiMediaScope = LocalUiMediaScope.current
    // Step 2: Define a custom object implementing the UiMediaScope interface.
    // The object overrides the windowPosture parameter.
    // The resolution of the remaining parameters is deferred to the currentUiMediaScope object.
    val uiMediaScope = remember(currentUiMediaScope) {
        object : UiMediaScope by currentUiMediaScope {
            override val windowPosture: UiMediaScope.Posture = UiMediaScope.Posture.Tabletop
        }
    }

    // Step 3: Set the object to the LocalUiMediaScope.
    CompositionLocalProvider(LocalUiMediaScope provides uiMediaScope) {
        // Step 4: Call the composable to preview.
        when {
            mediaQuery { windowPosture == UiMediaScope.Posture.Tabletop } -> TabletopLayout()
            mediaQuery { windowPosture == UiMediaScope.Posture.Book } -> BookLayout()
            mediaQuery { windowPosture == UiMediaScope.Posture.Flat } -> FlatLayout()
        }
    }
}