Elementi
Scroll
Scroll è una scroll view nativa (UIScrollView su iOS e Mac Catalyst, una ScrollView su Android). Dispone tutti i suoi
figli come contenuto e li fa scorrere in modo nativo: lo scorrimento, il fling, il rimbalzo e lo snapping vengono eseguiti dalla piattaforma, alla
frequenza del display. In Rust se ne crea una con scroll(), in TSX con <Scroll> da 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>Una scroll view crea tutti i figli, il che è adatto a pagine di dimensioni moderate. Per dati lunghi o illimitati usa una list: crea le righe vicino al viewport e le ricicla.
Una scroll view ha bisogno di una dimensione limitata lungo il suo asse: assegnale flex: 1 (.flex_1()) oppure un’altezza all’interno di un contenitore che
ne abbia una. Il suo contenuto viene disposto alla larghezza della scroll view (all’altezza, se è orizzontale) e con l’altezza (larghezza) che gli serve.
#Props
Oltre alle common props (con style di tipo ViewStyle). In Rust, le props di scorrimento
sono metodi del trait Scrollable, che le list condividono.
#Scrolling
| Prop | Rust | Tipo | Default | Descrizione |
|---|---|---|---|---|
horizontal | .horizontal() | boolean | false | Scorre in orizzontale. |
scrollable | .scrollable(..) | boolean | true | L’utente può farla scorrere. |
scrollbar | .scrollbar(..) | boolean | ScrollbarTheme | true | L’indicatore di scorrimento è visibile: quello della piattaforma, oppure la barra integrata dell’app una volta impostato un tema; un oggetto è un tema proprio della vista. Su desktop e nei browser desktop una vista orizzontale lo mantiene: è la barra di scorrimento del mouse. |
bounces | .bounces(..) | boolean | true | Overscroll oltre i bordi. |
pagingEnabled | .paging(..) | boolean | false | Si ferma ai multipli del viewport. |
snapToInterval | .snap_interval(px(..)) | number | 0 | Si ferma ai multipli di questa lunghezza (0: disattivato). |
snapToOffsets | .snap_offsets(..) | number[] | Si ferma a questi offset. | |
snapToAlignment | .snap_alignment(SnapAlignment::Center) | "start" | "center" | "end" | "start" | Quale punto di un elemento si allinea al viewport con snapToInterval. |
disableIntervalMomentum | .disable_interval_momentum(..) | boolean | false | Un fling si sposta al massimo di un punto di snap. |
decelerationRate | .deceleration(rate) | "normal" | "fast" | number | piattaforma | Quanto velocemente rallenta un fling: la velocità mantenuta per millisecondo (normal 0.998, fast 0.99). |
contentInset | .content_inset(EdgeInsets { .. }) | Partial<Edges> | Spazio attorno al contenuto. | |
contentOffset | .content_offset(point(..)) | { x?, y? } | Da dove parte. | |
contentStyle | gli stili propri della vista del contenuto | ViewStyle | Lo stile di una vista attorno ai figli che scorre insieme a loro; flexGrow: 1 la rende alta almeno quanto il viewport (uno stato vuoto centrato). | |
contentClass | gli stili propri della vista del contenuto | string | Come classes, ad es. "grow justify-center p-4". | |
keyboardDismissMode | .keyboard_dismiss_mode(KeyboardDismissMode::OnDrag) | "none" | "on-drag" | "interactive" | "none" | Indica se il trascinamento chiude la tastiera. |
keyboardShouldPersistTaps | .keyboard_taps(KeyboardTaps::Handled) | "never" | "always" | "handled" | boolean | "never" | Indica se un tap chiude la tastiera: never (il tap la chiude soltanto), always (rimane aperta), handled (rimane aperta se qualcosa gestisce il tap). |
scrollValue | .scroll_value(&value) | AnimatedValue | Un valore animato alimentato con l’offset di scorrimento (asse principale), per animazioni collegate allo scorrimento. | |
scrollEventThrottle | .event_throttle(ms) | number | Millisecondi minimi tra gli eventi onScroll (comunque al massimo uno per frame). |
#Pull to refresh
| Prop | Rust | Tipo | Descrizione |
|---|---|---|---|
refreshing | .refreshing(..) | boolean | Lo spinner di aggiornamento è visibile. |
onRefresh | .on_refresh(..) | () => void | L’utente ha trascinato per aggiornare: avvia il caricamento e imposta refreshing finché non termina. |
refreshIndicator | .refresh_indicator(Some(height)) | { height: number } | L’app disegna un proprio indicatore, alto height punti: lo spinner della piattaforma è nascosto, un pull aggiorna una volta raggiunto height, e durante l’aggiornamento height resta sopra il contenuto. Un’altezza pari o inferiore a 0 equivale a nessuno. |
pullValue | .pull_value(&value) | AnimatedValue | Un valore animato alimentato con quanto la vista è trascinata oltre il suo bordo superiore (0 a riposo, in crescita con il pull; lo spazio mantenuto sopra il contenuto durante l’aggiornamento: height con refreshIndicator), per l’indicatore dell’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
Impostare maximumZoomScale sopra minimumZoomScale (.zoom_range(min, max) in Rust) rende il contenuto ingrandibile con un
pinch; con lo zoom attivo, scorre in entrambe le direzioni. Le list mantengono la loro scala.
| Prop | Rust | Tipo | Default | Descrizione |
|---|---|---|---|---|
minimumZoomScale | .zoom_range(min, max) | number | 1 | Lo zoom minimo. |
maximumZoomScale | .zoom_range(min, max) | number | 1 | Lo zoom massimo. |
zoomScale | .zoom(..) | number | Esegue lo zoom a questa scala quando cambia; altrimenti lo zoom dell’utente viene mantenuto. | |
bouncesZoom | .bounces_zoom(..) | boolean | true | Il pinch può superare i limiti e poi torna indietro con un effetto elastico. |
zoomable | .zoomable(..) | boolean | true | L’utente può usare il pinch per lo zoom. |
centerContent | .center_content(..) | boolean | false | Il contenuto più piccolo del viewport (con zoom ridotto) viene centrato. |
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>;#Eventi
| Evento | Rust | Payload | Quando |
|---|---|---|---|
onScroll | .on_scroll | ScrollEvent | L’offset è cambiato (al massimo una volta per frame, e non più spesso di scrollEventThrottle). |
onScrollDragStart / onScrollDragEnd | .on_scroll_drag_start / .on_scroll_drag_end | { offset } (Point<Points>) | L’utente ha iniziato / smesso di trascinare. |
onMomentumStart / onMomentumEnd | .on_momentum_start / .on_momentum_end | { offset } | Uno scivolamento dopo il trascinamento è iniziato / terminato. Uno scivolamento interrotto da un nuovo trascinamento termina comunque con onMomentumEnd. |
onScrollEnd | .on_scroll_end | { offset } | Lo scorrimento si è fermato. |
onRefresh | .on_refresh | nessuno | Trascinato per aggiornare. |
ScrollEvent è { offset: Point; contentSize: Size; size: Size; zoomScale } (in Rust offset, content_size,
size, zoom_scale): l’offset, la dimensione del contenuto (con zoom), la dimensione del viewport e la scala di zoom (1 se la vista
non esegue zoom).
#Metodi del ref
| Metodo | Rust | Descrizione |
|---|---|---|
scrollTo({ x?, y?, animated? }) | scroll_to(point(x, y), animated) | Scorre fino a un offset. |
zoomTo({ scale, x?, y?, animated? }) | zoom_to(scale, Some(point(x, y)), animated) | Esegue lo zoom a scale (limitato ai limiti) con il punto del contenuto (x, y, punti senza zoom) al centro del viewport, oppure attorno al centro. Ad es. un doppio tap che ingrandisce sul punto toccato. |
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 });#Tastiera
Gli input all’interno di una scroll view vengono tenuti liberi dalla tastiera in modo nativo: la scroll view riceve un inset inferiore e scorre
da sola fino all’input con il focus. Per il contenuto fuori dalle scroll view usa env(keyboard-inset-height)
in un padding o margin inferiore. Vedi forms and the keyboard.
#Da destra a sinistra
Le scroll view orizzontali partono dall’estremità destra nei layout da destra a sinistra e mantengono la loro distanza da essa quando il contenuto cambia; i loro figli sono specchiati.
#Note sulle piattaforme
- Android: il pull to refresh è disegnato da NSH (l’overscroll resta nativo),
contentInsetviene applicato come padding, edecelerationRatedà forma agli scivolamenti di snapping. - Android esegue lo zoom con la scroll view propria di NSH, che scala il contenuto attorno all’angolo in alto a sinistra e scorre in entrambe le direzioni. I pinch iniziano dopo la distanza minima tra le dita di Android (circa 27 mm).
- Su Mac Catalyst (idioma Mac), NSH rileva il pull e mostra il proprio spinner.
- iOS ridisegna testi e canvas alla risoluzione dello zoom quando uno zoom termina.