Questa pagina è stata tradotta automaticamente e può contenere errori. Leggi l'originale in inglese

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.

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 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

PropRustTipoDefaultDescrizione
horizontal.horizontal()booleanfalseScorre in orizzontale.
scrollable.scrollable(..)booleantrueL’utente può farla scorrere.
scrollbar.scrollbar(..)boolean | ScrollbarThemetrueL’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(..)booleantrueOverscroll oltre i bordi.
pagingEnabled.paging(..)booleanfalseSi ferma ai multipli del viewport.
snapToInterval.snap_interval(px(..))number0Si 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(..)booleanfalseUn fling si sposta al massimo di un punto di snap.
decelerationRate.deceleration(rate)"normal" | "fast" | numberpiattaformaQuanto 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.
contentStylegli stili propri della vista del contenutoViewStyleLo 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).
contentClassgli stili propri della vista del contenutostringCome 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)AnimatedValueUn valore animato alimentato con l’offset di scorrimento (asse principale), per animazioni collegate allo scorrimento.
scrollEventThrottle.event_throttle(ms)numberMillisecondi minimi tra gli eventi onScroll (comunque al massimo uno per frame).

#Pull to refresh

PropRustTipoDescrizione
refreshing.refreshing(..)booleanLo spinner di aggiornamento è visibile.
onRefresh.on_refresh(..)() => voidL’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)AnimatedValueUn 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())

#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.

PropRustTipoDefaultDescrizione
minimumZoomScale.zoom_range(min, max)number1Lo zoom minimo.
maximumZoomScale.zoom_range(min, max)number1Lo zoom massimo.
zoomScale.zoom(..)numberEsegue lo zoom a questa scala quando cambia; altrimenti lo zoom dell’utente viene mantenuto.
bouncesZoom.bounces_zoom(..)booleantrueIl pinch può superare i limiti e poi torna indietro con un effetto elastico.
zoomable.zoomable(..)booleantrueL’utente può usare il pinch per lo zoom.
centerContent.center_content(..)booleanfalseIl 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)),
    )

#Eventi

EventoRustPayloadQuando
onScroll.on_scrollScrollEventL’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_refreshnessunoTrascinato 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

MetodoRustDescrizione
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);

#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), contentInset viene applicato come padding, e decelerationRate dà 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.