来回拉扯了几下,最后还是没用 react-virtualized ,造了轮子,设计的基础参考了网站 jimeng 交互,通过数据就可以得知单个卡片的宽高,如果resize,基于当前的滚动位置,要重算上方所有卡片的高度,因为计算高度的过程不需要重新渲染,所以性能上只是数字计算,可以接受
具体的代码就不放了,又臭又长,结构设计就在下面,现在ai随时能扩写出来,真的方便啊
type GetHeightFn = (item: any, columnWidth: number) => number;
/** 因为需求上可能有重复数据在同一个瀑布流中展示,所以索引应该与 index 有关 */
type GetKeyFn = (item: any, index: number) => string;
/** 获取分列数量 */
export function getColumnCount(containerWidth: number) {
}
const defaultOverScanPixels = 1000;
const defaultSpace = 12;
/** 静态容器在 positions 中的 key */
export const STATIC_CONTAINER_KEY = 'static_container';
interface InitOpts {
getHeightFn: GetHeightFn;
getKeyFn: GetKeyFn;
containerWidth: number;
columnsSpace?: number;
linesSpace?: number;
list?: any[];
/** 预加载的高度 */
overScanPixels?: number;
onContentHeightChange?: (height: number) => void; // 内容高度变化回调
/** 固定占位区域占据列数 */
staticContainerColumns?: number;
/** 固定占位区域比例尺 */
staticContainerRatio?: number;
}
interface Position {
top: number;
left: number;
height: number; // 元素高度
col: number; // 所在列
width: number; // 元素宽度(可选)
}
export default class MasonrySort {
private opts: InitOpts;
private columnCount = 0;
private columnWidth = 0;
constructor(initOpts: InitOpts) {
this.opts = initOpts;
this.opts.columnsSpace = initOpts.columnsSpace || defaultSpace;
this.opts.linesSpace = initOpts.linesSpace || defaultSpace;
this.opts.overScanPixels = initOpts.overScanPixels || defaultOverScanPixels;
// 避免传入引用改变导致直接修改 model 内的数据
this.opts.list = initOpts?.list ? [...initOpts.list] : initOpts?.list;
this.resetColumns();
}
resize = (newWidth: number) => {
};
/** 重设列相关 */
private resetColumns = () => {
};
get containerWidth(): number {
}
/** 每个元素的位置 */
private positions: Map<string, Position> = new Map();
/** 每列的堆高 - 用于一次构建流程 */
private columnHeights: number[] = [];
/** 获取当前最短的列索引 */
private getShortestColumn(): number {
}
/** 获取整体容器的高度(最长的那一列) */
getTotalHeight(): number {
}
/** 记录按高度对 key 的分组(组内有序) */
private sortedKeys: { [key in number]: { key: string; index: number }[] } = {};
/** 按照堆高对元素位置分组 - top 必须是整数
* - 用于快速获取某个高度区间内的所有元素
*/
private sortItem = (key: string, index: number, top = 0) => {
};
/** 重设数据 - 如果旧数据的位置没有变更,就不重新计算位置 */
resetList(list: any[]) {
}
/** 获取某个元素的位置 */
getItemPosition(key: string): Position | undefined {
}
/** 增量式插入新数据 - 如果 key 重复,删除掉旧的 */
private appendItem = (item: any, index: number) => {
};
/** 全量重置元素定位 */
private resetPositions = () => {
};
/** 滚动防抖 */
private lastViewSpace: { start: number; end: number; keys?: { key: string; index: number }[] } = {
start: 0,
end: 0,
keys: [],
};
/** 根据显示区域获取展示内容的 key & index
* @param viewTop 显示区域相对瀑布流顶部高度
* @param viewHeight 显示区域高度
*/
getVisible(viewTop: number, viewHeight: number, fromScroll = false): { key: string; index: number }[] {
}
}
除了这个些核心代码,向上一层是使用泛型数据构建dom结构的抽象业务层
再向上就是根据具体的数据类型,锚定渲染方法和高度计算方法
再向上才是根据list数据渲染瀑布流的数据业务层
比起造轮子,只要设计好结构,ai扩写真的很强大
更新1:
明天和意外你永远不知道哪个先来,原本基于 jimemng 交互设计的高度计算被否定了,新的需求要求卡片高度以实际内容渲染为准,所以要重做计算部分的逻辑,有点搞了,基本确定是抄小红书的交互
小红书的 title 设计是允许 1-2行,但是我发现比较搞得是,当我试图 resize 时,瀑布流直接重置了滚动高度,乐了,他直接规避了 resize 时可能存在的大数据量的重新渲染,希望产品不要既要又要吧,写完了更新