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.
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),
)),
)<Scroll style={{ flex: 1 }}>
<View style={{ padding: 16, gap: 8 }}>
<Each of={rows}>{(row) => <Text>{row().title}</Text>}</Each>
</View>
</Scroll>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())const refreshing = signal(false);
async function reload() {
refreshing.set(true);
await load();
refreshing.set(false);
}
<Scroll style={{ flex: 1 }} refreshing={refreshing()} onRefresh={reload}>
…
</Scroll>;#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)),
)let photo!: ScrollRef;
<Scroll ref={photo} maximumZoomScale={4} centerContent style={{ flex: 1, backgroundColor: "#000" }}>
<Image src={picture} style={{ width: 390, height: 520 }} onPress={() => photo.zoomTo({ scale: 1, animated: true })} />
</Scroll>;#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);let scroller!: ScrollRef;
<Scroll ref={scroller}>…</Scroll>;
scroller.scrollTo({ y: 0, animated: 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.
#Rechts nach links
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),
contentInsetwird als Padding angewendet, unddecelerationRateformt 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.