使用 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 参数值在 TabletopLayoutFlatLayout 之间进行选择。

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

根据窗口大小做出决定

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

derivedMediaQuery 函数会对 query lambda 进行评估,并将结果封装在 derivedStateOf 中。由于 windowWidthwindowHeight 可能会频繁更新,因此在 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 类中定义了四个值:FineCoarseBluntNoneNone 表示没有可用的指控设备。 精确度从高到低的顺序为:FineCoarseBlunt

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

以下示例展示了当用户使用低精度指控设备时,如何显示更大的按钮:

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

检查可用的键盘类型

keyboardKind 参数表示可用键盘的类型:PhysicalVirtualNone。 如果显示屏幕键盘,并且同时有硬件键盘可用,则该参数解析为 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 类中定义了三个值:NearMediumFarNear 表示屏幕在近距离范围内,Far 表示设备是从远处观看的。以下示例展示了当观看距离为 FarMedium 时如何增大字号:

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

预览界面组件

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

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

mediaQueryderivedMediaQuery 函数会在 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()
        }
    }
}