虚拟滚动(瀑布流)排版计算结构设计,以及方案迭代重做

65 阅读3分钟

来回拉扯了几下,最后还是没用 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 时可能存在的大数据量的重新渲染,希望产品不要既要又要吧,写完了更新