效果
实现功能:
无限滚动
动态插入document 动画
动画自定义暂停,倒转
拖拽控制动画
前言
在项目中经常会遇到需要无限滚动的动画录播效果,但是又和传统意义的轮播图有些区别,他不是一个tab页tab页的切换,而是一个一直连续滚动的效果,甚至产品希望提出滚动效果可以拖拽控制的情况。
针对这些难题本文章以一个单页面的react html文件为例做相关功能的详细展示。
本文以react为例
freedom-fj-rolling-react-htmlreact 组建
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. 动态插入动画
无限滚动效果的核心就是复制一份滚动的内容到下面,然后在动画滚动到刚好第一份滚动完的时候瞬间重置动画,就会有种首尾相连无限滚动的感觉。
- 复制一份滚动dom,react可以使用
react.children
复制,vue 用双插槽。 - 利用
document.styleSheets
获取动画列表 style.deleteRule
删除重复动画dom.offsetHeight
计算dom偏移长度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.拖拽动画倒转 *
核心:
document.addEventListener
监听鼠标按下后的移动事件element.getAnimations()
获取动画对象- 根据移动距离和比例修改动画播放
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>