IdbI

第29章—react-spring实现滑入滑出的转场动画

有的时候,直接展示一个组件会过于突兀,需要一些过渡效果。

比如这样:

点击结算的时候,展示结算页面的组件,点击返回的时候隐藏。

这里是通过滑入滑出的动画来实现的过渡。

这个效果是我公司的项目里真实在用的,这节我们来一起实现下。

我们说的转场动画、过渡动画是一个东西,你看下英文翻译就知道了:

这里的过渡动画很明显可以用我们学过的 react-transition-group 或者 react-spring 来做。

我们就用 reac-spring 结合上节学的 styled-components 来实现下。

创建个 vite 项目:

npx create-vite slide-in-out-transition

安装用到的包:

npm install

npm install --save @react-spring/web

npm install --save styled-components

要实现这样的效果:

首先,我们需要一个 div 包裹它。

创建 src/Overlay.tsx

import styled from "styled-components";

const Overlay = styled.div`
  position: fixed;
  top: 0;
  right: 0;
  bottom: 0;
  left: 0;
  background-color: rgba(0, 0, 0, 0.3);
  z-index: 10;
`;

export default Overlay;

然后在 src/SlideInOverlay.tsx 里用一下:

import React, { FC, PropsWithChildren } from "react";
import { useTransition, animated } from "@react-spring/web";
import Overlay from "./Overlay";

const DURATION = 300;

interface SlideInOverlayProps extends PropsWithChildren {
  isVisible: boolean;
  from?: "right" | "bottom";
}

const SlideInOverlay: FC<SlideInOverlayProps> = (props) => {
  const { isVisible, from = "right", children } = props;

  const x = React.useMemo(
    () => (from === "right" ? window.screen.width : window.screen.height),
    [from]
  );

  const transitions = useTransition(isVisible, {
    x,
    opacity: 1,
    from: {
      x,
      opacity: 1,
    },
    enter: { x: 0, opacity: 1 },
    leave: { x, opacity: 0 },
    config: { duration: DURATION },
  });

  const translate = React.useCallback(
    (x: number) => {
      switch (from) {
        case "right":
          return `translateX(${x}px)`;
        case "bottom":
          return `translateY(${x}px)`;
      }
    },
    [from]
  );

  return (
    <>
      {transitions(
        (props, isVisible) =>
          isVisible && (
            <Overlay
              as={animated.div}
              style={{
                transform: props.x.to((x) => (x === 0 ? "none" : translate(x))),
                opacity: props.opacity,
              }}
            >
              {children}
            </Overlay>
          )
      )}
    </>
  );
};

export { SlideInOverlay, DURATION };

代码比较多,我们一部分一部分来看下:

首先,这个 SlideInOverlay 组件有 3 个 props:

isVisible 是是否展示。

from 是从右向左还是从下向上来运动,取值为 right 或 bottom。

children 传入具体的内容。

用 react-spring 的 useTransition 来做动画,改变 x、opcity 属性。

设置初始值、from 的值、enter 的值,以及 leave 的值。

也就是进入动画开始、进入动画结束、离开动画结束的值。

然后下面的 div 使用 react-spring 传入的 x、opcity 来设置样式就好了。

Overlay 是样式组件,用 as 转为用 animated.div 渲染。

初始值 x 根据 from 参数是 right 还是 bottom 来设置 window.screen.width 或者 height。

这里用 useMemo 的好处是只要 from 参数没变,就直接用之前的值。

然后 react-spring 传入的 x 还需要根据 from 来转为 translateX 或者 translateY 的样式。

这样,转场动画就完成了。

我们来试一下:

去掉 main.tsx 的 StrictMode 和 index.css。

在 App.tsx 里用一下:

import { useState } from "react";
import reactLogo from "./assets/react.svg";
import viteLogo from "/vite.svg";
import "./App.css";
import { SlideInOverlay } from "./SlideInOverlay";

function App() {
  const [count, setCount] = useState(0);
  const [show, setShow] = useState(false);

  return (
    <>
      <button
        onClick={() => {
          setShow(true);
        }}
      >
        开启
      </button>
      <SlideInOverlay
        isVisible={show}
        from="right"
        className={"guangguang"}
        style={{
          border: "2px solid #000",
        }}
      >
        <div>
          <button
            onClick={() => {
              setShow(false);
            }}
          >
            关闭
          </button>
          <a href="https://vitejs.dev" target="_blank">
            <img src={viteLogo} className="logo" alt="Vite logo" />
          </a>
          <a href="https://react.dev" target="_blank">
            <img src={reactLogo} className="logo react" alt="React logo" />
          </a>
        </div>
        <h1>Vite + React</h1>
        <div className="card">
          <button onClick={() => setCount((count) => count + 1)}>
            count is {count}
          </button>
          <p>
            Edit <code>src/App.tsx</code> and save to test HMR
          </p>
        </div>
        <p className="read-the-docs">
          Click on the Vite and React logos to learn more
        </p>
      </SlideInOverlay>
    </>
  );
}

export default App;

我们加了一个 state 来保存显示隐藏状态,加了两个 button,点击的时候切换。

跑一下:

npm run dev

可以看到,滑入滑出的转场动画(或者叫过渡动画)生效了。

而且因为只是改变了 translate,组件不会销毁,所以状态也可以保留。

再来试下另一种效果,把 from 改为 bottom:

完美!

用在真实项目里就是这样的:

然后我们再完善一下细节:

加上 className 和 style 两个 props。

interface SlideInOverlayProps extends PropsWithChildren {
  isVisible: boolean;
  from?: "right" | "bottom";
  className?: string | string[];
  style?: CSSProperties;
}

传入样式组件:

安装用到的 classnames 包

npm install --save classnames

测试下:

d-14.png

可以看到 style 生效了,className 也加上了。

我们继续完善,添加 onEnter 参数:

然后加一下处理逻辑:

useEffect(() => {
  let timer = null;

  if (isVisible === true && onEnter != null) {
    timer = setTimeout(onEnter, DURATION);
  }

  return () => {
    if (timer != null) {
      clearTimeout(timer);
    }
  };
}, [isVisible, onEnter]);

因为我们设置了动画的时长是 DURATION 常量,所以这里用一个 setTimeout 就可以实现 onEnter

判断下 isVisible 是 true 的时候再执行 onEnter 的定时器。

并且当 isVisible、onEnter 变化的时候,销毁上次的定时器,重新跑。

测试下:

没啥问题。

接下来继续实现 onExit。

添加参数,然后加上 useEffect 通过 setTimeout 触发:

useEffect(() => {
  let timer = null;

  if (isVisible === false && onExit != null) {
    timer = setTimeout(onExit, DURATION);
  }

  return () => {
    if (timer != null) {
      clearTimeout(timer);
    }
  };
}, [isVisible, onExit]);

跑一下:

可以看到,滑入滑出时的回调没问题,但是最开始多回调了一次 onExit。

如何判断出最开始那一次呢?

记录下 isVisible 参数就可以了,如果是从 true 变为 false 才触发。

用 useRef 保存上次的 isVisible 参数的值,如果上次的是 true 而当前 isVisible 是 false 就触发。

const visibleRef = useRef(isVisible);

useEffect(() => {
    let timer = null;

    if (isVisible === false && visibleRef.current === true && onExit != null) {
      timer = setTimeout(onExit, DURATION);
    }

    visibleRef.current = isVisible;

    return () => {
      if (timer != null) {
        clearTimeout(timer);
      }
    };
}, [isVisible, onExit]);

测试下:

可以看到,现在最开始多的一次调用就没有了。

这样,这个 SlideInOverlay 组件就完成了。

当然,你还可以做更多的扩展,比如点击商品的时候从下面滑入商品详情:

这里是距离顶部有一段距离的,这个距离也可以作为参数传入。

如果想实现这种和手势结合的动画呢?

这个我们做过了呀,可以回去看看手势库那节。

拖动速度、方向、距离这类需求都可以用手势库搞定。

案例代码上传了小册仓库

总结

很多场景下,加上转场动画会使交互体验更好。

这节我们用 react-spring 实现了滑入滑出的转场动画(或者叫过渡动画)。

支持了 isVisible、from、children、onExit、onEnter、className、style 参数。

from 可以设置 right 或 bottom,然后根据它来设置 x 参数初始值为 window.screen.width 或者 window.screen.height。

改变 x、opacity 就可以实现滑入滑出的动画。

我们通过 useRef 记录之前的参数来实现了 onExit 的回调。

用 styled-components 写了外层 div 的样式。

这样的 SlideInOverlay 组件就比较完善了,可以直接用在项目里。

返回上级