一块儿逐步实现「水波纹效果」- 基于 React

介绍

  1. 阅读源码是学习的最好方式,material-ui 是知名的 react ui 库,目前在 github 上有 55.8k star,很是值得咱们去学习
  2. 本系列文章会借鉴 material-ui 源代码,一块儿实现属于咱们本身的 ui 组件 (文章最后会给出代码地址)
  3. 本篇是该系列的第一篇「水波纹效果 ripple」,后续会持续更新

我的能力有限,若有错误请你们多多点评react

实现思路

实现效果

  1. 鼠标按下时,开启水波纹 start 动画,若是此时鼠标不抬起,继续保持 start 动画结尾样式
  2. 鼠标抬起后,开启水波纹 end 动画
  3. 鼠标屡次点击,出现多个水波纹效果

思路

很是简单,咱们经过两个组件达到效果git

  1. TouchRipple 组件处理动画逻辑「水波纹位置、处理卸载 Ripple
  2. Ripple 组件用于展现单个 水波纹
function TouchRipple() {
  return <div> <Ripple /> <Ripple /> ..... </div>
}
复制代码
  1. 任何一个元素想要拥有水波纹效果,只须要引入 TouchRipple
// 伪代码
const ref = useRef()
<div onMouseDown={ref.start} onMouseUp={ref.exit}>
  <TouchRipple ref={ref} /> </div>
复制代码

代码实现

material-uiTouchRipple 组件用来附加水波纹效果,material TouchRipple 源码地址github

Ripple 组件

export function Ripple(props) {
  const {
    rippleX,
    rippleY,
    rippleSize,
    in: inProp,
    onExited = () => {},
    timeout
  } = props;

  const [leaving, setLeaving] = React.useState(false);

  const rippleClassName = clsx("ripple", "rippleVisible");

  const rippleStyles = {
    width: rippleSize,
    height: rippleSize,
    top: -(rippleSize / 2) + rippleY,
    left: -(rippleSize / 2) + rippleX
  };

  const childClassName = clsx("child", {
    // 根据 leaving,执行结束动画
    childLeaving: leaving
  });

  React.useEffect(() => {
    if (!inProp) {
      // 执行 exit 动画
      setLeaving(true);

      // Unmount
      const timeoutId = setTimeout(() => {
        onExited();
      }, timeout);

      return () => {
        clearTimeout(timeoutId);
      };
    }
    return undefined;
  }, [onExited, inProp, timeout]);

  return (
    <span className={rippleClassName} style={rippleStyles}> <span className={childClassName} /> </span> ); } 复制代码

TouchRipple 组件

  1. 记录当前有几个 ripple
import { TransitionGroup } from "react-transition-group";

function TouchRipple() {
  // 记录 ripples
  const [ripples, setRipples] = React.useState([]);
  // 获取 container ref
  const container = React.useRef(null);
  
  return (
    <span ref={container} className={"touchRippleRoot"}> <TransitionGroup component={null} exit> {ripples} </TransitionGroup> </span>
  )
}
复制代码
  1. 添加 ripple
const startCommit = React.useCallback(
  params => {
    const { rippleX, rippleY, rippleSize } = params;
    // 追加 ripple,开启动画
    setRipples(oldRipples => [
      ...oldRipples,
      <Ripple
        key={nextKey.current}
        classes={classes}
        timeout={DURATION}
        rippleX={rippleX}
        rippleY={rippleY}
        rippleSize={rippleSize}
      />
    ]);
    nextKey.current += 1;
  },
  [classes]
);

const start = React.useCallback(
    (event = {}, options = {}) => {
      // 默认是 centerProp
      const { center = centerProp } = options;
      const element = container.current;
      const rect = element.getBoundingClientRect()

      let rippleX;
      let rippleY;
      let rippleSize;

      // 设置 rippleX rippleY
      if (
        center
      ) {
        rippleX = Math.round(rect.width / 2);
        rippleY = Math.round(rect.height / 2);
      } else {
        const clientX = event.clientX
        const clientY = event.clientY
        rippleX = Math.round(clientX - rect.left);
        rippleY = Math.round(clientY - rect.top);
      }

      // 设置 rippleSize
      if (center) {
        rippleSize = Math.sqrt((2 * rect.width ** 2 + rect.height ** 2) / 3);
      } else {
        const sizeX =
          Math.max(Math.abs((element ? element.clientWidth : 0) - rippleX), rippleX) * 2 + 2;
        const sizeY =
          Math.max(Math.abs((element ? element.clientHeight : 0) - rippleY), rippleY) * 2 + 2;
        rippleSize = Math.sqrt(sizeX ** 2 + sizeY ** 2);
      }

      startCommit({ rippleX, rippleY, rippleSize });
    },
    [startCommit, centerProp]
  );
复制代码
  1. 移除 ripple
// 这里处理很简单,直接移除第一个 ripple
const stop = React.useCallback(() => {
  setRipples(oldRipples => {
    if (oldRipples.length > 0) {
      return oldRipples.slice(1);
    }
    return oldRipples;
  });
}, []);
复制代码
  1. 向外暴露接口
// 将 start stop 暴露出去
React.useImperativeHandle(
  ref,
  () => ({
    start,
    stop
  }),
  [start, stop]
);
复制代码

使用

const rippleRef = React.useRef();

<div className="test" onMouseDown={e => { rippleRef.current.start(e); }} onMouseUp={() => { rippleRef.current.stop(); }} > 注册 <TouchRipple ref={rippleRef} /> </div> 复制代码

结束

最后献上完整代码,供你们在线编辑,方便测试学习

在线编辑代码测试

相关文章
相关标签/搜索