针对不同固定大小项目的角度虚拟滚动策略

Mil*_*nov 11 angular-material angular angular-cdk angular-cdk-virtual-scroll

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

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

当所有项目都具有相同的高度时(在 css 和 中指定itemSize<cdk-virtual-scroll-viewport>,效果很好。现在我尝试添加不同类型的项目,其大小不同(可以说100pxvs 50px)。这并不与FixSizeVirtualScrollStrategy配合良好,因此我尝试使用autosizecdl-experimental使用AutoSizeVirtualScrollStrategy)。但是,使用动态策略,一旦将新元素添加到备份虚拟滚动的数据源,滚动位置就会闪烁(我假设是因为ItemAverager)。

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

Kla*_*r_1 17

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)
  }
}
Run Code Online (Sandbox Code Playgroud)

请参阅此 Stackblitz以获得完整、有效的实现。