Detecting counter clockwise rotary input, Wear OS Jetpack Compose

1k Views Asked by At

I'm trying to implement rotary input scrolling (currently with Galaxy Watch 4, so the bezel controls rotary input) on a Horizontal Pager. I can get it to move forward, but not backwards, no matter what direction I move the bezel in. How do I detect counter clockwise rotary to make the pager go back instead of forward?

Note: pagerState.scrollBy(it.horizontalScrollPixels) does work forwards and backwards but doesn't snap to the next page, only slowly scrolls partially. This can be solved this way too (barring janky animation, animateToPage flows better, but presents the same lack of backwards scroll issue). I will accept an answer that can get a value to snap to the next page for all different screen sizes centered using scrollBy. I'm thinking it's it.horizontalScrollPixels times something ("it" is a RotaryScrollEvent object)

This code moves the pager forward

val focusRequester = remember { FocusRequester() }

        LaunchedEffect(Unit) {
            focusRequester.requestFocus()
        }
    
    
        HorizontalPager(
    
            count = 4,
            state = pagerState,
            // Add 32.dp horizontal padding to 'center' the pages
            modifier = Modifier.fillMaxSize().onRotaryScrollEvent {
                
                coroutineScope.launch {
                    pagerState.animateScrollToPage(pagerState.targetPage, 1f)
    
                }
                true
            }
                .focusRequester(focusRequester)
                .focusable()
2

There are 2 best solutions below

0
On

You should check how the direction and size of the scroll event. Also scroll by changing the target page, not the offset within that page. You code happens to work because scrolling to 1.0 in the page moves to the next page.

/**
 * ScrollableState integration for Horizontal Pager.
 */
public class PagerScrollHandler(
    private val pagerState: PagerState,
    private val coroutineScope: CoroutineScope
) : ScrollableState {
    override val isScrollInProgress: Boolean
        get() = totalDelta != 0f

    override fun dispatchRawDelta(delta: Float): Float = scrollableState.dispatchRawDelta(delta)

    private var totalDelta = 0f

    private val scrollableState = ScrollableState { delta ->
        totalDelta += delta

        val offset = when {
            // tune to match device
            totalDelta > 40f -> {
                1
            }
            totalDelta < -40f -> {
                -1
            }
            else -> null
        }

        if (offset != null) {
            totalDelta = 0f
            val newTargetPage = pagerState.targetPage + offset
            if (newTargetPage in (0 until pagerState.pageCount)) {
                coroutineScope.launch {
                    pagerState.animateScrollToPage(newTargetPage, 0f)
                }
            }
        }

        delta
    }

    override suspend fun scroll(
        scrollPriority: MutatePriority,
        block: suspend ScrollScope.() -> Unit
    ) {
        scrollableState.scroll(block = block)
    }
}


val state = rememberPagerState()
val pagerScrollHandler = remember { PagerScrollHandler(state, coroutineScope) }

modifier = Modifier
                .fillMaxSize()
                .onRotaryScrollEvent {
                    coroutineScope.launch {
                        pagerScrollHandler.scrollBy(it.verticalScrollPixels)
                    }
                    true
                }
                .focusRequester(viewModel.focusRequester)
                .focusable()

Also you should check that targetPage + offset is a valid page.

0
On

I tested this on a Galaxy Watch 4. Using this guide in the official documentation (https://developer.android.com/training/wearables/user-input/rotary-input#kotlin) I printed the delta values when I scrolled using bezel of the watch. For each scroll that I made i clockwise direction I got a delta of 128 and -128 for each counterclockwise scroll. Using simple if else blocks I was able to distinguish when I scrolled above and below.

override fun onGenericMotionEvent(event: MotionEvent?): Boolean {
    if (event?.action == MotionEvent.ACTION_SCROLL && event.isFromSource(InputDeviceCompat.SOURCE_ROTARY_ENCODER)) {
        runBlocking {
            val delta = -event.getAxisValue(MotionEventCompat.AXIS_SCROLL) *
                    ViewConfigurationCompat.getScaledVerticalScrollFactor(
                        ViewConfiguration.get(baseContext), baseContext
                    )
            if (delta < 127) {
                scalingLazyListState.scrollBy(-100f)
            } else {
                scalingLazyListState.scrollBy(100f)
            }

        }
    }
    return super.onGenericMotionEvent(event)
}