使用 Compose 进行旋转输入

旋转输入是指来自手表旋转或转动的部件的输入。平均而言,用户在与手表互动时只花几秒钟。您可以通过使用旋转输入来允许用户快速完成各种任务,从而增强用户体验。

大多数手表上的三个主要旋转输入来源包括旋转侧按钮 (RSB),以及物理边框或触摸边框,即屏幕周围的圆形触摸区域。虽然预期行为可能根据输入类型而有所不同,但请确保为所有基本交互支持旋转输入。

滚动

大多数用户希望应用支持滚动手势。当内容在屏幕上滚动时,在响应旋转交互时为用户提供视觉反馈。视觉反馈可以包括 位置指示器 用于垂直滚动,或 页面指示器

使用 Compose for Wear OS 实现旋转滚动。此示例描述了一个具有脚手架和 ScalingLazyColumn 的应用,该应用垂直滚动。脚手架为 Wear OS 应用提供基本布局结构,并且已经有一个滚动指示器的插槽。要显示滚动进度,请根据列表状态对象创建位置指示器。可滚动视图(包括 ScalingLazyColumn)已经具有可滚动状态以添加旋转输入。要接收旋转滚动事件,请执行以下操作

  1. 使用 FocusRequester 明确请求焦点。对于更复杂的情况,例如 HorizontalPager 中的多个 ScalingLazyColumns 对象,请使用 HierarchicalFocusCoordinator

  2. 添加 onRotaryScrollEvent 修饰符以拦截系统在用户旋转表冠或表圈时生成的事件。每个旋转事件都具有一个固定的像素值,并垂直或水平滚动。该修饰符还具有一个回调函数,用于指示事件是否被消费,并在被消费时阻止事件传播到其父级。

val listState = rememberScalingLazyListState()
Scaffold(
    positionIndicator = {
        PositionIndicator(scalingLazyListState = listState)
    }
) {

    val focusRequester = rememberActiveFocusRequester()
    val coroutineScope = rememberCoroutineScope()

    ScalingLazyColumn(
        modifier = Modifier
            .onRotaryScrollEvent {
                coroutineScope.launch {
                    listState.scrollBy(it.verticalScrollPixels)
                    listState.animateScrollBy(0f)
                }
                true
            }
            .focusRequester(focusRequester)
            .focusable()
            .fillMaxSize(),
        state = listState
    ) {
        // Content goes here
        // ...
    }
}

离散值

还可以使用旋转交互来调整离散值,例如在设置中调整亮度或在设置闹钟时选择时间选择器中的数字。

ScalingLazyColumn 相似,选择器、滑块、步进器和其他可组合项 **需要获得焦点** 才能接收旋转输入。如果屏幕上存在多个可滚动目标,例如时间选择器中的小时和分钟,请为每个目标创建一个 FocusRequester,并在用户点击小时或分钟时相应地处理焦点更改。

var selectedColumn by remember { mutableIntStateOf(0) }

val hoursFocusRequester = remember { FocusRequester() }
val minutesRequester = remember { FocusRequester() }
// ...
Scaffold(modifier = Modifier.fillMaxSize()) {
    Row(
        // ...
        // ...
    ) {
        // ...
        Picker(
            readOnly = selectedColumn != 0,
            modifier = Modifier.size(64.dp, 100.dp)
                .onRotaryScrollEvent {
                    coroutineScope.launch {
                        hourState.scrollBy(it.verticalScrollPixels)
                    }
                    true
                }
                .focusRequester(hoursFocusRequester)
                .focusable(),
            onSelected = { selectedColumn = 0 },
            // ...
            // ...
        )
        // ...
        Picker(
            readOnly = selectedColumn != 1,
            modifier = Modifier.size(64.dp, 100.dp)
                .onRotaryScrollEvent {
                    coroutineScope.launch {
                        minuteState.scrollBy(it.verticalScrollPixels)
                    }
                    true
                }
                .focusRequester(minutesRequester)
                .focusable(),
            onSelected = { selectedColumn = 1 },
            // ...
            // ...
        )
        LaunchedEffect(selectedColumn) {
            listOf(
                hoursFocusRequester,
                minutesRequester
            )[selectedColumn]
                .requestFocus()
        }
    }
}

自定义操作

您还可以创建自定义操作来响应应用程序中的旋转输入。例如,使用旋转输入在媒体应用程序中放大和缩小或控制音量。

如果您的组件本身不支持滚动事件(如音量控制),您可以自己处理滚动事件。

// VolumeScreen.kt

val focusRequester: FocusRequester = remember { FocusRequester() }

Column(
    modifier = Modifier
        .fillMaxSize()
        .onRotaryScrollEvent {
            // handle rotary scroll events
            true
        }
        .focusRequester(focusRequester)
        .focusable(),
) { ... }

在视图模型中创建自定义状态,以及用于处理旋转滚动事件的自定义回调函数。

// VolumeViewModel.kt

object VolumeRange(
    public val max: Int = 10
    public val min: Int = 0
)

val volumeState: MutableStateFlow<Int> = ...

fun onVolumeChangeByScroll(pixels: Float) {
    volumeState.value = when {
        pixels > 0 -> min (volumeState.value + 1, VolumeRange.max)
        pixels < 0 -> max (volumeState.value - 1, VolumeRange.min)
    }
}

为了简单起见,前面的示例使用了像素值,如果实际使用,这些像素值可能过于敏感。

收到事件后,请使用回调函数,如以下代码片段所示。

val focusRequester: FocusRequester = remember { FocusRequester() }
val volumeState by volumeViewModel.volumeState.collectAsState()

Column(
    modifier = Modifier
        .fillMaxSize()
        .onRotaryScrollEvent {
            volumeViewModel
                .onVolumeChangeByScroll(it.verticalScrollPixels)
            true
        }
        .focusRequester(focusRequester)
        .focusable(),
) { ... }

其他资源

请考虑使用 Horologist,这是一个 Google 开源项目,提供了一组 Wear 库,这些库补充了 Compose for Wear OS 和其他 Wear OS API 提供的功能。Horologist 为高级用例提供实现,以及许多设备特定的细节。

例如,不同旋转输入源的灵敏度可能会有所不同。为了使值之间的过渡更加平滑,您可以对速度进行限制或添加捕捉或动画过渡。这使得转动速度对用户来说更自然。Horologist 包括用于可滚动组件和离散值的修饰符。它还包括用于处理焦点的实用程序以及一个音频 UI 库,用于通过触觉反馈实现音量控制。

有关更多信息,请参阅 GitHub 上的 Horologist