直播彈幕滾動列表效果實現

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;
}
相關文章
相關標籤/搜索