Skip to content

Commit ba84d5b

Browse files
authored
feat(virtual-scroll): change fixed strategy to use min & max buffer (#12557)
* feat(virtual-scroll): change fixed strategy to use min & max buffer * address comments
1 parent 915590e commit ba84d5b

File tree

7 files changed

+153
-90
lines changed

7 files changed

+153
-90
lines changed

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

Lines changed: 18 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ export class AutoSizeVirtualScrollStrategy implements VirtualScrollStrategy {
8383
private _minBufferPx: number;
8484

8585
/** The number of buffer items to render beyond the edge of the viewport (in pixels). */
86-
private _addBufferPx: number;
86+
private _maxBufferPx: number;
8787

8888
/** The estimator used to estimate the size of unseen items. */
8989
private _averager: ItemSizeAverager;
@@ -107,14 +107,14 @@ export class AutoSizeVirtualScrollStrategy implements VirtualScrollStrategy {
107107
/**
108108
* @param minBufferPx The minimum amount of buffer rendered beyond the viewport (in pixels).
109109
* If the amount of buffer dips below this number, more items will be rendered.
110-
* @param addBufferPx The number of pixels worth of buffer to shoot for when rendering new items.
110+
* @param maxBufferPx The number of pixels worth of buffer to shoot for when rendering new items.
111111
* If the actual amount turns out to be less it will not necessarily trigger an additional
112112
* rendering cycle (as long as the amount of buffer is still greater than `minBufferPx`).
113113
* @param averager The averager used to estimate the size of unseen items.
114114
*/
115-
constructor(minBufferPx: number, addBufferPx: number, averager = new ItemSizeAverager()) {
115+
constructor(minBufferPx: number, maxBufferPx: number, averager = new ItemSizeAverager()) {
116116
this._minBufferPx = minBufferPx;
117-
this._addBufferPx = addBufferPx;
117+
this._maxBufferPx = maxBufferPx;
118118
this._averager = averager;
119119
}
120120

@@ -172,12 +172,15 @@ export class AutoSizeVirtualScrollStrategy implements VirtualScrollStrategy {
172172
/**
173173
* Update the buffer parameters.
174174
* @param minBufferPx The minimum amount of buffer rendered beyond the viewport (in pixels).
175-
* @param addBufferPx The number of buffer items to render beyond the edge of the viewport (in
175+
* @param maxBufferPx The number of buffer items to render beyond the edge of the viewport (in
176176
* pixels).
177177
*/
178-
updateBufferSize(minBufferPx: number, addBufferPx: number) {
178+
updateBufferSize(minBufferPx: number, maxBufferPx: number) {
179+
if (maxBufferPx < minBufferPx) {
180+
throw('CDK virtual scroll: maxBufferPx must be greater than or equal to minBufferPx');
181+
}
179182
this._minBufferPx = minBufferPx;
180-
this._addBufferPx = addBufferPx;
183+
this._maxBufferPx = maxBufferPx;
181184
}
182185

183186
/** Update the rendered content after the user scrolls. */
@@ -242,8 +245,8 @@ export class AutoSizeVirtualScrollStrategy implements VirtualScrollStrategy {
242245
} else {
243246
// The number of new items to render on the side the user is scrolling towards. Rather than
244247
// just filling the underscan space, we actually fill enough to have a buffer size of
245-
// `addBufferPx`. This gives us a little wiggle room in case our item size estimate is off.
246-
const addItems = Math.max(0, Math.ceil((underscan - this._minBufferPx + this._addBufferPx) /
248+
// `maxBufferPx`. This gives us a little wiggle room in case our item size estimate is off.
249+
const addItems = Math.max(0, Math.ceil((underscan - this._minBufferPx + this._maxBufferPx) /
247250
this._averager.getAverageItemSize()));
248251
// The amount of filled space beyond what is necessary on the side the user is scrolling
249252
// away from.
@@ -361,7 +364,7 @@ export class AutoSizeVirtualScrollStrategy implements VirtualScrollStrategy {
361364
const itemSize = this._averager.getAverageItemSize();
362365
const firstVisibleIndex =
363366
Math.min(viewport.getDataLength() - 1, Math.floor(scrollOffset / itemSize));
364-
const bufferSize = Math.ceil(this._addBufferPx / itemSize);
367+
const bufferSize = Math.ceil(this._maxBufferPx / itemSize);
365368
const range = this._expandRange(
366369
this._getVisibleRangeForIndex(firstVisibleIndex), bufferSize, bufferSize);
367370

@@ -456,14 +459,14 @@ export class CdkAutoSizeVirtualScroll implements OnChanges {
456459
* Defaults to 200px.
457460
*/
458461
@Input()
459-
get addBufferPx(): number { return this._addBufferPx; }
460-
set addBufferPx(value: number) { this._addBufferPx = coerceNumberProperty(value); }
461-
_addBufferPx = 200;
462+
get maxBufferPx(): number { return this._maxBufferPx; }
463+
set maxBufferPx(value: number) { this._maxBufferPx = coerceNumberProperty(value); }
464+
_maxBufferPx = 200;
462465

463466
/** The scroll strategy used by this directive. */
464-
_scrollStrategy = new AutoSizeVirtualScrollStrategy(this.minBufferPx, this.addBufferPx);
467+
_scrollStrategy = new AutoSizeVirtualScrollStrategy(this.minBufferPx, this.maxBufferPx);
465468

466469
ngOnChanges() {
467-
this._scrollStrategy.updateBufferSize(this.minBufferPx, this.addBufferPx);
470+
this._scrollStrategy.updateBufferSize(this.minBufferPx, this.maxBufferPx);
468471
}
469472
}

src/cdk-experimental/scrolling/scrolling.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,19 +11,19 @@ This can be added to your viewport by using the `autosize` directive.
1111
</cdk-virtual-scroll-viewport>
1212
```
1313

14-
The `autosize` strategy is configured through two inputs: `minBufferPx` and `addBufferPx`.
14+
The `autosize` strategy is configured through two inputs: `minBufferPx` and `maxBufferPx`.
1515

1616
**`minBufferPx`** determines the minimum space outside virtual scrolling viewport that will be
1717
filled with content. Increasing this will increase the amount of content a user will see before more
1818
content must be rendered. However, too large a value will cause more content to be rendered than is
1919
necessary.
2020

21-
**`addBufferPx`** determines the amount of content that will be added incrementally as the viewport
21+
**`maxBufferPx`** determines the amount of content that will be added incrementally as the viewport
2222
is scrolled. This should be greater than the size of `minBufferPx` so that one "render" is needed at
2323
a time.
2424

2525
```html
26-
<cdk-virtual-scroll-viewport autosize minBufferPx="50" addBufferPx="100">
26+
<cdk-virtual-scroll-viewport autosize minBufferPx="50" maxBufferPx="100">
2727
...
2828
</cdk-virtual-scroll-viewport>
2929
```

src/cdk-experimental/scrolling/virtual-scroll-viewport.spec.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,12 @@ describe('CdkVirtualScrollViewport', () => {
4040
'should render 4 items to fill 200px space based on 50px estimate from first item');
4141
}));
4242

43+
it('should throw if maxBufferPx is less than minBufferPx', fakeAsync(() => {
44+
testComponent.minBufferPx = 100;
45+
testComponent.maxBufferPx = 99;
46+
expect(() => finishInit(fixture)).toThrow();
47+
}));
48+
4349
// TODO(mmalerba): Add test that it corrects the initial render if it didn't render enough,
4450
// once it actually does that.
4551
});
@@ -61,7 +67,7 @@ function finishInit(fixture: ComponentFixture<any>) {
6167
@Component({
6268
template: `
6369
<cdk-virtual-scroll-viewport
64-
autosize [minBufferPx]="minBufferSize" [addBufferPx]="addBufferSize"
70+
autosize [minBufferPx]="minBufferPx" [maxBufferPx]="maxBufferPx"
6571
[orientation]="orientation" [style.height.px]="viewportHeight"
6672
[style.width.px]="viewportWidth">
6773
<div class="item" *cdkVirtualFor="let size of items; let i = index" [style.height.px]="size"
@@ -88,8 +94,8 @@ class AutoSizeVirtualScroll {
8894
@Input() orientation = 'vertical';
8995
@Input() viewportSize = 200;
9096
@Input() viewportCrossSize = 100;
91-
@Input() minBufferSize = 0;
92-
@Input() addBufferSize = 0;
97+
@Input() minBufferPx = 0;
98+
@Input() maxBufferPx = 0;
9399
@Input() items = Array(10).fill(50);
94100

95101
get viewportWidth() {

src/cdk/scrolling/fixed-size-virtual-scroll.ts

Lines changed: 59 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77
*/
88

99
import {coerceNumberProperty} from '@angular/cdk/coercion';
10-
import {ListRange} from '@angular/cdk/collections';
1110
import {Directive, forwardRef, Input, OnChanges} from '@angular/core';
1211
import {Observable, Subject} from 'rxjs';
1312
import {distinctUntilChanged} from 'rxjs/operators';
@@ -28,16 +27,21 @@ export class FixedSizeVirtualScrollStrategy implements VirtualScrollStrategy {
2827
/** The size of the items in the virtually scrolling list. */
2928
private _itemSize: number;
3029

31-
/** The number of buffer items to render beyond the edge of the viewport. */
32-
private _bufferSize: number;
30+
/** The minimum amount of buffer rendered beyond the viewport (in pixels). */
31+
private _minBufferPx: number;
32+
33+
/** The number of buffer items to render beyond the edge of the viewport (in pixels). */
34+
private _maxBufferPx: number;
3335

3436
/**
3537
* @param itemSize The size of the items in the virtually scrolling list.
36-
* @param bufferSize The number of buffer items to render beyond the edge of the viewport.
38+
* @param minBufferPx The minimum amount of buffer (in pixels) before needing to render more
39+
* @param maxBufferPx The amount of buffer (in pixels) to render when rendering more.
3740
*/
38-
constructor(itemSize: number, bufferSize: number) {
41+
constructor(itemSize: number, minBufferPx: number, maxBufferPx: number) {
3942
this._itemSize = itemSize;
40-
this._bufferSize = bufferSize;
43+
this._minBufferPx = minBufferPx;
44+
this._maxBufferPx = maxBufferPx;
4145
}
4246

4347
/**
@@ -59,11 +63,16 @@ export class FixedSizeVirtualScrollStrategy implements VirtualScrollStrategy {
5963
/**
6064
* Update the item size and buffer size.
6165
* @param itemSize The size of the items in the virtually scrolling list.
62-
* @param bufferSize he number of buffer items to render beyond the edge of the viewport.
66+
* @param minBufferPx The minimum amount of buffer (in pixels) before needing to render more
67+
* @param maxBufferPx The amount of buffer (in pixels) to render when rendering more.
6368
*/
64-
updateItemAndBufferSize(itemSize: number, bufferSize: number) {
69+
updateItemAndBufferSize(itemSize: number, minBufferPx: number, maxBufferPx: number) {
70+
if (maxBufferPx < minBufferPx) {
71+
throw Error('CDK virtual scroll: maxBufferPx must be greater than or equal to minBufferPx');
72+
}
6573
this._itemSize = itemSize;
66-
this._bufferSize = bufferSize;
74+
this._minBufferPx = minBufferPx;
75+
this._maxBufferPx = maxBufferPx;
6776
this._updateTotalContentSize();
6877
this._updateRenderedRange();
6978
}
@@ -112,34 +121,33 @@ export class FixedSizeVirtualScrollStrategy implements VirtualScrollStrategy {
112121
}
113122

114123
const scrollOffset = this._viewport.measureScrollOffset();
115-
const firstVisibleIndex = Math.floor(scrollOffset / this._itemSize);
116-
const firstItemRemainder = scrollOffset % this._itemSize;
117-
const range = this._expandRange(
118-
{start: firstVisibleIndex, end: firstVisibleIndex},
119-
this._bufferSize,
120-
Math.ceil((this._viewport.getViewportSize() + firstItemRemainder) / this._itemSize) +
121-
this._bufferSize);
122-
this._viewport.setRenderedRange(range);
123-
this._viewport.setRenderedContentOffset(this._itemSize * range.start);
124-
125-
this._scrolledIndexChange.next(firstVisibleIndex);
126-
}
127-
128-
/**
129-
* Expand the given range by the given amount in either direction.
130-
* @param range The range to expand
131-
* @param expandStart The number of items to expand the start of the range by.
132-
* @param expandEnd The number of items to expand the end of the range by.
133-
* @return The expanded range.
134-
*/
135-
private _expandRange(range: ListRange, expandStart: number, expandEnd: number): ListRange {
136-
if (!this._viewport) {
137-
return {...range};
124+
const firstVisibleIndex = scrollOffset / this._itemSize;
125+
const renderedRange = this._viewport.getRenderedRange();
126+
const newRange = {start: renderedRange.start, end: renderedRange.end};
127+
const viewportSize = this._viewport.getViewportSize();
128+
const dataLength = this._viewport.getDataLength();
129+
130+
const startBuffer = scrollOffset - newRange.start * this._itemSize;
131+
if (startBuffer < this._minBufferPx && newRange.start != 0) {
132+
const expandStart = Math.ceil((this._maxBufferPx - startBuffer) / this._itemSize);
133+
newRange.start = Math.max(0, newRange.start - expandStart);
134+
newRange.end = Math.min(dataLength,
135+
Math.ceil(firstVisibleIndex + (viewportSize + this._minBufferPx) / this._itemSize));
136+
} else {
137+
const endBuffer = newRange.end * this._itemSize - (scrollOffset + viewportSize);
138+
if (endBuffer < this._minBufferPx && newRange.end != dataLength) {
139+
const expandEnd = Math.ceil((this._maxBufferPx - endBuffer) / this._itemSize);
140+
if (expandEnd > 0) {
141+
newRange.end = Math.min(dataLength, newRange.end + expandEnd);
142+
newRange.start = Math.max(0,
143+
Math.floor(firstVisibleIndex - this._minBufferPx / this._itemSize));
144+
}
145+
}
138146
}
139147

140-
const start = Math.max(0, range.start - expandStart);
141-
const end = Math.min(this._viewport.getDataLength(), range.end + expandEnd);
142-
return {start, end};
148+
this._viewport.setRenderedRange(newRange);
149+
this._viewport.setRenderedContentOffset(this._itemSize * newRange.start);
150+
this._scrolledIndexChange.next(Math.floor(firstVisibleIndex));
143151
}
144152
}
145153

@@ -172,18 +180,27 @@ export class CdkFixedSizeVirtualScroll implements OnChanges {
172180
_itemSize = 20;
173181

174182
/**
175-
* The number of extra elements to render on either side of the scrolling viewport.
176-
* Defaults to 5 elements.
183+
* The minimum amount of buffer rendered beyond the viewport (in pixels).
184+
* If the amount of buffer dips below this number, more items will be rendered. Defaults to 100px.
185+
*/
186+
@Input()
187+
get minBufferPx(): number { return this._minBufferPx; }
188+
set minBufferPx(value: number) { this._minBufferPx = coerceNumberProperty(value); }
189+
_minBufferPx = 100;
190+
191+
/**
192+
* The number of pixels worth of buffer to render for when rendering new items. Defaults to 200px.
177193
*/
178194
@Input()
179-
get bufferSize(): number { return this._bufferSize; }
180-
set bufferSize(value: number) { this._bufferSize = coerceNumberProperty(value); }
181-
_bufferSize = 5;
195+
get maxBufferPx(): number { return this._maxBufferPx; }
196+
set maxBufferPx(value: number) { this._maxBufferPx = coerceNumberProperty(value); }
197+
_maxBufferPx = 200;
182198

183199
/** The scroll strategy used by this directive. */
184-
_scrollStrategy = new FixedSizeVirtualScrollStrategy(this.itemSize, this.bufferSize);
200+
_scrollStrategy =
201+
new FixedSizeVirtualScrollStrategy(this.itemSize, this.minBufferPx, this.maxBufferPx);
185202

186203
ngOnChanges() {
187-
this._scrollStrategy.updateItemAndBufferSize(this.itemSize, this.bufferSize);
204+
this._scrollStrategy.updateItemAndBufferSize(this.itemSize, this.minBufferPx, this.maxBufferPx);
188205
}
189206
}

src/cdk/scrolling/scrolling.md

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -94,12 +94,22 @@ that it allows for better performance, since items do not need to be measured as
9494
</cdk-virtual-scroll-viewport>
9595
```
9696

97-
The fixed size strategy also supports setting the buffer size, i.e. the number of items rendered
98-
beyond the edge of the viewport. This can be adjusted by setting the `bufferSize` input. If
99-
`bufferSize` is not specified it defaults to 5 items.
97+
The fixed size strategy also supports setting a couple of buffer parameters that determine how much
98+
extra content is rendered beyond what is visible in the viewport. The first of these parameters is
99+
`minBufferPx`. The `minBufferPx` is the minimum amount of content buffer (in pixels) that the
100+
viewport must render. If the viewport ever detects that there is less buffered content it will
101+
immediately render more. The second buffer parameter is `maxBufferPx`. This tells the viewport how
102+
much buffer space to render back up to when it detects that more buffer is required.
103+
104+
The interaction of these two buffer parameters can be best illustrated with an example. Supposed
105+
that we have the following parameters: `itemSize = 50`, `minBufferPx = 100`, `maxBufferPx = 250`. As
106+
the user is scrolling through the content the viewport detects that there is only `90px` of buffer
107+
remaining. Since this is below `minBufferPx` the viewport must render more buffer. It must render at
108+
least enough buffer to get back to `maxBufferPx`. In this case, it renders 4 items (an additional
109+
`200px`) to bring the total buffer size to `290px`, back above `maxBufferPx`.
100110

101111
```html
102-
<cdk-virtual-scroll-viewport itemSize="50" bufferSize="1">
112+
<cdk-virtual-scroll-viewport itemSize="50" minBufferPx="100" maxBufferPx="250">
103113
...
104114
</cdk-virtual-scroll-viewport>
105115
```

0 commit comments

Comments
 (0)