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

一、实现目标

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

相关推荐
顾平安1 小时前
Promise/A+ 规范 - 中文版本
前端
聚名网1 小时前
域名和服务器是什么?域名和服务器是什么关系?
服务器·前端
桃园码工1 小时前
4-Gin HTML 模板渲染 --[Gin 框架入门精讲与实战案例]
前端·html·gin·模板渲染
沈剑心1 小时前
如何在鸿蒙系统上实现「沉浸式」页面?
前端·harmonyos
一棵开花的树,枝芽无限靠近你1 小时前
【PPTist】组件结构设计、主题切换
前端·笔记·学习·编辑器
m0_748237052 小时前
Chrome 关闭自动添加https
前端·chrome
prall2 小时前
实战小技巧:下划线转驼峰篇
前端·typescript
开心工作室_kaic2 小时前
springboot476基于vue篮球联盟管理系统(论文+源码)_kaic
前端·javascript·vue.js
川石教育2 小时前
Vue前端开发-缓存优化
前端·javascript·vue.js·缓存·前端框架·vue·数据缓存
搏博2 小时前
使用Vue创建前后端分离项目的过程(前端部分)
前端·javascript·vue.js