Skip to content

React Spring 动画处理

目录

  • 概述
  • 使用
  • 进阶

概述

React Spring 是一个用于构建交互式、数据驱动和动画 UI 组件的库。

react-spring 有以下五个版本,分别对应不同的渲染方式

  • web - react-dom
  • native - react-native
  • three - react-three-fiber
  • konva - react-konva
  • zdog - react-zdog

优点

使用命令式 API 方法来运行动画,无需更新状态。通过响应事件而避免 React 渲染开销,实现流畅、流动的动画效果。

react-spring 的基础在于SpringValuesanimated组件,也叫做 Animated Elements

目标元素

animated 组件可以从我们的任何 web 元素目标导入

tsx
import { animated } from '@react-spring/web'

// ✅ This will work because `div` is a web element
<animated.div />

// ❌ This will not work because `mesh` is not a web element.
<animated.mesh />
import { animated } from '@react-spring/web'

// ✅ This will work because `div` is a web element
<animated.div />

// ❌ This will not work because `mesh` is not a web element.
<animated.mesh />

如果动画目标是一个组件,可以使用animated作为 HOC 包装组件

tsx
import { animated, useSpring } from "@react-spring/web";

const ExternalComponent = (props) => {
	return <div {...props} />;
};

const AnimatedCom = animated(ExternalComponent);

const App = () => {
	const styles = useSpring({
		from: {
			x: 0,
		},
		to: {
			x: 200,
		},
	});

	return <AnimatedCom style={styles} />;
};
import { animated, useSpring } from "@react-spring/web";

const ExternalComponent = (props) => {
	return <div {...props} />;
};

const AnimatedCom = animated(ExternalComponent);

const App = () => {
	const styles = useSpring({
		from: {
			x: 0,
		},
		to: {
			x: 200,
		},
	});

	return <AnimatedCom style={styles} />;
};

使用

useSpring

ts 定义

ts
function useSpring(configuration: ConfigObject): SpringValues;

function useSpring(
	configurationFn: () => ConfigObject,
	deps?: any[]
): [springs: SpringValues, api: SpringRef];
function useSpring(configuration: ConfigObject): SpringValues;

function useSpring(
	configurationFn: () => ConfigObject,
	deps?: any[]
): [springs: SpringValues, api: SpringRef];
  • With a config object
tsx
import { useSpring, animated } from "@react-spring/web";

const MyComponent = () => {
	const springs = useSpring({
		from: { opacity: 0 },
		to: { opacity: 1 },
	});

	return <animated.div style={springs}>Hello World</animated.div>;
};
import { useSpring, animated } from "@react-spring/web";

const MyComponent = () => {
	const springs = useSpring({
		from: { opacity: 0 },
		to: { opacity: 1 },
	});

	return <animated.div style={springs}>Hello World</animated.div>;
};
  • With a function & deps
tsx
import { useSpring, animated } from "@react-spring/web";

const MyComponent = () => {
	const [springs, api] = useSpring(
		() => ({
			from: { opacity: 0 },
			to: { opacity: 1 },
		}),
		[]
	);

	return <animated.div style={springs}>Hello World</animated.div>;
};
import { useSpring, animated } from "@react-spring/web";

const MyComponent = () => {
	const [springs, api] = useSpring(
		() => ({
			from: { opacity: 0 },
			to: { opacity: 1 },
		}),
		[]
	);

	return <animated.div style={springs}>Hello World</animated.div>;
};

useSprings

用一个统一的 API 来编排多个spring

tsx
import { useSprings, animated } from "@react-spring/web";
import cx from "./index.module.less";

function MyComponent() {
	const [springs, api] = useSprings(
		5,
		(i: index) => {
			return { from: { top: 0, position: "absolute" } };
		},
		[]
	);
	return (
		<div className={cx("ball")}>
			{springs.map((props, index) => (
				<animated.div key={index} style={props} className={cx("ball")}>
					{index}
				</animated.div>
			))}
		</div>
	);
}
import { useSprings, animated } from "@react-spring/web";
import cx from "./index.module.less";

function MyComponent() {
	const [springs, api] = useSprings(
		5,
		(i: index) => {
			return { from: { top: 0, position: "absolute" } };
		},
		[]
	);
	return (
		<div className={cx("ball")}>
			{springs.map((props, index) => (
				<animated.div key={index} style={props} className={cx("ball")}>
					{index}
				</animated.div>
			))}
		</div>
	);
}

useSpringValue

useSpringValue是一个 SpringValue的简单包装器,运行不多的spring时可以使用,命令式 API

tsx
import { animated, useSpringValue } from "@react-spring/web";

function MyComponent() {
	const opacity = useSpringValue(0.5);

	return (
		<animated.div
			onClick={() => {
				opacity.start(opacity.get() === 1 ? 0.5 : 1);
			}}
			style={{ opacity }}
		>
			Hello World
		</animated.div>
	);
}
import { animated, useSpringValue } from "@react-spring/web";

function MyComponent() {
	const opacity = useSpringValue(0.5);

	return (
		<animated.div
			onClick={() => {
				opacity.start(opacity.get() === 1 ? 0.5 : 1);
			}}
			style={{ opacity }}
		>
			Hello World
		</animated.div>
	);
}

useTransition

这个钩子最适合对数据集进行动画处理。

trail参数控制每个动画起始时间的间隔

ts 定义

ts
function useTransition<Item extends any>(
  data: Item[],
  configuration: ConfigObject
): TransitionFn<Item>

function useTransition<Item extends any>(
  data: Item[],
  configurationFn: () => ConfigObject
  deps?: any[]
): [transition: TransitionFn<Item>, api: SpringRef]
function useTransition<Item extends any>(
  data: Item[],
  configuration: ConfigObject
): TransitionFn<Item>

function useTransition<Item extends any>(
  data: Item[],
  configurationFn: () => ConfigObject
  deps?: any[]
): [transition: TransitionFn<Item>, api: SpringRef]
tsx
import { useTransition, animated } from "@react-spring/web";

function MyComponent({ data = [1, 2, 3] }) {
	const [transitions, api] = useTransition(data, () => ({
		from: { opacity: 0 },
		enter: { opacity: 1 },
		leave: { opacity: 1 },
	}));

	return transitions((style, item) => (
		<animated.div style={style}>{item}</animated.div>
	));
}
import { useTransition, animated } from "@react-spring/web";

function MyComponent({ data = [1, 2, 3] }) {
	const [transitions, api] = useTransition(data, () => ({
		from: { opacity: 0 },
		enter: { opacity: 1 },
		leave: { opacity: 1 },
	}));

	return transitions((style, item) => (
		<animated.div style={style}>{item}</animated.div>
	));
}

useChain

useChain 用于按顺序编排动画,确保它们按照指定的顺序执行,适用于需要依次触发一组动画的场景。

tsx
import {
	useTransition,
	useSpring,
	useChain,
	useSpringRef,
} from "@react-spring/web";

const data = ["hello", "world!"];

function MyComponent() {
	const springRef = useSpringRef();
	const springs = useSpring({
		ref: springRef,
		from: { size: "20%" },
		to: { size: "50%" },
	});

	const transRef = useSpringRef();
	const transitions = useTransition(data, {
		ref: transRef,
		from: { scale: 0 },
		enter: { scale: 1 },
		leave: { scale: 0 },
	});

	useChain([springRef, transRef]);
}
import {
	useTransition,
	useSpring,
	useChain,
	useSpringRef,
} from "@react-spring/web";

const data = ["hello", "world!"];

function MyComponent() {
	const springRef = useSpringRef();
	const springs = useSpring({
		ref: springRef,
		from: { size: "20%" },
		to: { size: "50%" },
	});

	const transRef = useSpringRef();
	const transitions = useTransition(data, {
		ref: transRef,
		from: { scale: 0 },
		enter: { scale: 1 },
		leave: { scale: 0 },
	});

	useChain([springRef, transRef]);
}

useTrail

useTrail会自动编排 spring 动画,错开执行每一个动画

tsx
import { useTrail, animated } from "@react-spring/web";

export default function MyComponent() {
	const [trails, api] = useTrail(
		2,
		() => ({
			from: { opacity: 0 },
			to: { opacity: 1 },
		}),
		[]
	);

	return (
		<div>
			{trails.map((props) => (
				<animated.div style={props}>Hello World</animated.div>
			))}
		</div>
	);
}
import { useTrail, animated } from "@react-spring/web";

export default function MyComponent() {
	const [trails, api] = useTrail(
		2,
		() => ({
			from: { opacity: 0 },
			to: { opacity: 1 },
		}),
		[]
	);

	return (
		<div>
			{trails.map((props) => (
				<animated.div style={props}>Hello World</animated.div>
			))}
		</div>
	);
}
  • delay: 前置动画延迟,无法设置每个动画之间的时间间隔
  • config.duration 设置每个动画的时长,但后一个动画还是会在第一个动画结束前就开始执行

进阶用法

config 配置

config 是一个具有许多属性的对象,可用于自定义和微调动画。它直接控制弹簧的行为方式,并且可以逐个spring进行定制

config 最典型的用途是编辑质量、摩擦和张力属性

  • mass: 质量,默认值 1
  • friction: 摩擦阻力,默认值 170
  • tension: 弹簧张力,默认值 26
tsx
import { useSpring, animated } from "@react-spring/web";

const MyComponent = () => {
	const [springs, api] = useSpring(
		() => ({
			backgroundColor: "#00ff00",
			y: 0,
			config: (key) => {
				if (key === "y") {
					return {
						mass: 1,
						friction: 170,
						tension: 26,
					};
				}

				return {};
			},
		}),
		[]
	);

	return <animated.div style={springs} />;
};
import { useSpring, animated } from "@react-spring/web";

const MyComponent = () => {
	const [springs, api] = useSpring(
		() => ({
			backgroundColor: "#00ff00",
			y: 0,
			config: (key) => {
				if (key === "y") {
					return {
						mass: 1,
						friction: 170,
						tension: 26,
					};
				}

				return {};
			},
		}),
		[]
	);

	return <animated.div style={springs} />;
};
  • config 预设属性
ts
import { config } from "@react-spring/web";

// default { tension: 170, friction: 26 }
// gentle { tension: 120, friction: 14 }
// wobbly{ tension: 180, friction: 12 }
// stiff { tension: 210, friction: 20 }
// slow{ tension: 280, friction: 60 }
// molasses { tension: 280, friction: 120 }
{
	config: config.gentle;
}
import { config } from "@react-spring/web";

// default { tension: 170, friction: 26 }
// gentle { tension: 120, friction: 14 }
// wobbly{ tension: 180, friction: 12 }
// stiff { tension: 210, friction: 20 }
// slow{ tension: 280, friction: 60 }
// molasses { tension: 280, friction: 120 }
{
	config: config.gentle;
}
  • duration 持续时间

  • easing 动画效果

easing 仅可与 duration 同时使用。

ts
import { easings } from "@react-spring/web";

// easeInBack	easeOutBack	easeInOutBack...
const [springs1, api] = useSpring(() => ({
	x: 0,
	config: { duration: 500, easing: easings.easeInBack },
}));
import { easings } from "@react-spring/web";

// easeInBack	easeOutBack	easeInOutBack...
const [springs1, api] = useSpring(() => ({
	x: 0,
	config: { duration: 500, easing: easings.easeInBack },
}));

Events 事件

每个事件函数既可以是它自己的函数,也可以是它的键与spring相关的对象

tsx
useSpring(
	() => ({
		x: 0,
		y: 0,
		onStart: () => console.log("the spring has started"),
	}),
	[]
);

useSpring(
	() => ({
		x: 0,
		y: 0,
		onStart: {
			x: () => console.log("x key has started"),
			y: () => console.log("y key has started"),
		},
	}),
	[]
);
useSpring(
	() => ({
		x: 0,
		y: 0,
		onStart: () => console.log("the spring has started"),
	}),
	[]
);

useSpring(
	() => ({
		x: 0,
		y: 0,
		onStart: {
			x: () => console.log("x key has started"),
			y: () => console.log("y key has started"),
		},
	}),
	[]
);
  • onStart 在动画开始时调用
  • onChange 在每一帧上调用
  • onRest 在动画停止时调用
  • onPause 在动画暂停时调用
  • onResume 在恢复动画时调用
  • onResolve 在解析加载时调用
  • onProps 在动画被新道具更新后调用,即使动画没有执行也是如此
ts
// onStart onChange onRest onPause onResume onResolve 类型一致
type onStart = (
	result: AnimationResult,
	spring: Controller | SpringValue,
	item?: Item
) => void;
// onStart onChange onRest onPause onResume onResolve 类型一致
type onStart = (
	result: AnimationResult,
	spring: Controller | SpringValue,
	item?: Item
) => void;
ts
type OnProps = (
  props: {[key: string]: any}
  spring: SpringValue,
) => void
type OnProps = (
  props: {[key: string]: any}
  spring: SpringValue,
) => void
  • onDestroyed 在卸载过渡项后调用
ts
type OnDestroyed = (
  item: Item
  key: string | number
) => void
type OnDestroyed = (
  item: Item
  key: string | number
) => void

仅当 useTransition 在钩子中使用相关事件时,Item 参数才存在。

Interpolation 插值

最常见的用途是将 SpringValue 的值转换为另一个值。通过使用to 方法完成

tsx
import { useSpring, animated, to } from "@react-spring/web";

function MyComponent() {
	const props = useSpring({
		from: { x: 0 },
		to: { x: 360 },
	});

	return (
		<animated.div
			style={{ transform: props.x.to((value) => `rotateZ(${value}deg)`) }}
		>
			Hello World
		</animated.div>
	);
}

function MyComponent() {
	const props = useSpring({
		from: { x: 0 },
		to: { x: 360 },
	});

	return (
		<animated.div
			style={{ transform: to(props.x, (value) => `rotateZ(${value}deg)`) }}
		>
			Hello World
		</animated.div>
	);
}
import { useSpring, animated, to } from "@react-spring/web";

function MyComponent() {
	const props = useSpring({
		from: { x: 0 },
		to: { x: 360 },
	});

	return (
		<animated.div
			style={{ transform: props.x.to((value) => `rotateZ(${value}deg)`) }}
		>
			Hello World
		</animated.div>
	);
}

function MyComponent() {
	const props = useSpring({
		from: { x: 0 },
		to: { x: 360 },
	});

	return (
		<animated.div
			style={{ transform: to(props.x, (value) => `rotateZ(${value}deg)`) }}
		>
			Hello World
		</animated.div>
	);
}

更高级用途是组合多个 SpringValue , 需要使用 to 函数来创建

tsx
import { animated, to, useSpring } from "@react-spring/web";
export default function MyComponent() {
	const props = useSpring({
		from: { x: 0, y: 0, z: 0 },
		to: { x: 1, y: 1, z: 1 },
	});
	return (
		<animated.div
			style={{
				transform: to(
					[props.x, props.y, props.z],
					(x, y, z) => `rotate3d(${x}, ${y}, ${z}, 45deg)`
				),
			}}
			œ
		>
			Hello World
		</animated.div>
	);
}
import { animated, to, useSpring } from "@react-spring/web";
export default function MyComponent() {
	const props = useSpring({
		from: { x: 0, y: 0, z: 0 },
		to: { x: 1, y: 1, z: 1 },
	});
	return (
		<animated.div
			style={{
				transform: to(
					[props.x, props.y, props.z],
					(x, y, z) => `rotate3d(${x}, ${y}, ${z}, 45deg)`
				),
			}}
			œ
		>
			Hello World
		</animated.div>
	);
}

Async Animations 异步动画

  • 数组语法是异步动画完全控制,不需要为每次更新修改键值,每个更新将在上一个更新完成后触发。
  • 需要控制动画时,通过 to 函数编写。该函数接收两个参数,一个是接受更新对象的函数 next ,第二个停止动画  cancel
tsx
import { useSpring, animated } from "@react-spring/web";

export default function MyComponent() {
	const springs = useSpring({
		from: { background: "#ff6d6d", y: -40, x: 0 },
		// to: [
		// 	{ x: 80, background: "#fff59a" },
		// 	{ y: 40, background: "#88DFAB" },
		// 	{ x: 0, background: "#569AFF" },
		// 	{ y: -40, background: "#ff6d6d" },
		// ],
		to: async (next, cancel) => {
			await next({ x: 80, background: "#fff59a" });
			await next({ y: 40, background: "#88DFAB" });
			await next({ x: 0, background: "#569AFF" });
			await next({ y: -40, background: "#ff6d6d" });
		},
		loop: true,
	});

	return (
		<animated.div
			style={{
				width: 40,
				height: 40,
				borderRadius: 4,
				...springs,
			}}
		/>
	);
}
import { useSpring, animated } from "@react-spring/web";

export default function MyComponent() {
	const springs = useSpring({
		from: { background: "#ff6d6d", y: -40, x: 0 },
		// to: [
		// 	{ x: 80, background: "#fff59a" },
		// 	{ y: 40, background: "#88DFAB" },
		// 	{ x: 0, background: "#569AFF" },
		// 	{ y: -40, background: "#ff6d6d" },
		// ],
		to: async (next, cancel) => {
			await next({ x: 80, background: "#fff59a" });
			await next({ y: 40, background: "#88DFAB" });
			await next({ x: 0, background: "#569AFF" });
			await next({ y: -40, background: "#ff6d6d" });
		},
		loop: true,
	});

	return (
		<animated.div
			style={{
				width: 40,
				height: 40,
				borderRadius: 4,
				...springs,
			}}
		/>
	);
}