6

我正在使用 Angular 的cdk-virtual-scroll-viewport. 除了订阅视图位置之外,该功能不依赖于它的任何特殊内容,以便在用户滚动到底部时(在自定义中DataSource<Item>)加载新元素:

connect(collectionViewer: CollectionViewer): Observable<Item[]> {
  this.viewChanges = collectionViewer.viewChange.subscribe((listRange) => {
    this.loadItemsFor(listRange);
  });
  ..
}

当所有项目具有相同的高度(在css和itemSize.与FixedSizeVirtualScrollStrategy配合得很好,所以我尝试使用from (它使用AutoSizeVirtualScrollStrategy)。但是,使用动态策略,一旦将新元素添加到备份虚拟滚动的数据源,滚动位置就会闪烁(我假设是因为)。<cdk-virtual-scroll-viewport>100px50pxautosizecdl-experimentalItemAverager

有没有可行的方法来实现这两种策略的混合?我知道列表中每个项目的类型,因此它的高度,所以应该可以对显示的内容和要加载的内容进行精确计算?当然,对于大型集合,它可能不会那么高效。

4

1 回答 1

8

AngularcdkVirtualFor允许提供自定义虚拟滚动策略,这就是您提到的固定和自动高度策略的实现方式。在您的情况下,它将接受项目高度数组作为输入。我最近不得不处理这个确切的情况:用户可以添加任意数量的项目并且可以计算项目大小的列表表单,使用自定义虚拟滚动策略来提高性能。要了解虚拟滚动策略的内部工作原理,我发现深入研究固定和自动大小策略的源代码以及Alex Inkin 的这篇文章非常有帮助。

以下是这种策略的外观。这基本上是一种简化的固定高度策略,但使用高度计算而不是固定高度值。

 class CustomVirtualScrollStrategy implements VirtualScrollStrategy {
  constructor(private itemHeights: ItemHeight) {}
  private viewport?: CdkVirtualScrollViewport
  private scrolledIndexChange$ = new Subject<number>()
  public scrolledIndexChange: Observable<number> = this.scrolledIndexChange$.pipe(distinctUntilChanged())
  _minBufferPx = 100
  _maxBufferPx = 100
  attach(viewport: CdkVirtualScrollViewport) {
    this.viewport = viewport;
    this.updateTotalContentSize()
    this.updateRenderedRange()
  }
  detach() {
    this.scrolledIndexChange$.complete()
    delete this.viewport
  }
  public updateItemHeights(itemHeights: ItemHeight) {
    this.itemHeights = itemHeights
    this.updateTotalContentSize()
    this.updateRenderedRange()
  }
  private getItemOffset(index: number): number {
    return this.itemHeights.slice(0, index).reduce((acc, itemHeight) => acc + itemHeight, 0)
  }
  private getTotalContentSize(): number {
    return this.itemHeights.reduce((a,b)=>a+b, 0)
  }
  private getListRangeAt(scrollOffset: number, viewportSize: number): ListRange {
    type Acc = {itemIndexesInRange: number[], currentOffset: number}
    const visibleOffsetRange: Range = [scrollOffset, scrollOffset + viewportSize]
    const itemsInRange = this.itemHeights.reduce<Acc>((acc, itemHeight, index) => {
      const itemOffsetRange: Range = [acc.currentOffset, acc.currentOffset + itemHeight]
      return {
        currentOffset: acc.currentOffset + itemHeight,
        itemIndexesInRange: intersects(itemOffsetRange, visibleOffsetRange)
          ? [...acc.itemIndexesInRange, index]
          : acc.itemIndexesInRange
      }
    }, {itemIndexesInRange: [], currentOffset: 0}).itemIndexesInRange
    const BUFFER_BEFORE = 5
    const BUFFER_AFTER = 5
    return {
      start: clamp(0, (itemsInRange[0] ?? 0) - BUFFER_BEFORE, this.itemHeights.length - 1),
      end: clamp(0, (last(itemsInRange) ?? 0) + BUFFER_AFTER, this.itemHeights.length)
    }
  }
  private updateRenderedRange() {
    if (!this.viewport) return

    const viewportSize = this.viewport.getViewportSize();
    const scrollOffset = this.viewport.measureScrollOffset();
    const newRange = this.getListRangeAt(scrollOffset, viewportSize)
    const oldRange = this.viewport?.getRenderedRange()

    if (isEqual(newRange, oldRange)) return

    this.viewport.setRenderedRange(newRange);
    this.viewport.setRenderedContentOffset(this.getItemOffset(newRange.start));
    this.scrolledIndexChange$.next(newRange.start);
  }
  private updateTotalContentSize() {
    const contentSize = this.getTotalContentSize()
    console.log(contentSize)
    this.viewport?.setTotalContentSize(contentSize)
  }
  onContentScrolled() {
    this.updateRenderedRange()
  }
  onDataLengthChanged() {
    this.updateTotalContentSize()
    this.updateRenderedRange()
  }
  onContentRendered() {}
  onRenderedOffsetChanged() {}
  scrollToIndex(index: number, behavior: ScrollBehavior) {
    this.viewport?.scrollToOffset(this.getItemOffset(index), behavior)
  }
}

请参阅此 Stackblitz以获得完整的工作实现。

于 2021-02-14T06:58:52.003 回答