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

一、实现目标

为元素出现在视口时添加动画,这是很常见的需求,最近在工作中遇到了,项目里面使用的是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将样式传递进去。

相关推荐
JustHappy35 分钟前
古法编程秘籍(二):什么是代码模块化?别背概念,把房间收拾明白就够了
前端·后端
小江的记录本42 分钟前
【JVM虚拟机】堆内存分代模型:年轻代(Eden+Survivor)、老年代、元空间Metaspace(附《思维导图》+《面试高频考点清单》)
java·前端·jvm·后端·python·spring·面试
weixin_471383031 小时前
图片预解码缓存
前端·浏览器缓存·图片预解码
一起学开源1 小时前
一文读懂 ReAct 范式:让 AI Agent 真正学会“思考+行动“
java·javascript·react.js·ecmascript·react·alibaba·智能体开发
郑洁文3 小时前
基于网络爬虫的Web敏感信息泄露自动化检测工具
前端·爬虫·网络安全·自动化
郑洁文3 小时前
可视化Web渗透分析工具的设计与实现
前端
罗超驿4 小时前
18.Web API 实战:元素与表单属性的获取和修改
开发语言·前端·javascript
边界条件╝4 小时前
微前端进阶(四)
前端·状态模式
无风听海4 小时前
JSON Web Token(JWT)完全指南
java·前端·json
IT_陈寒4 小时前
Python闭包里藏的这个坑,差点让我加班到凌晨
前端·人工智能·后端