Documentação
Animação
Toda animação no NSH roda na thread de UI. Você a descreve uma vez, em Rust ou TSX, e a plataforma a executa quadro a quadro (Core Animation e um display link no iOS, o animador da thread de UI no Android) sem chamar de volta o seu código. Uma thread de engine ocupada não consegue fazer uma animação engasgar, e uma animação não consome nada do seu código enquanto roda.
import { mount, signal, Text, View } from "nsh";
import { reduceMotion } from "nsh";
import { Animated, createAnimatedValue, Easing, LayoutAnimation } from "nsh";
import { button, buttonText, desktop, focusRing, Inspector, Panel, size, Stage, ui, wide } from "./palette";
const TRACK = 250;
// Read when they render, so they follow the theme.
const palette = () => [ui().down, ui().warn, ui().accent, ui().up, ui().info];
const SWATCHES = 5;
function Chip(props: { label: string; onPress: () => void }) {
return (
<View onPress={props.onPress} {...button()}>
<Text style={buttonText()}>{props.label}</Text>
</View>
);
}
function App() {
// 1. A style transition: change the style, the platform animates it.
const on = signal(false);
// 2. Animated values: timing and spring, driven on the UI thread.
const timed = createAnimatedValue(0);
const sprung = createAnimatedValue(0);
let there = false;
const run = () => {
there = !there;
timed.timing(there ? TRACK : 0, { duration: 700, easing: Easing.inOut(Easing.cubic) });
sprung.spring(there ? TRACK : 0, { stiffness: 180, damping: 9 });
};
// 3. A composite: a staggered hop, then all of them spin; runs natively end to end.
const ys = Array.from({ length: SWATCHES }, () => createAnimatedValue(0));
const spin = createAnimatedValue(0);
const status = signal("idle");
const show = Animated.sequence([
Animated.stagger(
70,
ys.map((y) =>
Animated.sequence([
Animated.timing(y, { toValue: -30, duration: 200, easing: Easing.out(Easing.quad) }),
Animated.spring(y, { toValue: 0, stiffness: 300, damping: 12 }),
]),
),
),
Animated.timing(spin, { toValue: 1, duration: 600, easing: Easing.inOut(Easing.cubic) }),
]);
const play = () => {
status.set("running…");
show.reset();
show.start(({ finished }) => status.set(finished ? "finished" : "stopped"));
};
// 4. Layout animation: the next commit's frame changes animate.
const stacked = signal(false);
const heading = (title: string) => <Text style={{ fontWeight: "600", color: ui().text }}>{title}</Text>;
const transition = () => (
<View style={{ flexDirection: "row", alignItems: "center", gap: 12 }}>
<View
onPress={() => on.set(!on())}
style={{
width: 64,
height: 64,
borderRadius: on() ? 32 : 12,
backgroundColor: on() ? ui().info : ui().accent,
opacity: on() ? 0.6 : 1,
rotate: on() ? "45deg" : "0deg",
transitionProperty: "all",
transitionDuration: reduceMotion() ? 0 : 400,
transitionTimingFunction: "ease-in-out",
}}
hoverStyle={{ opacity: 0.88 }}
focusStyle={focusRing()}
activeStyle={{ opacity: 0.7 }}
/>
<View class="size-10 rounded-full border-4 animate-spin motion-reduce:animate-none" style={{ borderColor: ui().border, borderTopColor: ui().accent }} />
<View class="size-10 rounded-full animate-pulse motion-reduce:animate-none" style={{ backgroundColor: ui().up }} />
<Text style={{ flex: 1, color: ui().muted, fontSize: 13 }}>
{desktop ? "Click the square." : "Tap the square."} animate-spin / animate-pulse run natively.
</Text>
</View>
);
const tracks = () => (
<>
<View style={{ width: 20, height: 20, borderRadius: 10, backgroundColor: ui().accent, translate: [timed, 0] }} />
<View style={{ width: 20, height: 20, borderRadius: 4, backgroundColor: ui().info, translate: [sprung, 0] }} />
<View
style={{
height: 8,
borderRadius: 4,
// One value, interpolated into a color and a width fraction.
backgroundColor: timed.interpolate({ inputRange: [0, TRACK], outputRange: [ui().border, ui().up] }),
scale: [timed.interpolate({ inputRange: [0, TRACK], outputRange: [0.1, 1] }), 1],
transformOrigin: "left",
width: wide() ? TRACK + 20 : undefined,
}}
/>
</>
);
const move = () => <Chip label="Move" onPress={run} />;
const swatches = () => (
<View style={{ flexDirection: "row", justifyContent: "center", alignItems: "flex-end", gap: 10, height: 64 }}>
<Each of={palette()}>
{(color, i) => (
<View
style={{
width: 32,
height: 32,
borderRadius: 8,
backgroundColor: color(),
translate: [0, ys[i()]],
rotate: spin.interpolate({ inputRange: [0, 1], outputRange: ["0deg", "180deg"] }),
}}
/>
)}
</Each>
</View>
);
const playback = () => (
<View style={{ flexDirection: "row", alignItems: "center", gap: 8 }}>
<Chip label="Play" onPress={play} />
<Chip label="Stop" onPress={() => show.stop()} />
<Text style={{ color: ui().muted, fontSize: desktop ? 13 : undefined }}>{status()}</Text>
</View>
);
const bars = () => (
<View style={{ flexDirection: stacked() ? "column" : "row", gap: 8 }}>
<Each of={palette()}>
{(color) => <View style={{ height: stacked() ? 18 : 40, flex: stacked() ? undefined : 1, borderRadius: 6, backgroundColor: color() }} />}
</Each>
</View>
);
const stack = () => (
<Chip
label={stacked() ? "Back to a row" : "Stack them"}
onPress={() => {
LayoutAnimation.configureNext(LayoutAnimation.Presets.spring);
stacked.set(!stacked());
}}
/>
);
return (
<View style={{ flex: 1, backgroundColor: ui().page, padding: 16, paddingTop: 24, gap: 14 }}>
{() =>
wide() ? (
<View style={{ flex: 1, flexDirection: "row", gap: 16 }}>
<Stage width={520} gap={14}>
{heading("Transition (style changes animate)")}
{transition()}
{heading("timing and spring")}
<View style={{ gap: 8 }}>{tracks()}</View>
{heading("Animated.sequence / stagger")}
{swatches()}
{heading("LayoutAnimation")}
{bars()}
</Stage>
<Inspector>
<Panel title="timing · spring">
<View style={{ paddingTop: 6 }}>{move()}</View>
</Panel>
<Panel title="Animated.sequence · stagger">
<View style={{ paddingTop: 6 }}>{playback()}</View>
</Panel>
<Panel title="LayoutAnimation.configureNext">
<View style={{ paddingTop: 6 }}>{stack()}</View>
</Panel>
</Inspector>
</View>
) : (
<>
{heading("Transition (style changes animate)")}
{transition()}
{heading("timing and spring")}
<View style={{ gap: 8 }}>
{tracks()}
{move()}
</View>
{heading("Animated.sequence / stagger")}
{swatches()}
{playback()}
{heading("LayoutAnimation")}
{bars()}
{stack()}
</>
)
}
</View>
);
}
mount(App);Há seis ferramentas, da mais pronta à mais flexível:
| Ferramenta | Use para |
|---|---|
| Motion | Uma view que entra, sai, chama a atenção ou repete em loop: movimentos prontos (fadeInUp, shake, pulse, …) ou keyframes próprios, sobre sua opacidade e transformação. |
| Transições | Uma mudança de estilo que deve ser animada em vez de saltar: uma cor, uma opacidade, uma transformação. |
| Animações por keyframes | Um spinner, um placeholder pulsante, uma dica que quica. |
| Keyframes | Qualquer valor animado por keyframes, como o CSS os executa: offsets, uma easing por segmento, iterações e uma direção. |
| Valores animados | Qualquer coisa conduzida por um valor ao longo do tempo: springs, gestos, posição de rolagem, sequências. |
| Animações de layout | Mudanças de tamanho e posição após uma atualização: itens inseridos, removidos, reordenados, um painel que se expande. |
#Motion
Um movimento move a opacidade e a transformação de uma view por keyframes, de forma nativa. Crie um no componente dono da view (motion() em Rust, createMotion() de nsh em TSX), vincule-o à view e execute nele um movimento pronto ou um seu. enter inicia a view onde um movimento começa e o executa de imediato, de modo que a view entra desde o primeiro quadro.
#[component]
pub fn App() -> impl IntoElement {
let card = motion().entering(MotionPreset::FadeInUp);
view().flex_1().items_center().justify_center().child(
view()
.size(px(120.0))
.rounded(px(16.0))
.bg(rgb(0x0969da))
.bind_motion(&card)
.on_press(move || {
card.play(MotionPreset::Shake);
}),
)
}import { mount, View } from "nsh";
import { createMotion } from "nsh";
function App() {
const card = createMotion({ enter: "fadeInUp" });
return (
<View style={{ flex: 1, alignItems: "center", justifyContent: "center" }}>
<View
onPress={() => card.play("shake")}
style={{ width: 120, height: 120, borderRadius: 16, backgroundColor: "#0969da", ...card.style }}
/>
</View>
);
}
mount(App);Um movimento tem um valor animado por canal: opacity (0..1), translateX e translateY (pontos), rotate, rotateX e rotateY (graus), scale, scaleX e scaleY. Eles começam em repouso (opacidade e escalas em 1, o resto em 0) e são aplicados nesta ordem: translações, depois rotações, depois escalas, em torno da origem da transformação da view. Um movimento não tem aparência própria: ele move o que você estilizou.
Um movimento move apenas opacidade e transformação. Espalhar m.style define o opacity e o transform da view; para manter uma opacidade sua, vincule apenas os passos (transform: m.transform). Cores se movem com um valor animado e interpolate.
| TSX | Rust | Descrição |
|---|---|---|
createMotion(options?) |
motion() |
Um movimento com todos os canais em repouso. |
createMotion({ initial: { opacity: 0 } }) |
m.value(MotionChannel::Opacity).set(0.0) |
Onde os canais começam; os demais começam em repouso. |
createMotion({ enter, delay, duration, ... }) |
motion().entering(spec) |
Começa onde enter começa e o executa de imediato; as outras opções mudam sua temporização. enter define os canais que ele move, ao longo de initial. |
style={m.style}, m.transform |
.bind_motion(&m) |
Vincula os canais: m.style é { opacity, transform }, seu transform uma lista de sete passos (scale é incorporado em scaleX e scaleY). Eles são aplicados depois de translate, rotate e scale da view; uma perspectiva antes deles dá profundidade a um giro 3D: transform: [{ perspective: 800 }, ...m.transform]. Em Rust, a transformação do movimento é aplicada depois da transformação própria da view. |
m.opacity, m.translateX, … |
m.value(MotionChannel::Opacity), … |
O valor animado de um canal, para interpolar ou conduzir manualmente. |
m.play(motion, options?) |
m.play(spec) |
Executa um preset ou um movimento seu; um novo interrompe o que está em execução. Resolve com { finished }, true quando termina (AnimationEnd em Rust). |
m.animation(motion, options?) |
m.animation(spec) |
O mesmo movimento como uma animação composta, não iniciada, para sequenciar, escalonar ou repetir com outras. |
play(motion, { ignoreReduceMotion: true }) |
m.play_ignoring_reduce_motion(spec) |
Executa mesmo quando o usuário pede menos movimento. |
m.stop() |
m.stop() |
Para onde está. |
m.reset() |
m.reset() |
Para e devolve todos os canais ao repouso. |
#Presets
Os movimentos prontos são dados: motions de nsh (motions.fadeInUp é o que play("fadeInUp") executa) e MotionPreset em Rust (MotionPreset::FadeInUp.spec()). As distâncias são em pontos, dimensionadas para controles e cards; para views maiores, escale os valores de um movimento ou escreva seus próprios keyframes. swing e as demais rotações giram em torno da origem da transformação da view: dê a uma placa que balança transformOrigin: "top" (.origin_top()).
Entrada: começam ocultos ou deslocados e terminam em repouso.
| TSX | Rust | Duração | Descrição |
|---|---|---|---|
fadeIn |
MotionPreset::FadeIn |
300 ms | Aparece com fade. |
fadeInUp |
MotionPreset::FadeInUp |
450 ms | Aparece com fade, subindo até o lugar. |
fadeInDown |
MotionPreset::FadeInDown |
450 ms | Aparece com fade, descendo até o lugar. |
fadeInLeft |
MotionPreset::FadeInLeft |
450 ms | Aparece com fade, vindo da esquerda. |
fadeInRight |
MotionPreset::FadeInRight |
450 ms | Aparece com fade, vindo da direita. |
zoomIn |
MotionPreset::ZoomIn |
350 ms | Aparece com fade, crescendo a partir de 90%. |
popIn |
MotionPreset::PopIn |
450 ms | Surge com um pop: cresce a partir da metade do tamanho, passa do tamanho próprio e se acomoda. |
bounceIn |
MotionPreset::BounceIn |
650 ms | Entra quicando, ultrapassando duas vezes. |
slideInUp |
MotionPreset::SlideInUp |
400 ms | Desliza de baixo para cima, sem fade (sheets, toasts). |
slideInDown |
MotionPreset::SlideInDown |
400 ms | Desliza de cima para baixo, sem fade. |
flipInX |
MotionPreset::FlipInX |
600 ms | Entra girando em torno do eixo horizontal. |
flipInY |
MotionPreset::FlipInY |
600 ms | Entra girando em torno do eixo vertical. |
Saída: começam em repouso e terminam ocultos ou deslocados.
| TSX | Rust | Duração | Descrição |
|---|---|---|---|
fadeOut |
MotionPreset::FadeOut |
250 ms | Desaparece com fade. |
fadeOutUp |
MotionPreset::FadeOutUp |
300 ms | Desaparece com fade, subindo. |
fadeOutDown |
MotionPreset::FadeOutDown |
300 ms | Desaparece com fade, descendo. |
fadeOutLeft |
MotionPreset::FadeOutLeft |
300 ms | Desaparece com fade para a esquerda. |
fadeOutRight |
MotionPreset::FadeOutRight |
300 ms | Desaparece com fade para a direita. |
zoomOut |
MotionPreset::ZoomOut |
250 ms | Desaparece com fade, encolhendo até 90%. |
popOut |
MotionPreset::PopOut |
300 ms | Sai com um pop: incha um pouco e depois encolhe até sumir. |
slideOutDown |
MotionPreset::SlideOutDown |
300 ms | Desliza para baixo, saindo de vista, sem fade. |
slideOutUp |
MotionPreset::SlideOutUp |
300 ms | Desliza para cima, saindo de vista, sem fade. |
Atenção: começam e terminam em repouso.
| TSX | Rust | Duração | Descrição |
|---|---|---|---|
shake |
MotionPreset::Shake |
500 ms | Sacode de lado a lado: uma senha errada, um campo inválido. |
headShake |
MotionPreset::HeadShake |
800 ms | Balança a cabeça: um não mais suave. |
wiggle |
MotionPreset::Wiggle |
600 ms | Balança em torno do centro: algo a notar. |
swing |
MotionPreset::Swing |
900 ms | Balança a partir da borda superior, como uma placa. |
pulse |
MotionPreset::Pulse |
500 ms | Cresce um pouco e volta: uma ênfase suave. |
heartbeat |
MotionPreset::Heartbeat |
900 ms | Pulsa duas vezes, como um coração: uma curtida, um favorito. |
rubberBand |
MotionPreset::RubberBand |
800 ms | Comprime e estica como borracha. |
jello |
MotionPreset::Jello |
800 ms | Treme como gelatina. |
tada |
MotionPreset::Tada |
900 ms | Encolhe e depois cresce com um balanço: uma recompensa. |
jump |
MotionPreset::Jump |
700 ms | Pula e aterrissa com uma compressão. |
flash |
MotionPreset::Flash |
800 ms | Pisca duas vezes, sumindo e voltando. |
Repetição: repetem até serem interrompidos.
| TSX | Rust | Duração | Descrição |
|---|---|---|---|
spin |
MotionPreset::Spin |
1000 ms | Dá uma volta, para sempre. |
float |
MotionPreset::Float |
1500 ms | Flutua para cima e para baixo, para sempre. |
breathe |
MotionPreset::Breathe |
1800 ms | Respira: cresce e esmaece um pouco, para sempre. |
sway |
MotionPreset::Sway |
1600 ms | Oscila em torno do centro, para sempre. |
bob |
MotionPreset::Bob |
1000 ms | Balança como uma bola quicando, para sempre. |
glow |
MotionPreset::Glow |
2000 ms | Brilha e apaga, para sempre. |
play aceita sobre ele as opções de temporização de um preset (m.play("shake", { duration: 300 })), e um preset pode ser espalhado em um movimento seu (m.play({ ...motions.fadeInUp, duration: 600 })). Em Rust, altere a spec de um preset: MotionPreset::FadeInUp.spec().timing(|t| t.delay(100.0)).
#Movimentos próprios
Um movimento próprio é feito de keyframes por canal e uma única temporização (as opções de keyframes). Em TSX, escreva os keyframes de cada canal ({ opacity: [0, 1] }) ou uma lista de quadros como a Web Animations API os escreve ([{ opacity: 0, translateY: 24 }, { opacity: 1, translateY: 0 }]), cada quadro com seu offset e seu easing. Em uma lista de quadros, um quadro sem offset fica onde sua posição na lista o coloca, e um canal segue os quadros que o nomeiam. Os ângulos podem ser strings ("45deg", "0.5turn"). Em Rust, construa um MotionSpec.
let rise = MotionSpec::new(KeyframeTiming::new(400.0))
.values(MotionChannel::Opacity, [0.0, 1.0])
.track(
MotionChannel::TranslateY,
[Keyframe::at(0.0, 24.0).ease(Easing::EaseOut), Keyframe::at(1.0, 0.0)],
);
card.play(rise);
let flip = MotionSpec::new(KeyframeTiming::new(600.0).infinite())
.values(MotionChannel::RotateY, [0.0, 180.0, 360.0]);
card.play(flip);// Channels' keyframes.
card.play({
duration: 400,
keyframes: { opacity: [0, 1], translateY: [{ value: 24, easing: "easeOut" }, 0] },
});
// A list of frames.
card.play({
duration: 400,
keyframes: [
{ opacity: 0, translateY: 24, easing: "easeOut" },
{ opacity: 1, translateY: 0 },
],
});
card.play({ duration: 600, iterations: "infinite", keyframes: { rotateY: ["0deg", "180deg", "360deg"] } });#Entradas e listas escalonadas
Dê a cada linha um movimento de entrada, com um atraso que cresce com seu índice: linhas que aparecem juntas entram uma após a outra. Limite o atraso para que uma linha adicionada bem abaixo na lista não espere.
each(items, |item: &Item| item.id, |item, index| {
let delay = index.get_untracked().min(10) as f32 * 60.0;
let row = motion().entering(MotionPreset::FadeInUp.spec().timing(|t| t.delay(delay)));
view()
.rounded_xl()
.bg(ZINC.shade(100))
.p_4()
.bind_motion(&row)
.child(text(move || item.get().title))
})function Row(props: { title: string; index: number }) {
const row = createMotion({ enter: "fadeInUp", delay: Math.min(props.index, 10) * 60 });
return (
<View class="rounded-xl bg-zinc-100 p-4" style={row.style}>
<Text>{props.title}</Text>
</View>
);
}
<Each of={items()} key={(item) => item.id}>
{(item, index) => <Row title={item().title} index={index()} />}
</Each>Movimentos também se compõem: m.animation(..) é um movimento como uma animação composta, não iniciada, de modo que Animated.stagger executa um grupo deles de forma nativa, contando a partir do primeiro item, do último, do centro ou de qualquer índice. Dê a eles um initial oculto (ou inicie-os onde o movimento começa) para que nada apareça antes da sua vez.
let cards: Vec<Motion> = (0..5)
.map(|_| {
let m = motion();
m.value(MotionChannel::Opacity).set(0.0);
m
})
.collect();
Animation::stagger_from(60.0, StaggerFrom::Center, cards.iter().map(|m| m.animation(MotionPreset::FadeInUp))).start();const cards = [0, 1, 2, 3, 4].map(() => createMotion({ initial: { opacity: 0 } }));
Animated.stagger(60, cards.map((m) => m.animation("fadeInUp")), { from: "center" }).start();#Saídas
Execute a saída, espere por ela e então remova a view. Uma animação de layout permite que as views ao redor fechem o espaço.
let remove = move || {
spawn(async move {
row.play(MotionPreset::FadeOutLeft).await;
animate_layout(LayoutAnimation::EASE_IN_EASE_OUT);
items.update(|items| items.retain(|i| i.id != id));
});
};const remove = async () => {
await row.play("fadeOutLeft");
LayoutAnimation.easeInEaseOut();
items.set(items().filter((i) => i.id !== id));
};Os movimentos seguem por conta própria a configuração de movimento reduzido do usuário: quando o usuário pede menos movimento, o movimento chega de imediato ao ponto final, sem se mover.
#Transições
Diga o que animar e por quanto tempo (.transition_all().duration(400.0) em Rust, transitionProperty e transitionDuration em TSX), e as mudanças posteriores nessas propriedades são animadas de forma nativa, do que está na tela até o novo valor.
#[component]
pub fn App() -> impl IntoElement {
let on = signal(false);
view().flex_1().items_center().justify_center().child(
view()
.on_press(move || on.update(|on| *on = !*on))
.size(px(100.0))
.rounded(move || px(if on.get() { 50.0 } else { 16.0 }))
.bg(move || if on.get() { rgb(0x8250df) } else { rgb(0x0969da) })
.rotate(move || deg(if on.get() { 45.0 } else { 0.0 }))
.scale(move || if on.get() { 1.2_f32 } else { 1.0 })
.transition_all()
.duration(400.0)
.ease_in_out(),
)
}import { mount, signal, View } from "nsh";
function App() {
const on = signal(false);
return (
<View style={{ flex: 1, alignItems: "center", justifyContent: "center" }}>
<View
onPress={() => on.set(!on())}
style={{
width: 100,
height: 100,
borderRadius: on() ? 50 : 16,
backgroundColor: on() ? "#8250df" : "#0969da",
rotate: on() ? "45deg" : "0deg",
scale: on() ? 1.2 : 1,
transitionProperty: "all",
transitionDuration: 400,
transitionTimingFunction: "ease-in-out",
}}
/>
</View>
);
}
mount(App);| Estilo | Rust | Tipo | Descrição |
|---|---|---|---|
transitionProperty |
.transition(), .transition_all(), .transition_colors(), .transition_opacity(), .transition_transform(), .transition_shadow() |
string |
O que é animado, separado por vírgulas: "all", "colors", "opacity", "transform", "box-shadow", "border-radius", … |
transitionDuration |
.duration(ms) |
number | "150ms" | "0.2s" |
Milissegundos como número, ou um tempo CSS. |
transitionDelay |
.delay(ms) |
number | "150ms" | "0.2s" |
Antes de começar. |
transitionTimingFunction |
.ease_linear(), .ease_in(), .ease_out(), .ease_in_out(), .ease_bezier(..) |
"linear" | "ease" | "ease-in" | "ease-out" | "ease-in-out" | "cubic-bezier(…)" |
A curva. |
Propriedades que fazem transição, por grupo: cores (cores de fundo, borda, contorno, sombra e texto, e stops de gradiente), opacidade, transformação, sombras (box-shadow) e forma (raio e largura da borda). Tanto os nomes CSS quanto suas formas em camel case funcionam (background-color, backgroundColor, transform, scale, …). Mudanças de layout são animadas com animações de layout.
Com Tailwind, a mesma coisa é transition (cores, opacidade, sombras e transformações), transition-all, transition-colors, transition-opacity, transition-transform, transition-shadow, com duration-*, delay-* e ease-*. Combinado com o estado pressionado (.active(..), active:), resulta em estados pressionados animados:
view()
.on_press(save)
.rounded_xl()
.bg(BLUE.shade(600))
.px_5()
.py_3()
.transition()
.duration(150.0)
.active(|s| s.scale(0.95).bg(BLUE.shade(800)))
.child(text("Save").font_weight(FontWeight::SEMIBOLD).text_color(white()))<View onPress={save} class="rounded-xl bg-blue-600 px-5 py-3 transition duration-150 active:scale-95 active:bg-blue-800">
<Text class="font-semibold text-white">Save</Text>
</View>Uma transição parte do valor que está atualmente na tela, então mudar o destino novamente no meio do caminho faz uma curva suave.
#Animações por keyframes
Quatro animações por keyframes embutidas rodam de forma nativa, para sempre por padrão: spin (uma volta no sentido horário), ping (cresce até 2× e esmaece), pulse (esmaece até metade da opacidade e volta) e bounce (pula um quarto da própria altura e aterrissa).
view()
.size_8()
.rounded_full()
.border(px(4.0))
.border_color(ZINC.shade(300))
.border_t_color(BLUE.shade(600))
.animate_spin()
view().h_4().w_40().rounded(px(4.0)).bg(ZINC.shade(200)).animate_pulse()<View class="size-8 rounded-full border-4 border-zinc-300 border-t-blue-600 animate-spin" />
<View class="h-4 w-40 rounded bg-zinc-200 animate-pulse" />| Estilo | Rust | Tipo | Descrição |
|---|---|---|---|
animationName |
.animate_spin(), .animate_ping(), .animate_pulse(), .animate_bounce(), .animate_none() ou .animation(KeyframeAnimation::new(Keyframes::Spin)) |
"none" | "spin" | "ping" | "pulse" | "bounce" |
A animação. |
animationDuration |
KeyframeAnimation::duration(ms) |
number | "1s" | … |
Uma iteração. |
animationDelay |
KeyframeAnimation::delay(ms) |
number | "200ms" | … |
Antes de começar. |
animationTimingFunction |
KeyframeAnimation::easing([x1, y1, x2, y2]) |
como transitionTimingFunction |
A curva. |
animationIterationCount |
KeyframeAnimation::iterations(n) |
number | "infinite" |
Padrão "infinite". |
Tailwind: animate-spin, animate-ping, animate-pulse, animate-bounce, animate-none.
#Valores animados
Um valor animado é um número que vive na thread de UI. Crie um (animated_value(..) em Rust, createAnimatedValue de nsh em TSX), vincule-o a estilos e anime-o; as views vinculadas o seguem a cada quadro, sem uma atualização.
#[component]
pub fn App() -> impl IntoElement {
let x = animated_value(0.0);
let opacity = animated_value(1.0);
let play = move || {
spawn(async move {
x.timing(240.0, 600.0, Easing::Curve(Curve::Cubic, EaseMode::Out)).await;
opacity.timing(0.3, 200.0, Easing::EaseInOut).await;
x.spring(0.0, Spring::default());
opacity.timing_with(1.0, Timing::default());
});
};
view()
.flex_1()
.justify_center()
.p_6()
.gap_6()
.child(
view()
.size(px(60.0))
.rounded(px(12.0))
.bg(rgb(0x0969da))
.bind_opacity(&opacity)
.bind_translate_x(&x),
)
.child(view().on_press(play).child(text("Play").text_color(rgb(0x0969da))))
}import { mount, Text, View } from "nsh";
import { createAnimatedValue, Easing } from "nsh";
function App() {
const x = createAnimatedValue(0);
const opacity = createAnimatedValue(1);
return (
<View style={{ flex: 1, justifyContent: "center", padding: 24, gap: 24 }}>
<View style={{ width: 60, height: 60, borderRadius: 12, backgroundColor: "#0969da", opacity, translate: [x, 0] }} />
<View
onPress={async () => {
await x.timing(240, { duration: 600, easing: Easing.out(Easing.cubic) });
await opacity.timing(0.3, { duration: 200 });
x.spring(0);
opacity.timing(1);
}}
>
<Text style={{ color: "#0969da" }}>Play</Text>
</View>
</View>
);
}
mount(App);O que pode ser vinculado: opacity (.bind_opacity(..)); backgroundColor, borderColor (todos os lados) e o color de um texto (o texto inteiro), por meio de uma interpolação para cores (.bind_background(..), .bind_border_color(..), .bind_color(..)); translate ([x, y], um valor por eixo), rotate (graus) e scale ([x, y] por eixo, ou um valor para ambos); e os passos de uma lista transform: translateX, translateY, scale, scaleX, scaleY, rotate, rotateX, rotateY e rotateZ (.bind_translate_x(..), .bind_translate_y(..), .bind_scale(..), .bind_rotate(..) em graus, .bind_transform(kind, ..) em Rust). Sombras mudam com transições. Um anel de foco ou um preço que pisca em verde é uma interpolação de cor vinculada à cor da borda ou do texto:
let flash = animated_value(0.0);
let color = flash.interpolate_colors(&[0.0, 1.0], &[rgb(0x1f2328), rgb(0x1a7f37)]);
text(move || price.get().to_string()).bind_color(&color)
// On a new price: flash.set(1.0); flash.timing(0.0, 600.0, Easing::EaseInOut);const flash = createAnimatedValue(0);
const color = flash.interpolate({ inputRange: [0, 1], outputRange: ["#1f2328", "#1a7f37"] });
<Text style={{ color }}>{price()}</Text>
// On a new price: flash.set(1); flash.timing(0, { duration: 600 });Valores animados também conduzem desenhos em canvas, recebem offsets de rolagem (scrollValue) e gestos (pan, pinch, rotation).
O valor é mantido vivo pelas views vinculadas a ele e pelo componente que o criou, de modo que uma vinculação permanece conectada independentemente de como suas variáveis vão e vêm. Crie valores animados no componente que é dono deles (não no nível do módulo quando cada instância precisa do seu próprio).
#Métodos de AnimatedValue
| TSX | Rust | Descrição |
|---|---|---|
set(value) |
set(value) |
Salta para value, interrompendo qualquer animação. |
stop() |
stop() |
Para a animação em execução onde ela está; em TSX, resolve com { value }, o valor nesse ponto. |
getValue() |
value() |
O valor atual, lido na thread de UI, onde ele se move (estabiliza em até um frame): uma promise em TSX, uma resposta aguardável em Rust. |
setOffset(offset) |
set_offset(offset) |
Soma offset ao que o valor exibe: os bindings enxergam valor + offset, enquanto set, animações e arrastos movem o valor. |
flattenOffset() |
flatten_offset() |
Soma o offset ao valor e zera o offset. |
extractOffset() |
extract_offset() |
Move o valor para o offset e zera o valor: um arrasto então começa em 0, sobre o ponto onde o anterior parou. |
timing(to, { duration?, easing?, delay? }) |
timing(to, ms, easing), timing_with(to, Timing { .. }) |
Anima até to ao longo de duration ms (padrão 300, easing "easeInOut"). |
spring(to, { stiffness?, damping?, mass?, velocity? }) |
spring(to, Spring { .. }) |
Faz uma mola (spring) até to (padrões: stiffness 170, damping 26, mass 1). |
decay({ velocity?, deceleration?, clamp?, bounce? }) |
decay(velocity, deceleration), decay_within(velocity, deceleration, (min, max), bounce) |
Desliza a partir de velocity (unidades por segundo) e desacelera (desaceleração padrão de 0,998 por ms). Com um clamp de [min, max], ele para em um limite ou ricocheteia nele, mantendo bounce (0..1) da sua velocidade. |
Toda animação produz uma resposta que você pode aguardar, dizendo se ela terminou: true quando foi até o fim, false quando algo a interrompeu (stop(), set() ou uma nova animação no mesmo valor). Em TSX é Promise<{ finished: boolean }> e em Rust é AnimationEnd, que uma task aguarda com bool. Iniciar uma nova animação em um valor em movimento interrompe a antiga de forma suave: uma spring ou um decay sem velocidade continua com a velocidade atual do valor.
// Spring settings: lower damping overshoots more, higher stiffness is faster.
x.spring(200.0, Spring { stiffness: 180.0, damping: 8.0, ..Spring::default() }); // wobbly
x.spring(200.0, Spring { stiffness: 400.0, damping: 30.0, ..Spring::default() }); // stiff, no overshoot
// A fling that coasts to a stop, from a pan's release velocity.
view().pan_values(Some(&x), None).on_pan_end(move |e: &PanEvent| {
x.decay(Some(e.velocity_x), 0.998);
})
// A fling that stays on its track and bounces softly off its ends.
x.decay_within(Some(e.velocity_x), 0.998, (0.0, 300.0), 0.3);
// Where a value is now (it moves on the UI thread, so the answer is async).
let at = x.value().await;// Spring settings: lower damping overshoots more, higher stiffness is faster.
x.spring(200, { stiffness: 180, damping: 8 }); // wobbly
x.spring(200, { stiffness: 400, damping: 30 }); // stiff, no overshoot
// A fling that coasts to a stop, from a pan's release velocity.
<View pan={{ x }} onPanEnd={(e) => x.decay({ velocity: e.velocity.x })} />
// A fling that stays on its track and bounces softly off its ends.
x.decay({ velocity: e.velocity.x, clamp: [0, 300], bounce: 0.3 });
// Where a value is now (it moves on the UI thread, so the answer is async).
const where = await x.getValue();#Keyframes
keyframes reproduz um valor por meio de keyframes como os keyframes do CSS: value.keyframes(frames, options) em TSX, value.keyframes(frames, KeyframeTiming) em Rust (Animated.keyframes(value, { keyframes, ... }) e Animation::keyframes(..) como um passo de uma animação composta). Um keyframe é um número (ou uma string de ângulo, como "90deg"), distribuído uniformemente com os demais, ou { offset, value, easing } (Keyframe::new(value), Keyframe::at(offset, value), .ease(easing) em Rust).
let y = animated_value(0.0);
y.keyframes(
[Keyframe::new(0.0), Keyframe::at(0.3, -20.0).ease(Easing::EaseIn), Keyframe::new(0.0)],
KeyframeTiming::new(600.0).easing(Easing::EaseOut).iterations(Some(3)),
);
// A clock hand that ticks once a second.
let hand = animated_value(0.0);
hand.keyframes(
[Keyframe::new(0.0), Keyframe::new(360.0)],
KeyframeTiming::new(60_000.0).easing(Easing::Steps(60, StepPosition::JumpEnd)).infinite(),
);const y = createAnimatedValue(0);
y.keyframes([0, { offset: 0.3, value: -20, easing: "easeIn" }, 0], { duration: 600, easing: "easeOut", iterations: 3 });
// A clock hand that ticks once a second.
const hand = createAnimatedValue(0);
hand.keyframes([0, 360], { duration: 60_000, easing: Easing.steps(60), iterations: "infinite" });| Opção | Rust (KeyframeTiming) |
Padrão | Descrição |
|---|---|---|---|
duration |
KeyframeTiming::new(ms) |
300 | Uma iteração, em milissegundos. |
delay |
.delay(ms) |
0 | Antes da primeira iteração. |
easing |
.easing(easing) |
ease do CSS |
A curva de cada segmento sem easing próprio. |
iterations |
.iterations(Some(n)), .infinite() |
1 | Quantas iterações, ou "infinite". |
direction |
.direction(PlaybackDirection::Alternate) |
"normal" |
"normal", "reverse", "alternate" ou "alternate-reverse". |
Elas são reproduzidas como no CSS e na Web Animations API:
- Um easing molda cada segmento, não o conjunto: o
easingde um keyframe molda o segmento que começa nele, e oeasingdo timing molda todos os outros segmentos. - Keyframes sem offset são distribuídos uniformemente entre seus vizinhos; o primeiro tem 0 como padrão e o último, 1.
- Cada iteração começa com um salto para o seu primeiro keyframe. Sem um keyframe no offset 0, o valor parte de onde está.
- Durante um
delay, o valor já assume o primeiro keyframe (animation-fill-mode: backwardsdo CSS), de modo que uma entrada atrasada mostra o seu primeiro frame em vez da view em repouso. Isso vale também para motions. reversee as iterações reversas dealternatepercorrem os segmentos de trás para frente, cada um com a sua curva invertida.- O valor permanece onde a última iteração o deixa. A resposta informa se elas terminaram:
truequando terminaram,falsequando algo as interrompeu.
#Interpolação
interpolate mapeia um valor, por segmentos lineares por partes, para números, cores ou ângulos, e retorna um novo nó que você pode vincular. Um único valor pode controlar várias propriedades. Em Rust, interpolate(&input, &output) mapeia para números, interpolate_colors(&input, &colors) para cores, e interpolate_with(&input, output, Interpolation { .. }) recebe a extrapolação e o easing; em TSX, value.interpolate({ inputRange, outputRange }) recebe tudo isso.
let t = animated_value(0.0);
view()
.bind_background(&t.interpolate_colors(&[0.0, 1.0, 2.0], &[rgb(0x54aeff), rgb(0x8250df), rgb(0xcf222e)]))
.bind_opacity(&t.interpolate(&[0.0, 1.0], &[0.4, 1.0]))
.bind_rotate(&t.interpolate(&[0.0, 1.0], &[0.0, 180.0]))
.bind_scale(&t.interpolate_with(
&[0.0, 1.0],
AnimationOutput::Numbers(vec![0.6, 1.2]),
Interpolation {
easing: Easing::Curve(Curve::Back(1.70158), EaseMode::Out),
..Interpolation::default()
},
))const t = createAnimatedValue(0);
<View
style={{
backgroundColor: t.interpolate({ inputRange: [0, 1, 2], outputRange: ["#54aeff", "#8250df", "#cf222e"] }),
opacity: t.interpolate({ inputRange: [0, 1], outputRange: [0.4, 1] }),
rotate: t.interpolate({ inputRange: [0, 1], outputRange: ["0deg", "180deg"] }),
scale: t.interpolate({ inputRange: [0, 1], outputRange: [0.6, 1.2], easing: Easing.out(Easing.back()) }),
}}
/>| Opção | Rust | Descrição |
|---|---|---|
inputRange |
o slice de entrada | Pontos de entrada crescentes. |
outputRange |
o slice de saída, ou AnimationOutput |
Uma saída por ponto: números, cores ("#rrggbb", "rgba(…)", nomeadas) ou ângulos ("45deg", "1rad"). |
extrapolate |
Interpolation::extrapolate(Extrapolate::Extend) |
Fora do intervalo de entrada: "clamp" (padrão), "extend" (continua os segmentos das extremidades) ou "identity" (repassa a entrada). |
extrapolateLeft, extrapolateRight |
extrapolate_left, extrapolate_right |
O mesmo, para um dos lados. |
easing |
easing |
Molda cada segmento. |
#Aritmética e diffClamp
Os nós se combinam com aritmética, também de forma nativa: a.add(b), a.sub(b), a.mul(b), a.div(b), a.mod(b) (a.modulo(b) em Rust; nunca negativo para um b positivo, por exemplo ângulos em 0..360), onde b é um nó ou um número. Em TSX, Animated.add, subtract, multiply, divide e modulo são equivalentes, como funções.
diffClamp(min, max) (diff_clamp) acompanha as mudanças do nó, permanecendo dentro de min..max. Aplicado a um offset de scroll, ele cria um cabeçalho que se esconde conforme o conteúdo rola para cima e volta assim que ele rola para baixo:
let offset = animated_value(0.0);
let header = offset.diff_clamp(0.0, 56.0);
scroll().flex_1().scroll_value(&offset)
view().absolute().top_0().left_0().right_0().h(px(56.0)).bind_translate_y(&header.mul(-1.0))const scroll = createAnimatedValue(0);
const header = scroll.diffClamp(0, 56);
<Scroll style={{ flex: 1 }} scrollValue={scroll}>…</Scroll>
<View style={{ position: "absolute", top: 0, left: 0, right: 0, height: 56, translate: [0, header.mul(-1)] }}>…</View>Com { snap: true } (diff_clamp_with(min, max, DiffClamp::snapping())), um cabeçalho deixado parcialmente visível quando a rolagem para completa o movimento: ele se acomoda totalmente visível ou totalmente oculto, o que estiver mais próximo, em 200 ms na thread de UI. Ele só se esconde depois que a página rolou pelo menos a sua altura, de modo que uma página rolada menos do que isso mostra o cabeçalho inteiro. Rolar de novo durante o movimento assume a partir de onde o cabeçalho está.
let header = offset.diff_clamp_with(0.0, 56.0, DiffClamp::snapping());const header = scroll.diffClamp(0, 56, { snap: true });#Animações compostas
As animações são montadas a partir de passos (Animation em Rust, Animated de nsh em TSX) e a árvore inteira roda de forma nativa: cada passo começa na thread de UI no instante em que o anterior termina, sem intervalo e sem nenhum código seu no meio.
let ys: Vec<AnimatedValue> = (0..5).map(|_| animated_value(0.0)).collect();
let fade = animated_value(1.0);
let hop = Animation::stagger(
80.0,
ys.iter().map(|y| {
Animation::sequence([
Animation::timing(y, -30.0, Timing::new(200.0, Easing::Curve(Curve::Quad, EaseMode::Out))),
Animation::spring(y, 0.0, Spring { stiffness: 300.0, damping: 12.0, ..Spring::default() }),
])
}),
);
let breathe = Animation::sequence([
Animation::timing(&fade, 0.3, Timing::new(400.0, Easing::EaseInOut)),
Animation::timing(&fade, 1.0, Timing::new(400.0, Easing::EaseInOut)),
])
.looped(None);
let landed = hop.start();
spawn(async move {
status.set(if landed.await { "landed" } else { "stopped" });
});import { Animated, createAnimatedValue, Easing } from "nsh";
const ys = [0, 1, 2, 3, 4].map(() => createAnimatedValue(0));
const fade = createAnimatedValue(1);
const hop = Animated.stagger(
80,
ys.map((y) =>
Animated.sequence([
Animated.timing(y, { toValue: -30, duration: 200, easing: Easing.out(Easing.quad) }),
Animated.spring(y, { toValue: 0, stiffness: 300, damping: 12 }),
]),
),
);
const breathe = Animated.loop(
Animated.sequence([
Animated.timing(fade, { toValue: 0.3, duration: 400 }),
Animated.timing(fade, { toValue: 1, duration: 400 }),
]),
);
hop.start(({ finished }) => console.log(finished ? "landed" : "stopped"));| TSX | Rust | Descrição |
|---|---|---|
Animated.timing(value, { toValue, duration?, easing?, delay? }) |
Animation::timing(&value, to, Timing { .. }) |
Um passo de timing. |
Animated.spring(value, { toValue, stiffness?, damping?, mass?, velocity? }) |
Animation::spring(&value, to, Spring { .. }) |
Um passo de spring. |
Animated.decay(value, { velocity?, deceleration?, clamp?, bounce? }) |
Animation::decay(&value, velocity, deceleration), Animation::decay_within(..) |
Um passo de decay. |
Animated.sequence(list) |
Animation::sequence(list) |
Um após o outro. |
Animated.parallel(list, { stopTogether? }) |
Animation::parallel(list), .stop_together(..) |
Todos ao mesmo tempo. |
Animated.stagger(ms, list, { from? }) |
Animation::stagger(ms, list), Animation::stagger_from(ms, StaggerFrom::Center, list) |
Todos ao mesmo tempo, cada um começando ms depois do anterior, contando a partir de from: "first" (o padrão), "last", "center" (uma ondulação a partir do meio) ou um índice (StaggerFrom::First, Last, Center, Index(i)). |
Animated.keyframes(value, { keyframes, ...timing }) |
Animation::keyframes(&value, frames, KeyframeTiming) |
Um passo de keyframes. |
Animated.delay(ms) |
Animation::delay(ms) |
Espera. |
Animated.loop(animation, { iterations?, resetBeforeIteration? }) |
.looped(iterations), .reset_before_iteration(..) |
Repete (para sempre por padrão; None em Rust). |
Uma animação composta tem start() (em TSX também com um callback e uma promise, ambos recebendo { finished }; em Rust um bool aguardável; chamá-la de novo a executa de novo), stop() (para onde está) e reset() (para e devolve os seus valores para onde estavam quando ela começou). O callback é executado uma vez para a árvore inteira.
Uma animação em execução não mantém seus valores vivos: quando o componente que os possui desaparece (suas views e variáveis), um loop infinito termina junto com ele, e a sua resposta é que ela não terminou (false).
#Easing
As curvas de easing são dados, avaliados de forma nativa: Easing de nsh em TSX, o enum Easing em Rust.
| TSX | Rust | Descrição |
|---|---|---|
Easing.linear, Easing.ease |
Easing::Linear, Easing::Bezier(0.25, 0.1, 0.25, 1.0) |
Velocidade constante; o ease do CSS. |
Easing.quad, Easing.cubic, Easing.poly(n) |
Curve::Quad, Curve::Cubic, Curve::Poly(n) |
t², t³, tⁿ. |
Easing.sin, Easing.circle, Easing.exp |
Curve::Sin, Curve::Circle, Curve::Exp |
Senoidal, circular, exponencial. |
Easing.back(s?) |
Curve::Back(s) |
Recua antes de partir (overshoot s, 1.70158 por padrão). |
Easing.elastic(bounciness?) |
Curve::Elastic(bounciness) |
Overshoot elástico. |
Easing.bounce |
Curve::Bounce |
Quica no final. |
Easing.bezier(x1, y1, x2, y2) |
Easing::Bezier(x1, y1, x2, y2) |
Uma curva de Bézier cúbica, como no CSS. |
Easing.steps(count, position?) |
Easing::Steps(count, StepPosition::JumpEnd) |
Salta entre count níveis em vez de se mover, como o steps() do CSS: um sprite quadro a quadro, um ponteiro que marca o tempo. position é "jump-end" (o padrão, também "end"), "jump-start" ("start"), "jump-none" ou "jump-both". |
Easing.in(e), Easing.out(e), Easing.inOut(e) |
Easing::Curve(curve, EaseMode::In), EaseMode::Out, EaseMode::InOut |
As curvas são curvas “in”; out e inOut as invertem. |
Em TSX, em qualquer lugar onde um easing é aceito, você também pode passar um nome ("linear", "ease", "easeIn", "easeOut", "easeInOut") ou um array de Bézier [x1, y1, x2, y2]; em Rust, Easing::EaseIn, Easing::EaseOut e Easing::EaseInOut são as curvas do CSS.
x.timing(1.0, 300.0, Easing::Curve(Curve::Back(1.70158), EaseMode::Out));
x.timing(1.0, 300.0, Easing::EaseOut);
x.timing(1.0, 300.0, Easing::Bezier(0.2, 0.8, 0.2, 1.0));x.timing(1, { easing: Easing.out(Easing.back()) });
x.timing(1, { easing: "easeOut" });
x.timing(1, { easing: [0.2, 0.8, 0.2, 1] });Easing.sample(easing, count) (sample_easing em Rust) retorna a curva em count pontos igualmente espaçados de 0 a 1, e Animated.sampleSpring(config, fps?) / Animated.sampleDecay(config, fps?) (sample_spring, sample_decay) os valores pelos quais uma spring ou um decay passa, um por frame (60 por segundo por padrão) a partir de config.from (0) até terminar. Eles são calculados pelo mesmo código nativo que as animações executam, de modo que um gráfico de uma curva mostra exatamente o que a animação faz:
use nsh_core::element::{sample_easing, sample_spring};
let curve = sample_easing(Easing::Curve(Curve::Back(1.70158), EaseMode::Out), 50); // 50 progress values
let spring = sample_spring(0.0, 1.0, Spring { stiffness: 180.0, damping: 8.0, ..Spring::default() }, 60.0);const curve = Easing.sample(Easing.out(Easing.back()), 50); // 50 progress values
const spring = Animated.sampleSpring({ toValue: 1, stiffness: 180, damping: 8 });#LayoutAnimation
Uma layout animation anima de forma nativa as mudanças de frame da próxima atualização: views que se movem ou mudam de tamanho deslizam até seus novos frames, novas views surgem com fade (ou escala) e views removidas somem com fade. Chame-a logo antes da mudança de estado: animate_layout(..) em Rust, LayoutAnimation.configureNext(config) em TSX.
use nsh_core::element::LayoutAnimation;
let items = signal(vec![1, 2, 3]);
let next = stored(4);
let add = move || {
animate_layout(LayoutAnimation::SPRING);
let n = next.with(|n| {
*n += 1;
*n - 1
});
items.update(|i| i.insert(0, n));
};
let remove = move || {
animate_layout(LayoutAnimation::EASE_IN_EASE_OUT);
items.update(|i| {
i.remove(0);
});
};import { signal } from "nsh";
import { LayoutAnimation } from "nsh";
const items = signal([1, 2, 3]);
let next = 4;
const add = () => {
LayoutAnimation.configureNext(LayoutAnimation.Presets.spring);
items.set([next++, ...items()]);
};
const remove = () => {
LayoutAnimation.easeInEaseOut();
items.set(items().slice(1));
};| TSX | Rust | Descrição |
|---|---|---|
configureNext(config, onEnd?) |
animate_layout(config) |
Anima as mudanças do próximo commit; em TSX, onEnd é executado ao terminar. |
create(duration, type?, property?) |
LayoutAnimation::all(LayoutTransition::new(curve, ms)) |
Uma config com uma só curva para as três fases. |
Presets.easeInEaseOut, Presets.linear, Presets.spring |
LayoutAnimation::EASE_IN_EASE_OUT, LINEAR, SPRING |
Configs prontas. |
easeInEaseOut(onEnd?), linear(onEnd?), spring(onEnd?) |
animate_layout com um preset |
configureNext com um preset. |
Types, Properties |
LayoutCurve, LayoutProperty |
Os nomes, para montar configs. |
Uma config é { duration, create?, update?, delete? } (LayoutAnimation { create, update, delete } em Rust): create para views inseridas, update para as movidas ou redimensionadas, delete para as removidas. Cada fase é { type?, property?, duration?, delay?, springDamping? } (LayoutTransition { curve, property, duration_ms, delay_ms }), onde type é "easeInEaseOut", "spring", "linear", "easeIn", "easeOut" ou "keyboard" (a curva própria do teclado virtual, cronometrada a partir do momento em que o teclado começou a se mover, para que o que abre espaço para ele se mova junto; LayoutCurve::Keyboard), property é o que views que aparecem e desaparecem animam ("opacity", "scaleXY", "scale"), e springDamping (0..1, 0,7 por padrão; LayoutCurve::Spring { damping }) define o overshoot da spring.
use nsh_core::element::{LayoutAnimation, LayoutCurve, LayoutProperty, LayoutTransition};
animate_layout(LayoutAnimation {
create: Some(LayoutTransition::new(LayoutCurve::EaseOut, 300.0)),
update: Some(LayoutTransition::new(LayoutCurve::Spring { damping: 0.8 }, 300.0)),
delete: Some(LayoutTransition {
property: LayoutProperty::Scale,
..LayoutTransition::new(LayoutCurve::EaseIn, 300.0)
}),
});LayoutAnimation.configureNext({
duration: 300,
create: { type: "easeOut", property: "opacity" },
update: { type: "spring", springDamping: 0.8 },
delete: { type: "easeIn", property: "scaleXY" },
});Uma layout animation cobre todos os frames que a atualização altera, em qualquer lugar da tela: uma lista que é atualizada ou dados ao vivo que chegam na mesma atualização se movem junto. Em um componente Rust, anime apenas o que a mudança de um elemento move com ElementRef::animate_layout(transition): o frame do próprio elemento e o do seu conteúdo, e os das views que o seu tamanho empurra (seus irmãos e os deles, até a tela). Os demais frames da atualização mudam de uma vez. É assim que Collapsible e AccordionPanel de nsh/base revelam o seu conteúdo.
use nsh_core::element::{LayoutCurve, LayoutTransition};
let region = element_ref();
let open = signal(false);
let toggle = move || {
region.animate_layout(LayoutTransition::new(LayoutCurve::Spring { damping: 1.0 }, 265.0));
open.update(|o| *o = !*o);
};
view()
.element_ref(®ion)
.overflow_hidden()
.style_with(move |s| if open.get() { s } else { s.h(px(0.0)) })
#O que roda onde
| Roda em | Seu código por frame | |
|---|---|---|
| Transitions, animações de keyframes | Thread de UI | nenhum |
Motions, keyframes, timing / spring / decay, composições |
Thread de UI | nenhum; seu código é avisado quando terminam |
interpolate, aritmética, diffClamp |
Thread de UI | nenhum |
scrollValue, pan / pinch / rotation |
Thread de UI | nenhum; seu código é avisado do início e do fim |
| Layout animations | Thread de UI | nenhum |
Loops de requestAnimationFrame (request_animation_frame em Rust), handlers de onScroll, PanResponder |
seu código, depois uma atualização | a cada frame |
Recorra a requestAnimationFrame (um global em TSX, que acompanha o display) apenas para o que não pode ser descrito de antemão, como uma simulação de física no seu código. Tudo o que puder ser um animated value deve ser um.
#Movimento reduzido
Quando o usuário pede menos movimento (iOS “Reduzir Movimento”, Android “Remover animações”), reduce_motion() em Rust e reduceMotion() de nsh em TSX são true, e as variantes motion-reduce: / motion-safe: do Tailwind se aplicam.
As motions seguem a configuração por conta própria: play e enter chegam de imediato ao ponto onde a motion termina, sem se mover, e a resposta é que ela terminou (true), como para uma motion que foi reproduzida. Uma saída ainda termina oculta, uma entrada ainda termina em repouso. Passe ignoreReduceMotion: true (play_ignoring_reduce_motion em Rust) para uma motion que carrega um significado de que o usuário precisa, como um indicador de progresso giratório.
Transitions, animações de keyframes, animated values, keyframes e composições são reproduzidos como escritos; use-os para escolher uma alternativa mais calma:
let open = move || {
if reduce_motion() {
panel.set(0.0);
} else {
panel.spring(0.0, Spring::default());
}
};
view().style_when(|| !reduce_motion(), |s| s.animate_bounce())import { View } from "nsh";
import { reduceMotion } from "nsh";
const open = () => (reduceMotion() ? panel.set(0) : panel.spring(0));
<View class="motion-safe:animate-bounce" />Veja Acessibilidade.
#Relacionados
- API de animação de
nsh: a referência de TSX. - Toque e gestos: controlando animated values com o dedo.
- Rolagem:
scrollValue. - Canvas: animated values em desenhos.
- Referência de estilo: as chaves
transition*eanimation*.