@@ -34,7 +34,8 @@ export interface LayoutNode {
34
34
node ?: Node < unknown > ,
35
35
layoutInfo : LayoutInfo ,
36
36
header ?: LayoutInfo ,
37
- children ?: LayoutNode [ ]
37
+ children ?: LayoutNode [ ] ,
38
+ validRect : Rect
38
39
}
39
40
40
41
const DEFAULT_HEIGHT = 48 ;
@@ -70,6 +71,8 @@ export class ListLayout<T> extends Layout<Node<T>> implements KeyboardDelegate,
70
71
protected invalidateEverything : boolean ;
71
72
protected loaderHeight : number ;
72
73
protected placeholderHeight : number ;
74
+ protected lastValidRect : Rect ;
75
+ protected validRect : Rect ;
73
76
74
77
/**
75
78
* Creates a new ListLayout with options. See the list of properties below for a description
@@ -92,13 +95,37 @@ export class ListLayout<T> extends Layout<Node<T>> implements KeyboardDelegate,
92
95
this . lastWidth = 0 ;
93
96
this . lastCollection = null ;
94
97
this . allowDisabledKeyFocus = options . allowDisabledKeyFocus ;
98
+ this . lastValidRect = new Rect ( ) ;
99
+ this . validRect = new Rect ( ) ;
100
+ this . contentSize = new Size ( ) ;
95
101
}
96
102
97
103
getLayoutInfo ( key : Key ) {
98
- return this . layoutInfos . get ( key ) ;
104
+ let res = this . layoutInfos . get ( key ) ;
105
+
106
+ // If the layout info wasn't found, it might be outside the bounds of the area that we've
107
+ // computed layout for so far. This can happen when accessing a random key, e.g pressing Home/End.
108
+ // Compute the full layout and try again.
109
+ if ( ! res && this . validRect . area < this . contentSize . area && this . lastCollection ) {
110
+ this . lastValidRect = this . validRect ;
111
+ this . validRect = new Rect ( 0 , 0 , Infinity , Infinity ) ;
112
+ this . rootNodes = this . buildCollection ( ) ;
113
+ this . validRect = new Rect ( 0 , 0 , this . contentSize . width , this . contentSize . height ) ;
114
+ res = this . layoutInfos . get ( key ) ;
115
+ }
116
+
117
+ return res ;
99
118
}
100
119
101
120
getVisibleLayoutInfos ( rect : Rect ) {
121
+ // If layout hasn't yet been done for the requested rect, union the
122
+ // new rect with the existing valid rect, and recompute.
123
+ if ( ! this . validRect . containsRect ( rect ) && this . lastCollection ) {
124
+ this . lastValidRect = this . validRect ;
125
+ this . validRect = this . validRect . union ( rect ) ;
126
+ this . rootNodes = this . buildCollection ( ) ;
127
+ }
128
+
102
129
let res : LayoutInfo [ ] = [ ] ;
103
130
104
131
let addNodes = ( nodes : LayoutNode [ ] ) => {
@@ -124,16 +151,27 @@ export class ListLayout<T> extends Layout<Node<T>> implements KeyboardDelegate,
124
151
return node . layoutInfo . rect . intersects ( rect ) || node . layoutInfo . isSticky || this . virtualizer . isPersistedKey ( node . layoutInfo . key ) ;
125
152
}
126
153
127
- validate ( invalidationContext : InvalidationContext < Node < T > , unknown > ) {
154
+ protected shouldInvalidateEverything ( invalidationContext : InvalidationContext < Node < T > , unknown > ) {
128
155
// Invalidate cache if the size of the collection changed.
129
156
// In this case, we need to recalculate the entire layout.
130
- this . invalidateEverything = invalidationContext . sizeChanged ;
157
+ return invalidationContext . sizeChanged ;
158
+ }
131
159
160
+ validate ( invalidationContext : InvalidationContext < Node < T > , unknown > ) {
132
161
this . collection = this . virtualizer . collection ;
162
+
163
+ // Reset valid rect if we will have to invalidate everything.
164
+ // Otherwise we can reuse cached layout infos outside the current visible rect.
165
+ this . invalidateEverything = this . shouldInvalidateEverything ( invalidationContext ) ;
166
+ if ( this . invalidateEverything ) {
167
+ this . lastValidRect = this . validRect ;
168
+ this . validRect = this . virtualizer . getVisibleRect ( ) ;
169
+ }
170
+
133
171
this . rootNodes = this . buildCollection ( ) ;
134
172
135
173
// Remove deleted layout nodes
136
- if ( this . lastCollection ) {
174
+ if ( this . lastCollection && this . collection !== this . lastCollection ) {
137
175
for ( let key of this . lastCollection . getKeys ( ) ) {
138
176
if ( ! this . collection . getItem ( key ) ) {
139
177
let layoutNode = this . layoutNodes . get ( key ) ;
@@ -148,15 +186,31 @@ export class ListLayout<T> extends Layout<Node<T>> implements KeyboardDelegate,
148
186
149
187
this . lastWidth = this . virtualizer . visibleRect . width ;
150
188
this . lastCollection = this . collection ;
189
+ this . invalidateEverything = false ;
151
190
}
152
191
153
192
buildCollection ( ) : LayoutNode [ ] {
154
193
let y = this . padding ;
194
+ let skipped = 0 ;
155
195
let nodes = [ ] ;
156
196
for ( let node of this . collection ) {
197
+ let rowHeight = ( this . rowHeight ?? this . estimatedRowHeight ) ;
198
+
199
+ // Skip rows before the valid rectangle unless they are already cached.
200
+ if ( node . type === 'item' && y + rowHeight < this . validRect . y && ! this . isValid ( node , y ) ) {
201
+ y += rowHeight ;
202
+ skipped ++ ;
203
+ continue ;
204
+ }
205
+
157
206
let layoutNode = this . buildChild ( node , 0 , y ) ;
158
207
y = layoutNode . layoutInfo . rect . maxY ;
159
208
nodes . push ( layoutNode ) ;
209
+
210
+ if ( node . type === 'item' && y > this . validRect . maxY ) {
211
+ y += ( this . collection . size - ( nodes . length + skipped ) ) * rowHeight ;
212
+ break ;
213
+ }
160
214
}
161
215
162
216
if ( this . isLoading ) {
@@ -181,10 +235,21 @@ export class ListLayout<T> extends Layout<Node<T>> implements KeyboardDelegate,
181
235
return nodes ;
182
236
}
183
237
184
- buildChild ( node : Node < T > , x : number , y : number ) : LayoutNode {
238
+ isValid ( node : Node < T > , y : number ) {
185
239
let cached = this . layoutNodes . get ( node . key ) ;
186
- if ( ! this . invalidateEverything && cached && cached . node === node && y === ( cached . header || cached . layoutInfo ) . rect . y ) {
187
- return cached ;
240
+ return (
241
+ ! this . invalidateEverything &&
242
+ cached &&
243
+ cached . node === node &&
244
+ y === ( cached . header || cached . layoutInfo ) . rect . y &&
245
+ cached . layoutInfo . rect . intersects ( this . lastValidRect ) &&
246
+ cached . validRect . containsRect ( cached . layoutInfo . rect . intersection ( this . validRect ) )
247
+ ) ;
248
+ }
249
+
250
+ buildChild ( node : Node < T > , x : number , y : number ) : LayoutNode {
251
+ if ( this . isValid ( node , y ) ) {
252
+ return this . layoutNodes . get ( node . key ) ;
188
253
}
189
254
190
255
let layoutNode = this . buildNode ( node , x , y ) ;
@@ -245,19 +310,36 @@ export class ListLayout<T> extends Layout<Node<T>> implements KeyboardDelegate,
245
310
let layoutInfo = new LayoutInfo ( node . type , node . key , rect ) ;
246
311
247
312
let startY = y ;
313
+ let skipped = 0 ;
248
314
let children = [ ] ;
249
315
for ( let child of node . childNodes ) {
316
+ let rowHeight = ( this . rowHeight ?? this . estimatedRowHeight ) ;
317
+
318
+ // Skip rows before the valid rectangle unless they are already cached.
319
+ if ( y + rowHeight < this . validRect . y && ! this . isValid ( node , y ) ) {
320
+ y += rowHeight ;
321
+ skipped ++ ;
322
+ continue ;
323
+ }
324
+
250
325
let layoutNode = this . buildChild ( child , x , y ) ;
251
326
y = layoutNode . layoutInfo . rect . maxY ;
252
327
children . push ( layoutNode ) ;
328
+
329
+ if ( y > this . validRect . maxY ) {
330
+ // Estimate the remaining height for rows that we don't need to layout right now.
331
+ y += ( [ ...node . childNodes ] . length - ( children . length + skipped ) ) * rowHeight ;
332
+ break ;
333
+ }
253
334
}
254
335
255
336
rect . height = y - startY ;
256
337
257
338
return {
258
339
header,
259
340
layoutInfo,
260
- children
341
+ children,
342
+ validRect : layoutInfo . rect . intersection ( this . validRect )
261
343
} ;
262
344
}
263
345
@@ -273,10 +355,8 @@ export class ListLayout<T> extends Layout<Node<T>> implements KeyboardDelegate,
273
355
// or the content of the item changed.
274
356
let previousLayoutNode = this . layoutNodes . get ( node . key ) ;
275
357
if ( previousLayoutNode ) {
276
- let curNode = this . collection . getItem ( node . key ) ;
277
- let lastNode = this . lastCollection ? this . lastCollection . getItem ( node . key ) : null ;
278
358
rectHeight = previousLayoutNode . layoutInfo . rect . height ;
279
- isEstimated = width !== this . lastWidth || curNode !== lastNode || previousLayoutNode . layoutInfo . estimatedSize ;
359
+ isEstimated = width !== this . lastWidth || node !== previousLayoutNode . node || previousLayoutNode . layoutInfo . estimatedSize ;
280
360
} else {
281
361
rectHeight = this . estimatedRowHeight ;
282
362
isEstimated = true ;
@@ -297,7 +377,8 @@ export class ListLayout<T> extends Layout<Node<T>> implements KeyboardDelegate,
297
377
layoutInfo . allowOverflow = true ;
298
378
layoutInfo . estimatedSize = isEstimated ;
299
379
return {
300
- layoutInfo
380
+ layoutInfo,
381
+ validRect : layoutInfo . rect
301
382
} ;
302
383
}
303
384
@@ -333,8 +414,8 @@ export class ListLayout<T> extends Layout<Node<T>> implements KeyboardDelegate,
333
414
updateLayoutNode ( key : Key , oldLayoutInfo : LayoutInfo , newLayoutInfo : LayoutInfo ) {
334
415
let n = this . layoutNodes . get ( key ) ;
335
416
if ( n ) {
336
- // Invalidate by clearing node .
337
- n . node = null ;
417
+ // Invalidate by reseting validRect .
418
+ n . validRect = new Rect ( ) ;
338
419
339
420
// Replace layout info in LayoutNode
340
421
if ( n . header === oldLayoutInfo ) {
0 commit comments