Rotary Input in Wear OS with Compose
Tags: android, jetpackcompose, compose, wearos
Published: 2022-11-15 20:39:17

The Rotary Input is one more way to the user interact with their wearables devices. You must handle this type of input since your users will expect this even unconsciously.

Currently the users can perform the Rotary Input on smartwatches in 3 ways: crown, physical bezel, touch bezel. All of these option are transparent for us when handling the input, that is, for a simple handle of this input we don't need to know which one the user is using to interact with our app.

So, let's see some examples of how to handle this with `ScalingLazyColumn` and `Picker`.

<aside>
💡 The following examples are using the version 1.1.0-rc01 of Composer for Wear OS

</aside>

## ScalingLazyColumn

First of all, in order the handle the Rotary input, the component must have the focus. We can achieve this by manually requesting the focus to a component. So, let's create a instance of the FocusRequester:

```kotlin
val focusRequester = remember { FocusRequester() }
```

And then, set up this in our `ScalingLazyColumn` :

```kotlin
ScalingLazyColumn(
    modifier = Modifier
        .focusRequester(focusRequester)
        .focusable(),
    {...}
){
	{...}
}
```

Next, we need to force the request of the focus.

```kotlin
LaunchedEffect(Unit){
	focusRequester.requestFocus() 
}
```

Now our `ScalingLazyColumn` will always have the focus. The handle is not done yet, now we need to actually react to the Rotary Input and scroll our list with it.

First we need a instance of the `ScalingLazyListState` and use the modifier `onRotaryScrollEvent` to listen to the Rotary Inputs. Be aware that we need to perform this scroll with a Coroutine Scope.

```kotlin
val focusRequester = remember **{** FocusRequester() **}**
val scrollState = rememberScalingLazyListState()
val coroutineScope = rememberCoroutineScope()

ScalingLazyColumn(
    modifier = Modifier
				.onRotaryScrollEvent {
            coroutineScope.launch {
                scrollState.scrollBy(it.verticalScrollPixels)
            }
            true // it means that we are handling the event with this callback
        }
        .focusRequester(focusRequester)
        .focusable(),
		state = scrollState,
    {...}
){
	{...}
}

LaunchedEffect(Unit){
	focusRequester.requestFocus() 
}
```

That's all. With a small configuration the `ScalingLazyColumn` is reacting to the Rotary Input.

## Picker

For the `Picker` component the setup will be basically the same:

```kotlin
val pickerState = rememberPickerState(
    initialNumberOfOptions = 100,
    initiallySelectedOption = 50
)
val focusRequester = remember { FocusRequester() }
val coroutineScope = rememberCoroutineScope()

Picker(
    state = pickerState,
    contentDescription = pickerState.selectedOption.toString(),
    modifier = Modifier
        .onRotaryScrollEvent{
					coroutineScope.launch{
						pickerState.scrollBy(it.verticalScrollPixels)
					}
					true
				}
				.focusRequester(focusRequester)
	      .focusable()
		{...}
	) {
		{...}
	}
	
	LaunchedEffect(Unit) {
		focusRequester.requestFocus()
	}
```

## Conclusion

You notice some things:

- There is no haptic feedback
    - If you test other apps that handle this input you will notice a haptic feedback during the scrolling, sadly this is not automatic, you need to do this manually or use the **[Horologist](https://google.github.io/horologist/).**
- The Picker is not snapping to the selected item
    - With this approach sometimes the Picker doesn't snaps to the selected item, you can improve this with **[Horologist](https://google.github.io/horologist/)**, but by my test it isn't working 100%.

The Rotary Input callbacks from the foundation library and Horologist are still in Experimental state, so soon they can change and be improved.

## References

{% youtube 2CzWz5Ad4iM %}

## Code

You can check these and more example on my repository

{% embed https://github.com/Vnicius/rotary-input-samples %}