Esta página foi traduzida automaticamente e pode conter erros. Ler o original em inglês

Documentação

Toque e gestos

O NSH tem três níveis de tratamento de toque. Pressionamentos (.on_press em Rust, onPress em TSX e seus equivalentes) cobrem botões, linhas e links, com o feedback de pressionamento desenhado nativamente. Gestos nativos (pan, pinch e rotação) movem valores animados na thread de UI, então um card arrastado acompanha o dedo mesmo quando o seu código está ocupado. Toques brutos e o sistema de responders (.on_touch e .claim_touches com os handlers .on_responder_* em Rust; onTouchStart, onResponderMove e PanResponder em TSX) dão ao seu código a posição de cada dedo, para tudo o que os outros dois níveis não conseguem expressar, como desenhar.

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

Escolha o nível mais alto que resolva o problema: quanto mais alto o nível, menos trabalho passa da thread de UI para o seu código.

#Pressionamentos

Todo elemento aceita os handlers de pressionamento; não existe um componente “touchable” separado. Coloque .on_press / onPress em uma view, um texto, uma imagem ou qualquer outra coisa.

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),
            ),
    )
}
Rust TSX Padrão Descrição
.on_press(..) onPress O dedo foi levantado dentro da view (após um pressionamento, não um toque longo).
.on_long_press(..) onLongPress O dedo permaneceu pressionado pelo tempo de espera do toque longo. O handler de pressionamento não é chamado em seguida.
.on_press_down(..) onPressDown O pressionamento começou.
.on_press_up(..) onPressUp O pressionamento terminou: o dedo foi levantado, deslizou para fora ou foi assumido por uma scroll view.
.long_press_delay(ms) delayLongPress 500 Milissegundos antes de um pressionamento se tornar um toque longo.
.disabled(..) disabled false Não recebe pressionamentos e não mostra feedback; aplica o estilo de desabilitado; leitores de tela anunciam que está desabilitado.

Como os pressionamentos se comportam:

  • Apenas a view pressionável mais interna responde. Uma linha pressionável que contém um botão pressionável não recebe o pressionamento quando o botão é pressionado.
  • Dentro de uma scroll view, o pressionamento espera cerca de 130 ms antes de começar (e de mostrar o feedback), de modo que um arraste que começa em um botão rola a tela em vez de pressioná-lo.
  • Deslizar para fora cancela. Mover o dedo para fora da view encerra o pressionamento com o handler de press-up e sem pressionamento.
  • Desabilitar remove os listeners de pressionamento do que está montado, então a view não mostra nenhum feedback, e o estilo de desabilitado é aplicado (.disabled_style(..), class="disabled:opacity-40").
  • Spans de texto aninhados com um handler de pressionamento são links. O parágrafo é uma única view nativa; o pressionamento vai para o span sob o dedo. Veja Text.

#Feedback de pressionamento

O feedback de pressionamento é do app: são os estilos que a view assume enquanto está pressionada, com o ponteiro sobre ela, com foco ou desabilitada. Eles são aplicados nativamente no instante em que o estado muda, sem ida e volta até o seu código. Cada estado tem um método em Rust, uma prop de estilo em TSX e uma variante do Tailwind:

Rust Prop TSX Classe Descrição
.active(|s| ..) activeStyle active: Enquanto pressionada.
.hover(|s| ..) hoverStyle hover: Enquanto um ponteiro está sobre ela (desktops, ponteiros do iPad, navegadores).
.focus(|s| ..) focusStyle focus: Enquanto tem foco.
.focus_within(|s| ..) focusWithinStyle focus-within: Enquanto ela ou uma view dentro dela tem foco: a moldura de um campo ao redor do input que edita.
.disabled_style(|s| ..) disabledStyle disabled: Enquanto desabilitada.
.ripple(Ripple { .. }) android={{ ripple }} O ripple do Material no Android (color, borderless, radius, foreground), além dos estilos.

Os estilos de estado são aplicados sobre os estilos próprios da view. Os estados posteriores vencem (hover, focus, active), o estilo de desabilitado vence todos, e os demais não se aplicam enquanto a view está desabilitada.

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()
    })

Um ripple sem borda se espalha para fora da view, o que combina com botões de ícone: Ripple { borderless: true, radius: Some(px(24.0)), .. }, android={{ ripple: { borderless: true, radius: 24 } }}. Veja Platform-specific code.

Com uma transição (.transition(), a classe transition), a mudança de estado é animada. .group() (a classe group) transforma uma view em um grupo, e .group_active(..) (group-active:) estiliza seus filhos enquanto ela está pressionada:

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

#Áreas de toque: hitSlop

O hit slop (.hit_slop(..), hitSlop) estende a área que recebe toques além dos limites da view, sem alterar o layout dela. Use-o em alvos pequenos, como botões de fechar e toggles.

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)
Rust TSX Descrição
.hit_slop(EdgeInsets) hitSlop: number | { top?, right?, bottom?, left? } Pontos adicionados ao redor dos limites. EdgeInsets::all(..) e um número em TSX se aplicam aos quatro lados.

Mantenha a área estendida dentro dos limites do pai (dê à view pequena alguma margem ou padding ao redor): o hit testing chega a um filho através do pai, então um toque fora do pai nunca alcança o slop do filho.

#Filhos fora do pai

Um filho posicionado fora dos limites do pai (posicionado de forma absoluta com overflow deixado visível, como um dropdown abaixo do seu campo) recebe toques onde é desenhado, como qualquer outra view: o NSH faz nativamente o hit testing dos descendentes que se estendem para fora de uma view. Um pai que recorta o overflow também os recorta, e nesse caso eles não aparecem nem recebem toques ali. A área é calculada a partir do layout, então um filho movido para fora do pai por uma transformação só recebe toques dentro dos limites do pai.

Atenção à ordem de desenho: um dropdown só é desenhado sobre os irmãos do seu campo se o campo vier depois deles ou tiver um z-index maior, e os toques vão para o que é desenhado por cima.

#pointerEvents

A chave de estilo pointerEvents (.pointer_events(..) em Rust) decide quais views de uma subárvore podem ser o alvo de um toque. Ela é avaliada nativamente durante o hit testing, então não custa nada por toque.

Rust TSX A view Seus filhos
PointerEvents::Auto (padrão) "auto" Recebe toques Recebem toques
PointerEvents::None "none" Nunca é o alvo Nunca são o alvo, a menos que algum defina "auto": os toques passam para o que está atrás
PointerEvents::BoxNone "box-none" Nunca é o alvo Recebem toques
PointerEvents::BoxOnly "box-only" Recebe toques Nunca são o alvo

Os usos típicos: um overlay que não deve bloquear o que está embaixo dele (none), um container de tela cheia cujo espaço vazio deve deixar os toques passarem para o conteúdo atrás enquanto seus botões continuam funcionando (box-none), e uma linha que deve receber um pressionamento mesmo quando o dedo cai sobre um filho (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" é herdado como o pointer-events do CSS: uma view dentro dele que define "auto" volta a receber toques. "box-none" e "box-only" se aplicam à view que os define. As classes do Tailwind pointer-events-none e pointer-events-auto definem "none" e "auto", e, como chave de estilo, pointerEvents também aceita estilos de estado (activeStyle, …) e variantes.

#Gestos nativos: pan, pinch e rotação

Para arrastes, pinças e rotações que movem algo na tela, vincule valores animados ao gesto. O reconhecedor de gestos da plataforma escreve os valores na thread de UI e as views vinculadas a eles se movem no mesmo frame; seu código é avisado quando o gesto começa 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),
    )
}
Rust TSX Descrição
.pan_values(Some(&x), Some(&y)) pan={{ x?, y? }} Arrastes com um dedo movem esses valores, em pontos.
.pinch_value(&scale) pinch={{ scale? }} Pinças com dois dedos controlam scale (um fator).
.rotation_value(&angle) rotation={{ angle? }} Rotações com dois dedos controlam angle, em graus.
.on_pan_start / .on_pan_end onPanStart / onPanEnd O arraste começou / terminou, com um PanEvent.
.on_pinch_start / .on_pinch_end onPinchStart / onPinchEnd A pinça começou / terminou: sua escala e velocidade (GestureEvent).
.on_rotation_start / .on_rotation_end onRotationStart / onRotationEnd A rotação começou / terminou: seu ângulo (Rust: GestureEvent<Radians>) e velocidade.

Um evento de pan tem a translação (translation, um Point: pontos desde o início), a velocidade (velocity_x / velocity_y; TSX: velocity, um Point: pontos por segundo) e os valores vinculados naquele momento (value_x / value_y; TSX: value, { x?, y? } com os eixos que pan vincula).

O que o lado nativo faz por você:

  • Um arraste continua de onde o valor está. No início, os valores são lidos e a translação é somada a eles; pegar um valor que ainda está sendo animado (uma spring voltando para casa) interrompe a animação onde ela está.
  • Um único eixo deixa o outro rolar. Com apenas um valor horizontal vinculado, o arraste só começa quando o dedo se move mais na horizontal do que na vertical, então uma linha de deslize horizontal dentro de uma scroll view vertical ainda rola na vertical. Essa decisão é tomada nativamente quando o sistema pergunta se o gesto pode começar.
  • Pan, pinch e rotação rodam juntos na mesma view: um card pode ser arrastado, pinçado e rotacionado ao mesmo tempo.
  • Velocidade de soltura para a física. A velocidade do fim do pan alimenta diretamente a velocidade de uma spring ou de um decay; os valores permitem escolher um ponto de encaixe.
// 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)

#Toques brutos

Os handlers de toque informam cada dedo conforme ele toca a tela, se move e é levantado. São úteis para logging, hit testing personalizado e reação a multitoque; eles não reivindicam o toque (pressionamentos e scroll views continuam funcionando).

Em Rust, .on_touch(..) recebe todas as fases como um TouchEvent: seu phase (TouchPhase::Start, Move, End, Cancel), seu timestamp e seu points, cada dedo que está na tela, cada um com seu id, seu location na view em que tocou, sua posição page na superfície e changed para os dedos a que o evento se refere. Em TSX, cada fase tem seu próprio handler:

TSX Descrição
onTouchStart Um dedo tocou a view ou algo dentro dela.
onTouchMove Um dedo se moveu. Os movimentos são mesclados por turno do engine.
onTouchEnd Um dedo foi levantado.
onTouchCancel O sistema assumiu o toque (uma scroll view começou a rolar, chegou uma chamada).
onTouchStartCapture, onTouchMoveCapture, onTouchEndCapture, onTouchCancelCapture Os mesmos, chamados de fora para dentro, antes dos handlers de bubbling.

Cada handler em TSX recebe um GestureResponderEvent:

Campo Descrição
identifier Identifica o dedo enquanto ele está na tela.
locationX, locationY Posição na view em que o dedo tocou (target).
pageX, pageY Posição na superfície.
target O id da view em que o dedo tocou.
timestamp Milissegundos.
touches Todos os dedos que estão na tela.
changedTouches Os dedos a que este evento se refere.
nativeEvent O próprio evento, então e.nativeEvent.locationX também funciona.
stopPropagation() As views mais externas não veem este evento de toque.
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"),
    }
})

#Onde está uma view: measure

A localização de um toque é relativa à view em que o dedo tocou, que pode ser filha da view que lhe interessa (o knob de um slider). Medir uma view diz onde ela está, para que você possa usar a posição na página. Em Rust, coloque um element_ref() na view e aguarde (await) seu measure(); em TSX, chame measure() na ref dela:

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

Ele é resolvido quando as mudanças feitas até o momento já passaram pelo layout: em Rust, para um Measurement (frame, o frame no pai como o handler de layout o informa, e page, o canto superior esquerdo), em TSX, para { bounds, pagePosition }. O canto está na superfície, ou no diálogo em que a view está, nas mesmas coordenadas das posições de página dos toques. Os offsets das scroll views e listas ao redor são levados em conta; transformações (incluindo animações) não são. Ele é resolvido para None / null no caso de uma view que não está na tela.

#O sistema de responder de gestos

Quando o seu código precisa assumir o controle de uma sequência de toques (desenhar com o dedo, um slider personalizado, um gesto que os reconhecedores nativos não oferecem), use o sistema de responder de gestos. Uma view de cada vez é o responder e recebe os movimentos; as views negociam entre si quem será ele.

Tornar-se o responder:

Rust TSX Descrição
.claim_touches(TouchClaim::on_start()) onStartShouldSetResponder retornando true Consultado quando um dedo toca a tela, do alvo para fora.
.claim_touches(TouchClaim::on_move()) onMoveShouldSetResponder retornando true Consultado a cada movimento enquanto não há responder (ou para assumir o controle).
TouchClaim::on_start().capture() onStartShouldSetResponderCapture Como o anterior, mas consultado de fora para dentro, antes dos de bubbling. Um pai o usa para tomar um toque de seus filhos.
TouchClaim::on_move().capture() onMoveShouldSetResponderCapture A versão para movimentos.

Uma reivindicação é decidida apenas a partir da view, sem que nenhum código seu seja executado, a menos que ela peça: TouchClaim::on_move().ask(|r: &ClaimRequest| ..) reivindica quando o listener chama r.claim(), decidindo com o gesto até o momento (r.event.gesture). Os handlers de TSX recebem o gesto como segundo argumento, gestureState, com os mesmos campos do PanResponder (dx, dy, vx, vy, moveX, moveY, x0, y0, numberActiveTouches). Um swipe que reivindica apenas movimentos horizontais: onMoveShouldSetResponder={(e, g) => Math.abs(g.dx) > 10 && Math.abs(g.dx) > Math.abs(g.dy)}. Essa decisão ainda chega um movimento atrasada, então uma scroll view ao redor pode começar a rolar primeiro; para arrastes dentro de scroll views, use a ativação de pan.

Enquanto responde:

Rust TSX Descrição
.on_responder_grant(..) onResponderGrant Esta view agora é o responder. Em Rust, por padrão as scroll views nativas ficam impedidas de pegar os toques (TouchClaim::block_native(false) permite que peguem); em TSX, retorne true para isso.
.on_responder_reject(..) onResponderReject Outra view é o responder e não quis ceder.
.on_responder_start(..) onResponderStart Outro dedo tocou a tela.
.on_responder_move(..) onResponderMove Um dedo se moveu.
.on_responder_end(..) onResponderEnd Um dedo foi levantado.
.on_responder_release(..) onResponderRelease O último dedo foi levantado: o gesto terminou.
.keep_touches(true) onResponderTerminationRequest retornando false Outra view quer os toques: mantenha-os (por padrão eles são cedidos).
.on_responder_terminate(..) onResponderTerminate Outra view, ou um gesto nativo, tomou os toques.

O responder de um componente Rust permanece pressionado desde a sua concessão até o fim de seus toques, para onde quer que o dedo ou o ponteiro vá, assim como o :active do CSS se mantém até que o botão seja solto: seus estilos .active(..) e as classes group-active: dentro dele mantêm a aparência durante todo o arraste (um handle de redimensionamento arrastado para fora da sua faixa).

Os handlers de Rust recebem um ResponderEvent: o location do dedo na view em que ele tocou, sua posição page, o timestamp e o gesture desde a concessão.

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

Como funciona: uma view com qualquer handler de toque ou de responder escuta os toques de forma nativa; a plataforma reporta os toques sobre ela (e dentro dela) com o caminho das views que escutam, e a negociação roda no núcleo Rust do NSH, consultando as reivindicações e chamando seus handlers em ordem: primeiro as capturas, da view mais externa para dentro, depois as demais, do alvo para fora. Duas consequências:

  • Scroll views nativas e responders competem. Enquanto um responder as bloqueia, as scroll views ao redor deixam de pegar os toques até que ele termine. Se uma scroll view nativa começar a arrastar primeiro, ela vence: o responder recebe o handler de terminate e os handlers de toque recebem um cancelamento.
  • Cada movimento é um evento para o seu código. Arrastes conduzidos por responder são tão suaves quanto o seu código é rápido; para arrastes que apenas movem views, prefira valores vinculados.

#PanResponder

Em Rust, os eventos do responder já carregam o gesto: ResponderEvent::gesture é um PanGesture com a distância desde o início, a velocidade e o número de dedos. Em TSX, PanResponder de nsh envolve o sistema de responder e adiciona o mesmo estado de gesto; aplique o seu panHandlers com spread em uma view.

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

O estado do gesto:

Rust (PanGesture) TSX (gestureState) Descrição
stateID Identifica o gesto.
position moveX, moveY A posição mais recente do centroide dos toques, em pontos da superfície.
origin x0, y0 Onde o centroide estava quando o responder foi concedido.
delta dx, dy Distância percorrida desde a concessão.
vx, vy vx, vy Velocidade, em pontos por segundo.
active_touches numberActiveTouches Dedos na tela.

PanGesture::begin e PanGesture::update acompanham um gesto a partir de toques brutos (.on_touch) em Rust. Os callbacks de TSX recebem (event, gestureState): onStartShouldSetPanResponder(Capture), onMoveShouldSetPanResponder(Capture), onPanResponderGrant, onPanResponderReject, onPanResponderStart, onPanResponderMove, onPanResponderEnd, onPanResponderRelease, onPanResponderTerminate, onPanResponderTerminationRequest e onShouldBlockNativeResponder. O último tem como padrão true: um PanResponder concedido impede que as scroll views nativas peguem os seus toques.

#Pans dentro de scroll views: panActivation

Um gráfico explorado lateralmente dentro de uma scroll view vertical precisa pegar os arrastes horizontais e deixar os verticais para a scroll view. Reivindicar o toque no início bloqueia a rolagem desde o primeiro pixel, e decidir nos movimentos em seu código é tarde demais: quando ele responde, a scroll view pode já ter tomado o toque. A ativação de pan (TouchClaim::on_pan(..), panActivation) deixa a plataforma decidir na main thread, conforme o dedo se move:

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) TSX Descrição
active_offset_x, active_offset_y activeOffsetX, activeOffsetY O pan é ativado quando o dedo se move além desta distância do ponto em que tocou, ao longo daquele eixo (em qualquer direção, em pontos).
fail_offset_x, fail_offset_y failOffsetX, failOffsetY O pan falha quando o dedo se move além desta distância primeiro: as scroll views ficam com os toques.

PanActivation::horizontal(active, fail) e PanActivation::vertical(active, fail) definem os pares mais comuns. Sem offsets ativos, o pan é ativado após 10 pontos em qualquer direção. Até que seja ativado, as scroll views ao redor esperam por ele (iOS) e as outras reivindicações da view não são consultadas. Quando ele é ativado, as scroll views não podem mais pegar esses toques e a view se torna o responder (a concessão, depois os movimentos); quando falha, a scroll view rola normalmente. Em TSX, os panHandlers de PanResponder também funcionam com ele: aplique-os com spread ao lado da prop, e onPanResponderGrant é executado na ativação.

#Por que algumas respostas são props

A main thread nunca espera pelo seu código. Quando o sistema precisa de uma resposta na hora (este gesto de pan pode começar, este ponto atinge a view, este toque deve dispensar o teclado), o NSH responde de forma nativa a partir do que foi montado de antemão: os valores de pan vinculados (seus eixos), o valor de pinch, o hit slop, os eventos de ponteiro, a configuração de toques com teclado das scroll views, a ativação de pan. Seus handlers só ficam sabendo do que aconteceu depois. É por isso que essas são configurações declarativas em vez de callbacks, e por isso a negociação de responder, que executa o seu código, apenas decide entre responders e nunca bloqueia um gesto nativo.

#Veja também

  • View: todas as props de toque do elemento básico.
  • Animação: valores animados, springs e decay para movimento conduzido por gestos.
  • Rolagem: a configuração de toques com teclado, scroll views e gestos aninhados.
  • PanResponder: a referência de PanResponder.
  • Acessibilidade: views pressionáveis são elementos de acessibilidade; dê a elas uma role e um label.