|前言
最近接到一个需求,要实现精选的用户评论在大屏电视上竖向无限滚动播放,当时第一个想法就是通过swiper实现,只需要简单的将几个参数拼接在一起就能实现。实际上拼接完各个需要的属性之后,发现并不能实现我所期待的效果,无奈之下只能自己手写一个实现该功能。(该功能其实也不难,只是一开始就想着借助第三方组件库来实现,没有去思考实现思路)
|简介
本次功能的实现使用了Window.requestAnimationFrame()方法实现,用到的框架是react。整体的实现思路是:拷贝两份数据,判断容器的 滚动高度 + 可视高度 = 整体高度,可以得出容器已经触底,这个时候再拷贝一份数据放进容器中继续滚动,同时删除第一份数据(这里删除第一份数据的原因是防止页面DOM节点过多)
|API
这里简单介绍一下 Window.requestAnimationFrame(),Window.requestAnimationFrame()告诉浏览器器------你希望执行一个动画,并且要求浏览器在下次重绘之前调用指定的回调函数更新动画。该方法需要传入一个回调函数作为参数,该回调函数会在浏览器下一次重绘之前执行。
|敲重点
调用该方法只能帮我们执行一次我们传入的回调函数,所以在我们传入的回调函数中需要再次主动去调用该方法。
语法:
scss
requestAnimationFrame(callback)
参数:
arduino
callback // 需要执行的动画函数
返回值:
arduino
一个整数ID // 可以用来取消动画的执行
取消动画
javascript
window.cancelAnimationFrame(ID)
|为什么不使用定时器而使用requestAnimationFrame
这里我们需要知道这两者的区别:
- 定时器可以帮助我们隔一段时间执行一段逻辑代码,理论上也是可以实现定时滚动一定距离的需求的,但是问题就在于每次执行动画函数时不能保证动画执行在浏览器进行重绘之前,无法卡在浏览器的更新动画帧上。简单来说就是:定时执行的滚动,可能在浏览器重新渲染之前,也可能在浏览器重新渲染之后,导致的结果就是页面滚动不流畅,有时候看起来没滚动,有时候看起来滚动的距离又比较大,会有抖动的问题。
- requestAnimationFrame方法可以保证每次动画的执行都完美卡在浏览器重绘之前,使得同样是滚动却可以非常流畅。
|代码实现
js
import React, { useRef, useEffect } from 'react'
// 样式
import styles from './index.scss'
// 列表数据
import { userList } from './utils'
// 滚动容器
const wrapperRef = useRef()
// 是否已经开启动画
const flagRef = useRef(false)
// 所有滚动item容器
const allItemRef = useRef()
// 保存动画ID
const animationRef = useRef()
// 右边部分元素无限滚动
function nodeMove() {
const container = wrapperRef.current // 容器元素
const allNode = allItemRef.current // 全部元素
container.appendChild(allNode.cloneNode(true)) // 复制元素到后方,实现两份数据
const allNodeHeight = container.scrollHeight // 获取全部元素高度
const loop = () => {
if(container.scrollTop + container.offsetHeight >= wrapperRef.current?.scrollHeight) {
// 触底则拷贝一份元素加进去,并删除已经隐藏的第一份数据
console.log('到底了');
container.appendChild(allNode.cloneNode(true))
container.removeChild(container.firstChild)
} else {
// 没有触底则正常滚动
container.scrollTop++
}
animationRef.current = window.requestAnimationFrame(() => loop()) // 动画递归调用
}
animationRef.current = window.requestAnimationFrame(() => loop()) // 启动动画
flagRef.current = true
}
useEffect(() => {
if(!flagRef.current && allItemRef.current) {
nodeMove()
}
window.addEventListener('fullscreenchange', watchFullState)
return () => {
window.removeEventListener('fullscreenchange', watchFullState)
}
}, [])
// DOM结构
// 最外层容器,需要设置超出部分隐藏
<div className={styles['swiper-container']}>
// 滚动容器
<div className={styles['swiper-wrapper-container']} ref={wrapperRef}>
// 所有item的容器,用来获取所有item的总高度,判断是否触底
<div ref={allItemRef}>
{
// 以下是所有item,大家可根据需要编写结构和样式
userList.map((item, index) => (
<div key={index} className={styles['swiper-item']}>
<div className={styles['item-header']}>
<div className={styles['header-left']}>
<div className={styles['header-left-img']}>
<img src={item.avatarUrl}></img>
</div>
<p className={styles['header-left-name']}>{item.username}</p>
</div>
<div className={styles['header-right']}>
<p className={styles['header-right-title']}>{item.title}</p>
<p className={styles['header-right-time']}>{item.time}</p>
</div>
</div>
<div className={styles['item-bottom']}>
{
item.detail.map((value, idx) => (
<p key={idx}>{value}</p>
))
}
</div>
</div>
))
}
</div>
</div>
</div>
scss
.swiper-container {
width: 100%;
height: calc(100% - 81px);
overflow: hidden;
}
.swiper-wrapper-container {
width: 100%;
height: 100%;
overflow-y: auto;
&::-webkit-scrollbar {
width: 0 !important;
display: none !important;
}
&::-webkit-scrollbar-track {
display: none !important;
}
&::-webkit-scrollbar-button {
display: none !important;
}
}
.swiper-item {
background: rgba(0,172,231,0.08);
border-radius: 23px;
padding-top: 12px;
padding-bottom: 18px;
margin-bottom: 15px;
.item-header {
border-bottom: 1px solid rgba(0, 172, 231, .2);
padding-left: 18px;
padding-bottom: 11px;
display: flex;
.header-left {
width: 105px;
.header-left-img {
width: 53px;
height: 53px;
margin-left: 24px;
border-radius: 50%;
overflow: hidden;
img {
display: block;
width: 100%;
height: 100%;
}
}
.header-left-name {
margin-top: 5px;
text-align: center;
font-size: 14px;
font-weight: 400;
color: #000000;
opacity: .8;
line-height: 19px;
}
}
.header-right {
margin-left: 18px;
padding-top: 12px;
.header-right-title {
font-size: 20px;
font-weight: 600;
color: #000000;
opacity: .8;
line-height: 28px;
margin-bottom: 17px;
}
.header-right-time {
font-size: 14px;
opacity: .3;
font-weight: 400;
color: #000000;
line-height: 19px;
}
}
}
.item-bottom {
padding: 11px 18px 0 18px;
p {
font-size: 17px;
font-weight: 400;
opacity: .6;
color: #000000;
line-height: 23px;
}
}
}
|总结
除了通过控制滚动高度,也可以通过控制容器的margin-top为负值来实现向上滚动,当然触底的判断和细节处理也有所不同,本次就只讲解通过控制滚动高度来实现无限轮播,由于页面涉及公司内部业务就不在这里展示了,大家可以复制代码在本地修改下数据跑一下看看实现效果。
如果大家有更好的实现方案也可以在评论区讨论!!欢迎大家提出更多的方案一起学习。