究级animation实现:无限滚动,暂停,倒转,拖拽!

效果

实现功能:

无限滚动

动态插入document 动画

动画自定义暂停,倒转

拖拽控制动画

前言

在项目中经常会遇到需要无限滚动的动画录播效果,但是又和传统意义的轮播图有些区别,他不是一个tab页tab页的切换,而是一个一直连续滚动的效果,甚至产品希望提出滚动效果可以拖拽控制的情况。

针对这些难题本文章以一个单页面的react html文件为例做相关功能的详细展示。

本文以react为例
freedom-fj-rolling-react-html

react 组建
freedom-fj-rolling-react-component

用此思想实现的vue组建
freedom-fj-rolling-vue-component

1. react 单页面

引入react线上资源和babel,并绘制简单的UI样式

html 复制代码
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>react hook</title>
</head>

<body>
  <div id="root"></div>
  <script src="https://unpkg.com/react@18/umd/react.production.min.js" crossorigin></script> 
  <script src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js" crossorigin></script>
  <script src="https://unpkg.com/babel-standalone@6/babel.min.js"></script>
  <script></script>


  <script type="text/babel">
    window.onload = function () {
      const {
        useState,
        useEffect,
        useRef,
        createElement
      } = React

      const root = document.getElementById('root')
      function Rolling() {
        
        return (
          <div className="rolling-box" >
            <div className="rolling-offset-box">
              {new Array(5).fill(0).map((_, index) => {
                return <div className="rolling-item" key={index}>hello world {index}</div>
              })}
            </div>
          </div>
        )
      }
      ReactDOM.render(createElement(Rolling), root)
    }
  </script>
</body>
<style>
  #root {
    display: flex;
    align-items: center;
    justify-content: center;
    height: 100vh;
  }

  * {
    margin: 0;
    padding: 0;
  }

  .rolling-offset-box {
    width: 100%;
  }

  .rolling-box {
    height: 300px;
    width: 100px;
    overflow: hidden;
    cursor: pointer;
    box-shadow: 0 3px 12px rgba(0, 0, 0, .07), 0 1px 4px rgba(0, 0, 0, .07);
  }

  .rolling-item {
    height: 100px;
    width: 100px;
    color: white;
    text-align: center;
    line-height: 100px;
    margin: 2px 0;
    background-color: #79bbff;
  }
</style>

</html>

ui效果:

2. 动态插入动画

无限滚动效果的核心就是复制一份滚动的内容到下面,然后在动画滚动到刚好第一份滚动完的时候瞬间重置动画,就会有种首尾相连无限滚动的感觉。

  1. 复制一份滚动dom,react可以使用react.children 复制,vue 用双插槽。
  2. 利用 document.styleSheets 获取动画列表
  3. style.deleteRule 删除重复动画
  4. dom.offsetHeight 计算dom偏移长度
  5. style.insertRule 插入动画到document
html 复制代码
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>react hook</title>
</head>

<body>
  <div id="root"></div>
  <script src="https://unpkg.com/react@18/umd/react.production.min.js" crossorigin></script> 
  <script src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js" crossorigin></script>
  <script src="https://unpkg.com/babel-standalone@6/babel.min.js"></script>
  <script></script>


  <script type="text/babel">
    window.onload = function () {
      const {
        useState,
        useEffect,
        useRef,
        createElement
      } = React

      const root = document.getElementById('root')
      function Rolling() {
        // 动画名称
        const animationNameRef = useRef(`rollingsAnnualTasks${Math.floor(Math.random() * 100000)}`) // 动画名称
        // 滚动盒子dom
        const rollingBodyRef = useRef()
        // 滚动时长
        const time = 3
        
        useEffect(() => {
          controlAnimation()
        }, [])
        // 生成动画
        const controlAnimation = () => {
          const dom = rollingBodyRef.current
          const distance = getDistance().currDistance
          if (!dom) return
          const style = clearAnimation()
          if (!style) return
          style.insertRule(`@keyframes ${animationNameRef.current} {0%{ transform: translateX(0%);}100%{transform: translateY(-${distance}px);}}`, 0)
        }
        // 初始化清除动画
        const clearAnimation = () => {
          const style = document.styleSheets[0]
          if (!style) return
          const styleArray = [].slice.call(style.cssRules) // 将伪数组变成数组
          const index = styleArray.findIndex(item => item.name === animationNameRef.current)
          if (index !== -1) style.deleteRule(index) // 如果有此动画就先删除
          return style
        }
        // 获取dom大小
        const getDistance = () => {
            const dom = rollingBodyRef.current
            const currDistance = dom.offsetHeight / 2
            return { currDistance }
        }
        return (
          <div className="rolling-box" >
            <div
              id={animationNameRef.current}
              ref={rollingBodyRef}
              className="rolling-offset-box"
              style={{
                animation: `${animationNameRef.current} ${time}s linear infinite`,
                animationPlayState: isRolling ? 'running' : 'paused',
              }}>
              >
              {new Array(5).fill(0).map((_, index) => {
                return <div className="rolling-item" key={index}>hello world {index}</div>
              })}
              {new Array(5).fill(0).map((_, index) => {
                return <div className="rolling-item" key={index}>hello world {index}</div>
              })}
            </div>
          </div>
        )
      }
      ReactDOM.render(createElement(Rolling), root)
    }
  </script>
</body>
<style>
  #root {
    display: flex;
    align-items: center;
    justify-content: center;
    height: 100vh;
  }

  * {
    margin: 0;
    padding: 0;
  }

  .rolling-offset-box {
    width: 100%;
  }

  .rolling-box {
    height: 300px;
    width: 100px;
    overflow: hidden;
    cursor: pointer;
    box-shadow: 0 3px 12px rgba(0, 0, 0, .07), 0 1px 4px rgba(0, 0, 0, .07);
  }

  .rolling-item {
    height: 100px;
    width: 100px;
    color: white;
    text-align: center;
    line-height: 100px;
    margin: 2px 0;
    background-color: #79bbff;
  }
</style>

</html>

效果:

3.鼠标悬浮暂停

核心:

js 复制代码
dom.style.animationPlayState = '' // 继续动画
dom.style.animationPlayState = 'paused' // 暂停动画

修改dom响应的鼠标事件

jsx 复制代码
function Rolling() {
    const [isRolling, setRolling] = useState(true)
    .... 省略代码
    const controlAnimation = () => {
      const dom = rollingBodyRef.current
      const distance = getDistance().currDistance
      if (!dom) return
      const style = clearAnimation()
      if (!isRolling || !style) return
      dom.style.animationPlayState = '' // 继续动画
      style.insertRule(`@keyframes ${animationNameRef.current} {0%{ transform: translateX(0%);}100%{transform: translateY(-${distance}px);}}`, 0)
    }
    // 动画暂停
    const hoverStart = () => {
      setRolling(false)
    }
    // 动画开始
    const hoverEnd = () => {
      setRolling(true)
    }
    return (
        .... 省略代码
        <div
          id={animationNameRef.current}
          ref={rollingBodyRef}
          className="rolling-offset-box"
          onMouseEnter={hoverStart}
          onMouseLeave={hoverEnd}
          style={{
            animation: `${animationNameRef.current} ${time}s linear infinite`,
            animationPlayState: isRolling ? 'running' : 'paused',
          }}>
          {new Array(5).fill(0).map((_, index) => {
            return <div className="rolling-item" key={index}>hello world {index}</div>
          })}
          {new Array(5).fill(0).map((_, index) => {
            return <div className="rolling-item" key={index}>hello world {index}</div>
          })}
        </div>
        .... 省略代码
    )
}

4.拖拽动画倒转 *

核心:

  1. document.addEventListener 监听鼠标按下后的移动事件
  2. element.getAnimations() 获取动画对象
  3. 根据移动距离和比例修改动画播放currentTime,从而调整播放位置,实现拖拽动画倒转效果。
jsx 复制代码
function Rolling() {
    .... 省略代码
    /**
     * 鼠标按下边
     */
    const onMouseDownBorder = (e) => {
      const element = document.getElementById(animationNameRef.current)
      if (!element) return
      const animation = element.getAnimations()
      const startDis = e.clientY
      const currDistance = getDistance().currDistance
      const speed = currDistance / (time * 1000)
      let rememberDis
      const mouseMoveHander = (e) => {
        const endDis = e.clientY
        const distance = endDis - startDis
        animation.forEach((item) => {
          if (!rememberDis) rememberDis = (item.currentTime) || 0
          const currTime = rememberDis - (distance / speed)
          item.currentTime = currTime < 0 ? time * 1000 + currTime : currTime
        })
      }
      document.addEventListener('mousemove', mouseMoveHander)
      const mouseUpHandler = (e) => {
        document.removeEventListener('mousemove', mouseMoveHander)
        document.removeEventListener('mouseup', mouseUpHandler)
      }
      document.addEventListener('mouseup', mouseUpHandler)
    }
    return (
      <div className="rolling-box" >
        <div
          id={animationNameRef.current}
          ref={rollingBodyRef}
          className="rolling-offset-box"
          onMouseEnter={hoverStart}
          onMouseLeave={hoverEnd}
          onMouseDown={onMouseDownBorder}
          style={{
            animation: `${animationNameRef.current} ${time}s linear infinite`,
            animationPlayState: isRolling ? 'running' : 'paused',
          }}>
          {new Array(5).fill(0).map((_, index) => {
            return <div className="rolling-item" key={index}>hello world {index}</div>
          })}
          {new Array(5).fill(0).map((_, index) => {
            return <div className="rolling-item" key={index}>hello world {index}</div>
          })}
        </div>
      </div>
    )
}

效果:

完整代码

html 复制代码
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>react hook</title>
</head>

<body>
  <div id="root"></div>
  <script src="https://unpkg.com/react@18/umd/react.production.min.js" crossorigin></script>
  <script src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js" crossorigin></script>
  <script src="https://unpkg.com/babel-standalone@6/babel.min.js"></script>
  <script></script>

  <script type="text/babel">
    window.onload = function () {
      const {
        useState,
        useEffect,
        useRef,
        createElement
      } = React

      const root = document.getElementById('root')
      function Rolling() {
        const [isRolling, setRolling] = useState(true)
        // 动画名称
        const animationNameRef = useRef(`rollingsAnnualTasks${Math.floor(Math.random() * 100000)}`) // 动画名称
        // 滚动盒子dom
        const rollingBodyRef = useRef()
        // 滚动时长
        const time = 3
        useEffect(() => {
          controlAnimation()
        }, [])

        const clearAnimation = () => {
          const style = document.styleSheets[0]
          if (!style) return
          const styleArray = [].slice.call(style.cssRules) // 将伪数组变成数组
          const index = styleArray.findIndex(item => item.name === animationNameRef.current)
          if (index !== -1) style.deleteRule(index) // 如果有此动画就先删除
          return style
        }

        const controlAnimation = () => {
          const dom = rollingBodyRef.current
          const distance = getDistance().currDistance
          if (!dom) return
          const style = clearAnimation()
          if (!isRolling || !style) return
          dom.style.animationPlayState = '' // 继续动画
          style.insertRule(`@keyframes ${animationNameRef.current} {0%{ transform: translateX(0%);}100%{transform: translateY(-${distance}px);}}`, 0)
        }

        const hoverStart = () => {
          setRolling(false)
        }
        const hoverEnd = () => {
          setRolling(true)
        }
        // 获取dom大小
        const getDistance = () => {
            const dom = rollingBodyRef.current
            const currDistance = dom.offsetHeight / 2
            return { currDistance }
        }

        /**
         * 鼠标按下边
         */
        const onMouseDownBorder = (e) => {
          const element = document.getElementById(animationNameRef.current)
          if (!element) return
          const animation = element.getAnimations()
          const startDis = e.clientY
          const currDistance = getDistance().currDistance
          const speed = currDistance / (time * 1000)
          let rememberDis
          const mouseMoveHander = (e) => {
            const endDis = e.clientY
            const distance = endDis - startDis
            animation.forEach((item) => {
              if (!rememberDis) rememberDis = (item.currentTime) || 0
              const currTime = rememberDis - (distance / speed)
              item.currentTime = currTime < 0 ? time * 1000 + currTime : currTime
            })
          }
          document.addEventListener('mousemove', mouseMoveHander)
          const mouseUpHandler = (e) => {
            document.removeEventListener('mousemove', mouseMoveHander)
            document.removeEventListener('mouseup', mouseUpHandler)
          }
          document.addEventListener('mouseup', mouseUpHandler)
        }

        return (
          <div className="rolling-box" >
            <div
              id={animationNameRef.current}
              ref={rollingBodyRef}
              className="rolling-offset-box"
              onMouseEnter={hoverStart}
              onMouseLeave={hoverEnd}
              onMouseDown={onMouseDownBorder}
              style={{
                animation: `${animationNameRef.current} ${time}s linear infinite`,
                animationPlayState: isRolling ? 'running' : 'paused',
              }}>
              {new Array(5).fill(0).map((_, index) => {
                return <div className="rolling-item" key={index}>hello world {index}</div>
              })}
              {new Array(5).fill(0).map((_, index) => {
                return <div className="rolling-item" key={index}>hello world {index}</div>
              })}
            </div>
          </div>
        )
      }
      ReactDOM.render(createElement(Rolling), root)
    }
  </script>
</body>
<style>
  * {
    margin: 0;
    padding: 0;
  }

  #root {
    display: flex;
    align-items: center;
    justify-content: center;
    height: 100vh;
  }

  .rolling-offset-box {
    width: 100%;
  }

  .rolling-box {
    height: 300px;
    width: 100px;
    overflow: hidden;
    cursor: pointer;
    box-shadow: 0 3px 12px rgba(0, 0, 0, .07), 0 1px 4px rgba(0, 0, 0, .07);
  }

  .rolling-item {
    height: 100px;
    width: 100px;
    color: white;
    text-align: center;
    line-height: 100px;
    margin: 2px 0;
    background-color: #79bbff;
  }
</style>

</html>
相关推荐
巴拉巴拉~~1 分钟前
KMP 算法通用图表组件:KmpChartWidget 多维度可视化 + PMT 表渲染 + 性能对比
前端·javascript·microsoft
智算菩萨7 分钟前
基于spaCy的英文自然语言处理系统:低频词提取与高级文本分析
前端·javascript·easyui
刘一说18 分钟前
Vue单页应用(SPA)开发全解析:从原理到最佳实践
前端·javascript·vue.js
疯狂成瘾者19 分钟前
前端vue核心知识点
前端·javascript·vue.js
Laravel技术社区1 小时前
用PHP8实现斗地主游戏,实现三带一,三带二,四带二,顺子,王炸功能(第二集)
前端·游戏·php
m0_738120722 小时前
应急响应——知攻善防Web-3靶机详细教程
服务器·前端·网络·安全·web安全·php
程序员爱钓鱼9 小时前
Node.js 编程实战:文件读写操作
前端·后端·node.js
PineappleCoder9 小时前
工程化必备!SVG 雪碧图的最佳实践:ID 引用 + 缓存友好,无需手动算坐标
前端·性能优化
JIngJaneIL10 小时前
基于springboot + vue古城景区管理系统(源码+数据库+文档)
java·开发语言·前端·数据库·vue.js·spring boot·后端
敲敲了个代码10 小时前
隐式类型转换:哈基米 == 猫 ? true :false
开发语言·前端·javascript·学习·面试·web