需要实现的效果如下:
需要将用户点赞的信息,一条一条的展示在页面顶部,这样的效果有多种实现方式,下面一一来了解一下吧~
纯css实现
scss如下:(如果要将scss改为less,将$改为@就可以了)
当移动到第8行结束的时候,同屏出现的两行(第9行和第10行),就需要结束循环,重头开始了
这是一个上移的动画,动画执行的时间就是8s
<math xmlns="http://www.w3.org/1998/Math/MathML"> i t e m S h o w T i m e + ( itemShowTime + ( </math>itemShowTime+(oneCycleItemNum - <math xmlns="http://www.w3.org/1998/Math/MathML"> o n e S c r e e n I t e m N u m ) ∗ ( oneScreenItemNum) * ( </math>oneScreenItemNum)∗(itemShowTime / $oneScreenItemNum)
scss
$itemHeight: 60px; // 单个item的高度
$itemShowTime: 2s; // 单个item从完整出现到消失的时长
$oneCycleItemNum: 8; // 单个循环上移的item条数
$oneScreenItemNum: 2; // 同屏出现的item条数(不能大于 oneCycleItemNum)
$oneCycleItemTime: calc($itemShowTime + ($oneCycleItemNum - $oneScreenItemNum) * ($itemShowTime / $oneScreenItemNum));
@keyframes dynamics-rolling {
from {
transform: translateY(0);
}
to {
transform: translateY(-$itemHeight * $oneCycleItemNum);
}
}
.container {
height: 600px;
animation: dynamics-rolling $oneCycleItemTime linear infinite;
.div {
line-height: 60px;
}
}
.visibleView {
width: 100%;
height: 120px;
overflow: hidden;
background-color: skyblue;
}
.box {
width: 100%;
height: 100vh;
display: flex;
justify-content: center;
align-items: center;
}
简单的demo:
tsx
import React, { useEffect, useRef, useState } from 'react'
import styles from './index.module.scss'
const dataSource = new Array(50).fill(0).map((_, index) => index + 1)
export default function CycleScrollList() {
const [data, setData] = useState(dataSource.slice(0, 10))
return (
<div className={styles.box}>
<div className={styles.visibleView}>
<div className={styles.container}>
{
data.map((item, index) => (
<div key={ index } className={styles.div}>{ item }</div>
))
}
</div>
</div>
</div>
)
}
setInterval监听
css动画是定时的,所以可以定时更新列表内容,但是会有很明显的抖动,效果不太友好,应该是定时器的时间还不能太准
tsx
import React, { useEffect, useRef, useState } from 'react'
import styles from './index.module.scss'
const dataSource = new Array(50).fill(0).map((_, index) => index + 1)
export default function CycleScrollList() {
const [data, setData] = useState(dataSource.slice(0, 10))
const nextIndex = useRef(10) // 持续从 dataSource 拿数据的下一个 index
useEffect(() => {
const timer = setInterval(() => {
replaceData()
},4900)
return () => {
clearInterval(timer)
}
}, [])
const replaceData = () => {
let newData = []
if (nextIndex.current-5 < 0) {
newData = [...dataSource.slice(nextIndex.current-5) ,...dataSource.slice(0, nextIndex.current + 5)]
} else {
newData = [...dataSource.slice(nextIndex.current-5, nextIndex.current + 5)]
}
// 使用当前的后半份数据,再从 dataSource 中拿新数据
console.log(newData)
const nextIndexTemp = nextIndex.current + 5
const diff = nextIndexTemp - dataSource.length
if (diff < 0) {
nextIndex.current = nextIndexTemp
} else {
// 一轮数据用完,从头继续
nextIndex.current = diff
}
setData(newData)
}
return (
<div className={styles.box}>
<div className={styles.visibleView}>
<div className={styles.container}>
{
data.map((item, index) => (
<div key={ index } className={styles.div}>{ item }</div>
))
}
</div>
</div>
</div>
)
}
IntersectionObserver监听
监听第5个元素
如果第五个元素可见了,意味着不可见时,需要更换数据了
如果第五个元素不可见了,立刻替换数据
替换的数据如下:
使用IntersectionObserver监听元素,注意页面卸载时,需要去除绑定
tsx如下:
tsx
import React, { useEffect, useRef, useState } from 'react'
import styles from './index.module.scss'
const dataSource = new Array(50).fill(0).map((_, index) => index + 1)
const ITEM_5_ID = 'item-5'
export default function CycleScrollList() {
const [data, setData] = useState(dataSource.slice(0, 10))
const intersectionObserverRef = useRef<IntersectionObserver | null>()
const item5Ref = useRef<HTMLDivElement | null>(null)
const nextIndex = useRef(10) // 持续从 dataSource 拿数据的下一个 index
const justVisible5 = useRef<boolean>(false) // 原来是否为可视
useEffect(() => {
intersectionObserverRef.current = new IntersectionObserver((entries) => {
entries.forEach((item) => {
if (item.target.id === ITEM_5_ID) {
// 与视图相交(开始出现)
if (item.isIntersecting) {
justVisible5.current = true
}
// 从可视变为不可视
else if (justVisible5.current) {
replaceData()
justVisible5.current = false
}
}
})
})
startObserver()
return () => {
intersectionObserverRef.current?.disconnect()
intersectionObserverRef.current = null
}
}, [])
const startObserver = () => {
if (item5Ref.current) {
// 对第五个 item 进行监测
intersectionObserverRef.current?.observe(item5Ref.current)
}
}
const replaceData = () => {
let newData = []
if (nextIndex.current-5 < 0) {
newData = [...dataSource.slice(nextIndex.current-5) ,...dataSource.slice(0, nextIndex.current + 5)]
} else {
newData = [...dataSource.slice(nextIndex.current-5, nextIndex.current + 5)]
}
// 使用当前的后半份数据,再从 dataSource 中拿新数据
console.log(newData)
const nextIndexTemp = nextIndex.current + 5
const diff = nextIndexTemp - dataSource.length
if (diff < 0) {
nextIndex.current = nextIndexTemp
} else {
// 一轮数据用完,从头继续
nextIndex.current = diff
}
setData(newData)
}
return (
<div className={styles.box}>
<div className={styles.visibleView}>
<div className={styles.container}>
{
data.map((item, index) => (
index === 4 ?
<div id={ ITEM_5_ID } ref={ item5Ref } key={ index } className={styles.div}>{ item }</div>
:
<div key={ index } className={styles.div}>{ item }</div>
))
}
</div>
</div>
</div>
)
}
scss样式
scss
$itemHeight: 60px; // 单个item的高度
$itemShowTime: 3s; // 单个item从完整出现到消失的时长
$oneCycleItemNum: 5; // 单个循环上移的item条数
$oneScreenItemNum: 3; // 同屏出现的item条数(不能大于 oneCycleItemNum)
$oneCycleItemTime: calc($itemShowTime + ($oneCycleItemNum - $oneScreenItemNum) * ($itemShowTime / $oneScreenItemNum));
@keyframes dynamics-rolling {
from {
transform: translateY(0);
}
to {
transform: translateY(-$itemHeight * $oneCycleItemNum);
}
}
.container {
height: 600px;
animation: dynamics-rolling $oneCycleItemTime linear infinite;
.div {
line-height: 60px;
}
}
.visibleView {
width: 100%;
height: 120px;
overflow: hidden;
background-color: skyblue;
}
.box {
width: 100%;
height: 100vh;
display: flex;
justify-content: center;
align-items: center;
}