封装基于canvas实现的长列表组件(1)

前言

作为使用React前端开发,可能vue也一样,应该会经常遇到长列表的功能需求,如果不做性能优化,那会导致渲染性能低,带来网页卡顿的问题。常见的解决方案应该是使用虚拟列表,只渲染可见的部分。但是对于虚拟列表的功能,经常不仅仅只需要渲染,还会有拖拽排序,多选组合等功能,这会带来很多的业务复杂度。首先,因为很少会自己出造轮子,所以光相关组件库的引用,就需要虚拟列表库,拖拽库等,因为虚拟列表无法渲染树结构,所以还需要讲树结构展平,然后还需要维护父子关系。那除了使用虚拟列表,还有哪些高性能渲染长列表的方式呢?这里介绍一种使用canvas画布绘制长列表的方法。先看下成品的效果,

封装基于canvas实现的长列表组件(1)

这个是我们编辑器项目的canvas列表效果,这里的icon和滚动条也是使用canvas绘制的,使用体验跟dom体验相差无几,甚至应该比虚拟列表的体验好。

如果你的项目恰好也是一个基于canvas的产品,那使用canvas应该也是更好的选择。

接下来,本文会教大家封装一个开源的canvas列表的组件,这篇文章主要讲下列表的渲染以及滚动条的实现

技术分析

1. 渲染部分

 每个列表项就是矩形,矩形里面绘制一个icon,绘制一个文字,如果是父元素,那需要绘制一个文件夹图标和一个折叠图标
 滚动条也是一个矩形,在弄个圆角美观点

2. 交互部分

 点击的时候需要知道点击的时候需要点击了哪个部分
 需要拖拽功能   

3. 技术选型

 为了更快的实现上面的功能,选个canvas库进行开发,常见的canvas库有fabricjs,konvajs,pixijs
 fabricjs文档不友好,
 pixijs是基于webgl的,性能比较好,但是demo 没适合这个组件的
 konvajs 文档挺好,官方demo 提供了很多这个组件需要的功能
 所以这个组件选择基于konvajs进行开发
 

实现步骤

数据结构

常见的树结构

export type TreeItemOption = {
	label: string;
	id: string | number;
	data?: any;
	children?: TreeItemOption[];
};

canvas列表类

export class CanvasTree {
	private _stage: Stage;
	private _layer: Layer;
	private scrollBar: Rect;
	private totalHeight = 30;
	private _renderIndex = 0;
	constructor(container: HTMLElement, private _data: TreeItemOption[] = []) {
        this._stage = new Stage({
                container: container as HTMLDivElement,
                width: container.clientWidth,
                height: container.clientHeight,
        });

        const layer = new Layer();
        this._layer = layer;

        this._stage.add(layer);
        
            // 渲染滚动条
        const rect = new Rect({
                name: SCROLL_BAR_NAME,
                height: 100,
                width: SCROLL_BAR_WIDTH,
                cornerRadius: 5,
                fill: "#ccc",
                ID: SCROLL_BAR_NAME,
                x: this._stage.width() - SCROLL_BAR_WIDTH,
                hitStrokeWidth: 10,
                y: 0,
                draggable: true,
                dragBoundFunc: (pos) => {
                        return {
                                x: this.scrollBar.x(),
                                y: clamp(pos.y, 0, this.Height - this.scrollBar.height()),
                        };
                },
		});

		layer.add(rect);

		this.scrollBar = rect;

		this.render();

		this.register();
    }
    get Width() {
            return this._stage.width();
    }
    get Height() {
            return this._stage.height();
    }
  }

以上代码主要是初始化konvajs的画布场景,场景中加入一个层,层里面添加一个rect 作为滚动条。滚动条固定在画布的最右边,拖拽的时候固定x坐标,y的范围限制在画布内。

滚动条的高度为 (场景高度/列表总高度)*场景高度

绘制列表项

renderNodeItem(item: TreeItemOption, depth: number) {
        const x = depth * 30;
        const y = this._renderIndex * 30;
        const group = new Group({
                x,
                y,
        });

        const background = new Rect({
                name: "Backgroud",
                width: this.Width,
                height: 30,
                stroke: "black",
                strokeWidth: 1,
        });
        group.add(background);

        const text = new Text({
                text: item.label,
                x: 30,
                height: 30,
                verticalAlign: "middle",
        });
        group.add(text);

        this._layer.add(group);

        this._renderIndex++;

        if (item.children) {
                for (const element of item.children) {
                        this.renderNodeItem(element, depth + 1);
                }
        }
}

列表项使用Group渲染,每个Group 加入Rect作为背景。使用·renderIndex记录项的索引,每个项的y值根据索引决定,每个项的x值由项所在的深度决定

监听滚动行为

this._stage.on("wheel", (e) => {
        if (this.totalHeight <= this.Height) return;

        e.evt.preventDefault();

        const direction = -Math.sign(e.evt.deltaY);

        if (direction < 0 && -this._stage.y() + this.Height >= this.totalHeight) {
                return;
        }

        if (direction > 0 && this._stage.y() >= 0) {
                return;
        }

        let y = this._stage.y();
        y += direction * (itemHeight + 1) * (Math.abs(e.evt.deltaY) / 100);

        if (direction < 0 && -y + this.Height >= this.totalHeight) {
                y = this.Height - this.totalHeight;
        }

        if (direction > 0 && y >= 0) {
                y = 0;
        }

        this._stage.y(y);

        this.updateScrollBar(y);
});

updateScrollBar(y: number) {
        if (this.totalHeight <= this.Height) return;
        const scale = Math.abs(y) / this.totalHeight;

        const dist = scale * this.Height;
        this.scrollBar.y(-y + dist);
}

监听场景的 wheel事件,如果场景的y 值在有效范围内(即在0 到列表总高度之间,总高度就是列表项数*列表项高度),那更新场景的y值,让场景上下滑动,同时需要更新滚动条的位置。

滚动条的位置 根据滚动高度和总列表高度的比例决定

拖拽滚动条

scrollY2StageY() {
        const y = this.scrollBar.y();
        const stageY = Math.abs(this._stage.y());
        const dist = y - stageY;
        const scale = dist / this.Height;
        this._stage.y(-this.totalHeight * scale);
        this.updateScrollBar(-this.totalHeight * scale);
}

将滚动条的y值 转换为场景的y值,然后在根据最新的场景高度,更新下滚动条的位置

效果

封装基于canvas实现的长列表组件(1)

完整代码

请前往代码仓库查看

总结

以上基于konvajs实现了长列表的渲染以及对滚动条的模拟,下一步讲实现列表项的拖动,编组等。

参考资料

Konvajs

原文链接:https://juejin.cn/post/7326268986797113381 作者:ZoeLee

(0)
上一篇 2024年1月22日 上午10:41
下一篇 2024年1月22日 上午10:51

相关推荐

发表回复

登录后才能评论