pc版快手、移動端b站彈幕列表效果實現
- 效果描述:頁面上部爲直播視頻播放器,下半部分是彈幕列表,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;
}