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

Documentazione

Tocco e gesti

NSH ha tre livelli di gestione del tocco. Le pressioni (.on_press in Rust, onPress in TSX e i loro analoghi) coprono pulsanti, righe e link, con il feedback di pressione disegnato in modo nativo. I gesti nativi (pan, pinch e rotazione) muovono dei valori animati sul thread UI, quindi una card trascinata segue il dito anche mentre il tuo codice è occupato. I tocchi grezzi e il sistema dei responder (.on_touch e .claim_touches con i gestori .on_responder_* in Rust; onTouchStart, onResponderMove e PanResponder in TSX) danno al tuo codice la posizione di ogni dito per tutto ciò che gli altri due livelli non possono esprimere, come il disegno.

guide-gestures.tsx
guide-gestures.tsxLive
import { mount, signal, Text, View } from "nsh";
import { createAnimatedValue } from "nsh";
import { PanResponder } from "nsh";
import { button, buttonText, desktop, focusRing, Inspector, Log, tint, ui, wide } from "./palette";

/** A card dragged on the UI thread: `pan` drives two animated values. */
function DragCard() {
  const x = createAnimatedValue(0);
  const y = createAnimatedValue(0);
  const flings = signal(0);
  return (
    <View style={{ ...ui().inset, height: 150, borderRadius: 12, alignItems: "center", justifyContent: "center", overflow: "hidden" }}>
      <View
        pan={{ x, y }}
        onPanEnd={(e) => {
          flings.update((n) => n + 1);
          // Spring home, starting with the release velocity.
          x.spring(0, { velocity: e.velocity.x, stiffness: 200, damping: 16 });
          y.spring(0, { velocity: e.velocity.y, stiffness: 200, damping: 16 });
        }}
        style={{
          width: 120,
          height: 80,
          borderRadius: 16,
          ...ui().accentFill,
          alignItems: "center",
          justifyContent: "center",
          translate: [x, y],
        }}
      >
        <Text style={{ color: ui().onAccent, fontWeight: "700" }}>Drag me</Text>
        <Text style={{ color: ui().onAccent, fontSize: 12 }}>released {flings()}×</Text>
      </View>
    </View>
  );
}

/** A dot moved with PanResponder: every move goes through JS. */
function ResponderDot() {
  const pos = signal({ x: 0, y: 0 });
  const state = signal("idle");
  let start = { x: 0, y: 0 };
  const responder = PanResponder.create({
    onStartShouldSetPanResponder: () => true,
    onPanResponderGrant: () => {
      start = pos();
      state.set("dragging");
    },
    onPanResponderMove: (_, g) => pos.set({ x: start.x + g.dx, y: start.y + g.dy }),
    onPanResponderRelease: (_, g) => state.set(`released, vx ${g.vx.toFixed(0)} pt/s`),
    onPanResponderTerminate: () => state.set("taken over"),
  });
  return (
    <View style={{ gap: 6 }}>
      <View style={{ ...ui().inset, height: 110, borderRadius: 12, overflow: "hidden" }}>
        <View
          {...responder.panHandlers}
          style={{
            position: "absolute",
            left: 150,
            top: 30,
            width: 50,
            height: 50,
            borderRadius: 25,
            backgroundColor: ui().info,
            translate: [pos().x, pos().y],
          }}
        />
      </View>
      <Text style={{ color: ui().muted, fontSize: 13 }}>PanResponder: {state()}</Text>
    </View>
  );
}

function App() {
  const log = signal<string[]>([]);
  const add = (event: string) => log.update((l) => [event, ...l].slice(0, 4));
  const dot = signal(0);
  const under = signal(0);
  // With a pointer the buttons are as wide as their labels; on a phone they share the row.
  const share = desktop ? {} : { flex: 1 };
  const buttons = () => (
    <View style={{ flexDirection: "row", gap: desktop ? 8 : 10 }}>
      <View
        onPressDown={() => add("pressIn")}
        onPressUp={() => add("pressOut")}
        onPress={() => add("press")}
        onLongPress={() => add("longPress")}
        {...button("primary", { style: share })}
        activeStyle={{ opacity: 0.5 }}
      >
        <Text style={buttonText("primary")}>activeStyle opacity</Text>
      </View>
      <View
        onPress={() => add("underlay")}
        {...button("secondary", { style: share })}
        activeStyle={{ backgroundColor: ui().border, backgroundImage: "none" }}
      >
        <Text style={buttonText()}>activeStyle background</Text>
      </View>
      <View
        onPress={() => add("active:")}
        class={
          desktop
            ? "h-8 items-center justify-center rounded-lg bg-[#0f8a55] px-3 transition motion-reduce:transition-none hover:bg-[#12a368] active:scale-95 active:bg-[#0b6b42] dark:bg-[#45d49a] dark:hover:bg-[#5be0a9] dark:active:bg-[#2fae7b]"
            : "flex-1 items-center rounded-xl bg-[#0f8a55] p-3.5 transition motion-reduce:transition-none hover:bg-[#12a368] active:scale-95 active:bg-[#0b6b42] dark:bg-[#45d49a] dark:hover:bg-[#5be0a9] dark:active:bg-[#2fae7b]"
        }
        focusStyle={focusRing()}
      >
        <Text class={desktop ? "text-[13px] font-semibold text-white dark:text-[#0c0d0f]" : "font-semibold text-white dark:text-[#0c0d0f]"}>active:</Text>
      </View>
    </View>
  );
  const slop = () => (
    <View style={{ flexDirection: "row", alignItems: "center", gap: 16 }}>
      <View
        hitSlop={24}
        onPress={() => dot.update((n) => n + 1)}
        style={{ width: 12, height: 12, borderRadius: 6, backgroundColor: ui().down, margin: 12 }}
        hoverStyle={{ opacity: 0.88 }}
        focusStyle={focusRing()}
        activeStyle={{ opacity: 0.7 }}
      />
      <Text style={{ flex: 1, color: ui().text }}>
        hitSlop 24: the dot takes {desktop ? "clicks" : "taps"} around it ({dot()})
      </Text>
    </View>
  );
  const overlay = () => (
    <View style={{ height: 52 }}>
      <View
        onPress={() => under.update((n) => n + 1)}
        style={{ flex: 1, borderRadius: 12, backgroundColor: tint(ui().up, 0.14), alignItems: "center", justifyContent: "center" }}
        hoverStyle={{ opacity: 0.88 }}
        focusStyle={focusRing()}
        activeStyle={{ opacity: 0.7 }}
      >
        <Text style={{ color: ui().up }}>Under an overlay: {under()}</Text>
      </View>
      <View
        style={{ pointerEvents: "none", position: "absolute", top: 0, left: 0, right: 0, bottom: 0, borderRadius: 12, borderWidth: 3, borderColor: ui().warn, borderStyle: "dashed" }}
      />
    </View>
  );
  return (
    <View style={{ flex: 1, backgroundColor: ui().page, padding: 16, paddingTop: 24, gap: 14 }}>
      {() =>
        wide() ? (
          <View style={{ flex: 1, flexDirection: "row", gap: 16 }}>
            <View style={{ flex: 1, minWidth: 0, gap: 14 }}>
              {buttons()}
              <Text style={{ color: ui().muted, fontSize: 13 }}>Click, hold, or move off a button while pressing</Text>
              {slop()}
              {overlay()}
              <DragCard />
              <ResponderDot />
            </View>
            <Inspector>
              <Log title="Press events, newest first" lines={log()} minHeight={92} />
            </Inspector>
          </View>
        ) : (
          <>
            {buttons()}
            <Text style={{ color: ui().muted, fontSize: 13 }}>{log().join(" · ") || "Tap, hold, or slide off a button"}</Text>
            {slop()}
            {overlay()}
            <DragCard />
            <ResponderDot />
          </>
        )
      }
    </View>
  );
}

mount(App);

Scegli il livello più alto che svolge il compito: più il livello è alto, meno lavoro passa dal thread UI al tuo codice.

#Pressioni

Ogni elemento accetta i gestori di pressione; non esiste un componente “touchable” separato. Metti .on_press / onPress su una view, un testo, un’immagine o qualsiasi altro elemento.

use nsh_core::prelude::*;

#[component]
fn App() -> impl IntoElement {
    let count = signal(0_u32);
    view().flex_1().items_center().justify_center().child(
        view()
            .on_press(move || count.update(|n| *n += 1))
            .on_long_press(move || count.set(0))
            .active(|s| s.opacity(0.6))
            .role(AccessibilityRole::Button)
            .px(px(20.0))
            .py(px(12.0))
            .rounded(px(10.0))
            .bg(rgb(0x0969da))
            .child(
                text(move || format!("Pressed {} times", count.get()))
                    .text_color(white())
                    .font_weight(FontWeight::SEMIBOLD),
            ),
    )
}
RustTSXPredefinitoDescrizione
.on_press(..)onPressIl dito si è sollevato all’interno della view (dopo una pressione, non una pressione lunga).
.on_long_press(..)onLongPressIl dito è rimasto premuto per il ritardo della pressione lunga. Il gestore di pressione non viene chiamato.
.on_press_down(..)onPressDownLa pressione è iniziata.
.on_press_up(..)onPressUpLa pressione è terminata: dito sollevato, scivolato fuori o rilevato da una scroll view.
.long_press_delay(ms)delayLongPress500Millisecondi prima che una pressione diventi una pressione lunga.
.disabled(..)disabledfalseNon accetta pressioni e non mostra feedback; applica lo stile disabilitato; gli screen reader lo annunciano come disabilitato.

Come si comportano le pressioni:

  • Risponde solo la view pressabile più interna. Una riga pressabile che contiene un pulsante pressabile non riceve la pressione quando si preme il pulsante.
  • All’interno di una scroll view, la pressione attende circa 130 ms prima di iniziare (e di mostrare il feedback), così un trascinamento che parte da un pulsante scorre invece di premerlo.
  • Scivolare fuori annulla. Spostare il dito fuori dalla view termina la pressione con il gestore di rilascio e senza alcuna pressione.
  • Disabilitare rimuove i listener di pressione da ciò che è montato, quindi la view non mostra alcun feedback e si applica lo stile disabilitato (.disabled_style(..), class="disabled:opacity-40").
  • Gli span di testo annidati con un gestore di pressione sono link. Il paragrafo è una singola view nativa; la pressione va allo span sotto il dito. Vedi Text.

#Feedback di pressione

Il feedback di pressione è dell’app: sono gli stili che la view assume mentre è premuta, sotto il puntatore, con il focus o disabilitata. Vengono applicati in modo nativo nel momento in cui lo stato cambia, senza andata e ritorno verso il tuo codice. Ogni stato ha un metodo Rust, una prop di stile TSX e una variante Tailwind:

RustProp TSXClasseDescrizione
.active(|s| ..)activeStyleactive:Mentre è premuta.
.hover(|s| ..)hoverStylehover:Mentre un puntatore si trova sopra di essa (desktop, puntatori su iPad, browser).
.focus(|s| ..)focusStylefocus:Mentre ha il focus.
.focus_within(|s| ..)focusWithinStylefocus-within:Mentre essa o una view al suo interno ha il focus: la cornice di un campo attorno all’input che modifica.
.disabled_style(|s| ..)disabledStyledisabled:Mentre è disabilitata.
.ripple(Ripple { .. })android={{ ripple }}Il ripple material di Android (color, borderless, radius, foreground), in aggiunta agli stili.

Gli stili di stato si sovrappongono agli stili propri della view. Gli stati successivi prevalgono (hover, focus, active), lo stile disabilitato prevale su tutti e gli altri non si applicano mentre la view è disabilitata.

use nsh_core::{Ripple, prelude::*};

view().on_press(open).active(|s| s.opacity(0.5))

view().on_press(open).active(|s| s.bg(rgb(0xddf4ff))).p(px(16.0))

view()
    .on_press(open)
    .active(|s| s.opacity(0.6))
    .ripple(Ripple {
        color: Some(rgba(0x0969da4d).into()),
        ..Ripple::default()
    })

Un ripple senza bordo si espande fuori dalla view, il che si adatta ai pulsanti con icona: Ripple { borderless: true, radius: Some(px(24.0)), .. }, android={{ ripple: { borderless: true, radius: 24 } }}. Vedi Codice specifico per piattaforma.

Con una transizione (.transition(), la classe transition) il cambio di stato è animato. .group() (la classe group) rende una view un gruppo, e .group_active(..) (group-active:) stilizza i suoi figli mentre è premuta:

view()
    .on_press(save)
    .group()
    .rounded_xl()
    .bg(rgb(0x059669))
    .p_4()
    .transition()
    .active(|s| s.scale(0.95).bg(rgb(0x065f46)))
    .child(
        text("Save")
            .font_weight(FontWeight::SEMIBOLD)
            .text_color(white())
            .group_active(|s| s.opacity(0.85)),
    )

#Aree di tocco: hitSlop

Lo hit slop (.hit_slop(..), hitSlop) estende l’area che accetta i tocchi oltre i limiti della view, senza modificarne il layout. Usalo per bersagli piccoli come pulsanti di chiusura e interruttori.

view().hit_slop(EdgeInsets::all(px(12.0))).on_press(close).size(px(20.0))

view()
    .hit_slop(EdgeInsets { top: px(10.0), bottom: px(10.0), left: px(4.0), right: px(4.0) })
    .on_press(next)
RustTSXDescrizione
.hit_slop(EdgeInsets)hitSlop: number | { top?, right?, bottom?, left? }Punti aggiunti attorno ai limiti. EdgeInsets::all(..) e un numero in TSX si applicano a tutti e quattro i lati.

Mantieni l’area estesa all’interno dei limiti del genitore (lascia un po’ di margin o padding attorno alla view piccola): l’hit testing raggiunge un figlio attraverso il suo genitore, quindi un tocco fuori dal genitore non arriva mai allo slop del figlio.

#Figli fuori dal genitore

Un figlio posizionato fuori dai limiti del suo genitore (posizionato in modo assoluto con overflow lasciato visibile, come un dropdown sotto il suo campo) accetta i tocchi dove viene disegnato, come qualsiasi altra view: NSH esegue in modo nativo l’hit test dei discendenti che escono dai limiti di una view. Un genitore che ritaglia l’overflow li ritaglia, e in quel caso non vengono mostrati né accettano tocchi in quell’area. L’area è calcolata dal layout, quindi un figlio spostato fuori dal genitore da una transform accetta i tocchi solo all’interno dei limiti del genitore.

Attenzione all’ordine di disegno: un dropdown viene disegnato sopra i fratelli del suo campo solo se il campo viene dopo di loro o ha un z-index più alto, e i tocchi vanno a ciò che è disegnato sopra.

#pointerEvents

La chiave di stile pointerEvents (.pointer_events(..) in Rust) decide quali view di un sottoalbero possono essere il bersaglio di un tocco. Viene valutata in modo nativo durante l’hit testing, quindi non costa nulla per ogni tocco.

RustTSXLa viewI suoi figli
PointerEvents::Auto (predefinito)"auto"Accetta i tocchiAccettano i tocchi
PointerEvents::None"none"Mai il bersaglioMai il bersaglio, a meno che uno imposti "auto": i tocchi passano a ciò che sta dietro
PointerEvents::BoxNone"box-none"Mai il bersaglioAccettano i tocchi
PointerEvents::BoxOnly"box-only"Accetta i tocchiMai il bersaglio

Gli usi tipici: un overlay che non deve bloccare ciò che c’è sotto (none), un contenitore a schermo intero il cui spazio vuoto deve lasciar passare i tocchi al contenuto dietro mentre i suoi pulsanti continuano a funzionare (box-none), e una riga che deve ricevere una pressione anche quando il dito cade su un figlio (box-only).

view().flex_1().child(map()).child(
    // A floating toolbar: the empty area passes touches to the map.
    view()
        .pointer_events(PointerEvents::BoxNone)
        .absolute()
        .inset_0()
        .justify_end()
        .p(px(16.0))
        .child(
            view()
                .on_press(locate)
                .self_end()
                .p(px(12.0))
                .rounded(px(24.0))
                .bg(white())
                .child(text("Locate")),
        ),
)

"none" viene ereditato come pointer-events in CSS: una view al suo interno che imposta "auto" torna ad accettare i tocchi. "box-none" e "box-only" si applicano alla view che li imposta. Le classi Tailwind pointer-events-none e pointer-events-auto impostano "none" e "auto", e come chiave di stile pointerEvents accetta anche stili di stato (activeStyle, …) e varianti.

#Gesti nativi: pan, pinch e rotazione

Per trascinamenti, pinch e rotazioni che spostano qualcosa sullo schermo, associa dei valori animati al gesto. Il riconoscitore di gesti della piattaforma scrive i valori sul thread UI e le view associate ad essi si muovono nello stesso frame; il tuo codice viene avvisato quando il gesto inizia e termina.

use nsh_core::prelude::*;

#[component]
fn App() -> impl IntoElement {
    let x = animated_value(0.0);
    let y = animated_value(0.0);
    view().flex_1().items_center().justify_center().child(
        view()
            .pan_values(Some(&x), Some(&y))
            .on_pan_end(move |e: &PanEvent| {
                // Spring back, starting at the speed the finger let go with.
                let spring = |velocity| Spring { velocity: Some(velocity), ..Spring::default() };
                x.spring(0.0, spring(e.velocity_x));
                y.spring(0.0, spring(e.velocity_y));
            })
            .size(px(120.0))
            .rounded(px(24.0))
            .bg(rgb(0x8250df))
            .bind_translate_x(&x)
            .bind_translate_y(&y),
    )
}
RustTSXDescrizione
.pan_values(Some(&x), Some(&y))pan={{ x?, y? }}I trascinamenti con un dito muovono questi valori, in punti.
.pinch_value(&scale)pinch={{ scale? }}I pinch con due dita guidano scale (un fattore).
.rotation_value(&angle)rotation={{ angle? }}Le rotazioni con due dita guidano angle, in gradi.
.on_pan_start / .on_pan_endonPanStart / onPanEndIl trascinamento è iniziato / terminato, con un PanEvent.
.on_pinch_start / .on_pinch_endonPinchStart / onPinchEndIl pinch è iniziato / terminato: la sua scala e velocità (GestureEvent).
.on_rotation_start / .on_rotation_endonRotationStart / onRotationEndLa rotazione è iniziata / terminata: il suo angolo (Rust: GestureEvent<Radians>) e la velocità.

Un evento pan contiene la traslazione (translation, un Point: punti dall’inizio), la velocità (velocity_x / velocity_y; TSX: velocity, un Point: punti al secondo) e i valori associati in quel momento (value_x / value_y; TSX: value, { x?, y? } con gli assi che pan associa).

Cosa fa per te il lato nativo:

  • Un trascinamento continua da dove si trova il valore. All’inizio i valori vengono letti e la traslazione viene sommata ad essi; afferrare un valore ancora in animazione (una molla che sta tornando a casa) ferma l’animazione dov’è.
  • Un solo asse lascia scorrere l’altro. Con un solo valore orizzontale associato il trascinamento inizia solo quando il dito si muove più in orizzontale che in verticale, quindi una riga con swipe orizzontale dentro una scroll view verticale continua a scorrere in verticale. Questa decisione viene presa in modo nativo quando il sistema chiede se il gesto può iniziare.
  • Pan, pinch e rotazione funzionano insieme sulla stessa view: una card può essere trascinata, pizzicata e ruotata contemporaneamente.
  • Velocità di rilascio per la fisica. La velocità alla fine del pan alimenta direttamente la velocità di una molla o di un decadimento; i valori ti permettono di scegliere un punto di aggancio.
// A card that snaps to the left or right edge.
view()
    .pan_values(Some(&x), None)
    .on_pan_end(move |e: &PanEvent| {
        let projected = e.value_x.unwrap_or(0.0) + e.velocity_x * 0.2;
        let to = if projected > 0.0 { 140.0 } else { -140.0 };
        x.spring(to, Spring { velocity: Some(e.velocity_x), ..Spring::default() });
    })
    .bind_translate_x(&x)

#Tocchi grezzi

I gestori dei tocchi riportano ogni dito mentre si appoggia, si muove e si solleva. Sono utili per il logging, l’hit testing personalizzato e per reagire al multi-touch; non rivendicano il tocco (le pressioni e le scroll view continuano a funzionare).

In Rust, .on_touch(..) riceve ogni fase come un TouchEvent: il suo phase (TouchPhase::Start, Move, End, Cancel), il suo timestamp e il suo points, ogni dito appoggiato, ciascuno con il proprio id, la propria location nella view su cui si è appoggiato, la propria posizione page nella surface, e changed per le dita a cui si riferisce l’evento. In TSX, ogni fase ha il proprio gestore:

TSXDescrizione
onTouchStartUn dito si è appoggiato sulla view o al suo interno.
onTouchMoveUn dito si è mosso. I movimenti vengono uniti per ogni turno del motore.
onTouchEndUn dito si è sollevato.
onTouchCancelIl sistema ha preso il tocco (una scroll view ha iniziato a scorrere, è arrivata una chiamata).
onTouchStartCapture, onTouchMoveCapture, onTouchEndCapture, onTouchCancelCaptureGli stessi, chiamati dall’esterno verso l’interno, prima dei gestori di bubbling.

Ogni gestore TSX riceve un GestureResponderEvent:

CampoDescrizione
identifierIdentifica il dito finché è appoggiato.
locationX, locationYPosizione nella view su cui il dito si è appoggiato (target).
pageX, pageYPosizione nella surface.
targetL’id della view su cui il dito si è appoggiato.
timestampMillisecondi.
touchesOgni dito appoggiato.
changedTouchesLe dita a cui si riferisce questo evento.
nativeEventL’evento stesso, quindi funziona anche e.nativeEvent.locationX.
stopPropagation()Le view più esterne non vedono questo evento di tocco.
use nsh_core::{TouchPhase, element::{TouchEvent}, prelude::*};

view().on_touch(|e: &TouchEvent| {
    let Some(p) = e.points.iter().find(|p| p.changed) else { return };
    match e.phase {
        TouchPhase::Start => log::info!("down {:?} {}", p.location, e.points.len()),
        TouchPhase::Move => log::info!("move {:?}", p.page),
        TouchPhase::End | TouchPhase::Cancel => log::info!("up"),
    }
})

#Dove si trova una view: measure

La posizione di un tocco è relativa alla view su cui il dito si è appoggiato, che può essere un figlio della view che ti interessa (il cursore di uno slider). Misurare una view ti dice dove si trova, così puoi usare invece la posizione nella pagina. In Rust, metti un element_ref() sulla view e attendi il suo measure(); in TSX, chiama measure() sul suo ref:

let track = element_ref();
let origin = stored(px(0.0));
view()
    .element_ref(&track)
    .claim_touches(TouchClaim::on_start())
    .on_responder_grant(move |e: &ResponderEvent| {
        let x = e.page.x;
        spawn(async move {
            let page = track.measure().await.map_or(px(0.0), |m| m.page.x);
            origin.replace(page);
            seek(x - page);
        });
    })
    .on_responder_move(move |e: &ResponderEvent| seek(e.page.x - origin.get()))

Si risolve una volta che le modifiche fatte finora sono state impaginate: in Rust in un Measurement (frame, il frame nel genitore come lo riporta il gestore del layout, e page, l’angolo in alto a sinistra), in TSX in { bounds, pagePosition }. L’angolo è nella surface, o nel dialog in cui si trova la view, le stesse coordinate delle posizioni nella pagina dei tocchi. Gli offset delle scroll view e delle liste che la circondano vengono presi in considerazione; le transform (animazioni incluse) no. Si risolve in None / null per una view che non è sullo schermo.

#Il sistema responder

Quando il tuo codice deve gestire in prima persona una sequenza di tocchi (disegno con il dito, uno slider personalizzato, un gesto che i recognizer nativi non offrono), usa il sistema gesture responder. Una vista alla volta è il responder e riceve i movimenti; le viste negoziano chi debba esserlo.

Diventare il responder:

RustTSXDescrizione
.claim_touches(TouchClaim::on_start())onStartShouldSetResponder che restituisce trueRichiesto quando un dito tocca lo schermo, dal bersaglio verso l’esterno.
.claim_touches(TouchClaim::on_move())onMoveShouldSetResponder che restituisce trueRichiesto a ogni movimento finché non c’è un responder (o per subentrare).
TouchClaim::on_start().capture()onStartShouldSetResponderCaptureCome sopra, ma richiesto dall’esterno verso l’interno, prima di quelli in bubbling. Un genitore lo usa per sottrarre un tocco ai figli.
TouchClaim::on_move().capture()onMoveShouldSetResponderCaptureLa versione per il movimento.

Una richiesta viene decisa dalla sola vista, senza eseguire il tuo codice, a meno che non lo chiami in causa: TouchClaim::on_move().ask(|r: &ClaimRequest| ..) la effettua quando il listener chiama r.claim(), decidendo in base al gesto compiuto finora (r.event.gesture). Gli handler TSX ricevono il gesto come secondo argomento, gestureState, con gli stessi campi di quello di PanResponder (dx, dy, vx, vy, moveX, moveY, x0, y0, numberActiveTouches). Uno swipe che rivendica solo i movimenti orizzontali: onMoveShouldSetResponder={(e, g) => Math.abs(g.dx) > 10 && Math.abs(g.dx) > Math.abs(g.dy)}. Questa decisione arriva comunque con un movimento di ritardo, quindi una scroll view circostante potrebbe iniziare a scorrere per prima; per i trascinamenti all’interno delle scroll view usa la pan activation.

Durante la risposta:

RustTSXDescrizione
.on_responder_grant(..)onResponderGrantQuesta vista è ora il responder. Per impostazione predefinita Rust impedisce alle scroll view native di prendere i tocchi (TouchClaim::block_native(false) glielo consente); in TSX, per questo restituisci true.
.on_responder_reject(..)onResponderRejectUn’altra vista è il responder e non intende cederlo.
.on_responder_start(..)onResponderStartUn altro dito ha toccato lo schermo.
.on_responder_move(..)onResponderMoveUn dito si è mosso.
.on_responder_end(..)onResponderEndUn dito si è sollevato.
.on_responder_release(..)onResponderReleaseL’ultimo dito si è sollevato: il gesto è terminato.
.keep_touches(true)onResponderTerminationRequest che restituisce falseUn’altra vista vuole i tocchi: mantienili (per impostazione predefinita vengono ceduti).
.on_responder_terminate(..)onResponderTerminateUn’altra vista, o un gesto nativo, ha preso i tocchi.

Il responder di un componente Rust resta premuto dalla concessione fino al termine dei suoi tocchi, ovunque vada il dito o il puntatore, come :active di CSS resta attivo finché il pulsante non viene rilasciato: i suoi stili .active(..) e le classi group-active: al suo interno mantengono il loro aspetto per tutto il trascinamento (una maniglia di ridimensionamento trascinata fuori dalla propria fascia).

Gli handler Rust ricevono un ResponderEvent: il location del dito nella vista su cui ha toccato lo schermo, la sua posizione page, il timestamp e il gesture dalla concessione.

use nsh_core::prelude::*;

fn sketch() -> impl IntoElement {
    let strokes = signal(Vec::<Vec<Point<Points>>>::new());
    canvas(move || {
        strokes.with(|all| {
            all.iter().fold(Painter::new().line_width(3.0), |p, s| {
                let mut p = p.begin_path();
                for (i, pt) in s.iter().enumerate() {
                    p = if i == 0 { p.move_to(pt.x, pt.y) } else { p.line_to(pt.x, pt.y) };
                }
                p.stroke()
            })
        })
    })
    .flex_1()
    // The responder: the scroll view around does not take the touches.
    .claim_touches(TouchClaim::on_start())
    .keep_touches(true)
    .on_responder_grant(move |e: &ResponderEvent| strokes.update(|all| all.push(vec![e.location])))
    .on_responder_move(move |e: &ResponderEvent| {
        strokes.update(|all| {
            if let Some(last) = all.last_mut() {
                last.push(e.location);
            }
        })
    })
}

Come funziona: una vista con un qualsiasi handler di tocco o di responder ascolta i tocchi in modo nativo; la piattaforma riporta i tocchi su di essa (e al suo interno) insieme al percorso delle viste in ascolto, e la negoziazione avviene nel core Rust di NSH, che interroga le richieste e chiama i tuoi handler in ordine: prima le capture dalla vista più esterna verso l’interno, poi le altre dal bersaglio verso l’esterno. Ne derivano due conseguenze:

  • Le scroll view native e i responder competono. Finché un responder le blocca, le scroll view circostanti smettono di prendere i tocchi fino alla sua conclusione. Se una scroll view nativa inizia a trascinare per prima, vince: il responder riceve l’handler di terminazione e gli handler di tocco ricevono un annullamento.
  • Ogni movimento è un evento per il tuo codice. I trascinamenti guidati dal responder sono fluidi quanto è veloce il tuo codice; per i trascinamenti che si limitano a spostare viste, preferisci i valori associati.

#PanResponder

In Rust, gli eventi del responder trasportano già il gesto: ResponderEvent::gesture è un PanGesture con la distanza dall’inizio, la velocità e il numero di dita. In TSX, PanResponder da nsh incapsula il sistema responder e aggiunge lo stesso stato del gesto; applica con lo spread il suo panHandlers su una vista.

use nsh_core::prelude::*;

fn ball() -> impl IntoElement {
    let pos = signal((0.0_f32, 0.0_f32));
    let start = stored((0.0_f32, 0.0_f32));
    view()
        .size(px(60.0))
        .rounded(px(30.0))
        .bg(rgb(0x0969da))
        .style_with(move |s| {
            let (x, y) = pos.get();
            s.translate_x(px(x)).translate_y(px(y))
        })
        .claim_touches(TouchClaim::on_start())
        .on_responder_grant(move |_: &ResponderEvent| {
            start.replace(pos.get_untracked());
        })
        .on_responder_move(move |e: &ResponderEvent| {
            let (x, y) = start.get();
            let d = e.gesture.delta;
            pos.set((x + d.x.0, y + d.y.0));
        })
        .on_responder_release(move |e: &ResponderEvent| {
            log::info!("released at {} {} pt/s", e.gesture.vx, e.gesture.vy)
        })
}

Lo stato del gesto:

Rust (PanGesture)TSX (gestureState)Descrizione
stateIDIdentifica il gesto.
positionmoveX, moveYL’ultima posizione del centroide dei tocchi, in punti della superficie.
originx0, y0Dove si trovava il centroide quando il responder è stato concesso.
deltadx, dyDistanza percorsa dalla concessione.
vx, vyvx, vyVelocità, in punti al secondo.
active_touchesnumberActiveTouchesDita appoggiate.

PanGesture::begin e PanGesture::update seguono un gesto a partire dai tocchi grezzi (.on_touch) in Rust. Le callback TSX ricevono (event, gestureState): onStartShouldSetPanResponder(Capture), onMoveShouldSetPanResponder(Capture), onPanResponderGrant, onPanResponderReject, onPanResponderStart, onPanResponderMove, onPanResponderEnd, onPanResponderRelease, onPanResponderTerminate, onPanResponderTerminationRequest e onShouldBlockNativeResponder. L’ultimo ha come valore predefinito true: un PanResponder concesso impedisce alle scroll view native di prendere i suoi tocchi.

#Pan all’interno delle scroll view: panActivation

Un grafico scorso lateralmente all’interno di una scroll view verticale deve prendere i trascinamenti orizzontali e lasciare quelli verticali alla scroll view. Rivendicare il tocco all’inizio blocca lo scorrimento fin dal primo pixel, e decidere sui movimenti nel tuo codice è troppo tardi: quando risponde, la scroll view potrebbe aver già preso il tocco. La pan activation (TouchClaim::on_pan(..), panActivation) lascia decidere alla piattaforma sul main thread, mentre il dito si muove:

view()
    .claim_touches(TouchClaim::on_pan(PanActivation::horizontal(px(10.0), px(10.0))))
    .on_responder_grant(|e: &ResponderEvent| scrub(Some(e.location.x)))
    .on_responder_move(|e: &ResponderEvent| scrub(Some(e.location.x)))
    .on_responder_release(|_: &ResponderEvent| scrub(None))
    .child(chart())
Rust (PanActivation)TSXDescrizione
active_offset_x, active_offset_yactiveOffsetX, activeOffsetYIl pan si attiva quando il dito si è spostato, lungo quell’asse, più di questo valore rispetto al punto in cui ha toccato lo schermo (in entrambe le direzioni, in punti).
fail_offset_x, fail_offset_yfailOffsetX, failOffsetYIl pan fallisce quando il dito si è spostato più di questo valore per primo: le scroll view mantengono i tocchi.

PanActivation::horizontal(active, fail) e PanActivation::vertical(active, fail) impostano le coppie più comuni. Senza offset attivi, il pan si attiva dopo 10 punti in qualsiasi direzione. Finché non si attiva, le scroll view circostanti lo attendono (iOS) e le altre richieste della vista non vengono interrogate. Quando si attiva, le scroll view non possono più prendere questi tocchi e la vista diventa il responder (la concessione, poi i movimenti); quando fallisce, la scroll view scorre normalmente. In TSX, anche i panHandlers di PanResponder funzionano con essa: applicali con lo spread accanto alla prop, e onPanResponderGrant viene eseguito all’attivazione.

#Perché alcune risposte sono prop

Il main thread non attende mai il tuo codice. Quando il sistema ha bisogno di una risposta immediata (questo gesto pan può iniziare, questo punto colpisce la vista, questo tap deve chiudere la tastiera), NSH risponde in modo nativo in base a ciò che è stato montato in precedenza: i valori pan associati (i loro assi), il valore pinch, lo hit slop, i pointer events, l’impostazione dei tap sulla tastiera delle scroll view, la pan activation. I tuoi handler vengono a sapere solo in seguito di ciò che è accaduto. Per questo si tratta di impostazioni dichiarative anziché di callback, e per questo la negoziazione del responder, che esegue il tuo codice, decide soltanto tra responder e non blocca mai un gesto nativo.

#Vedi anche

  • View: ogni prop di tocco dell’elemento di base.
  • Animation: valori animati, spring e decay per il movimento guidato dai gesti.
  • Scrolling: l’impostazione dei tap sulla tastiera, le scroll view e i gesti annidati.
  • PanResponder: il riferimento di PanResponder.
  • Accessibility: le viste pressable sono elementi di accessibilità; assegna loro un ruolo e un’etichetta.