-
Notifications
You must be signed in to change notification settings - Fork 11
/
DynamicVirtualScroll.js
151 lines (151 loc) · 6.51 KB
/
DynamicVirtualScroll.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
/**
* Virtual scroll driver for dynamic row heights
*
* License: GNU LGPLv3.0+
* (c) Vitaliy Filippov 2018+
*
* @param props { totalItems, minRowHeight, viewportHeight, scrollTop }
* @param oldState - previous state object
* @param getRenderedItemHeight = (itemIndex) => height
* this function MUST return the height of currently rendered item or 0 if it's not currently rendered
* the returned height MUST be >= props.minRowHeight
* the function MAY cache heights of rendered items if you want your list to be more responsive
* @returns new state object
* you MUST re-render your list when any state values change
* you MUST preserve all keys in the state object and pass it back via `oldState` on the next run
* you MUST use the following keys for rendering:
* newState.targetHeight - height of the 1px wide invisible div you should render in the scroll container
* newState.topPlaceholderHeight - height of the first (top) placeholder. omit placeholder if it is 0
* newState.firstMiddleItem - first item to be rendered after top placeholder
* newState.middleItemCount - item count to be renderer after top placeholder. omit items if it is 0
* newState.middlePlaceholderHeight - height of the second (middle) placeholder. omit placeholder if it is 0
* newState.lastItemCount - item count to be rendered in the end of the list
*/
export function virtualScrollDriver(props, oldState, getRenderedItemHeight)
{
const viewportHeight = props.viewportHeight;
const viewportItemCount = Math.ceil(viewportHeight/props.minRowHeight); // +border?
const newState = {
viewportHeight,
viewportItemCount,
totalItems: props.totalItems,
scrollHeightInItems: oldState.scrollHeightInItems,
avgRowHeight: oldState.avgRowHeight,
targetHeight: 0,
topPlaceholderHeight: 0,
firstMiddleItem: 0,
middleItemCount: 0,
middlePlaceholderHeight: 0,
lastItemCount: props.totalItems,
lastItemsTotalHeight: oldState.lastItemsTotalHeight,
};
if (!oldState.viewportHeight)
{
oldState = { ...oldState };
for (let k in newState)
{
oldState[k] = oldState[k] || 0;
}
}
if (2*newState.viewportItemCount >= props.totalItems)
{
// We need at least 2*viewportItemCount to perform virtual scrolling
return newState;
}
newState.lastItemCount = newState.viewportItemCount;
{
let lastItemsHeight = 0, lastVisibleItems = 0;
let lastItemSize;
while (lastItemsHeight < viewportHeight)
{
lastItemSize = getRenderedItemHeight(props.totalItems - 1 - lastVisibleItems);
if (!lastItemSize)
{
// Some required items in the end are missing
lastItemSize = 0;
}
lastItemsHeight += lastItemSize < props.minRowHeight ? props.minRowHeight : lastItemSize;
lastVisibleItems++;
}
newState.scrollHeightInItems = props.totalItems - lastVisibleItems + (lastItemsHeight-viewportHeight) / lastItemSize;
// Calculate heights of the rest of items
while (lastVisibleItems < newState.viewportItemCount)
{
lastItemsHeight += getRenderedItemHeight(props.totalItems - 1 - lastVisibleItems);
lastVisibleItems++;
}
newState.lastItemsTotalHeight = lastItemsHeight;
newState.avgRowHeight = lastItemsHeight / lastVisibleItems;
newState.avgRowHeight = !oldState.avgRowHeight || newState.avgRowHeight > oldState.avgRowHeight
? newState.avgRowHeight
: oldState.avgRowHeight;
}
newState.targetHeight = newState.avgRowHeight * newState.scrollHeightInItems + newState.viewportHeight;
const scrollTop = props.scrollTop;
let scrollPos = scrollTop / (newState.targetHeight - newState.viewportHeight);
if (scrollPos > 1)
{
// Rare case - avgRowHeight isn't enough and we need more
// avgRowHeight will be corrected after rendering all items
scrollPos = 1;
}
let firstVisibleItem = scrollPos * newState.scrollHeightInItems;
const firstVisibleItemOffset = firstVisibleItem - Math.floor(firstVisibleItem);
// FIXME: Render some items before current for smoothness
firstVisibleItem = Math.floor(firstVisibleItem);
let firstVisibleItemHeight = getRenderedItemHeight(firstVisibleItem) || newState.avgRowHeight;
newState.topPlaceholderHeight = scrollTop - firstVisibleItemHeight*firstVisibleItemOffset;
if (newState.topPlaceholderHeight < 0)
{
newState.topPlaceholderHeight = 0;
}
if (firstVisibleItem + newState.viewportItemCount >= props.totalItems - newState.viewportItemCount)
{
// Only one placeholder is required
newState.lastItemCount = props.totalItems - firstVisibleItem;
let sum = 0, count = props.totalItems - newState.viewportItemCount - firstVisibleItem;
count = count > 0 ? count : 0;
for (let i = 0; i < count; i++)
{
const itemSize = getRenderedItemHeight(i+newState.firstMiddleItem);
if (!itemSize)
{
// Some required items in the middle are missing
return newState;
}
sum += itemSize;
}
const correctedAvg = (sum + newState.lastItemsTotalHeight) / (count + newState.viewportItemCount);
if (correctedAvg > newState.avgRowHeight)
{
newState.avgRowHeight = correctedAvg;
}
}
else
{
newState.firstMiddleItem = firstVisibleItem;
newState.middleItemCount = newState.viewportItemCount;
let sum = 0;
for (let i = 0; i < newState.middleItemCount; i++)
{
const itemSize = getRenderedItemHeight(i+newState.firstMiddleItem);
if (!itemSize)
{
// Some required items in the middle are missing
return newState;
}
sum += itemSize;
}
newState.middlePlaceholderHeight = newState.targetHeight - sum - newState.lastItemsTotalHeight - newState.topPlaceholderHeight;
if (newState.middlePlaceholderHeight < 0)
{
newState.middlePlaceholderHeight = 0;
}
const correctedAvg = (sum + newState.lastItemsTotalHeight) / (newState.middleItemCount + newState.viewportItemCount);
if (correctedAvg > newState.avgRowHeight)
{
newState.avgRowHeight = correctedAvg;
}
}
return newState;
}