Entrada por seletor giratório com o Compose

O termo "entrada por seletor giratório" se refere a ações feitas usando peças do relógio que giram. Em média, os usuários passam apenas alguns segundos interagindo com o relógio. É possível melhorar a experiência usando a entrada por seletor giratório para que os usuários possam realizar várias tarefas rapidamente.

As três principais fontes de entrada por seletor giratório na maioria dos relógios incluem o botão lateral giratório (RSB, na sigla em inglês) e uma borda física ou sensível ao toque, que é uma zona de toque circular ao redor da tela. Embora o comportamento esperado possa variar de acordo com o tipo de entrada, recomendamos que você ofereça suporte à entrada por seletor giratório para todas as interações essenciais.

Rolagem

A maioria dos usuários espera que os apps tenham suporte ao gesto de rolagem. À medida que o conteúdo passa pela tela, ofereça feedback visual aos usuários em resposta a interações pelo seletor giratório. O feedback visual pode incluir indicadores de posição para rolagem vertical ou indicadores de página.

ScalingLazyColumn e Picker oferecem suporte ao gesto de rolagem por padrão, desde que você precise colocar esses componentes dentro de um Scaffold. O Scaffold fornece a estrutura de layout básica usada em apps para Wear OS e já tem um slot para um indicador de rolagem. Para mostrar o progresso da rolagem, crie um indicador de posição com base no objeto de estado da lista, conforme mostrado no snippet de código abaixo:

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

É possível configurar um comportamento de ajuste para ScalingLazyColumn usando ScalingLazyColumnDefaults.snapFlingBehavior, conforme mostrado no snippet de código a seguir:

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

    val state = rememberScalingLazyListState()
    ScalingLazyColumn(
        modifier = Modifier.fillMaxWidth(),
        state = state,
        flingBehavior = ScalingLazyColumnDefaults.snapFlingBehavior(state = state)
    ) {
        // Content goes here
        // ...
    }
}

Ações personalizadas

Também é possível criar ações personalizadas que respondam à entrada por seletor giratório no app. Por exemplo, use a entrada por seletor giratório para aumentar e diminuir o zoom ou controlar o volume em um app de música.

Se o componente não tiver suporte nativo a eventos de rolagem, como o controle de volume, você poderá processar esses eventos por conta própria.

// VolumeScreen.kt

val focusRequester: FocusRequester = remember { FocusRequester() }

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

Crie um estado personalizado gerenciado no modelo de visualização e um callback individualizado que seja usado para processar eventos de rolagem por seletor giratório.

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

Para simplificar, o exemplo anterior usa valores de pixel que, se usados de fato, serão muito sensíveis.

Use o callback quando receber os eventos, conforme mostrado no snippet a seguir.

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

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