pc版快手、移动端b站弹幕列表效果实现
一个原生js开发的横向弹幕
- 效果如下:
- 效果描述:页面上部为直播视频播放器,下半部分是弹幕列表,ui效果类似b站app分享出去的h5直播间,要实现的效果,当弹幕滚动到最底部的时候,新来的的弹幕会自动往上顶,如果向下滑动去看历史弹幕列表新来的弹幕则停止往上顶,然后左下角会出现新消息提示,当点击新消息提示则滚动到最底端新消息提示消失,或者手动滚动到最底端消息提示消失
- 难点:手动滚动到最底部新消失提示消失(因为要监听scroll事件并在其中获取dom元素尺寸)
- 误区:刚开始开发中,我的想法是弹幕列表最多存储 150 条数据,如果超出则新push一个,顶部就shift一个,这样有一个问题就是
scroll
事件是会不断的触发,在观察完pc端快手直播
的弹幕列表,发现其大概原理是超出150条,则将前50条一次性去除,这样 scroll
仅仅只会触发一次,再加上防抖操作
,监听scroll事件的开销就很小了
/**
* h5 直播间弹幕列表组件
*
* 使用方式
* {liveInfo.danmuChannel && <DanmuPanel ref={ref => this.danmuRef = ref} wrapH={DANMUWRAPH}/> }
* 父组件socket拿到数据通过组件 ref 实例调用 addDanmu(data)
*
* 如需样式调整请自行修改less样式
*/
import React from "react";
import classnames from "classnames";
import { debounce, isScrollBottom } from "@/util/index"; // 自行实现或参考我下面的代码
import "./index.less";
interface propsType {
wrapH?: string; // 弹幕容器高度
}
// 弹幕对象类型
type contentType = {
name: string;
content: string;
key: string;
reactId?: any; // 列表唯一标识,如不传会自动添加
}
export default class DanmuPanel extends React.Component<propsType, any> {
danmuWrapHeight: number; // 弹幕容器dom高度
danmuWrapRef: HTMLElement; // 弹幕容器dom
danmuListRef: HTMLElement; // 弹幕列表dom
restNums: number;
reactId: number; // 弹幕列表 key
debounceCb: Function;
isBindScrolled: boolean;
constructor(props) {
super(props)
this.state = {
danmuList: [],
restDanmu: 0,
}
this.restNums = 0;
this.reactId = 0;
this.debounceCb = debounce(this.danmuScroll, 200)
}
componentDidMount() {
this.initDom();
// this.testAddDanmu();
}
// 测试代码,后期删掉
testAddDanmu() {
let i = 0;
setInterval(() => {
++i
this.addDanmu({
name: i + '-我是名字',
content: i + '-我是内容',
key: "danmu",
reactId: i
})
}, 100)
}
private initDom() {
this.danmuWrapRef = document.querySelector('.danmu-wrap');
this.danmuListRef = document.querySelector(".danmu-wrap .list");
this.danmuWrapHeight = this.danmuWrapRef.offsetHeight;
}
private addScroll = () => {
this.debounceCb();
this.isBindScrolled = true;
}
// 弹幕列表滚动到底部回调
private danmuScroll = () => {
const ele: HTMLElement = this.danmuWrapRef;
const isBottom = isScrollBottom(ele, ele.clientHeight);
if (isBottom) {
this.restNums = 0;
this.setState({ restDanmu: 0 })
}
}
// 供父组件调用 socket拿到结果后调用
public addDanmu = (data: contentType) => {
const { danmuList } = this.state;
data.reactId = ++this.reactId;
if (danmuList.length >= 150) {
danmuList.splice(0, 50)
};
danmuList.push(data);
this.setState({ danmuList }, this.renderDanmu)
}
private renderDanmu = () => {
const listH = this.danmuListRef.offsetHeight;
const diff = listH - this.danmuWrapHeight;
const top = this.danmuWrapRef.scrollTop;
if (diff - top < 50) {
if (diff > 0) {
if (this.isBindScrolled) {
this.isBindScrolled = false;
this.danmuWrapRef.removeEventListener('scroll', this.addScroll)
}
this.danmuWrapRef.scrollTo({ top: diff + 40, left: 0, behavior: 'smooth' });
this.restNums = 0;
}
} else {
++this.restNums;
if (!this.isBindScrolled) {
this.isBindScrolled = true;
this.danmuWrapRef.addEventListener('scroll', this.addScroll)
}
}
this.setState({ restDanmu: this.restNums >= 99 ? '99+' : this.restNums });
}
private scrollBottom = () => {
this.restNums = 0;
this.setState({ restDanmu: this.restNums })
this.danmuWrapRef.scrollTo({ top: this.danmuListRef.offsetHeight, left: 0, behavior: 'smooth' });
}
render() {
const { danmuList, restDanmu } = this.state;
return (
<div className="danmu-panel">
<div className="danmu-wrap" style={{ height: this.props.wrapH }}>
<ul className="list">
{
danmuList.map((v) => (
<li key={v.reactId}>
<span className="name">{v.name}:</span>
<span className={classnames("content", v.key)}>{v.content}</span>
</li>
))
}
</ul>
</div>
{
!!restDanmu &&
<div className="rest-nums" onClick={this.scrollBottom}>{ restDanmu }条新消息</div>
}
</div>
)
}
}
debounce isScrollBottom
实现如下
/**
* 防抖函数
* @param fn
* @param wait
*/
export function debounce(fn:Function, wait:number = 500) {
let timeout:number = 0;
return function () {
// 每次触发 scroll handler 时先清除定时器
clearTimeout(timeout);
// 指定 xx ms 后触发真正想进行的操作 handler
timeout = setTimeout(fn, wait);
};
};
/**
* 是否滚到到容器底部
* @param ele 滚动容器
* @param wrapHeight 容器高度
*/
export function isScrollBottom(ele: HTMLElement, wrapHeight:number, threshold: number = 30) {
const h1 = ele.scrollHeight - ele.scrollTop;
const h2 = wrapHeight + threshold;
const isBottom = h1 <= h2;
// console.log('-->', isBottom, ele.scrollHeight, ele.scrollTop, h2)
return isBottom;
}
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。