google / accompanist

A collection of extension libraries for Jetpack Compose
https://google.github.io/accompanist
Apache License 2.0
7.32k stars 593 forks source link

[Idea] Add PagerState API to distinguish between programatic vs. touch scrolls #651

Closed alexjlockwood closed 2 years ago

alexjlockwood commented 2 years ago

ViewPager and ViewPager2 have an API called OnPageChangeCallback#onPageScrollStateChanged() which allows you to determine whether the current scroll is settling, programatically initiated (i.e. part of a "fake drag" using ViewPager2#fakeDragBy()), or part of an active gesture.

Currently there doesn't seem to be a similar API for PagerState, as PagerState#isScrollInProgress returns whether there is an active scroll. but provides no information about how it was initiated or whether there is still an active gesture happening.

This API would be ideal as in some cases you want to react to a view pager page change, but only once the user has finished interacting with the screen and the next target page is known for certain.

alexjlockwood commented 2 years ago

A quick update... I did end up finding a workaround that seems to work, but it wasn't very intuitive. Essentially it required creating a custom FlingBehavior that wraps around the default:


@Stable
internal class CustomFlingBehavior(
    // Can just be `PagerDefaults#defaultPagerFlingConfig()`
    private val flingBehavior: FlingBehavior,
) : FlingBehavior {
    var isSettling by mutableStateOf(false)
        private set

    override suspend fun ScrollScope.performFling(initialVelocity: Float) = try {
        isSettling = true
        with(flingBehavior) { performFling(initialVelocity) }
    } finally {
        isSettling = false
    }
}

Then pass this FlingBehavior to HorizontalPager(flingBehavior = myFlingBehavior). Then in order to determine whether a scroll was initiated via a scroll vs. a fling, you would just use myFlingBehavior.isSettling.

Like I said, it works... but not very straightforward and would be nice if this extra fling state could be consolidated under PagerState somehow.

chrisbanes commented 2 years ago

This is more of a question for ScrollableState since all of that API comes from there.

/cc @matvei-g

matvei-g commented 2 years ago

We expose InteractionSource as a part of LazyListState and ScrollState. This InteractionSource emmits DragInteraction.Start when drag started allowing developers to react to this emission or to the fact that's it's gone (DragInteraction.Stop emitted).

Does it make sense to provide the same InteractionSource in the Pager as well?

alexjlockwood commented 2 years ago

@chrisbanes Yeah that's fair, I was thinking that as well. Makes sense for accompanist to stay consistent with whatever API ScrollableState uses.

@matvei-g I like the idea of adding an InteractionSource property to PagerState as well!

chrisbanes commented 2 years ago

InteractionSource looks good to me, I'll whip up a quick PR now. Should this be part of the ScrollableState contract?

// EDIT: just saw that it's part of the Modifier.scrollable API. Ignore this.