在项目实践中应该有不少场景会用到弹幕,那么如何实现一个完美版本的弹幕呢?接下来咱们原理加代码带你实现一个完整的弹幕组件(react版本)css
针对实现原理,这里我画了一张原理图,你们能够看一下:node
水平弹幕的实现有两种状况:react
一、当弹幕的个数加起来的宽度不足以覆盖屏幕的可视化区域web
二、当弹幕的个数加起来的宽度超过屏幕的可视化区域bash
针对以上两种状况咱们有不一样的展现效果,以下连接的展现效果:app
针对第一种状况,实现原理很简单,当从初始化位置开始滚动的时候,计算滚动的距离,当滚动结束后,立马让其回到初始化位置。flex
第二种状况稍微复杂一些,咱们须要利用人眼的视觉暂留效果,实现弹幕的偷梁换柱,具体怎么实现呢?动画
初始化位置在屏幕最右侧,也就是隐藏在屏幕外面,这一点和上一种状况一致ui
咱们须要使用一个计算好的速度作一次动画滑到屏幕的最左侧,也就是后面循环往复的动画的初始化位置,这个初始化位置和第一点的初始化位置不是同一个。 2.1. 计算这个速度很简单,只须要知道咱们作彻底部动画的时间以及弹幕的总长度,获得的即是平均速度,以后再乘以屏幕的可视化区域宽度this
须要计算好咱们在原有弹幕个数的基础上须要补充多少个弹幕才能超过屏幕可视化区域,作这个步骤是由于,只有补充这些弹幕,才能保证补充的弹幕的第一个滑到最左侧的时候整个弹幕总体瞬间回到初始化位置的时候,不会让用户看出端倪,也就是没有顿挫感。
定好keyframe的具体参数便可开始作动画。
实现的代码是一个组件,这个组件有兴趣的童鞋能够将其丰富化,增长更多的参数,支持各类方向的循环滚动。
class InfiniteScroll extends React.Component {
componentDidMount() {
const { animationName, scrollDirection } = this.props
setTimeout(() => {
if (this.scrollInstance) {
const appendElementWidth = []
const appendElement = []
const visibleWidth = this.scrollInstance.clientWidth
// 滚动的初始位置从视口的最右边开始,后面支持更多方向
const initPosition = visibleWidth
let scrollContainerWidth = 0
let isCoverViewPort = false
// 遍历滚动的全部元素
for (let i = 0; i < this.scrollInstance.children.length; i += 1) {
const style = this.scrollInstance.children[i].currentStyle || window.getComputedStyle(this.scrollInstance.children[i])
// width 已经包含了border和padding,若是box-sizing变化了呢?
const width = this.scrollInstance.children[i].offsetWidth// or use style.width
const margin = parseFloat(style.marginLeft) + parseFloat(style.marginRight)
const clientWidth = (width + margin)
scrollContainerWidth += clientWidth
// 保存须要追加到原有滚动元素的列表后面
if (scrollContainerWidth < visibleWidth * 2 && !isCoverViewPort) {
appendElementWidth.push(clientWidth)
appendElement.push(this.scrollInstance.children[i].cloneNode(true))
if (scrollContainerWidth >= visibleWidth && !isCoverViewPort) {
isCoverViewPort = true
}
}
}
// 该参数记录是否弹幕的宽度超过了屏幕可视化区域的宽度
const isScrollWidthLargeViewPort = scrollContainerWidth > visibleWidth
// const styleSheet = document.styleSheets[0]
// 注意这里的动画初始化位置两种状况是不同的,在以前的步骤上有说过的(垂直方向的没实现,能够忽略掉~)
const keyframes = `
@keyframes ${animationName}{
from{
transform: ${scrollDirection === 'horizon' ? `translateX(${isScrollWidthLargeViewPort ? 0 : initPosition}px)` : 'translateY(0px)'};
}
to {
transform: ${scrollDirection === 'horizon' ? `translateX(-${scrollContainerWidth}px)` : `translateX(-${scrollContainerWidth}px)`};
}
}
@-webkit-keyframes ${animationName}{
from{
-webkit-transform: ${scrollDirection === 'horizon' ? `translateX(${isScrollWidthLargeViewPort ? 0 : initPosition}px)` : 'translateY(0px)'};
}
to {
-webkit-transform: ${scrollDirection === 'horizon' ? `translateX(-${scrollContainerWidth}px)` : `translateX(-${scrollContainerWidth}px)`};
}
}
`
const style = document.createElement('style')
const head = document.head || document.getElementsByTagName('head')[0]
style.type = 'text/css'
const textNode = document.createTextNode(keyframes);
style.appendChild(textNode);
head.appendChild(style)
// 若是css是external的话会报错:Uncaught DOMException: Failed to read the 'cssRules' property from 'CSSStyleSheet': Cannot access rules
// styleSheet.insertRule(keyframes, styleSheet.cssRules.length);
const previousWidth = scrollContainerWidth
// 这个计算以后scrollContainerWidth就会包含那些补充了的弹幕的宽度,因此须要保留一个原始值,供后面的过渡动画使用
if (isScrollWidthLargeViewPort) {
appendElement.map(node => this.scrollInstance.appendChild(node))
appendElementWidth.map(it => scrollContainerWidth += it)
}
// TODO: 动画的速度之后须要使用props
// 由于初始化位置在视口外,可是咱们动画的初始位置都是在0px上,因此就会有一个时差出现,
// 由于在animation生效以前须要有一个过渡动画,两者的时间是相等的
const delay = (this.scrollInstance.children.length * 3 * visibleWidth) / previousWidth
const styleText = isScrollWidthLargeViewPort ? `
width: ${scrollContainerWidth}px;
transform: translateX(0px);
-webkit-transform: translateX(0px);
transition: ${delay}s linear;
-webkit-transition: ${delay}s linear;
animation: ${animationName} ${this.scrollInstance.children.length * 3}s linear ${delay}s infinite;
-webkit-animation: ${animationName} ${this.scrollInstance.children.length * 3}s linear ${delay}s infinite;
` : `
width: ${scrollContainerWidth}px;
animation: ${animationName} ${this.scrollInstance.children.length * 3}s linear infinite;
-webkit-animation: ${animationName} ${this.scrollInstance.children.length * 3}s linear infinite;
`
this.scrollInstance.style.cssText = styleText
}
}, 500)
}
render() {
const { scrollContent, scrollItemClass, scrollClass, scrollDirection } = this.props
const scrollClasses = scrollDirection === 'vertical' ? `scroll-list vertical ${scrollClass}` : `scroll-list horizon ${scrollClass}`
return (
<div className={scrollClasses} ref={ref => this.scrollInstance = ref}>
{
scrollContent.map((content, index) => (<div key={index} className={scrollItemClass}>{content}</div>))
}
</div>
)
}
}
复制代码
对应的CSS文件以下:
.scroll-list{
display: flex;
&.horizon {
flex-direction: row;
}
&.vertical{
flex-direction: column;
}
}
复制代码
完整的应用参考:jsFiddle
至此完整版的水平循环弹幕实现完毕,有问题的欢迎留言~~