【组件封装】为元素出现在视口添加动画

一、实现目标

为元素出现在视口时添加动画,这是很常见的需求,最近在工作中遇到了,项目里面使用的是React框架。因此自己封装了一个组件。如果是Vue框架,可以封装一个自定义指令来实现,更方便一点。

二、实现思路

使用IntersectionObserver API 监听元素是否出现在视口,使用animate API为元素添加动画

三、实现过程

TypeScript 复制代码
import classNames from 'classnames';
import { CSSProperties, useEffect, useRef } from 'react';

interface IntersectionWithAnimationBoxProp {
  /**
   * 子节点
   *
   * @type {JSX.Element}
   * @memberOf AnimationBoxProp
   */
  children: JSX.Element;
  /**
   * 动画效果
   *
   * @type {{ duration: number; fill: string }}
   * @memberOf AnimationBoxProp
   */
  optionalEffectTiming?: OptionalEffectTiming;
  /**
   * 关键帧数组
   *
   * @type {Keyframe[]}
   * @memberOf AnimationBoxProp
   */
  keyframes?: Keyframe[];
  /**
   * 类名
   *
   * @type {string}
   * @memberOf AnimationBoxProp
   */
  className?: string;
  /**
   * 样式
   *
   * @type {CSSProperties}
   * @memberOf AnimationBoxProp
   */
  style?: CSSProperties;
  /**
   * 默认关键帧, 可选
   *
   * @type {('left' | 'right' | 'top' | 'bottom')}
   * @memberOf AnimationBoxProp
   */
  defaultKeyFramePosition?: 'left' | 'right' | 'top' | 'bottom';
}

/**
 * 给元素添加效果(元素出现在视口, 执行动画)的容器, 函数re-render阶段不会重复执行动画, 只在挂载阶段执行一次
 */
export default function IntersectionWithAnimationBox(
  prop: IntersectionWithAnimationBoxProp
) {
  const {
    children,
    optionalEffectTiming = { duration: 300, fill: 'forwards' },
    keyframes,
    className,
    style,
    defaultKeyFramePosition = 'bottom',
  } = prop;
  const container = useRef<HTMLDivElement>(null);

  // 获取关键帧数组
  const getKeyFrames = (): Keyframe[] => {
    if (keyframes && !!keyframes.length) return keyframes;

    switch (defaultKeyFramePosition) {
      case 'bottom':
        return [
          {
            opacity: 0,
            transform: 'translateY(100%)',
          },
          {
            opacity: 1,
            transform: 'translateY(0)',
          },
        ];
      case 'top':
        return [
          {
            opacity: 0,
            transform: 'translateY(-100%)',
          },
          {
            opacity: 1,
          },
        ];
      case 'left':
        return [
          {
            opacity: 0,
            transform: 'translateX(-100%)',
          },
          {
            opacity: 1,
            transform: 'translateX(0)',
          },
        ];
      case 'right':
        return [
          {
            opacity: 0,
            transform: 'translateX(100%)',
          },
          {
            opacity: 1,
            transform: 'translateX(0)',
          },
        ];
    }
  };

  const addOpacity = () => {
    container.current && container.current.classList.add('opacity-100');
  };

  // 执行动画
  const animate = (element: Element) => {
    if (element instanceof HTMLElement) {
      const animation = element.animate(getKeyFrames(), optionalEffectTiming);

      animation.finished.then(() => {
        addOpacity();
      });
    }
  };

  // 通过IntersectionObserver API 监听元素是否出现在视口
  useEffect(() => {
    if (!container.current) return;

    const io = new IntersectionObserver((entries) => {
      entries.forEach(async (item) => {
        if (item.isIntersecting) {
          io.unobserve(item.target);
          animate(item.target);
        }
      });
    });
    io.observe(container.current);

    return () => {
      container.current && io.unobserve(container.current);
    };
  }, []);

  return (
    <div ref={container} className={classNames(['opacity-0', className])} style={style}>
      {children}
    </div>
  );
}

四、如何使用

1. 单个元素场景

使用封装好的组件直接包裹就可以

TypeScript 复制代码
  <IntersectionWithAnimationBox optionalEffectTiming={{ duration: 2000 }}>
     <div className="h-40 w-40 rounded-lg bg-theme-primary"></div>
  </IntersectionWithAnimationBox>

2. 多个元素场景

一般来说,多个元素场景需要有一个动画时间的递进关系,这个只需要改变delay就可以,可以利用index动态改变deley时间

TypeScript 复制代码
      <div className="flex gap-x-5">
        {DATA.map((data, index) => (
          <IntersectionWithAnimationBox
            optionalEffectTiming={{
              duration: 2000,
              delay: index * 150 + 150,
              easing: 'cubic-bezier(0.250, 0.460, 0.450, 0.940)',
            }}
            className="h-40"
            key={data}
          >
            <div className="rounded-lg bg-theme-primary p-6 text-center">{data}</div>
          </IntersectionWithAnimationBox>
        ))}
      </div>

五、总结

优点

封装为slot的形式,在使用上会方便很多,使用者无需去关心内部实现的逻辑,只需将动画传入或者选中默认的动画效果即可。

缺点

使用该组件包裹后,最终渲染会多了一个div标签。但是一般来说不会影响,如果有class的问题,可以使用prop:className将样式传递进去。

相关推荐
10年前端老司机2 分钟前
React 受控组件和非受控组件区别和使用场景
前端·javascript·react.js
夏晚星2 分钟前
vue实现微信聊天emoji表情
前端·javascript
停止重构4 分钟前
【方案】前端UI布局的绝技,响应式布局,多端适配
前端·网页布局·响应式布局·grid布局·网页适配多端
極光未晚5 分钟前
TypeScript在前端项目中的那些事儿:不止于类型的守护者
前端·javascript·typescript
ze_juejin6 分钟前
Vue3 + Vite + Ant Design Vue + Axios + Pinia 脚手架搭建
前端·vue.js
lichenyang4537 分钟前
React项目(移动app)
前端
用户61848240219519 分钟前
Vue-library-start,一个基于Vite的vue组件库开发模板
前端
美团技术团队20 分钟前
报名 | 美团技术沙龙第86期:多业务场景下,美团如何做性能优化
前端
安替-AnTi1 小时前
基于 React 和 TypeScript 搭建的机器学米其林餐厅数据分析项目
react.js·typescript·数据分析·毕设·米其林
Rrvive1 小时前
localhost 和 127.0.0.1 的核心区别
前端