Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 14 additions & 9 deletions android/src/main/java/com/reactnativepagerview/ComposePagerView.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -548,23 +549,27 @@ 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")
}
}
}
}
launch {
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")
}
}
}
Expand Down
45 changes: 45 additions & 0 deletions e2e/tests/issues/issue-1199-scroll-state.e2e.ts
Original file line number Diff line number Diff line change
@@ -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<void> {
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'
);
}
);
5 changes: 5 additions & 0 deletions example/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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',
Expand Down
64 changes: 64 additions & 0 deletions example/src/gh-issues/Issue1199ScrollStateRepro.tsx
Original file line number Diff line number Diff line change
@@ -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<PagerView>(null);
const states = useRef<string[]>([]);
const offset = useRef(0);
const [status, setStatus] = useState('No scroll yet');

return (
<View style={styles.screen}>
<Text testID="issue-1199-status" style={styles.status}>
{status}
</Text>
<Button
testID="issue-1199-next"
title="setPage(1)"
onPress={() => pager.current?.setPage(1)}
/>
<PagerView
ref={pager}
style={styles.pager}
onPageScroll={(e) => {
offset.current = e.nativeEvent.offset;
}}
onPageScrollStateChanged={(e) => {
const state = e.nativeEvent.pageScrollState;
states.current.push(state);
if (state === 'idle') {
const atRest = offset.current === 0 ? 'yes' : 'no';
setStatus(
`${states.current.join(' > ')} | idle at rest: ${atRest}`
);
states.current = [];
}
}}
>
{PAGES.map((label, i) => (
<View key={label} testID={`issue-1199-page-${i}`} style={styles.page}>
<Text style={styles.label}>{label}</Text>
</View>
))}
</PagerView>
</View>
);
}

const styles = StyleSheet.create({
screen: { flex: 1 },
status: { padding: 12, fontSize: 16 },
pager: { flex: 1 },
page: { alignItems: 'center', justifyContent: 'center' },
label: { fontSize: 24 },
});
Loading