Esta página se tradujo automáticamente y puede contener errores. Leer el original en inglés

Elementos

Scroll

Scroll es una vista de desplazamiento nativa (un UIScrollView en iOS y Mac Catalyst, un ScrollView en Android). Dispone todos sus hijos como contenido y los desplaza de forma nativa: el desplazamiento, el lanzamiento (fling), el rebote y el ajuste (snapping) se ejecutan en la plataforma, a la frecuencia de la pantalla. Rust crea una con scroll(), TSX con <Scroll> desde nsh.

el-scroll.tsx
el-scroll.tsxLive
import { mount, Scroll, type ScrollRef, signal, Text, View } from "nsh";
import { button, buttonText, desktop, Inspector, Panel, size, ui, wide } from "./palette";

// The page colors, read when they render: they follow the theme.
const colors = (): string[] => [ui().accent, ui().info, ui().violet, ui().down, ui().warn, ui().up];

function App() {
  const page = signal(0);
  // A page is as wide as the scroll view: 390 on a phone, wider on a desktop.
  const width = signal(390);
  const y = signal(0);
  let scroller!: ScrollRef;
  const rows = Array.from({ length: 40 }, (_, i) => i + 1);

  /** Pages as wide as the scroll view; `inset`: each page's margin. */
  const pager = (inset: number, style: object) => (
    <Scroll
      horizontal
      pagingEnabled
      scrollbar={false}
      style={style}
      onLayout={({ bounds }) => width.set(bounds.size.width)}
      onMomentumEnd={({ offset }) => page.set(Math.round(offset.x / width()))}
    >
      <Each of={colors()}>
        {(color, index) => (
          <View style={{ width: width(), padding: inset }}>
            <View style={{ flex: 1, borderRadius: 16, backgroundColor: color(), alignItems: "center", justifyContent: "center" }}>
              <Text style={{ color: ui().page, fontSize: 32, fontWeight: "700" }}>{index() + 1}</Text>
            </View>
          </View>
        )}
      </Each>
    </Scroll>
  );
  const toTop = (compact: boolean) => (
    <View
      onPress={() => scroller.scrollTo({ y: 0, animated: true })}
      {...button("secondary", { size: compact ? "md" : "sm" })}
    >
      <Text style={buttonText()}>scrollTo top</Text>
    </View>
  );

  return (
    <View style={{ flex: 1, backgroundColor: ui().page, paddingTop: 24, gap: 12 }}>
      {() =>
        wide() ? (
          <View style={{ flex: 1, paddingHorizontal: 20, paddingBottom: 20, gap: 14 }}>
            <Text style={{ fontSize: 22, fontWeight: "700", color: ui().text }}>scroll</Text>
            <Text style={{ color: ui().muted }}>
              {desktop ? "Shift+wheel or swipe the trackpad across the pages; the wheel scrolls the rows" : "Swipe the pages and the rows"}
            </Text>
            <View style={{ flex: 1, flexDirection: "row", gap: 16 }}>
              <View style={{ flex: 1, minWidth: 0, gap: 12 }}>
                {pager(0, { height: 150, flexGrow: 0, borderRadius: 16 })}
                {/* The scroll view fills its region, so its scrollbar sits at the region's edge. */}
                <View style={{ flex: 1, borderRadius: 16, borderWidth: 1, borderColor: ui().line, overflow: "hidden" }}>
                  <Scroll ref={scroller} style={{ flex: 1 }} scrollEventThrottle={16} onScroll={(e) => y.set(e.offset.y)}>
                    <View style={{ padding: 12, gap: 6 }}>
                      <Each of={rows}>
                        {(n) => (
                          <View style={{ ...ui().row, borderRadius: 8, height: size.row, paddingHorizontal: 12, justifyContent: "center" }}>
                            <Text style={{ color: ui().text, fontSize: size.label }}>Row {n()}</Text>
                          </View>
                        )}
                      </Each>
                    </View>
                  </Scroll>
                </View>
              </View>
              <Inspector>
                <Panel title="Horizontal, pagingEnabled · onMomentumEnd">
                  <Text style={{ fontSize: 13, color: ui().text, fontVariant: ["tabular-nums"] }}>
                    page {page() + 1} of {colors().length}
                  </Text>
                </Panel>
                <Panel title="Vertical · onScroll">
                  <Text style={{ fontSize: 13, color: ui().text, fontVariant: ["tabular-nums"] }}>y = {Math.round(y())}</Text>
                </Panel>
                {toTop(true)}
              </Inspector>
            </View>
          </View>
        ) : (
          <>
            <Text style={{ fontSize: 22, fontWeight: "700", color: ui().text, paddingHorizontal: 20 }}>scroll</Text>

            <Text style={{ color: ui().muted, paddingHorizontal: 20 }}>
              Horizontal, pagingEnabled · page {page() + 1} · {desktop ? "Shift+wheel or swipe the trackpad" : "swipe"}
            </Text>
            {pager(12, { height: 140, flexGrow: 0 })}

            <View style={{ width: "100%", maxWidth: 640, alignSelf: "center", flexDirection: "row", justifyContent: "space-between", alignItems: "center", paddingHorizontal: 20 }}>
              <Text style={{ color: ui().muted }}>Vertical · y = {Math.round(y())}</Text>
              {toTop(false)}
            </View>
            <Scroll ref={scroller} style={{ flex: 1 }} scrollEventThrottle={16} onScroll={(e) => y.set(e.offset.y)}>
              <View style={{ width: "100%", maxWidth: 640, alignSelf: "center", paddingHorizontal: 20, paddingBottom: 20, gap: 8 }}>
                <Each of={rows}>
                  {(n) => (
                    <View style={{ ...ui().row, borderRadius: 12, padding: 14 }}>
                      <Text style={{ color: ui().text }}>Row {n()}</Text>
                    </View>
                  )}
                </Each>
              </View>
            </Scroll>
          </>
        )
      }
    </View>
  );
}

mount(App);
use nsh_core::prelude::*;

scroll().flex_1().child(
    view().p_4().gap_2().child(each(
        rows,
        |row: &Row| row.id,
        |row, _index| text(move || row.get().title),
    )),
)

Una vista de desplazamiento crea todos sus hijos, lo que resulta adecuado para páginas de tamaño moderado. Para datos largos o ilimitados usa una lista: crea las filas cercanas al viewport y las recicla.

Una vista de desplazamiento necesita un tamaño acotado a lo largo de su eje: dale flex: 1 (.flex_1()) o una altura dentro de un contenedor que tenga una. Su contenido se dispone al ancho de la vista de desplazamiento (a su alto si es horizontal) y tan alto (ancho) como necesite.

#Props

Además de las props comunes (con style tipado como ViewStyle). En Rust, las props de desplazamiento son métodos del trait Scrollable, que las listas comparten.

#Desplazamiento

Prop Rust Tipo Valor por defecto Descripción
horizontal .horizontal() boolean false Se desplaza lateralmente.
scrollable .scrollable(..) boolean true El usuario puede desplazarla.
scrollbar .scrollbar(..) boolean | ScrollbarTheme true Se muestra el indicador de desplazamiento: el de la plataforma, o la barra integrada de la app una vez que ha definido un tema; un objeto es un tema propio de la vista. En el escritorio y en los navegadores de escritorio una vista horizontal lo conserva: es la barra de desplazamiento del ratón.
bounces .bounces(..) boolean true Desplazamiento excesivo más allá de los bordes.
pagingEnabled .paging(..) boolean false Se detiene en múltiplos del viewport.
snapToInterval .snap_interval(px(..)) number 0 Se detiene en múltiplos de esta longitud (0: desactivado).
snapToOffsets .snap_offsets(..) number[] Se detiene en estos desplazamientos.
snapToAlignment .snap_alignment(SnapAlignment::Center) "start" | "center" | "end" "start" Qué punto de un elemento se alinea con el viewport con snapToInterval.
disableIntervalMomentum .disable_interval_momentum(..) boolean false Un fling avanza como máximo un punto de ajuste.
decelerationRate .deceleration(rate) "normal" | "fast" | number plataforma Con qué rapidez se frena un fling: la velocidad que se conserva por milisegundo (normal 0.998, fast 0.99).
contentInset .content_inset(EdgeInsets { .. }) Partial<Edges> Espacio alrededor del contenido.
contentOffset .content_offset(point(..)) { x?, y? } Dónde comienza.
contentStyle los estilos propios de la vista de contenido ViewStyle El estilo de una vista alrededor de los hijos que se desplaza con ellos; flexGrow: 1 hace que sea al menos tan alta como el viewport (un estado vacío centrado).
contentClass los estilos propios de la vista de contenido string Lo mismo que classes, p. ej. "grow justify-center p-4".
keyboardDismissMode .keyboard_dismiss_mode(KeyboardDismissMode::OnDrag) "none" | "on-drag" | "interactive" "none" Si arrastrar descarta el teclado.
keyboardShouldPersistTaps .keyboard_taps(KeyboardTaps::Handled) "never" | "always" | "handled" | boolean "never" Si un toque descarta el teclado: never (el toque solo lo descarta), always (permanece visible), handled (permanece visible cuando algo gestiona el toque).
scrollValue .scroll_value(&value) AnimatedValue Un valor animado alimentado con el desplazamiento (eje principal), para animaciones vinculadas al scroll.
scrollEventThrottle .event_throttle(ms) number Milisegundos mínimos entre eventos onScroll (como máximo uno por fotograma de todos modos).

#Pull to refresh

Prop Rust Tipo Descripción
refreshing .refreshing(..) boolean Se muestra el indicador de actualización.
onRefresh .on_refresh(..) () => void El usuario tiró para actualizar: inicia la carga y establece refreshing hasta que termine.
refreshIndicator .refresh_indicator(Some(height)) { height: number } La app dibuja su propio indicador, de height puntos de alto: el spinner de la plataforma se oculta, un tirón actualiza al alcanzar height, y mientras se actualiza se mantiene height por encima del contenido. Una altura de 0 o menos significa ninguno.
pullValue .pull_value(&value) AnimatedValue Un valor animado alimentado con cuánto se tira de la vista más allá de su parte superior (0 en reposo, creciendo con el tirón; el espacio que se mantiene sobre el contenido mientras se actualiza: height con refreshIndicator), para el indicador de la app.
let refreshing = signal(false);

scroll()
    .flex_1()
    .refreshing(refreshing)
    .on_refresh(move || {
        refreshing.set(true);
        spawn(async move {
            load().await;
            refreshing.set(false);
        });
    })
    .child(content())

#Zoom

Al establecer maximumZoomScale por encima de minimumZoomScale (.zoom_range(min, max) en Rust) el contenido se puede ampliar con un gesto de pellizco; con zoom, se desplaza en ambos sentidos. Las listas mantienen su escala.

Prop Rust Tipo Valor por defecto Descripción
minimumZoomScale .zoom_range(min, max) number 1 El zoom mínimo.
maximumZoomScale .zoom_range(min, max) number 1 El zoom máximo.
zoomScale .zoom(..) number Hace zoom a esta escala cuando cambia; de lo contrario se conserva el zoom del usuario.
bouncesZoom .bounces_zoom(..) boolean true El pellizco puede sobrepasar los límites y vuelve con un efecto de resorte.
zoomable .zoomable(..) boolean true El usuario puede hacer zoom con un pellizco.
centerContent .center_content(..) boolean false El contenido más pequeño que el viewport (alejado) se centra.
let photo = element_ref();

scroll()
    .element_ref(&photo)
    .zoom_range(1.0, 4.0)
    .center_content(true)
    .flex_1()
    .bg(black())
    .child(
        image(PICTURE)
            .w(px(390.0))
            .h(px(520.0))
            .on_press(move || photo.zoom_to(1.0, None, true)),
    )

#Eventos

Evento Rust Payload Cuándo
onScroll .on_scroll ScrollEvent El desplazamiento cambió (como máximo una vez por fotograma, y no con más frecuencia que scrollEventThrottle).
onScrollDragStart / onScrollDragEnd .on_scroll_drag_start / .on_scroll_drag_end { offset } (Point<Points>) El usuario empezó / dejó de arrastrar.
onMomentumStart / onMomentumEnd .on_momentum_start / .on_momentum_end { offset } Un deslizamiento posterior al arrastre empezó / terminó. Un deslizamiento interrumpido por un nuevo arrastre termina igualmente con onMomentumEnd.
onScrollEnd .on_scroll_end { offset } El desplazamiento llegó al reposo.
onRefresh .on_refresh ninguno Se tiró para actualizar.

ScrollEvent es { offset: Point; contentSize: Size; size: Size; zoomScale } (en Rust offset, content_size, size, zoom_scale): el desplazamiento, el tamaño del contenido (con zoom), el tamaño del viewport y la escala de zoom (1 a menos que la vista tenga zoom).

#Métodos de ref

Método Rust Descripción
scrollTo({ x?, y?, animated? }) scroll_to(point(x, y), animated) Se desplaza a una posición.
zoomTo({ scale, x?, y?, animated? }) zoom_to(scale, Some(point(x, y)), animated) Hace zoom a scale (limitado a los límites) con el punto de contenido (x, y, puntos sin zoom) en el centro del viewport, o alrededor del centro. P. ej. un doble toque que amplía el punto tocado.
let scroller = element_ref();
scroll().element_ref(&scroller).child(content());

scroller.scroll_to(point(px(0.0), px(0.0)), true);

#Teclado

Los campos de entrada dentro de una vista de desplazamiento se mantienen libres del teclado de forma nativa: la vista de desplazamiento recibe un inset inferior y se desplaza por sí sola hasta el campo enfocado. Para contenido fuera de las vistas de desplazamiento usa env(keyboard-inset-height) en un padding o margin inferior. Consulta formularios y el teclado.

#De derecha a izquierda

Las vistas de desplazamiento horizontales comienzan en el extremo derecho en los diseños de derecha a izquierda y mantienen su distancia a él cuando el contenido cambia; sus hijos se reflejan.

#Notas de plataforma

  • Android: NSH dibuja el pull to refresh (el overscroll sigue siendo nativo), contentInset se aplica como padding, y decelerationRate da forma a los deslizamientos de ajuste.
  • Android hace zoom con el scroll view propio de NSH, que escala el contenido alrededor de su esquina superior izquierda y se desplaza en ambos sentidos. Los pellizcos comienzan después de la separación mínima entre los dedos de Android (unos 27 mm).
  • En Mac Catalyst (idioma Mac), NSH detecta el tirón y muestra su spinner.
  • iOS vuelve a dibujar textos y canvases a la resolución con zoom cuando termina un zoom.