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

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

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

由于信息是动态更新的,因此您需要对其进行监控,并在发生任何更新时触发重组。mediaQuery 函数会提取信息检索的详细信息,让您可以专注于定义触发布局更新的条件。以下示例会在折叠姿态为桌面时将布局切换为 TabletopLayout

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

启用 mediaQuery 函数

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

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

使用参数定义条件

您可以将条件定义为 lambda ,它在 UiMediaScope 中求值。 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 的设备),则 pointerPrecision 参数的值为 Fine

以下示例会在用户使用低精度指针设备时显示较大的按钮:

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()
}

`mediaQuery` 和 `derivedMediaQuery` 函数会在 `UiMediaScope` 对象中评估给定的 `query` lambda,该对象以 `LocalUiMediaScope.current` 的形式提供。您可以按照以下步骤替换它:

  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()
        }
    }
}