Hacktoberfest 2026: los issues que los mantenedores marcaron para octubre, abiertos y aptos para principiantes. Explorar issues de Hacktoberfest

[Android] v9: pager starts on the last page (initialPage ignored) when first laid out with zero width, e.g. inside `display: 'none'`

Abierto
#1,191 0 comentarios 0 reacciones 0 asignados Ver en GitHub

Los mantenedores suelen responder en 1 día

Nadie ha tomado este issue todavía.

Evaluación

Dificultad
3/5
Tiempo estimado
1-2 días
Aptitud para principiantes
73/100
Tipo de issue
Error
Claridad
Bien especificado
Estado de actividad
Activo
Stack tecnológico
android, react-native, typescript
Área
mobile

Línea de trabajo

Start by reading ComposePagerView.onMeasure and measureComposeView() in the Android implementation; the issue describes how Fabric's zero-size measurement affects the Compose pager. Check the Android pager tests for coverage of initial pages and layout changes, then reproduce the zero-width cases from the supplied component. Done means initialPage remains selected when the pager is first measured at zero width, while the listed height-zero and hide/show cases continue to behave as reported.

Escrito por el modelo de indexación a partir del texto del issue.

Descripción

Environment

Package Version
react-native-pager-view 9.0.9 (same with 9.0.7; not seen with 8.0.2 in the same app)
react-native 0.88.0 (New Architecture, Hermes)
react 19.3.0
expo 58.0.14
  • Platform: Android (iOS not tested)
  • Device: Samsung SM-F976N, Android API 37, release build

Description

If a PagerView is mounted while its width is 0 (for example inside a display: 'none' container, or a container with width: 0), it starts on the last page instead of initialPage. It reports that with onPageSelected(lastIndex) right away, while it's still hidden. When the container later gets its real size, the pager stays on the last page.

We hit this with tab screens that are all mounted at startup while the inactive ones are hidden. A pager in an inactive tab came up on its last page. Our JS tracked the page from onPageScroll and still thought it was on page 0, so tapping the last tab's header did nothing and the visible page was blank.

These cases do not trigger it:

  • zero height with full width (height: 0)
  • hiding an already laid-out pager with display: 'none' and showing it again
  • adding a page while it's hidden

It only happens when the pager is first laid out with a zero-width viewport.

Steps to reproduce

Render the component below (written in the style of example/src/gh-issues) and:

  1. A: tap "A show" (the pager was mounted inside a display: 'none' container).
  2. D: tap "D expand" (the pager was mounted inside a width: 0 container).

Expected: page 0 is shown and onPageSelected(0) is reported.

Actual: page 3 (the last page) is shown. onPageSelected(3) is reported at mount, before the container is shown.

Results on the device above. Each run started from a cold start, 3 runs per build:

Case 9.0.9 9.0.9 + change below
A: mounted with display: 'none', then shown last page, onPageSelected(3) (3/3) page 0, onPageSelected(0) (3/3)
D: mounted with width: 0, then expanded last page, onPageSelected(3) (3/3) page 0, onPageSelected(0) (3/3)
B: laid out, setPage(1), hidden, shown stays on page 1 stays on page 1
C: mounted with height: 0, then expanded page 0 page 0
E: hidden, page appended while hidden, shown page 0 page 0

Screen recordings of A and D (tap "A show" / "D expand"), before and after the change:

9.0.9:

https://github.com/user-attachments/assets/8265a998-5228-4fda-903d-504d898acab1

https://github.com/user-attachments/assets/2de148ea-a9bc-48c5-b8c0-9b4bf04b3449

9.0.9 + change below:

https://github.com/user-attachments/assets/58fb6a43-4a30-4907-a400-5618d2f74bf1

https://github.com/user-attachments/assets/9320320d-9ece-49b4-bb95-410d04fc1571

Last frame of each recording, side by side (left: 9.0.9, right: with the change):

Image Image
Repro component
import { useCallback, useRef, useState } from 'react';
import { Button, ScrollView, StyleSheet, Text, View } from 'react-native';
import PagerView from 'react-native-pager-view';

const COLORS = ['#ffb3ba', '#bae1ff', '#baffc9', '#ffffba'];
const HOST_HEIGHT = 120;

function usePagerLog() {
  const [selected, setSelected] = useState<number | null>(null);
  const [events, setEvents] = useState<string[]>([]);
  const push = useCallback(
    (e: string) => setEvents((prev) => [...prev.slice(-5), e]),
    []
  );
  const onPageSelected = useCallback(
    (e: { nativeEvent: { position: number } }) => {
      setSelected(e.nativeEvent.position);
      push(`selected:${e.nativeEvent.position}`);
    },
    [push]
  );
  const onPageScroll = useCallback(
    (e: { nativeEvent: { position: number; offset: number } }) => {
      const { position, offset } = e.nativeEvent;
      if (offset === 0) push(`scroll:${position}`);
    },
    [push]
  );
  return { selected, events, onPageSelected, onPageScroll };
}

function Pages({ tag, count = 4 }: { tag: string; count?: number }) {
  return Array.from({ length: count }, (_, i) => COLORS[i % COLORS.length]).map(
    (color, i) => (
      <View key={i} collapsable={false} style={[styles.page, { backgroundColor: color }]}>
        <Text style={styles.pageLabel}>{`${tag} PAGE ${i}`}</Text>
      </View>
    )
  );
}

function Log({ tag, log }: { tag: string; log: ReturnType<typeof usePagerLog> }) {
  return (
    <Text style={styles.log}>
      {`${tag} selected=${log.selected ?? '-'} events=[${log.events.join(' ')}]`}
    </Text>
  );
}

/** A: mounted inside a `display: 'none'` container, then shown. */
function HiddenOnMountCase() {
  const [shown, setShown] = useState(false);
  const log = usePagerLog();
  return (
    <View style={styles.case}>
      <Text style={styles.heading}>A: mounted hidden (display: none), then shown</Text>
      <Button title="A show" onPress={() => setShown(true)} />
      <View style={[styles.host, { display: shown ? 'flex' : 'none' }]}>
        <PagerView
          style={styles.pager}
          initialPage={0}
          onPageSelected={log.onPageSelected}
          onPageScroll={log.onPageScroll}
        >
          {Pages({ tag: 'A' })}
        </PagerView>
      </View>
      <Log tag="A" log={log} />
    </View>
  );
}

/** B: laid out, moved to page 1, hidden with `display: 'none'`, shown again. */
function HideAfterMountCase() {
  const [shown, setShown] = useState(true);
  const ref = useRef<PagerView>(null);
  const log = usePagerLog();
  return (
    <View style={styles.case}>
      <Text style={styles.heading}>B: go to page 1, hide, show</Text>
      <View style={styles.row}>
        <Button title="B page 1" onPress={() => ref.current?.setPage(1)} />
        <Button title="B hide" onPress={() => setShown(false)} />
        <Button title="B show" onPress={() => setShown(true)} />
      </View>
      <View style={[styles.host, { display: shown ? 'flex' : 'none' }]}>
        <PagerView
          ref={ref}
          style={styles.pager}
          initialPage={0}
          onPageSelected={log.onPageSelected}
          onPageScroll={log.onPageScroll}
        >
          {Pages({ tag: 'B' })}
        </PagerView>
      </View>
      <Log tag="B" log={log} />
    </View>
  );
}

/** C: mounted inside a zero-height container, then expanded. */
function ZeroHeightOnMountCase() {
  const [expanded, setExpanded] = useState(false);
  const log = usePagerLog();
  return (
    <View style={styles.case}>
      <Text style={styles.heading}>C: mounted at height 0, then expanded</Text>
      <Button title="C expand" onPress={() => setExpanded(true)} />
      <View style={[styles.host, { height: expanded ? HOST_HEIGHT : 0 }]}>
        <PagerView
          style={styles.pager}
          initialPage={0}
          onPageSelected={log.onPageSelected}
          onPageScroll={log.onPageScroll}
        >
          {Pages({ tag: 'C' })}
        </PagerView>
      </View>
      <Log tag="C" log={log} />
    </View>
  );
}

/** D: mounted inside a zero-width container, then expanded. */
function ZeroWidthOnMountCase() {
  const [expanded, setExpanded] = useState(false);
  const log = usePagerLog();
  return (
    <View style={styles.case}>
      <Text style={styles.heading}>D: mounted at width 0, then expanded</Text>
      <Button title="D expand" onPress={() => setExpanded(true)} />
      <View style={[styles.host, expanded ? null : { width: 0 }]}>
        <PagerView
          style={styles.pager}
          initialPage={0}
          onPageSelected={log.onPageSelected}
          onPageScroll={log.onPageScroll}
        >
          {Pages({ tag: 'D' })}
        </PagerView>
      </View>
      <Log tag="D" log={log} />
    </View>
  );
}

/** E: laid out, hidden with `display: 'none'`, a page appended while hidden, shown again. */
function AddPageWhileHiddenCase() {
  const [shown, setShown] = useState(true);
  const [count, setCount] = useState(4);
  const log = usePagerLog();
  return (
    <View style={styles.case}>
      <Text style={styles.heading}>E: hide, add a page while hidden, show</Text>
      <View style={styles.row}>
        <Button title="E hide" onPress={() => setShown(false)} />
        <Button title="E add" onPress={() => setCount((c) => c + 1)} />
        <Button title="E show" onPress={() => setShown(true)} />
      </View>
      <View style={[styles.host, { display: shown ? 'flex' : 'none' }]}>
        <PagerView
          style={styles.pager}
          initialPage={0}
          onPageSelected={log.onPageSelected}
          onPageScroll={log.onPageScroll}
        >
          {Pages({ tag: 'E', count })}
        </PagerView>
      </View>
      <Log tag="E" log={log} />
    </View>
  );
}

export function ZeroWidthInitialPageRepro() {
  return (
    <ScrollView contentContainerStyle={styles.container}>
      <HiddenOnMountCase />
      <HideAfterMountCase />
      <ZeroHeightOnMountCase />
      <ZeroWidthOnMountCase />
      <AddPageWhileHiddenCase />
    </ScrollView>
  );
}

const styles = StyleSheet.create({
  container: { padding: 12, gap: 12, backgroundColor: '#fff' },
  case: { gap: 6 },
  heading: { fontWeight: '600', color: '#000' },
  row: { flexDirection: 'row', gap: 8 },
  host: { height: HOST_HEIGHT, overflow: 'hidden' },
  pager: { flex: 1 },
  page: { flex: 1, alignItems: 'center', justifyContent: 'center' },
  pageLabel: { fontSize: 20, color: '#000' },
  log: { fontSize: 12, color: '#000' },
});

Cause (as far as we can tell)

Fabric lays out views under display: 'none', or inside a zero-width container, at 0×0 and calls measure(EXACTLY 0, EXACTLY 0) on them (SurfaceMountingManager.updateLayout). ComposePagerView.onMeasure first calls super.onMeasure (FrameLayout), which measures the child ComposeView with that 0×0 spec. measureComposeView() already skips zero sizes, but by then the ComposeView has been measured.

After that first measurement, pagerState.currentPage is the last page. The first LaunchedEffect(pageCount) then reports it as the initial onPageSelected, which matches the selected:3 we see at mount. We didn't dig into why HorizontalPager settles on the last page with a zero-width viewport. We only observed that it does, and that it keeps that page once it gets a real size.

Possible fix

Don't measure the ComposeView when the incoming size is zero. The pager keeps its last layout, and the normal path runs once Fabric measures it with a real size:

   override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
+    val width = MeasureSpec.getSize(widthMeasureSpec)
+    val height = MeasureSpec.getSize(heightMeasureSpec)
+    if (width <= 0 || height <= 0) {
+      setMeasuredDimension(width, height)
+      return
+    }
     super.onMeasure(widthMeasureSpec, heightMeasureSpec)
     measureComposeView()
   }

With this change all five cases behave as expected (table above). In our app, the pager in the hidden tab starts on page 0. Swiping, setPage, and hiding/showing behave the same as before. We only tested this on the device above.

Happy to open a PR with this change if it looks right.

Lenguaje dominante
TypeScript
Estrellas
3.4k
Forks
485
Merge medio
2 d 11 h
PR fusionados (30 d)
26

Preparar el entorno

Primeros pasos

  1. Lee el issue completo y luego la guía de contribución del proyecto.
  2. Comenta en el issue que vas a ocuparte — evita que dos personas hagan lo mismo.
  3. Haz un fork del repositorio y trabaja en una rama.
  4. Abre un pull request que haga referencia al número del issue.

Más de callstack/react-native-pager-view

Todos los issues de callstack/react-native-pager-view

Issues similares

Más issues de TypeScript

Recibe los nuevos issues en tu correo

Un resumen breve de issues de GitHub para principiantes.