网页弹幕展示
前言
目前视频播放平台弹幕几乎都是使用js操作dom的方式实现,由于篇幅的原因这次只展示js操作dom的实现方案。
下文代码展示使用的是react 16.2版本库。
大家可以在npm中安装page-construct-template_component_barrage这个插件来直接使用。
正文
功能
弹幕文字各种样式:字体大小、字体类型、字体颜色(字体透明度)
弹幕展示速度
弹幕行高度
弹幕事件:鼠标左右点击事件、鼠标滑入滑出事件
调用方式如下:
const div = document.createElement('div');
const div.innerText = 'hello word';
div.style.color = 'orange';
div.syle.fontSize = '20px';
<Barrage
data={[
{
text: 'hello'
},
{
text: 'word',
// 控制单个弹幕元素的样式
color: 'rgba(255, 255, 255, 0.7)',
speed: [3, 4]
},
div
]}
fontSize={25} // 弹幕字体大小
lineHeight={40} // 弹幕行高
speed={[1, 2]} // 控制弹幕速度
onMouseOver={}
onMouseOut={}
/>
js+dom实现方案
在开始正式代码开发之前需要弄清楚这种方法实现的逻辑:
- 首先我们需要创建一个容器来承载弹幕元素,将监听函数写到这个容器上面
- 初始化弹幕信息(弹幕内容、样式、速度,同时判断对象是否是dom节点)、初始弹幕容器能够显示多少行
- 创建弹幕dom,设置属性,插入页面
- transition动画结束,删除弹幕dom
基本流程就是上面这几步了,下面我们进入每一步的程序编写。
初始项目
这一步要做的事情有:
- 创建弹幕容器
- 向弹幕容器添加监听器,我们将所有弹幕节点的监听事件都委托到弹幕容器节点上面,减少内存占用
- 弹幕容器宽高存入state
import React, { Component } from 'react';
// 弹幕之间的最小距离
const barrageAway = 30;
export default class extends Component {
// 容器宽高
state = {
width: 0,
height: 0
}
barrageList = [] // 弹幕元素信息
rowArr = [] // 容器可以展示弹幕的行
timer = null // 存放定时器
componentDidMount() {
this.setSize(() => {
// 后面再展示这两个回调函数代码
this.init();
this.draw();
});
// 弹幕容器大小发生改变一般事因为屏幕大小改变导致的
window.addEventListener('resize', this.setSize);
}
componentWillUnmount() {
clearTimeout(this.timer);
window.removeEventListener('resize', this.setSize);
}
// 获取弹幕容器的宽高
setSize = cb => {
const container = this.refs.container;
const fn = typeof cb === 'function' ? cb : () => {};
if (!isDom(container)) {
return;
}
this.setState({
width: container.clientWidth,
height: container.clientHeight
}, fn);
}
init = () => {/*初始行、初始弹幕信息*/}
getIdleRow = () => {/*获取空闲行*/}
getAwayRight = () => {/*获取元素距离容器右边框的距离*/}
draw => () => {/*渲染弹幕元素*/}
handleTransitionEnd = e => {/*delete dom*/}
handleClick = () => {/*do something*/}
handleContextMenu = () => {/*do something*/}
handleMouseOver = () => {/*do something*/}
handleMouseOut = () => {/*do something*/}
render() {
return (
// 弹幕容器
<div
ref="container"
onTransitionEnd={this.handleTransitionEnd}
onClick={this.handleClick}
onContextMenu={this.handleContextMenu}
onMouseOver={this.handleMouseOver}
onMouseOut={this.handleMouseOut}
style={{
position: 'absolute',
width: '100%',
height: '100%',
backgroundColor: 'rgba(0, 0, 0, 0)',
overflow: 'hidden',
transform: 'translateZ(0)'
}}
/>
);
}
}
初始化弹幕信息
需要运行的任务有:
- 初始化弹幕展示行数
- 初始弹幕信息(需要判断对象是否是dom节点)
const defaultFont = {
fontSize: 16,
speed: [1, 3],
color: '#000',
fontFamily: 'microsoft yahei'
};
// 函数位置上面有标明
init = () => {
const { data, lineHeight, font } = this.props;
const { height } = this.state;
filter(font, [null, undefined]);
// 计算行数
if (parseInt(height / lineHeight, 10) > this.rowArr.length) {
// 可展示行数增加
for (let i = 0; i < parseInt(height / lineHeight, 10) - this.rowArr.length; i++) {
this.rowArr.push({ idle: true }); this.rowArr用来存放行容器是否空闲,以及当前行末尾元素
}
} else {
// 可展示行数减少
this.rowArr.splice(-1, this.rowArr.length - parseInt(height / lineHeight, 10));
}
// 初始化弹幕信息
data.forEach(item => {
// 属性优先级如下:弹幕对象中定义 > 全局定义 > 默认样式
let barrage = item;
// 如果弹幕对象是一个dom节点
if (isDom(item)) {
barrage = {
domContent: item,
speed: item.speed || font.speed || defaultFont.speed
};
// 开发者传入的是普通对象
}
barrage = {
...defaultFont,
...font,
...item,
...barrage
};
barrage.speed = Math.random() * (barrage.speed[1] - barrage.speed[0]) + barrage.speed[0]; // 随机速度,让弹幕元素错开
this.barrageList.push(barrage); // this.barrageList 用来存放弹幕信息列表
});
}
创建弹幕dom
需要执行的任务有:
- 随机获取空闲行
- 随机一个行数,判断该行是否可以插入新的弹幕
- 可以使用,就将该行行数返回
- 不可以使用,就向后继续寻找可以使用的行
- 找到了就返回对应的行数
- 没找到,找随机行前面是否有可用的行,有就返回对应行数,没有就返回undefined
- 随机一个行数,判断该行是否可以插入新的弹幕
// 获取空闲行
getIdleRow = () => {
if (this.rowArr.length === 0) {
return;
}
const randomRow = Math.floor(Math.random() * this.rowArr.length);
// 随机找到的行为空闲
if (this.rowArr[randomRow].idle || this.getAwayRight(this.rowArr[randomRow].dom) >= barrageAway) {
return randomRow;
}
// 随机找到的行被占用
let increase = randomRow + 1;
// 向后查找空闲的行
while (increase < this.rowArr.length) {
if (this.rowArr[increase].idle || this.getAwayRight(this.rowArr[increase].dom) >= barrageAway) {
return increase;
}
increase++;
}
// 向前查找空闲的行
let decrease = randomRow - 1;
while (decrease > -1) {
if (this.rowArr[decrease].idle || this.getAwayRight(this.rowArr[decrease].dom) >= barrageAway) {
return decrease;
}
decrease--;
}
// 目前没有空闲的行容器
return;
}
// 获取弹幕dom距离容器右边框的距离
getAwayRight = dom => {
const container = this.refs.container;
const { width } = this.state;
const containerRect = container.getBoundingClientRect();
const domRect = dom.getBoundingClientRect();
return containerRect.left + width - domRect.left - dom.offsetWidth;
}
- 创建弹幕dom
- 需要判断是否有可用的行
- 有,就可以创建dom
- 没有,就跳出循环,等下一次再来创建
- 需要判断是否有可用的行
- 设置dom属性
- 弹幕dom写入弹幕容器中
- 设置transition、tranform
- 这里我们使用translate替换left将元素移动到容器最左边,同时开启硬件加速减少页面重排重绘,提高性能
draw = () => {
const { lineHeight } = this.props;
const { width } = this.state;
for (const _ in this.barrageList) {
const barrage = this.barrageList.shift();
const { text, fontSize, color, fontFamily, speed } = barrage;
const idleRowIndex = this.getIdleRow(); // 获取一个空闲行
// 判断是否有可用的行
if (idleRowIndex === undefined) {
break;
}
const randomAway = Math.floor(Math.random() * width / 2); // 随机初始弹幕距离右边框距离,让弹幕错位
// 常见弹幕dom,开发者传入的dom节点也存放到这个dom中
const div = document.createElement('div');
if (!barrage.domContent) {
div.innerText = text;
} else {
div.appendChild(barrage.domContent);
}
// 设置弹幕样式
div.style.fontSize = `${fontSize}px`;
div.style.fontFamily = fontFamily;
div.style.color = color;
div.style.transform = `translate3d(${width + randomAway}px, 0, 0)`;
div.style.position = 'absolute';
div.style.left = 0;
div.style.top = `${idleRowIndex * lineHeight}px`; // 根据空闲的行,计算对应的top值
// 将弹幕dom插入弹幕容器中
this.refs.container.appendChild(div);
this.rowArr[idleRowIndex] = { dom: div, idle: false }; // 该行改成非空闲状态
// 计算弹幕动画
const divWidth = div.offsetWidth;
const runTime = (width + divWidth) / (60 * speed); // 弹幕展示完需要多少时间
div.style.transform = `translate3d(${-divWidth}px, 0, 0)`;
div.style.transition = `transform ${runTime}s linear`;
}
// 没有空闲行,需要等100ms再渲染
if (this.barrageList.length) {
this.timer = setTimeout(this.draw, 100);
}
}
删除弹幕dom
当弹幕展示完成以后我们需要将对应的弹幕dom从页面中移除,之前弹幕动画借助的是transition,因此我们可以通过监听transitionend事件
handleTransitionEnd = e => {
this.refs.container.removeChild(e.target);
}
数据更新
前面实现只能展示第一次传入的数据,对于后面再传入的弹幕数据就不能展示出来,我们这里使用shouldComponentUpdate这个api将新的弹幕数据存入,并对之前的init函数做简单的修改。
shouldComponentUpdate(nextProps) {
if (nextProps.data !== this.props.data) {
const length = this.barrageList.length;
this.init(nextProps);
if (length === 0) {
this.draw();
}
}
return true;
}
init = nextProps => {
const { data, lineHeight, font } = nextProps || this.props;
}
这样之后的传入的弹幕就能够展示了。
结语
以上就基本完成了一个简单的弹幕功能,这里还有很多拓展还没有做或者由于篇幅问题没有展示,例如:
- 弹幕很多的时候我们如何控制弹幕速度
- 弹幕停止运动
- 屏幕变化如何控制弹幕显示的位置