Skip to content

Commit 1eee9e7

Browse files
committed
add logic for jumping rendered content based on scroll position
1 parent 1ddd749 commit 1eee9e7

File tree

3 files changed

+100
-18
lines changed

3 files changed

+100
-18
lines changed

src/cdk-experimental/scrolling/auto-size-virtual-scroll.ts

Lines changed: 78 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import {CdkVirtualScrollViewport} from './virtual-scroll-viewport';
1616
* A class that tracks the size of items that have been seen and uses it to estimate the average
1717
* item size.
1818
*/
19-
export class ItemSizeEstimator {
19+
export class ItemSizeAverager {
2020
/** The total amount of weight behind the current average. */
2121
private _totalWeight = 0;
2222

@@ -65,20 +65,20 @@ export class AutoSizeVirtualScrollStrategy implements VirtualScrollStrategy {
6565
private _addBufferPx: number;
6666

6767
/** The estimator used to estimate the size of unseen items. */
68-
private _estimator: ItemSizeEstimator;
68+
private _averager: ItemSizeAverager;
6969

7070
/**
7171
* @param minBufferPx The minimum amount of buffer rendered beyond the viewport (in pixels).
7272
* If the amount of buffer dips below this number, more items will be rendered.
7373
* @param addBufferPx The number of pixels worth of buffer to shoot for when rendering new items.
7474
* If the actual amount turns out to be less it will not necessarily trigger an additional
7575
* rendering cycle (as long as the amount of buffer is still greater than `minBufferPx`).
76-
* @param estimator The estimator used to estimate the size of unseen items.
76+
* @param averager The averager used to estimate the size of unseen items.
7777
*/
78-
constructor(minBufferPx: number, addBufferPx: number, estimator = new ItemSizeEstimator()) {
78+
constructor(minBufferPx: number, addBufferPx: number, averager = new ItemSizeAverager()) {
7979
this._minBufferPx = minBufferPx;
8080
this._addBufferPx = addBufferPx;
81-
this._estimator = estimator;
81+
this._averager = averager;
8282
}
8383

8484
/**
@@ -87,7 +87,8 @@ export class AutoSizeVirtualScrollStrategy implements VirtualScrollStrategy {
8787
*/
8888
attach(viewport: CdkVirtualScrollViewport) {
8989
this._viewport = viewport;
90-
// TODO: kick off rendering (start with totally made up size estimate).
90+
this._updateTotalContentSize();
91+
this._renderContentForOffset(this._viewport.measureScrollOffset());
9192
}
9293

9394
/** Detaches this scroll strategy from the currently attached viewport. */
@@ -97,12 +98,17 @@ export class AutoSizeVirtualScrollStrategy implements VirtualScrollStrategy {
9798

9899
/** Called when the viewport is scrolled. */
99100
onContentScrolled() {
100-
// TODO: do stuff.
101+
if (this._viewport) {
102+
this._renderContentForOffset(this._viewport.measureScrollOffset());
103+
}
101104
}
102105

103106
/** Called when the length of the data changes. */
104107
onDataLengthChanged() {
105-
// TODO: do stuff.
108+
if (this._viewport) {
109+
this._updateTotalContentSize();
110+
this._renderContentForOffset(this._viewport.measureScrollOffset());
111+
}
106112
}
107113

108114
/**
@@ -115,6 +121,68 @@ export class AutoSizeVirtualScrollStrategy implements VirtualScrollStrategy {
115121
this._minBufferPx = minBufferPx;
116122
this._addBufferPx = addBufferPx;
117123
}
124+
125+
/**
126+
* Render the content that we estimate should be shown for the given scroll offset.
127+
* Note: must not be called if `this._viewport` is null
128+
*/
129+
private _renderContentForOffset(scrollOffset: number) {
130+
const viewport = this._viewport!;
131+
const itemSize = this._averager.getAverageItemSize();
132+
const firstVisibleIndex =
133+
Math.min(viewport.getDataLength() - 1, Math.floor(scrollOffset / itemSize));
134+
const bufferSize = Math.ceil(this._addBufferPx / itemSize);
135+
const range = this._expandRange(
136+
this._getVisibleRangeForIndex(firstVisibleIndex), bufferSize, bufferSize);
137+
138+
viewport.setRenderedRange(range);
139+
viewport.setRenderedContentOffset(itemSize * range.start);
140+
}
141+
142+
// TODO: maybe move to base class, can probably share with fixed size strategy.
143+
/**
144+
* Gets the visible range of data for the given start index. If the start index is too close to
145+
* the end of the list it may be backed up to ensure the estimated size of the range is enough to
146+
* fill the viewport.
147+
* Note: must not be called if `this._viewport` is null
148+
* @param startIndex The index to start the range at
149+
* @return a range estimated to be large enough to fill the viewport when rendered.
150+
*/
151+
private _getVisibleRangeForIndex(startIndex: number): Range {
152+
const viewport = this._viewport!;
153+
let range = {
154+
start: startIndex,
155+
end: startIndex +
156+
Math.ceil(viewport.getViewportSize() / this._averager.getAverageItemSize())
157+
};
158+
const extra = range.end - viewport.getDataLength();
159+
if (extra > 0) {
160+
range.start = Math.max(0, range.start - extra);
161+
}
162+
return range;
163+
}
164+
165+
// TODO: maybe move to base class, can probably share with fixed size strategy.
166+
/**
167+
* Expand the given range by the given amount in either direction.
168+
* Note: must not be called if `this._viewport` is null
169+
* @param range The range to expand
170+
* @param expandStart The number of items to expand the start of the range by.
171+
* @param expandEnd The number of items to expand the end of the range by.
172+
* @return The expanded range.
173+
*/
174+
private _expandRange(range: Range, expandStart: number, expandEnd: number): Range {
175+
const viewport = this._viewport!;
176+
const start = Math.max(0, range.start - expandStart);
177+
const end = Math.min(viewport.getDataLength(), range.end + expandEnd);
178+
return {start, end};
179+
}
180+
181+
/** Update the viewport's total content size. */
182+
private _updateTotalContentSize() {
183+
const viewport = this._viewport!;
184+
viewport.setTotalContentSize(viewport.getDataLength() * this._averager.getAverageItemSize());
185+
}
118186
}
119187

120188
/**
@@ -142,14 +210,14 @@ export class CdkAutoSizeVirtualScroll implements OnChanges {
142210
* The minimum amount of buffer rendered beyond the viewport (in pixels).
143211
* If the amount of buffer dips below this number, more items will be rendered.
144212
*/
145-
@Input() minBufferPx = 20;
213+
@Input() minBufferPx = 100;
146214

147215
/**
148216
* The number of pixels worth of buffer to shoot for when rendering new items.
149217
* If the actual amount turns out to be less it will not necessarily trigger an additional
150218
* rendering cycle (as long as the amount of buffer is still greater than `minBufferPx`).
151219
*/
152-
@Input() addBufferPx = 5;
220+
@Input() addBufferPx = 200;
153221

154222
/** The scroll strategy used by this directive. */
155223
_scrollStrategy = new AutoSizeVirtualScrollStrategy(this.minBufferPx, this.addBufferPx);
Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,32 @@
1-
<cdk-virtual-scroll-viewport class="demo-viewport" [itemSize]="20">
2-
<div *cdkVirtualFor="let size of data; let i = index" class="demo-item" [style.height.px]="size">
1+
<h2>Autosize</h2>
2+
3+
<cdk-virtual-scroll-viewport class="demo-viewport" autosize>
4+
<div *cdkVirtualFor="let size of fixedSizeData; let i = index" class="demo-item"
5+
[style.height.px]="size">
36
Item #{{i}} - ({{size}}px)
47
</div>
58
</cdk-virtual-scroll-viewport>
69

7-
<cdk-virtual-scroll-viewport class="demo-viewport demo-horizontal" [itemSize]="20"
8-
orientation="horizontal">
9-
<div *cdkVirtualFor="let size of data; let i = index" class="demo-item" [style.width.px]="size">
10+
<cdk-virtual-scroll-viewport class="demo-viewport" autosize>
11+
<div *cdkVirtualFor="let size of randomData; let i = index" class="demo-item"
12+
[style.height.px]="size">
1013
Item #{{i}} - ({{size}}px)
1114
</div>
1215
</cdk-virtual-scroll-viewport>
1316

17+
<h2>Fixed size</h2>
1418

15-
<cdk-virtual-scroll-viewport class="demo-viewport" autosize>
16-
<div *cdkVirtualFor="let size of data; let i = index" class="demo-item" [style.height.px]="size">
19+
<cdk-virtual-scroll-viewport class="demo-viewport" [itemSize]="50">
20+
<div *cdkVirtualFor="let size of fixedSizeData; let i = index" class="demo-item"
21+
[style.height.px]="size">
22+
Item #{{i}} - ({{size}}px)
23+
</div>
24+
</cdk-virtual-scroll-viewport>
25+
26+
<cdk-virtual-scroll-viewport class="demo-viewport demo-horizontal" [itemSize]="50"
27+
orientation="horizontal">
28+
<div *cdkVirtualFor="let size of fixedSizeData; let i = index" class="demo-item"
29+
[style.width.px]="size">
1730
Item #{{i}} - ({{size}}px)
1831
</div>
1932
</cdk-virtual-scroll-viewport>

src/demo-app/virtual-scroll/virtual-scroll-demo.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,5 +16,6 @@ import {Component, ViewEncapsulation} from '@angular/core';
1616
encapsulation: ViewEncapsulation.None,
1717
})
1818
export class VirtualScrollDemo {
19-
data = Array(10000).fill(20);
19+
fixedSizeData = Array(10000).fill(50);
20+
randomData = Array(10000).fill(0).map(() => Math.round(Math.random() * 100));
2021
}

0 commit comments

Comments
 (0)