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以获得完整的工作实现。