@@ -23,8 +23,12 @@ export class ItemSizeAverager {
23
23
/** The current average item size. */
24
24
private _averageItemSize : number ;
25
25
26
+ /** The default size to use for items when no data is available. */
27
+ private _defaultItemSize : number ;
28
+
26
29
/** @param defaultItemSize The default size to use for items when no data is available. */
27
30
constructor ( defaultItemSize = 50 ) {
31
+ this . _defaultItemSize = defaultItemSize ;
28
32
this . _averageItemSize = defaultItemSize ;
29
33
}
30
34
@@ -49,6 +53,12 @@ export class ItemSizeAverager {
49
53
}
50
54
}
51
55
}
56
+
57
+ /** Resets the averager. */
58
+ reset ( ) {
59
+ this . _averageItemSize = this . _defaultItemSize ;
60
+ this . _totalWeight = 0 ;
61
+ }
52
62
}
53
63
54
64
@@ -66,6 +76,15 @@ export class AutoSizeVirtualScrollStrategy implements VirtualScrollStrategy {
66
76
/** The estimator used to estimate the size of unseen items. */
67
77
private _averager : ItemSizeAverager ;
68
78
79
+ /** The last measured scroll offset of the viewport. */
80
+ private _lastScrollOffset : number ;
81
+
82
+ /** The last measured size of the rendered content in the viewport. */
83
+ private _lastRenderedContentSize : number ;
84
+
85
+ /** The last measured size of the rendered content in the viewport. */
86
+ private _lastRenderedContentOffset : number ;
87
+
69
88
/**
70
89
* @param minBufferPx The minimum amount of buffer rendered beyond the viewport (in pixels).
71
90
* If the amount of buffer dips below this number, more items will be rendered.
@@ -85,8 +104,9 @@ export class AutoSizeVirtualScrollStrategy implements VirtualScrollStrategy {
85
104
* @param viewport The viewport to attach this strategy to.
86
105
*/
87
106
attach ( viewport : CdkVirtualScrollViewport ) {
107
+ this . _averager . reset ( ) ;
88
108
this . _viewport = viewport ;
89
- this . _renderContentForOffset ( this . _viewport . measureScrollOffset ( ) ) ;
109
+ this . _setScrollOffset ( ) ;
90
110
}
91
111
92
112
/** Detaches this scroll strategy from the currently attached viewport. */
@@ -97,14 +117,15 @@ export class AutoSizeVirtualScrollStrategy implements VirtualScrollStrategy {
97
117
/** Implemented as part of VirtualScrollStrategy. */
98
118
onContentScrolled ( ) {
99
119
if ( this . _viewport ) {
100
- this . _renderContentForOffset ( this . _viewport . measureScrollOffset ( ) ) ;
120
+ this . _updateRenderedContentAfterScroll ( ) ;
101
121
}
102
122
}
103
123
104
124
/** Implemented as part of VirtualScrollStrategy. */
105
125
onDataLengthChanged ( ) {
106
126
if ( this . _viewport ) {
107
- this . _renderContentForOffset ( this . _viewport . measureScrollOffset ( ) ) ;
127
+ // TODO(mmalebra): Do something smarter here.
128
+ this . _setScrollOffset ( ) ;
108
129
}
109
130
}
110
131
@@ -126,23 +147,127 @@ export class AutoSizeVirtualScrollStrategy implements VirtualScrollStrategy {
126
147
this . _addBufferPx = addBufferPx ;
127
148
}
128
149
150
+ /** Update the rendered content after the user scrolls. */
151
+ private _updateRenderedContentAfterScroll ( ) {
152
+ const viewport = this . _viewport ! ;
153
+
154
+ // The current scroll offset.
155
+ const scrollOffset = viewport . measureScrollOffset ( ) ;
156
+ // The delta between the current scroll offset and the previously recorded scroll offset.
157
+ const scrollDelta = scrollOffset - this . _lastScrollOffset ;
158
+ // The magnitude of the scroll delta.
159
+ const scrollMagnitude = Math . abs ( scrollDelta ) ;
160
+
161
+ // TODO(mmalerba): Record error between actual scroll offset and predicted scroll offset given
162
+ // the index of the first rendered element. Fudge the scroll delta to slowly eliminate the error
163
+ // as the user scrolls.
164
+
165
+ // The current amount of buffer past the start of the viewport.
166
+ const startBuffer = this . _lastScrollOffset - this . _lastRenderedContentOffset ;
167
+ // The current amount of buffer past the end of the viewport.
168
+ const endBuffer = ( this . _lastRenderedContentOffset + this . _lastRenderedContentSize ) -
169
+ ( this . _lastScrollOffset + viewport . getViewportSize ( ) ) ;
170
+ // The amount of unfilled space that should be filled on the side the user is scrolling toward
171
+ // in order to safely absorb the scroll delta.
172
+ const underscan = scrollMagnitude + this . _minBufferPx -
173
+ ( scrollDelta < 0 ? startBuffer : endBuffer ) ;
174
+
175
+ // Check if there's unfilled space that we need to render new elements to fill.
176
+ if ( underscan > 0 ) {
177
+ // Check if the scroll magnitude was larger than the viewport size. In this case the user
178
+ // won't notice a discontinuity if we just jump to the new estimated position in the list.
179
+ // However, if the scroll magnitude is smaller than the viewport the user might notice some
180
+ // jitteriness if we just jump to the estimated position. Instead we make sure to scroll by
181
+ // the same number of pixels as the scroll magnitude.
182
+ if ( scrollMagnitude >= viewport . getViewportSize ( ) ) {
183
+ this . _setScrollOffset ( ) ;
184
+ } else {
185
+ // The number of new items to render on the side the user is scrolling towards. Rather than
186
+ // just filling the underscan space, we actually fill enough to have a buffer size of
187
+ // `addBufferPx`. This gives us a little wiggle room in case our item size estimate is off.
188
+ const addItems = Math . max ( 0 , Math . ceil ( ( underscan - this . _minBufferPx + this . _addBufferPx ) /
189
+ this . _averager . getAverageItemSize ( ) ) ) ;
190
+ // The amount of filled space beyond what is necessary on the side the user is scrolling
191
+ // away from.
192
+ const overscan = ( scrollDelta < 0 ? endBuffer : startBuffer ) - this . _minBufferPx +
193
+ scrollMagnitude ;
194
+ // The number of currently rendered items to remove on the side the user is scrolling away
195
+ // from.
196
+ const removeItems = Math . max ( 0 , Math . floor ( overscan / this . _averager . getAverageItemSize ( ) ) ) ;
197
+
198
+ // The currently rendered range.
199
+ const renderedRange = viewport . getRenderedRange ( ) ;
200
+ // The new range we will tell the viewport to render. We first expand it to include the new
201
+ // items we want rendered, we then contract the opposite side to remove items we no longer
202
+ // want rendered.
203
+ const range = this . _expandRange (
204
+ renderedRange , scrollDelta < 0 ? addItems : 0 , scrollDelta > 0 ? addItems : 0 ) ;
205
+ if ( scrollDelta < 0 ) {
206
+ range . end = Math . max ( range . start + 1 , range . end - removeItems ) ;
207
+ } else {
208
+ range . start = Math . min ( range . end - 1 , range . start + removeItems ) ;
209
+ }
210
+
211
+ // The new offset we want to set on the rendered content. To determine this we measure the
212
+ // number of pixels we removed and then adjust the offset to the start of the rendered
213
+ // content or to the end of the rendered content accordingly (whichever one doesn't require
214
+ // that the newly added items to be rendered to calculate.)
215
+ let contentOffset : number ;
216
+ let contentOffsetTo : 'to-start' | 'to-end' ;
217
+ if ( scrollDelta < 0 ) {
218
+ const removedSize = viewport . measureRangeSize ( {
219
+ start : range . end ,
220
+ end : renderedRange . end ,
221
+ } ) ;
222
+ contentOffset =
223
+ this . _lastRenderedContentOffset + this . _lastRenderedContentSize - removedSize ;
224
+ contentOffsetTo = 'to-end' ;
225
+ } else {
226
+ const removedSize = viewport . measureRangeSize ( {
227
+ start : renderedRange . start ,
228
+ end : range . start ,
229
+ } ) ;
230
+ contentOffset = this . _lastRenderedContentOffset + removedSize ;
231
+ contentOffsetTo = 'to-start' ;
232
+ }
233
+
234
+ // Set the range and offset we calculated above.
235
+ viewport . setRenderedRange ( range ) ;
236
+ viewport . setRenderedContentOffset ( contentOffset , contentOffsetTo ) ;
237
+ }
238
+ }
239
+
240
+ // Save the scroll offset to be compared to the new value on the next scroll event.
241
+ this . _lastScrollOffset = scrollOffset ;
242
+ }
243
+
129
244
/**
130
245
* Checks the size of the currently rendered content and uses it to update the estimated item size
131
246
* and estimated total content size.
132
247
*/
133
248
private _checkRenderedContentSize ( ) {
134
249
const viewport = this . _viewport ! ;
135
- const renderedContentSize = viewport . measureRenderedContentSize ( ) ;
136
- this . _averager . addSample ( viewport . getRenderedRange ( ) , renderedContentSize ) ;
137
- this . _updateTotalContentSize ( renderedContentSize ) ;
250
+ this . _lastRenderedContentOffset = viewport . measureRenderedContentOffset ( ) ;
251
+ this . _lastRenderedContentSize = viewport . measureRenderedContentSize ( ) ;
252
+ this . _averager . addSample ( viewport . getRenderedRange ( ) , this . _lastRenderedContentSize ) ;
253
+ this . _updateTotalContentSize ( this . _lastRenderedContentSize ) ;
138
254
}
139
255
140
256
/**
141
- * Render the content that we estimate should be shown for the given scroll offset.
142
- * Note: must not be called if `this._viewport` is null
257
+ * Sets the scroll offset and renders the content we estimate should be shown at that point.
258
+ * @param scrollOffset The offset to jump to. If not specified the scroll offset will not be
259
+ * changed, but the rendered content will be recalculated based on our estimate of what should
260
+ * be shown at the current scroll offset.
143
261
*/
144
- private _renderContentForOffset ( scrollOffset : number ) {
262
+ private _setScrollOffset ( scrollOffset ? : number ) {
145
263
const viewport = this . _viewport ! ;
264
+ if ( scrollOffset == null ) {
265
+ scrollOffset = viewport . measureScrollOffset ( ) ;
266
+ } else {
267
+ viewport . setScrollOffset ( scrollOffset ) ;
268
+ }
269
+ this . _lastScrollOffset = scrollOffset ;
270
+
146
271
const itemSize = this . _averager . getAverageItemSize ( ) ;
147
272
const firstVisibleIndex =
148
273
Math . min ( viewport . getDataLength ( ) - 1 , Math . floor ( scrollOffset / itemSize ) ) ;
0 commit comments