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
27 changes: 27 additions & 0 deletions android/src/main/java/com/reactnativepagerview/ComposePagerView.kt
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,33 @@ class ComposePagerView(context: Context) : FrameLayout(context) {
measureAndLayoutComposeView()
}

// React Native's parent views drop layout requests, so the pager isn't laid out again when
// Compose asks for it, like when pages arrive in an empty pager, and stays blank until a
// touch (see #219). Lay it out again at its current size.
override fun requestLayout() {
super.requestLayout()
post { layoutAtCurrentSize() }
}

// A configuration change handled in place, like dark mode, calls this on every view instead
// of requestLayout(), with the same result.
override fun forceLayout() {
super.forceLayout()
post { layoutAtCurrentSize() }
}

private fun layoutAtCurrentSize() {
// dispose() requests a layout too, and measuring then would create a new composition.
if (composeView?.hasComposition != true || width <= 0 || height <= 0) {
return
}
measure(
MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY)
)
layout(left, top, right, bottom)
}

private fun measureAndLayoutComposeView() {
val width = width.takeIf { it > 0 } ?: measuredWidth
val height = height.takeIf { it > 0 } ?: measuredHeight
Expand Down
70 changes: 70 additions & 0 deletions e2e/tests/issues/issue-219-empty-pager.e2e.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
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: iOS already shows pages that arrive later, and it reports page 0
// before it has any.
const options = {
tags: ['regression', 'issue-219'],
platforms: ['android'],
};

/** Opens the repro and waits for the empty pager. */
async function openRepro(app: App, screen: Screen): Promise<void> {
await openExample(app, screen, 'Issue #219 Empty Pager Repro');
await expect(screen.getByTestId('issue-219-status')).toHaveText(
'Pages: 0 | Selected: - | Events: 0'
);
}

/** Adds the pages and waits for the pager to report page A. */
async function addPages(screen: Screen): Promise<void> {
await screen.getByTestId('issue-219-add-pages').tap();
await expect(screen.getByTestId('issue-219-status')).toHaveText(
'Pages: 4 | Selected: 0 | Events: 1'
);
}

test(
'a pager that mounts empty shows its first page when pages arrive',
options,
async ({ app, screen }) => {
await openRepro(app, screen);
await addPages(screen);
// Before the fix the pager stayed blank until it was touched.
await expect(screen.getByText('Page A')).toBeVisible();
}
);

test(
'a pager that loses all its pages shows them again when they come back',
options,
async ({ app, screen }) => {
await openRepro(app, screen);
await addPages(screen);
await expect(screen.getByText('Page A')).toBeVisible();
for (let cycle = 0; cycle < 2; cycle++) {
await screen.getByTestId('issue-219-remove-pages').tap();
await expect(screen.getByText('Page A')).toBeHidden();
await screen.getByTestId('issue-219-add-pages').tap();
await expect(screen.getByText('Page A')).toBeVisible();
}
}
);

test(
'setPage works as soon as pages arrive in a pager that mounted empty',
options,
async ({ app, screen }) => {
await openRepro(app, screen);
await addPages(screen);
// Before the fix setPage did nothing until the pager was touched.
await screen.getByTestId('issue-219-select-c').tap();
await expect(screen.getByTestId('issue-219-status')).toHaveText(
'Pages: 4 | Selected: 2 | Events: 2'
);
await expect(screen.getByText('Page C')).toBeVisible();
}
);
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 { Issue219EmptyPagerRepro } from './gh-issues/Issue219EmptyPagerRepro';
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: Issue219EmptyPagerRepro,
name: 'Issue #219 Empty Pager Repro',
},
{
component: Issue424SwipeCancelsPressRepro,
name: 'Issue #424 Swipe Cancels Press Repro',
Expand Down
71 changes: 71 additions & 0 deletions example/src/gh-issues/Issue219EmptyPagerRepro.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
/**
* Repro for #219 on Android: a pager that mounts with no pages, or loses all
* of them, must show its first page as soon as pages arrive, without a touch.
*/
import React, { useRef, useState } from 'react';
import { Button, StyleSheet, Text, View } from 'react-native';
import PagerView from 'react-native-pager-view';

const PAGES = ['A', 'B', 'C', 'D'];
const COLORS: Record<string, string> = {
A: '#fca5a5',
B: '#86efac',
C: '#93c5fd',
D: '#fde68a',
};

export function Issue219EmptyPagerRepro() {
const pagerRef = useRef<PagerView>(null);
const [count, setCount] = useState(0);
const [selected, setSelected] = useState<number | null>(null);
const [events, setEvents] = useState(0);

return (
<View style={styles.screen}>
<Text testID="issue-219-status" style={styles.status}>
{`Pages: ${count} | Selected: ${selected ?? '-'} | Events: ${events}`}
</Text>
<Button
testID="issue-219-add-pages"
title="Add pages"
onPress={() => setCount(PAGES.length)}
/>
<Button
testID="issue-219-remove-pages"
title="Remove pages"
onPress={() => setCount(0)}
/>
<Button
testID="issue-219-select-c"
title="Select C"
onPress={() => pagerRef.current?.setPageWithoutAnimation(2)}
/>
<PagerView
ref={pagerRef}
style={styles.pager}
initialPage={0}
onPageSelected={(e) => {
setSelected(e.nativeEvent.position);
setEvents((value) => value + 1);
}}
>
{PAGES.slice(0, count).map((page) => (
<View
key={page}
style={[styles.page, { backgroundColor: COLORS[page] }]}
>
<Text style={styles.label}>{`Page ${page}`}</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: 32, fontWeight: 'bold' },
});
Loading