androidx.wear.compose.foundation.rotary

Interfaces

RotaryScrollableBehavior

An interface for handling scroll events.

RotarySnapLayoutInfoProvider

A provider which connects scrollableState to a rotary input for snapping scroll actions.

Objects

RotaryScrollableDefaults

Defaults for rotaryScrollable modifier

Extension functions summary

Modifier
Modifier.rotaryScrollable(
    behavior: RotaryScrollableBehavior,
    focusRequester: FocusRequester,
    reverseDirection: Boolean,
    overscrollEffect: OverscrollEffect?
)

A modifier which connects rotary events with scrollable containers such as Column, LazyList and others.

Extension functions

fun Modifier.rotaryScrollable(
    behavior: RotaryScrollableBehavior,
    focusRequester: FocusRequester,
    reverseDirection: Boolean = false,
    overscrollEffect: OverscrollEffect? = null
): Modifier

A modifier which connects rotary events with scrollable containers such as Column, LazyList and others. ScalingLazyColumn has a build-in rotary support, and accepts RotaryScrollableBehavior directly as a parameter.

This modifier handles rotary input devices, used for scrolling. These devices can be categorized as high-resolution or low-resolution based on their precision.

  • High-res devices: Offer finer control and can detect smaller rotations. This allows for more precise adjustments during scrolling. One example of a high-res device is the crown (also known as rotating side button), located on the side of the watch.

  • Low-res devices: Have less granular control, registering larger rotations at a time. Scrolling behavior is adapted to compensate for these larger jumps. Examples include physical or virtual bezels, positioned around the screen.

This modifier supports rotary scrolling and snapping. The behaviour is configured by the provided RotaryScrollableBehavior: either provide RotaryScrollableDefaults.behavior for scrolling with/without fling or pass RotaryScrollableDefaults.snapBehavior when snap is required.

The default scroll direction of this modifier is aligned with the scroll direction of the Modifier.verticalScroll and Modifier.horizontalScroll, (please be aware that Modifier.scrollable has the opposite direction by default).

To keep the scroll direction aligned, reverseDirection flag should have the same value as the reverseScrolling parameter in Modifier.verticalScroll and Modifier.horizontalScroll, and the opposite value to the reverseDirection parameter used in Modifier.scrollable. When used for horizontal scrolling, RTL/LTR orientations should be taken into account, as these can affect the expected scroll behavior. It's recommended to use ScrollableDefaults.reverseDirection for handling LTR/RTL layouts for horizontal scrolling.

This overload provides the access to OverscrollEffect that defines the behaviour of the rotary over scrolling logic. Use androidx.compose.foundation.rememberOverscrollEffect to create an instance of the current provided overscroll implementation.

Example of scrolling with fling:

import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.text.BasicText
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.TextStyle
import androidx.wear.compose.foundation.rememberActiveFocusRequester
import androidx.wear.compose.foundation.rotary.RotaryScrollableDefaults
import androidx.wear.compose.foundation.rotary.rotaryScrollable
import androidx.wear.compose.material.Text

val scrollableState = rememberLazyListState()
val focusRequester = rememberActiveFocusRequester()
LazyColumn(
    modifier =
        Modifier.fillMaxSize()
            .rotaryScrollable(
                behavior = RotaryScrollableDefaults.behavior(scrollableState),
                focusRequester = focusRequester
            ),
    horizontalAlignment = Alignment.CenterHorizontally,
    state = scrollableState
) {
    items(300) {
        BasicText(
            text = "item $it",
            modifier = Modifier.background(Color.Gray),
            style = TextStyle.Default.copy()
        )
    }
}

Example of scrolling with snap:

import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.text.BasicText
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import androidx.compose.ui.util.fastSumBy
import androidx.wear.compose.foundation.rememberActiveFocusRequester
import androidx.wear.compose.foundation.rotary.RotaryScrollableDefaults
import androidx.wear.compose.foundation.rotary.RotarySnapLayoutInfoProvider
import androidx.wear.compose.foundation.rotary.rotaryScrollable
import androidx.wear.compose.material.Text

val scrollableState = rememberLazyListState()
val focusRequester = rememberActiveFocusRequester()
LazyColumn(
    modifier =
        Modifier.fillMaxSize()
            .rotaryScrollable(
                behavior =
                    RotaryScrollableDefaults.snapBehavior(
                        scrollableState,
                        // This sample has a custom implementation of
                        // RotarySnapLayoutInfoProvider
                        // which is required for snapping behavior. ScalingLazyColumn has it
                        // built-in,
                        // so it's not required there.
                        remember(scrollableState) {
                            object : RotarySnapLayoutInfoProvider {

                                override val averageItemSize: Float
                                    get() {
                                        val items = scrollableState.layoutInfo.visibleItemsInfo
                                        return (items.fastSumBy { it.size } / items.size)
                                            .toFloat()
                                    }

                                override val currentItemIndex: Int
                                    get() = scrollableState.firstVisibleItemIndex

                                override val currentItemOffset: Float
                                    get() =
                                        scrollableState.firstVisibleItemScrollOffset.toFloat()

                                override val totalItemCount: Int
                                    get() = scrollableState.layoutInfo.totalItemsCount
                            }
                        }
                    ),
                focusRequester = focusRequester,
            ),
    horizontalAlignment = Alignment.CenterHorizontally,
    state = scrollableState
) {
    items(300) {
        BasicText(text = "item $it", modifier = Modifier.background(Color.Gray).height(50.dp))
    }
}

Example of scrolling with overscroll:

import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.overscroll
import androidx.compose.foundation.rememberOverscrollEffect
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.wear.compose.foundation.rememberActiveFocusRequester
import androidx.wear.compose.foundation.rotary.RotaryScrollableDefaults
import androidx.wear.compose.foundation.rotary.rotaryScrollable
import androidx.wear.compose.material.Text

val scrollableState = rememberScrollState()
val focusRequester = rememberActiveFocusRequester()
val overscrollEffect = rememberOverscrollEffect()

val screenHeightDp = LocalConfiguration.current.screenHeightDp.dp

Column(
    Modifier.fillMaxSize()
        .rotaryScrollable(
            behavior = RotaryScrollableDefaults.behavior(scrollableState),
            focusRequester = focusRequester,
            overscrollEffect = overscrollEffect
        )
        .verticalScroll(scrollableState, overscrollEffect)
        .overscroll(overscrollEffect),
    horizontalAlignment = Alignment.CenterHorizontally
) {
    Text("Top")
    Spacer(modifier = Modifier.height(screenHeightDp / 2))
    Text("Scroll this list up and down with rotary input", textAlign = TextAlign.Center)
    Spacer(modifier = Modifier.height(screenHeightDp / 2))
    Text("Bottom")
}
Parameters
behavior: RotaryScrollableBehavior

Specified RotaryScrollableBehavior for rotary handling with snap or fling.

focusRequester: FocusRequester

Used to request the focus for rotary input. Each composable with this modifier should have a separate focusRequester, and only one of them at a time can be active. We recommend using rememberActiveFocusRequester to obtain a FocusRequester, as this will guarantee the proper behavior.

reverseDirection: Boolean = false

Reverses the direction of the rotary scroll. This direction should be aligned with the general touch scroll direction - and should be reversed if, for example, it was reversed in .verticalScroll or .horizontalScroll modifiers. If used with a .scrollable modifier - the scroll direction should be the opposite to the one specified there. When used for horizontal scrolling, RTL/LTR orientations should be taken into account, as these can affect the expected scroll behavior. It's recommended to use ScrollableDefaults.reverseDirection for handling LTR/RTL layouts for horizontal scrolling.

overscrollEffect: OverscrollEffect? = null

effect to which the deltas will be fed when the scrollable have some scrolling delta left. Pass null for no overscroll. If you pass an effect you should also apply androidx.compose.foundation.overscroll modifier.