网页弹幕展示
- 2020 年 3 月 28 日
- 笔记
前言
目前视频播放平台弹幕几乎都是使用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; }
这样之后的传入的弹幕就能够展示了。
结语
以上就基本完成了一个简单的弹幕功能,这里还有很多拓展还没有做或者由于篇幅问题没有展示,例如:
- 弹幕很多的时候我们如何控制弹幕速度
- 弹幕停止运动
- 屏幕变化如何控制弹幕显示的位置