• Babel 插件通关秘籍
  • Git 原理详解及实用指南
  • Nest 通关秘籍
  • React 通关秘籍
  • TypeScript 全面进阶指南
  • TypeScript 类型体操通关秘籍
  • 现代CSS
  • Babel 插件通关秘籍
  • Git 原理详解及实用指南
  • Nest 通关秘籍
  • React 通关秘籍
  • TypeScript 全面进阶指南
  • TypeScript 类型体操通关秘籍
  • 现代CSS
  • React 通关秘籍

    • 1.关于本小册
    • 2.一网打尽组件常用Hook
    • 3.Hook的闭包陷阱的成因和解决方案
    • 4.React组件如何写TypeScript类型
    • 5.React组件如何调试
    • 6.受控模式VS非受控模式
    • 7.组件实战:迷你Calendar
    • 8.组件实战:Calendar日历组件(上)
    • 9.组件实战:Calendar日历组件(下)
    • 10.快速掌握Storybook
    • 11.React组件如何写单测
    • 12.深入理解Suspense和ErrorBoundary
    • 13.组件实战:Icon图标组件
    • 14.组件实战:Space间距组件
    • 15.React.Children和它的两种替代方案
    • 16.三个简单组件的封装
    • 17.浏览器的5种Observer
    • 18.组件实战:Watermark防删除水印组件
    • 19.手写react-lazyload
    • 20.图解网页的各种距离
    • 21.自定义hook练习
    • 22.自定义hook练习(二)
    • 23.用react-spring做弹簧动画
    • 24.react-spring结合use-gesture手势库实现交互动画
    • 25.用react-transition-group和react-spring做过渡动画
    • 26.快速掌握Tailwind:最流行的原子化CSS框架
    • 27.用CSSModules避免样式冲突
    • 28.CSSInJS:快速掌握styled-components
    • 29.react-spring实现滑入滑出的转场动画
    • 30.组件实战:Message全局提示组件
    • 31.组件实战:Popover气泡卡片组件
    • 32.项目里如何快速定位组件源码
    • 33.一次超爽的React调试体验
    • 34.组件实战:ColorPicker颜色选择器(一)
    • 35.组件实战:ColorPicker颜色选择器(二)
    • 36.组件实战:onBoarding漫游式引导组件
    • 37.组件实战:Upload拖拽上传
    • 38.组件实战:Form表单组件
    • 39.React组件库都是怎么构建的
    • 40.组件库实战:构建esm和cjs产物,发布到npm
    • 41.组件库实战:构建umd产物,通过unpkg访问
    • 42.数据不可变:immutable和immer
    • 43.基于ReactRouter实现keepalive
    • 44.Historyapi和ReactRouter实现原理
    • 45.ReactContext的实现原理和在antd里的应用
    • 46.ReactContext的性能缺点和解决方案
    • 47.手写一个Zustand
    • 48.原子化状态管理库Jotai
    • 49.用react-intl实现国际化
    • 50.国际化资源包如何通过Excel和GoogleSheet分享给产品经理
    • 51.基于react-dnd实现拖拽排序
    • 52.react-dnd实战:拖拽版TodoList
    • 53.ReactPlayground项目实战:需求分析、实现原理
    • 54.ReactPlayground项目实战:布局、代码编辑器
    • 55.ReactPlayground项目实战:多文件切换
    • 56.ReactPlayground项目实战:babel编译、iframe预览
    • 57.ReactPlayground项目实战:文件增删改
    • 58.ReactPlayground项目实战:错误显示、主题切换
    • 59.ReactPlayground项目实战:链接分享、代码下载
    • 60.ReactPlayground项目实战:WebWorker性能优化
    • 61.ReactPlayground项目实战:总结
    • 62.手写MiniReact:思路分析
    • 63.手写MiniReact:代码实现
    • 64.手写MiniReact:和真实React源码的对比
    • 65.React18的并发机制是怎么实现的
    • 66.Ref的实现原理
    • 67.低代码编辑器:核心数据结构、全局store
    • 68.低代码编辑器:拖拽组件到画布、拖拽编辑json
    • 69.低代码编辑器:画布区hover展示高亮框
    • 70.低代码编辑器:画布区click展示编辑框
    • 71.低代码编辑器:组件属性、样式编辑
    • 72.低代码编辑器:预览、大纲
    • 73.低代码编辑器:事件绑定
    • 74.低代码编辑器:动作弹窗
    • 75.低代码编辑器:自定义JS
    • 76.低代码编辑器:组件联动
    • 77.低代码编辑器:拖拽优化、Table组件
    • 78.低代码编辑器:Form组件、store持久化
    • 79.低代码编辑器:项目总结
    • 80.快速掌握ReactFlow画流程图
    • 81.ReactFlow振荡器调音:项目介绍
    • 82.ReactFlow振荡器调音:流程图绘制
    • 83.ReactFlow振荡器调音:合成声音
    • 84.AudioContext实现在线钢琴
    • 85.React服务端渲染:从SSR到hydrate
    • 86.小册总结

上节我们学了用 react-spring 做动画。

其实也简单,就是指定某些属性的开始值、结束值,动画的时长或者弹簧动画的一些参数,react-spring 就会实现这些属性的动画。

再就是多个元素可以设置并行、依次执行或者间隔一段时间执行的顺序。

但是很多情况下,动画不是直接触发的,而是由 drag、hover、scroll 等事件触发。

这节我们就结合事件来实现一些交互动画。

我们会用到一个手势库 @use-gesture/react

可能很多同学都没用过手势库,其实手势库里就是对 drag、hover、scroll 这些事件的封装:

直接给元素绑定事件不行么,为啥还要加一个手势库呢?

那如果我想知道移动的方向、移动的距离、移动的速率呢?

自己算这些就很麻烦,而用了手势库,这些就都有了。

新建个项目来试下:

npx create-react-app --template=typescript use-gesture-test

我们来实现这样一个案例:

拖拽的时候触发动画,通过 use-gesture 实现拖动,拿到方向、距离等信息,然后用 react-spring 做属性变化的动画。

安装这两个包:

npm install --save @react-spring/web @use-gesture/react

改下 App.tsx

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

import "./App.css";

const pages = [
    "https://images.pexels.com/photos/62689/pexels-photo-62689.jpeg?auto=compress&cs=tinysrgb&dpr=2&h=750&w=1260",
    "https://images.pexels.com/photos/733853/pexels-photo-733853.jpeg?auto=compress&cs=tinysrgb&dpr=2&h=750&w=1260",
    "https://images.pexels.com/photos/4016596/pexels-photo-4016596.jpeg?auto=compress&cs=tinysrgb&dpr=2&h=750&w=1260",
    "https://images.pexels.com/photos/351265/pexels-photo-351265.jpeg?auto=compress&cs=tinysrgb&dpr=2&h=750&w=1260",
    "https://images.pexels.com/photos/924675/pexels-photo-924675.jpeg?auto=compress&cs=tinysrgb&dpr=2&h=750&w=1260",
];

function Viewpager() {
    const width = window.innerWidth;

    const [props, api] = useSprings(pages.length, (i) => ({
        x: i * width,
        scale: 1,
    }));

    return (
        <div className="wrapper">
            {props.map(({ x, scale }, i) => (
                <animated.div className="page" key={i} style={{ x }}>
                    <animated.div
                        style={{ scale, backgroundImage: `url(${pages[i]})` }}
                    />
                </animated.div>
            ))}
        </div>
    );
}

export default Viewpager;

这里是多个元素并行的动画,用 useSprings。

然后改变 x 和 scale 属性。

x 的初始值是 width * i,也就是依次平铺。

所有接收动画属性的地方都要用 <animated.div> ,这里用到 x 和 scale 属性的两个 div 换成 <animated.div>

然后是 App.css:

html,
body,
#root {
    height: 100%;
    width: 100%;
}

.wrapper {
    position: relative;
    width: 100%;
    height: 100%;
    overflow: hidden;
}

.page {
    position: absolute;
    width: 100%;
    height: 100%;
    touch-action: none;
}

.page > div {
    touch-action: none;
    background-size: cover;
    background-repeat: no-repeat;
    background-position: center center;
    width: 100%;
    height: 100%;
    box-shadow: 0 0 50px #000;
}

这里图片要充满屏幕,从 html、body、#root 到 .wrapper、.page 都要设置宽高 100%:

touch-action 设置为 none 是禁止移动端的默认 touch 处理。

不然默认会导致页面的缩放和滑动:

可以看到,渲染的结果是对的:

这里我们设置的 x,但是 react-spring 用 translate3d 来实现的,这是它内部做的性能优化。

接下来用 use-gesture 来加上手势的处理:

import { useRef } from "react";
import { useSprings, animated } from "@react-spring/web";
import { useDrag } from "@use-gesture/react";

import "./App.css";

const pages = [
    "https://images.pexels.com/photos/62689/pexels-photo-62689.jpeg?auto=compress&cs=tinysrgb&dpr=2&h=750&w=1260",
    "https://images.pexels.com/photos/733853/pexels-photo-733853.jpeg?auto=compress&cs=tinysrgb&dpr=2&h=750&w=1260",
    "https://images.pexels.com/photos/4016596/pexels-photo-4016596.jpeg?auto=compress&cs=tinysrgb&dpr=2&h=750&w=1260",
    "https://images.pexels.com/photos/351265/pexels-photo-351265.jpeg?auto=compress&cs=tinysrgb&dpr=2&h=750&w=1260",
    "https://images.pexels.com/photos/924675/pexels-photo-924675.jpeg?auto=compress&cs=tinysrgb&dpr=2&h=750&w=1260",
];

function Viewpager() {
    const index = useRef(0);
    const width = window.innerWidth;

    const [props, api] = useSprings(pages.length, (i) => ({
        x: i * width,
        scale: 1,
    }));

    const bind = useDrag(
        ({ active, movement: [mx], direction: [xDir], cancel }) => {}
    );

    return (
        <div className="wrapper">
            {props.map(({ x, scale }, i) => (
                <animated.div
                    className="page"
                    {...bind()}
                    key={i}
                    style={{ x }}>
                    <animated.div
                        style={{ scale, backgroundImage: `url(${pages[i]})` }}
                    />
                </animated.div>
            ))}
        </div>
    );
}

export default Viewpager;

用 useRef 保存当前的 index,初始值是 0。

用 use-gesture 也很简单,绑定啥事件就用 useXxx,比如 useDrag、useHover、useScroll 等。

或者用 useGesture 同时绑定多种事件:

手势库最大的好处是可以拿到移动的方向、速率、距离等信息。

这里我们拿到的这几个参数:

movement 是拖动距离 [x, y]

direction 是拖动方向 [x, y],1 代表向左(向上)、-1 代表向右(向下)。

active 是当前是否在拖动。

cancel 方法可以中止事件。

拖动时的处理如下:

const bind = useDrag(
    ({ active, movement: [mx], direction: [xDir], cancel }) => {
        if (active && Math.abs(mx) > width / 2) {
            let newIndex = index.current + (xDir > 0 ? -1 : 1);

            if (newIndex < 0) {
                newIndex = 0;
            }

            if (newIndex > pages.length - 1) {
                newIndex = pages.length - 1;
            }

            index.current = newIndex;

            cancel();
        }
        api.start((i) => {
            const x = (i - index.current) * width + (active ? mx : 0);
            const scale = active ? 1 - Math.abs(mx) / width : 1;
            return { x, scale };
        });
    }
);

当正在拖动并且拖动的距离超过了宽度的一半,就改变 index。

改变 index 之后调用 cancel,就不再处理后续 drag 事件了。

index 根据移动的方向来计算,xDir 大于 0,就是向左,index 减一,反之加一

然后根据拖动距离来计算每个元素的 x 和 scale:

x 根据和当前 index 的差值 * width 计算,然后加上拖动的距离。

比如当前 index 为 1,那 index 为 2 的 x 就是 (2 - 1) _ width,而 index 为 0 的就是 (0 - 1) _ width

而 scale 则是用拖动的距离除以 width 算一个比值,然后用 1 减去它,因为刚开始拖动的时候 scale 大。

但是现在 scale 的变化范围有点大。

可以调整下:

计算出来的比值除以 2 或者除以 3 就好了

这样我们就完成了 use-gesture 手势库和 react-spring 动画库的结合使用的案例。

用 use-gesture 手势库处理拖拽等事件,拿到移动距离、方向、速率等信息,然后再根据这些信息用 react-spring 做动画。

use-gesture 文档里还有个案例也很有意思:

如果拖动速度慢了,牌会回到原位置,只有快速拖动,牌才会移到一边。

它的实现就是用了 velocity 速率,也就是每 ms 移动的距离,如果大于 0.2 就算移到一边,设置对应的 x,否则就设置 0:

具体流程都差不多,也是 use-gesture 和 react-spring 的结合使用,感兴趣可以看看。

案例代码上传了小册仓库

总结

这节我们学了手势库 use-gesture。

手势库也是处理 drag、scroll、hover 等事件,但是它封装了额外的信息,比如移动方向、距离、速率等。

用这些信息结合 react-spring 就可以实现各种交互动画。

上次更新: 6/21/25, 9:42 AM
贡献者: YNight
Prev
23.用react-spring做弹簧动画
Next
25.用react-transition-group和react-spring做过渡动画