Diese Seite wurde maschinell übersetzt und kann Fehler enthalten. Das englische Original lesen

Elemente

Scroll

Scroll ist eine native Scroll-View (UIScrollView unter iOS und Mac Catalyst, eine ScrollView unter Android). Sie legt alle ihre Kinder als Inhalt aus und scrollt sie nativ: Scrollen, Fling, Bouncing und Snapping laufen auf der Plattform, mit der Bildwiederholrate des Displays. In Rust erzeugst du sie mit scroll(), in TSX mit <Scroll> aus 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),
    )),
)

Eine Scroll-View erzeugt jedes Kind, was für Seiten moderater Größe geeignet ist. Für lange oder unbegrenzte Daten verwende eine Liste: Sie erzeugt die Zeilen in der Nähe des Viewports und recycelt sie.

Eine Scroll-View braucht entlang ihrer Achse eine begrenzte Größe: Gib ihr flex: 1 (.flex_1()) oder eine Höhe innerhalb eines Containers, der eine hat. Ihr Inhalt wird in der Breite der Scroll-View (bei horizontaler Ausrichtung in deren Höhe) und so hoch (breit) wie nötig ausgelegt.

#Props

Zusätzlich zu den allgemeinen Props (mit style vom Typ ViewStyle). In Rust sind die Scrolling-Props Methoden des Traits Scrollable, die sich Listen teilen.

#Scrolling

Prop Rust Typ Standard Beschreibung
horizontal .horizontal() boolean false Scrollt seitwärts.
scrollable .scrollable(..) boolean true Der Benutzer kann scrollen.
scrollbar .scrollbar(..) boolean | ScrollbarTheme true Der Scroll-Indikator wird angezeigt: der der Plattform oder die integrierte Leiste der App, sobald diese ein Theme gesetzt hat; ein Objekt ist ein eigenes Theme der View. Auf dem Desktop und in Desktop-Browsern behält eine horizontale View ihn: Er ist die Scroll-Leiste der Maus.
bounces .bounces(..) boolean true Overscroll über die Ränder hinaus.
pagingEnabled .paging(..) boolean false Hält bei Vielfachen des Viewports an.
snapToInterval .snap_interval(px(..)) number 0 Hält bei Vielfachen dieser Länge an (0: aus).
snapToOffsets .snap_offsets(..) number[] Hält bei diesen Offsets an.
snapToAlignment .snap_alignment(SnapAlignment::Center) "start" | "center" | "end" "start" Welcher Punkt eines Elements mit snapToInterval am Viewport ausgerichtet wird.
disableIntervalMomentum .disable_interval_momentum(..) boolean false Ein Fling bewegt höchstens einen Snap-Punkt weit.
decelerationRate .deceleration(rate) "normal" | "fast" | number Plattform Wie schnell ein Fling langsamer wird: die pro Millisekunde beibehaltene Geschwindigkeit (normal 0,998, fast 0,99).
contentInset .content_inset(EdgeInsets { .. }) Partial<Edges> Abstand um den Inhalt.
contentOffset .content_offset(point(..)) { x?, y? } Wo sie beginnt.
contentStyle die eigenen Styles der Content-View ViewStyle Der Style einer View um die Kinder, die mit ihnen scrollt; flexGrow: 1 macht sie mindestens so hoch wie den Viewport (ein zentrierter Leerzustand).
contentClass die eigenen Styles der Content-View string Dasselbe wie Klassen, z. B. "grow justify-center p-4".
keyboardDismissMode .keyboard_dismiss_mode(KeyboardDismissMode::OnDrag) "none" | "on-drag" | "interactive" "none" Ob Ziehen die Tastatur schließt.
keyboardShouldPersistTaps .keyboard_taps(KeyboardTaps::Handled) "never" | "always" | "handled" | boolean "never" Ob ein Tippen die Tastatur schließt: never (das Tippen schließt sie nur), always (sie bleibt oben), handled (sie bleibt oben, wenn etwas das Tippen verarbeitet).
scrollValue .scroll_value(&value) AnimatedValue Ein animierter Wert, der mit dem Scroll-Offset (Hauptachse) gespeist wird, für scrollgekoppelte Animationen.
scrollEventThrottle .event_throttle(ms) number Minimale Millisekunden zwischen onScroll-Events (ohnehin höchstens eines pro Frame).

#Pull to refresh

Prop Rust Typ Beschreibung
refreshing .refreshing(..) boolean Der Refresh-Spinner wird angezeigt.
onRefresh .on_refresh(..) () => void Der Benutzer hat zum Aktualisieren gezogen: Starte das Laden und setze refreshing, bis es endet.
refreshIndicator .refresh_indicator(Some(height)) { height: number } Die App zeichnet ihren eigenen Indikator, height Punkte hoch: Der Spinner der Plattform wird ausgeblendet, ein Ziehen löst die Aktualisierung aus, sobald es height erreicht, und während der Aktualisierung bleibt height über dem Inhalt. Eine Höhe von 0 oder weniger bedeutet keinen.
pullValue .pull_value(&value) AnimatedValue Ein animierter Wert, der damit gespeist wird, wie weit die View über ihren oberen Rand hinausgezogen ist (0 in Ruhe, wächst mit dem Ziehen; der Platz, der während der Aktualisierung über dem Inhalt bleibt: height mit refreshIndicator), für den Indikator der 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())

#Zooming

Wenn maximumZoomScale über minimumZoomScale (.zoom_range(min, max) in Rust) gesetzt wird, lässt sich der Inhalt per Pinch zoomen; hineingezoomt scrollt er in beide Richtungen. Listen behalten ihren Maßstab.

Prop Rust Typ Standard Beschreibung
minimumZoomScale .zoom_range(min, max) number 1 Der kleinste Zoom.
maximumZoomScale .zoom_range(min, max) number 1 Der größte Zoom.
zoomScale .zoom(..) number Zoomt auf diesen Maßstab, wenn er sich ändert; andernfalls bleibt der Zoom des Benutzers erhalten.
bouncesZoom .bounces_zoom(..) boolean true Pinchen darf die Grenzen überschreiten und federt zurück.
zoomable .zoomable(..) boolean true Der Benutzer kann per Pinch zoomen.
centerContent .center_content(..) boolean false Inhalt, der kleiner als der Viewport ist (herausgezoomt), wird zentriert.
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)),
    )

#Events

Event Rust Payload Wann
onScroll .on_scroll ScrollEvent Der Offset hat sich geändert (höchstens einmal pro Frame und nicht öfter als scrollEventThrottle).
onScrollDragStart / onScrollDragEnd .on_scroll_drag_start / .on_scroll_drag_end { offset } (Point<Points>) Der Benutzer hat begonnen / aufgehört zu ziehen.
onMomentumStart / onMomentumEnd .on_momentum_start / .on_momentum_end { offset } Ein Gleiten nach dem Ziehen hat begonnen / aufgehört. Ein Gleiten, das von einem neuen Ziehen abgefangen wird, endet trotzdem mit onMomentumEnd.
onScrollEnd .on_scroll_end { offset } Das Scrollen ist zur Ruhe gekommen.
onRefresh .on_refresh keine Zum Aktualisieren gezogen.

ScrollEvent ist { offset: Point; contentSize: Size; size: Size; zoomScale } (in Rust offset, content_size, size, zoom_scale): der Offset, die (gezoomte) Inhaltsgröße, die Viewport-Größe und der Zoom-Maßstab (1, sofern die View nicht zoomt).

#Ref-Methoden

Methode Rust Beschreibung
scrollTo({ x?, y?, animated? }) scroll_to(point(x, y), animated) Scrollt zu einem Offset.
zoomTo({ scale, x?, y?, animated? }) zoom_to(scale, Some(point(x, y)), animated) Zoomt auf scale (auf die Grenzen begrenzt), wobei der Inhaltspunkt (x, y, ungezoomte Punkte) in der Mitte des Viewports liegt, oder um die Mitte herum. Z. B. ein Doppeltippen, das auf den getippten Punkt hineinzoomt.
let scroller = element_ref();
scroll().element_ref(&scroller).child(content());

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

#Tastatur

Eingabefelder in einer Scroll-View werden nativ von der Tastatur freigehalten: Die Scroll-View erhält einen unteren Inset und scrollt von selbst zum fokussierten Eingabefeld. Für Inhalte außerhalb von Scroll-Views verwende env(keyboard-inset-height) in einem unteren Padding oder Margin. Siehe Formulare und die Tastatur.

Horizontale Scroll-Views beginnen in Rechts-nach-links-Layouts am rechten Ende und behalten ihren Abstand davon bei, wenn sich der Inhalt ändert; ihre Kinder werden gespiegelt.

#Hinweise zu Plattformen

  • Android: Pull to refresh wird von NSH gezeichnet (Overscroll bleibt nativ), contentInset wird als Padding angewendet, und decelerationRate formt Snapping-Gleitbewegungen.
  • Android zoomt mit NSHs eigener Scroll-View, die den Inhalt um seine obere linke Ecke skaliert und in beide Richtungen scrollt. Pinches beginnen nach dem minimalen Abstand zwischen den Fingern unter Android (etwa 27 mm).
  • Unter Mac Catalyst (Mac-Idiom) erkennt NSH das Ziehen und zeigt seinen Spinner an.
  • iOS zeichnet Texte und Canvases in der gezoomten Auflösung neu, wenn ein Zoom endet.