diff --git a/android/src/main/java/com/reactnativepagerview/ComposePagerView.kt b/android/src/main/java/com/reactnativepagerview/ComposePagerView.kt index 068b4e1d..7c5ba2c9 100644 --- a/android/src/main/java/com/reactnativepagerview/ComposePagerView.kt +++ b/android/src/main/java/com/reactnativepagerview/ComposePagerView.kt @@ -21,6 +21,7 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.snapshotFlow +import androidx.compose.runtime.withFrameNanos import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.ui.Modifier import androidx.compose.ui.platform.ComposeView @@ -548,8 +549,15 @@ class ComposePagerView(context: Context) : FrameLayout(context) { .drop(1) .collect { inProgress -> if (!inProgress) { - dispatchPageScroll(pagerState.settledPage, 0f) - dispatchScrollState("idle") + // A drag's scroll ends before the fling that follows it starts + // its own, in a coroutine launched after DragInteraction.Stop, + // so isScrollInProgress is briefly false between the two. Wait a + // frame so that gap isn't reported as the pager coming to rest. + withFrameNanos {} + if (!pagerState.isScrollInProgress) { + dispatchPageScroll(pagerState.settledPage, 0f) + dispatchScrollState("idle") + } } } } @@ -557,14 +565,11 @@ class ComposePagerView(context: Context) : FrameLayout(context) { pagerState.interactionSource.interactions.collect { interaction -> when (interaction) { is DragInteraction.Start -> dispatchScrollState("dragging") + // Every drag ends in a fling that snaps to a page, even when + // released at rest, and its end is reported as idle above. The + // fling hasn't started yet, so isScrollInProgress can't tell. is DragInteraction.Stop, - is DragInteraction.Cancel -> { - if (pagerState.isScrollInProgress) { - dispatchScrollState("settling") - } else { - dispatchScrollState("idle") - } - } + is DragInteraction.Cancel -> dispatchScrollState("settling") } } } diff --git a/e2e/tests/issues/issue-1199-scroll-state.e2e.ts b/e2e/tests/issues/issue-1199-scroll-state.e2e.ts new file mode 100644 index 00000000..d1d694fd --- /dev/null +++ b/e2e/tests/issues/issue-1199-scroll-state.e2e.ts @@ -0,0 +1,45 @@ +import { freshAppBeforeEach, expect, test } from '../support/test.ts'; +import type { App, Screen } from '../support/test.ts'; +import { openExample } from '../support/app.ts'; + +freshAppBeforeEach(); + +// Android-only: the Compose pager has to keep reporting settling until the +// snap after a drag has finished. +const options = { tags: ['regression', 'issue-1199'], platforms: ['android'] }; + +/** Opens the repro and waits for its status line. */ +async function openRepro(app: App, screen: Screen): Promise { + await openExample(app, screen, 'Issue #1199 Scroll State Repro'); + await expect(screen.getByTestId('issue-1199-status')).toHaveText( + 'No scroll yet' + ); +} + +test( + 'setPage reports settling, then idle once the page has stopped', + options, + async ({ app, screen }) => { + await openRepro(app, screen); + await screen.getByTestId('issue-1199-next').tap(); + await expect(screen.getByTestId('issue-1199-status')).toHaveText( + 'settling > idle | idle at rest: yes' + ); + } +); + +test( + 'a swipe reports settling when the finger lifts, then idle once the page has stopped', + options, + async ({ app, screen }) => { + await openRepro(app, screen); + // Before the fix this was "dragging > idle | idle at rest: no": idle + // arrived as the finger lifted, while the page was still moving. + await screen + .getByTestId('issue-1199-page-0') + .swipe({ direction: 'right', momentum: 'fast' }); + await expect(screen.getByTestId('issue-1199-status')).toHaveText( + 'dragging > settling > idle | idle at rest: yes' + ); + } +); diff --git a/example/src/App.tsx b/example/src/App.tsx index 92f5d899..a60dc42b 100644 --- a/example/src/App.tsx +++ b/example/src/App.tsx @@ -60,6 +60,7 @@ import { } from './gh-issues/Issue1142SearchBarInsetRepro'; import { Issue1154PageChangeFocusRepro } from './gh-issues/Issue1154PageChangeFocusRepro'; import { Issue1156NestedHorizontalScrollRepro } from './gh-issues/Issue1156NestedHorizontalScrollRepro'; +import { Issue1199ScrollStateRepro } from './gh-issues/Issue1199ScrollStateRepro'; import { Issue424SwipeCancelsPressRepro } from './gh-issues/Issue424SwipeCancelsPressRepro'; import { Issue965PagesBeforeCurrentRepro } from './gh-issues/Issue965PagesBeforeCurrentRepro'; import { colors, radius } from './theme'; @@ -188,6 +189,10 @@ const sections: { title: string; data: Example[] }[] = [ component: Issue1156NestedHorizontalScrollRepro, name: 'Issue #1156 Nested Horizontal Scroll Repro', }, + { + component: Issue1199ScrollStateRepro, + name: 'Issue #1199 Scroll State Repro', + }, { component: Issue424SwipeCancelsPressRepro, name: 'Issue #424 Swipe Cancels Press Repro', diff --git a/example/src/gh-issues/Issue1199ScrollStateRepro.tsx b/example/src/gh-issues/Issue1199ScrollStateRepro.tsx new file mode 100644 index 00000000..c80def1c --- /dev/null +++ b/example/src/gh-issues/Issue1199ScrollStateRepro.tsx @@ -0,0 +1,64 @@ +/** + * Repro for #1199 on Android: a swipe must report `settling` when the finger + * lifts and `idle` only once the page has stopped. Before the fix the pager + * reported `idle` as the finger lifted, while the page was still moving, and + * never `settling`. The status lists the states since the previous `idle` and + * whether the last `onPageScroll` offset was 0 when `idle` arrived. + */ +import React, { useRef, useState } from 'react'; +import { Button, StyleSheet, Text, View } from 'react-native'; +import PagerView from 'react-native-pager-view'; + +const PAGES = ['Page 0', 'Page 1', 'Page 2']; + +export function Issue1199ScrollStateRepro() { + const pager = useRef(null); + const states = useRef([]); + const offset = useRef(0); + const [status, setStatus] = useState('No scroll yet'); + + return ( + + + {status} + +