究级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>
相关推荐
栈老师不回家10 分钟前
Vue 计算属性和监听器
前端·javascript·vue.js
前端啊龙16 分钟前
用vue3封装丶高仿element-plus里面的日期联级选择器,日期选择器
前端·javascript·vue.js
一颗松鼠20 分钟前
JavaScript 闭包是什么?简单到看完就理解!
开发语言·前端·javascript·ecmascript
小远yyds40 分钟前
前端Web用户 token 持久化
开发语言·前端·javascript·vue.js
吕彬-前端2 小时前
使用vite+react+ts+Ant Design开发后台管理项目(五)
前端·javascript·react.js
学前端的小朱2 小时前
Redux的简介及其在React中的应用
前端·javascript·react.js·redux·store
guai_guai_guai2 小时前
uniapp
前端·javascript·vue.js·uni-app
bysking3 小时前
【前端-组件】定义行分组的表格表单实现-bysking
前端·react.js
王哲晓3 小时前
第三十章 章节练习商品列表组件封装
前端·javascript·vue.js
fg_4113 小时前
无网络安装ionic和运行
前端·npm