Cette page a été traduite automatiquement et peut contenir des erreurs. Lire l'original en anglais

Éléments

Scroll

Scroll est une vue de défilement native (UIScrollView sur iOS et Mac Catalyst, un ScrollView sur Android). Elle dispose tous ses enfants comme son contenu et les fait défiler nativement : le défilement, le lancer (fling), le rebond et l’aimantation s’exécutent sur la plateforme, à la fréquence de l’écran. Rust en crée une avec scroll(), TSX avec <Scroll> depuis 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),
    )),
)

Une vue de défilement crée tous ses enfants, ce qui convient aux pages de taille modérée. Pour des données longues ou non bornées, utilisez une liste : elle crée les lignes proches de la zone visible et les recycle.

Une vue de défilement a besoin d’une taille bornée selon son axe : donnez-lui flex: 1 (.flex_1()) ou une hauteur dans un conteneur qui en a une. Son contenu est disposé à la largeur de la vue de défilement (à sa hauteur si elle est horizontale) et aussi haut (large) que nécessaire.

#Props

En plus des props communes (avec style typé comme ViewStyle). En Rust, les props de défilement sont des méthodes du trait Scrollable, que les listes partagent.

#Scrolling

PropRustTypeDéfautDescription
horizontal.horizontal()booleanfalseDéfile latéralement.
scrollable.scrollable(..)booleantrueL’utilisateur peut la faire défiler.
scrollbar.scrollbar(..)boolean | ScrollbarThemetrueL’indicateur de défilement s’affiche : celui de la plateforme, ou la barre intégrée de l’app dès qu’elle a défini un thème ; un objet est un thème propre à la vue. Sur le bureau et dans les navigateurs de bureau, une vue horizontale le conserve : c’est la barre de défilement de la souris.
bounces.bounces(..)booleantrueDéfilement au-delà des bords (overscroll).
pagingEnabled.paging(..)booleanfalseS’arrête aux multiples de la zone visible.
snapToInterval.snap_interval(px(..))number0S’arrête aux multiples de cette longueur (0 : désactivé).
snapToOffsets.snap_offsets(..)number[]S’arrête à ces décalages.
snapToAlignment.snap_alignment(SnapAlignment::Center)"start" | "center" | "end""start"Quel point d’un élément s’aligne avec la zone visible avec snapToInterval.
disableIntervalMomentum.disable_interval_momentum(..)booleanfalseUn lancer avance d’un point d’aimantation au plus.
decelerationRate.deceleration(rate)"normal" | "fast" | numberplateformeLa vitesse à laquelle un lancer ralentit : la vitesse conservée par milliseconde (normal 0,998, fast 0,99).
contentInset.content_inset(EdgeInsets { .. })Partial<Edges>Espace autour du contenu.
contentOffset.content_offset(point(..)){ x?, y? }Où elle démarre.
contentStyleles styles propres de la vue de contenuViewStyleLe style d’une vue autour des enfants qui défile avec eux ; flexGrow: 1 la rend au moins aussi haute que la zone visible (un état vide centré).
contentClassles styles propres de la vue de contenustringIdem avec des classes, p. ex. "grow justify-center p-4".
keyboardDismissMode.keyboard_dismiss_mode(KeyboardDismissMode::OnDrag)"none" | "on-drag" | "interactive""none"Indique si le glissement ferme le clavier.
keyboardShouldPersistTaps.keyboard_taps(KeyboardTaps::Handled)"never" | "always" | "handled" | boolean"never"Indique si un tap ferme le clavier : never (le tap ne fait que le fermer), always (il reste affiché), handled (il reste affiché quand quelque chose gère le tap).
scrollValue.scroll_value(&value)AnimatedValueUne valeur animée alimentée par le décalage de défilement (axe principal), pour les animations liées au défilement.
scrollEventThrottle.event_throttle(ms)numberNombre minimal de millisecondes entre les événements onScroll (au plus un par frame de toute façon).

#Pull to refresh

PropRustTypeDescription
refreshing.refreshing(..)booleanLe spinner d’actualisation s’affiche.
onRefresh.on_refresh(..)() => voidL’utilisateur a tiré pour actualiser : lancez le chargement et définissez refreshing jusqu’à sa fin.
refreshIndicator.refresh_indicator(Some(height)){ height: number }L’app dessine son propre indicateur, de height points de haut : le spinner de la plateforme est masqué, un tirage actualise une fois height atteint, et pendant l’actualisation height est conservé au-dessus du contenu. Une hauteur de 0 ou moins signifie aucun.
pullValue.pull_value(&value)AnimatedValueUne valeur animée alimentée par la distance dont la vue est tirée au-delà de son haut (0 au repos, croissant avec le tirage ; l’espace conservé au-dessus du contenu pendant l’actualisation : height avec refreshIndicator), pour l’indicateur de l’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

Définir maximumZoomScale au-dessus de minimumZoomScale (.zoom_range(min, max) en Rust) rend le contenu zoomable par pincement ; une fois zoomé, il défile dans les deux sens. Les listes conservent leur échelle.

PropRustTypeDéfautDescription
minimumZoomScale.zoom_range(min, max)number1Le zoom minimal.
maximumZoomScale.zoom_range(min, max)number1Le zoom maximal.
zoomScale.zoom(..)numberZoome à cette échelle lorsqu’elle change ; sinon le zoom de l’utilisateur est conservé.
bouncesZoom.bounces_zoom(..)booleantrueLe pincement peut dépasser les limites et revient en arrière avec un effet de ressort.
zoomable.zoomable(..)booleantrueL’utilisateur peut pincer pour zoomer.
centerContent.center_content(..)booleanfalseUn contenu plus petit que la zone visible (dézoomé) est centré.
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)),
    )

#Événements

ÉvénementRustPayloadQuand
onScroll.on_scrollScrollEventLe décalage a changé (au plus une fois par frame, et pas plus souvent que scrollEventThrottle).
onScrollDragStart / onScrollDragEnd.on_scroll_drag_start / .on_scroll_drag_end{ offset } (Point<Points>)L’utilisateur a commencé / arrêté de faire glisser.
onMomentumStart / onMomentumEnd.on_momentum_start / .on_momentum_end{ offset }Un glissement après le drag a commencé / s’est arrêté. Un glissement interrompu par un nouveau drag se termine tout de même par onMomentumEnd.
onScrollEnd.on_scroll_end{ offset }Le défilement s’est immobilisé.
onRefresh.on_refreshaucunTiré pour actualiser.

ScrollEvent est { offset: Point; contentSize: Size; size: Size; zoomScale } (en Rust offset, content_size, size, zoom_scale) : le décalage, la taille du contenu (zoomé), la taille de la zone visible et l’échelle de zoom (1 sauf si la vue zoome).

#Méthodes de ref

MéthodeRustDescription
scrollTo({ x?, y?, animated? })scroll_to(point(x, y), animated)Défile jusqu’à un décalage.
zoomTo({ scale, x?, y?, animated? })zoom_to(scale, Some(point(x, y)), animated)Zoome à scale (limité aux bornes) avec le point du contenu (x, y, points non zoomés) au milieu de la zone visible, ou autour du milieu. P. ex. un double tap qui zoome sur le point touché.
let scroller = element_ref();
scroll().element_ref(&scroller).child(content());

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

#Clavier

Les champs de saisie à l’intérieur d’une vue de défilement restent dégagés du clavier nativement : la vue de défilement reçoit une marge intérieure en bas et défile d’elle-même jusqu’au champ ayant le focus. Pour le contenu en dehors des vues de défilement, utilisez env(keyboard-inset-height) dans un padding ou une marge du bas. Voir formulaires et clavier.

#Droite à gauche

Les vues de défilement horizontales démarrent à l’extrémité droite dans les mises en page de droite à gauche et conservent leur distance à celle-ci lorsque le contenu change ; leurs enfants sont inversés.

#Notes par plateforme

  • Android : l’actualisation par tirage est dessinée par NSH (l’overscroll reste natif), contentInset est appliqué comme padding, et decelerationRate façonne les glissements d’aimantation.
  • Android zoome avec la propre vue de défilement de NSH, qui met le contenu à l’échelle autour de son coin supérieur gauche et défile dans les deux sens. Les pincements démarrent après l’écartement minimal d’Android entre les doigts (environ 27 mm).
  • Sur Mac Catalyst (idiome Mac), NSH détecte le tirage et affiche son spinner.
  • iOS redessine les textes et les canvas à la résolution zoomée lorsqu’un zoom se termine.